Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Une API avec Laravel 6 : un exemple
Vendredi 14 février 2020 17:34

Dans le précédent article j'ai évoqué la création d'API avec Laravel 6. Maintenant je vous propose un exemple de réalisation encore avec l'application de tâches. J'ai utilisé l'authentification de base que j'ai présenté dans le précédent article. Comme le code est assez chargé vous pouvez le télécharger ici.

Je n'ai utilisé aucun framework Javascript pour montrer que désormais les API des navigateurs sont suffisamment mâtures pour s'en passer pour des cas pas trop complexes.

Installation

Pour installer l'application c'est classique, déjà avec Composer :
composer install
Créer une base de données et renseigner les identifiants dans le fichier .env, par exemple :
DB_DATABASE=monapi
DB_USERNAME=root
DB_PASSWORD=
Vous pouvez alors lancer les migrations et la population :
php artisan migrate --seed
Ensuite si vous voulez bricoler dans le Javascript il faut installer les librairies :
npm install
Et pour répercuter les changements :
npm run watch
Au chargement vous obtenez cette page : Comme les clés sont générées de façon aléatoire allez en chercher une dans la base : Entrez la clé dans la zone de saisie et tapez sur "Entrée". Vous allez obtenir une page dans ce genre : Vérifiez que tout fonctionne correctement.

La clé est mémorisée en local storage pour éviter d'avoir à la rentrer systématiquement. C'est un scénario pratique pour nos essais d'API.

La partie client est une réécriture et une évolution de l'application que j'ai présentée ici. Essentiellement le modèle qui était orienté local storage est maintenant branché sur l'APi de Laravel. J'en ai profité pour apporter quelques améliorations.

Intégration dans Laravel

Laravel utilise mix pour compiler les assets, c'est un superbe wrapper de Webpack qui rend ce dernier bien plus convivial. Le fichier de configuration est situé à la racine : webpack.mix.js avec ce code de base :
const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');
On a donc deux fichiers d'entrée :
  • app.js pour le Javascript
  • app.scss pour le style
Il suffit ensuite qu'il y ait les bons chargements dans les fichiers pour que Webpack compile notre code.

Le style

Comme j'utilise paper.css pour l'aspect de l'application je l'ai chargé avec npm :
npm i -D papercss
On le retrouve dans la modules de npm : Du coup dans le fichier d'entrée du style : On commence par importer cette librairie pour ensuite préciser quelques règles de style :
@import "~papercss/src/styles.scss";

button, .paper-btn {
  float: right;
  margin: 0 0 0 5px;
}

...

Le Javascript

Le code Javascript est organisé en composants web :Avec ce découpage :

Je ne rentrerai pas dans cet article dans le détail de ce code parce que j'ai déjà bien détaillé tout ça ici. Je vais par contre m'attarder sur la partie qui concerne l'utilisation de l'API.

On démarre l'application cliente en chargeant le composant todo-app dans la vue welcome :
<body>
  <br>
  <div class="paper container">
    <h1 class="shadow border border-primary">Ma liste de tâches V4</h1>
    <todo-app></todo-app>
  </div>
</body>

La clé de l'authentification

J'ai présenté l'authentification basique dans mon précédent article. On a vu qu'on ajoute une colonne api_token pour mémoriser un token dans la table users. D'autre part on protège les routes concernées avec un middleware auth:api. Il faut donc envoyer systématiquement ce token pour être reconnu par l'API. La première action de notre application va donc être de récupérer ce token.

Dans le fichier resources/js/app.js, qui est le fichier d'entrée, et qui correspond au composant todo-app, on a ce code à la création du composant :

connectedCallback() {

  // Est-ce que la clé a été déjà mémorisée ?
  if(localStorage.getItem('KEY') === null) {

    // On charge la vue d'initialisation
    this.innerHTML = view.init();

    // Entrée pour saisie clé secrète
    this.querySelector('#secret').addEventListener('keydown', e => {
      if(e.key === 'Enter') {
        this.check(e.target);
      }
    }); 

  } else {
    // On a déjà la clé alors on démarre l'application
    this.init();
  }
}

Si on ne trouve pas la clé dans le local storage, ce qui est le cas au départ, on charge la vue d'initialisation dans le fichier resources/js/view.js :

const init = () => `
  <div id="error" class="row flex-spaces" style="display:none">
    <input class="alert-state" id="alert-5" type="checkbox">
    <div class="alert alert-danger dismissible">
      La clé n'est pas correcte !
      <label class="btn-close" for="alert-5">X</label>
    </div>
  </div>
  <div class="form-group">
    <input type="password" placeholder="Votre clé secrète" id="secret">
  </div>
`;
On se retrouve avec le formulaire de saisie de la clé : On a prévu l'écoute du clavier pour la saisie :
this.querySelector('#secret').addEventListener('keydown', e => {
  if(e.key === 'Enter') {
    this.check(e.target);
  }
});
On lance alors une demande au serveur histoire de tester la validité de la clé :
async check(target) {
  const response = await fetch(`${ window.location }/api/todos/check?api_token=${ target.value }`);
  if(response.redirected) {
    this.querySelector('#error').style.display = 'block';
  } else {
    this.init(target);
  }
}
Au niveau de Laravel on a cette route :
Route::middleware('auth:api')->prefix('todos')->group(function () {
  Route::get('check', 'TodoController@check');
  ...  
});

Comme on passe par le middleware pour l'api si la clé n'est pas bonne on va avoir une redirection. Dans ce cas on affiche l'erreur :

Si la clé est correcte on la récupère, on la mémorise, et on charge la vue de l'application :
init(target = null) {

  // On mémorise la clé
  if(target) localStorage.setItem('KEY', target.value);

  // On charge la vue de l'application
  this.innerHTML = view.html();
Cette vue contient pas mal de choses mais au niveau des composants on a les alertes et la liste :
const html = () => `
  <todo-alert id="alert-danger" type="alert-danger" text="Cette tâche existe déjà !" display="none"></todo-alert>
  <todo-alert id="alert-success" type="alert-success" text="La tâche a bien été ajoutée !" display="none"></todo-alert>

  ...

  <todo-list></todo-list>
`;

Le chargement des tâches

Maintenant on peut aller interroger l'API pour récupérer les tâches. Le composant todo-list est organisé selon le pattern MVC :

Dans le code de chargement du composant on a l'appel du modèle tasks :
connectedCallback() 
{
  ...

  tasks.getTasks().then(() => {
    this.innerHTML = html(this.page, tasks.checkStill(), tasks.checkComplete());
    this.reset(); 
  });    
}
On trouve l'appel à l'API dans le modèle :
// Dictionnaire
let tasks = [];

// Clé secrète
let key = null;

// Base de l'url
const baseUrl = `${ window.location }/api/todos`;

// Récupération des tâches
const getTasks = async () =>
{
  key = localStorage.getItem('KEY');
  const response = await fetch(`${ baseUrl }?api_token=${ key }`);
  const data = await response.json();
  tasks = data.data;
}

On voit qu'on récupère la clé dans le local storage. Ensuite on utilise fetch pour l'appel à l'API. Fetch fonctionne avec des promesses. La requête générée est de la forme :

http://monapi.oo/api/todos?api_token=464JYhZGFXX5hehkQhquIwvhgJakeMPmSnRBAKHF3PKMhl5JVpJ1j5VG3O87enZ8DshoTSDCgAt4aLfu
Avec le verbe GET par défaut. Du côté de Laravel on a cette route :
Route::middleware('auth:api')->prefix('todos')->group(function () {
  ...
  Route::get('/', 'TodoController@index');
  ...
});
Et dans le contrôleur :
public function index(Request $request)
{
  return TodoResource::collection($request->user()->todos()->latest()->get());
}
On a une relation hasMany entre les users et les todos. On ordonne les tâches à partir des plus récentes (latest). On utilise une ressource qui filtre les colonnes :
public function toArray($request)
{
    return [
        'id' => $this->id,
        'text'=> $this->text,
        'completed'=> $this->completed,
    ];
}
On retourne une information JSON de cette forme : On mémorise dans le modèle directement ces informations. Dans le composant todo-list on peut alors charger le HTML en construisant toutes les lignes des tâches :
tasks.getTasks().then(() => {
  this.innerHTML = html(this.page, tasks.checkStill(), tasks.checkComplete());
  this.reset(); 
});
Dans la vue on trouve ce code :
export default (page, number, btn) =>  `
  <ul></ul>
  <todo-filters filter="${ page }"></todo-filters>
  <todo-footer number="${ number }" btn="${ btn }"></todo-footer>
`;

On prépare une liste non ordonnée (ul) puis on insère les composants todo-filters et todo-footer en leur transmettant leurs informations. Le premier doit savoir quelles tâches on veut afficher (toutes, finies ou en cours) et le second doit connaître le nombre tâche en cours et aussi si le bouton de purge doit apparaître (dans le cas où il y a au moins une tâche achevée).

La création de la liste se fait dans la méthode reset :
reset() 
{
  // Liste
  const ul = document.createElement('ul');
  tasks.getFiltered(this.page).map(obj => ul.appendChild(new TodoLine(obj.text, obj.completed, obj.id)));
  this.querySelector('ul').replaceWith(ul);
  ... 
}

On génère autant d'instance du composant todo-line qu'il y a de tâches à afficher et on régénère la liste.

L'ajout d'une tâche

On a une zone de saisie pour ajouter une tâche : On a l'écoute de l'événement dans le composant todo-app :
this.querySelector('#add').addEventListener('keydown', e => {
  if(e.key === 'Enter') {
    this.addTask(e.target);
  }
});
On active la méthode addTask :
addTask(target) {    
  // Si le texte n'est pas vide
  if(target.value) {
    this.hideAlerts();
    this.querySelector('todo-list').add(target.value);
    target.value = ''; 
  }   
}
Essentiellement on appelle la méthode add du composant todo-list en transmettant l'information :
add(text) 
{
  // Validation du texte
  const result = tasks.valid(text);
  if(result === 'ok') {
    this.loader(true);
    // Envoi de la requête
    tasks.add(text).then(() => {
      // Mise à jour
      this.reset();
      this.sendEvent('alert-success');
    });
  } else {
    this.sendEvent('alert-danger', { error: result });
  }
}
On valide l'entrée en affichant les erreurs éventuelles : Si c'est bon on affiche le loader et surtout on appelle la méthode add du modèle :
const add = async value => {
  const response = await fetch(baseUrl, {
    method: 'POST',
    body: JSON.stringify({ 
      api_token: key,
      text: value 
    }),
    headers: { 'Content-Type': 'application/json' }
  });
  const data = await response.json();  
  tasks.unshift({ id: data.id, text: data.text, completed: 0 });
};

On utilise encore fetch avec la méthode POST cette fois, on transmet le token dans les paramètres en compagnie du texte de la nouvelle tâche et on attend la réponse de l'API...

On a cette route :
Route::middleware('auth:api')->prefix('todos')->group(function () {
  ...
  Route::post('/', 'TodoController@store');
  ...  
});
Et dans le contrôleur on crée l'enregistrement et de le renvoie :
public function store(Request $request)
{    
  return $request->user()->todos()->create($request->all());
}

Une information importante est l'id du nouvel enregistrement qui va être très utile pour identifier la tâche pour les autres opérations.

Dans le modèle on ajoute la tâche :
tasks.unshift({ id: data.id, text: data.text, completed: 0 });
Dans le composant todo-list on régénère la liste et on envoie l'événement pour l'affichage de l'alerte :
tasks.add(text).then(() => {
  // Mise à jour
  this.reset();
  this.sendEvent('alert-success');
});

La suppression d'une tâche

Pour chaque tâche on a un bouton de suppression au niveau du composant todo-line :

Quand on clique on envoie un événement delete :

li.addEventListener('click', e => {
  if(e.target.matches('button')) {
    this.sendEvent(e.target.matches('.btn-danger') ? 'delete' : 'toggle');
  }
})
Cet événement est intercepté par le composant todo-list :
this.addEventListener('delete', e => this.handleDelete(e));
On appelle la méthode handleDelete :
handleDelete(e) 
{
  this.loader(true);    
  tasks.del(e.target.id).then(() => this.reset());  
}
On active le loader et on appelle la méthode del du modèle :
const del = async id => {
  const response = await fetch(`${ baseUrl }/${ id }?api_token=${ key }`, { 
    method: 'DELETE'
  });
  const data = await response.json(); 
  tasks = tasks.filter(task => task.id !== data);
};

Là on utilise encore Fetch avec le verbe DELETE en transmettant dans l'url l'identifiant de la tâche, par exemple :

http://monapi.oo/api/todos/21?api_token=464JYhZGFXX5hehkQhquIwvhgJakeMPmSnRBAKHF3PKMhl5JVpJ1j5VG3O87enZ8DshoTSDCgAt4aLfu
Côté API on a cette route :
Route::middleware('auth:api')->prefix('todos')->group(function () {
  ...
  Route::delete('{todo}', 'TodoController@destroy');   
});
Et dans le contrôleur :
public function destroy(Todo $todo)
{
    $todo->delete();
    return $todo->id;
}
On supprime la tâche et on renvoie l'identifiant. Dans le modèle on met à jour tasks grâce à cet identifiant :
tasks = tasks.filter(task => task.id !== data);
Et au retour du modèle on actualise la liste (reset) :
tasks.del(e.target.id).then(() => this.reset());

Changement d'état d'une tâche

Pour chaque tâche on dispose d'un bouton qui permet de commuter l'état (Marquer / Démarquer) : Quand on clique on envoie un événement toggle :
this.sendEvent(e.target.matches('.btn-danger') ? 'delete' : 'toggle');
Cet événement est intercepté par le composant todo-list :
this.addEventListener('toggle', e => this.handleToggle(e));
On appelle la méthode handleToggle :
handleToggle(e) 
{
  this.loader(true);
  tasks.update(e.target.id, e.target.completed, e.target.text).then(() => this.reset()); 
}
On active le loader et on appelle la méthode update du modèle :
const update = async (id, completed, value) => {
  const response = await fetch(baseUrl + '/' + id, {
    method: 'PUT',
    body: JSON.stringify({
      api_token: key,
      text: value,
      completed: completed ? 0 : 1
    }),
    headers: { 'Content-Type': 'application/json' }
  });
  const data = await response.json();
  tasks = tasks.map(task => { 
    if(task.id == data.id) {
      task.text = data.text;
      task.completed = data.completed
    }
    return task;
  }); 
};
Cette méthode sert également pour le changement du texte comme on le verra. Là on utilise encore Fetch avec le verbe PUT en transmettant les valeurs en paramètres, par exemple : Côté API on a cette route :
Route::middleware('auth:api')->prefix('todos')->group(function () {
  ...
  Route::put('{todo}', 'TodoController@update');
  ...
});
Et dans le contrôleur :
public function update(Request $request, Todo $todo)
{
  $todo->update($request->all());
  return $todo;
}
On met à jour la tâche et on la renvoie. Dans le modèle on met à jour tasks grâce à ces valeurs transmises :
tasks = tasks.map(task => { 
  if(task.id == data.id) {
    task.text = data.text;
    task.completed = data.completed
  }
  return task;
});
Et au retour du modèle on actualise la liste (reset) :
tasks.update(e.target.id, e.target.completed, e.target.text).then(() => this.reset());

Changement du texte d'une tâche

Au niveau du composant todo-line si on fait un double clic sur le texte d'une tâche on passe en mode édition :

Le composant gère tout ce qui concerne la gestion de cette saisie (il faut tenir compte de la sortie avec Escape ou de la perte du focus), je ne vais pas entrer dans le détail. Si la saisie est correcte on envoie un événement update en transmettant l'ancienne et la nouvelle valeur du texte :

this.sendEvent('update', { old: this.text, new: e.target.value });
Cet événement est intercepté par le composant todo-list :
this.addEventListener('update', e => this.handleUpdate(e));
On appelle la méthode handleUpdate :
handleUpdate(e) 
{
  this.sendEvent('alerts-off');
  // Validation du texte
  const result = tasks.valid(e.detail.new, e.target.id);
  if(result === 'ok') {
    this.loader(true);
    tasks.update(e.target.id, !e.target.completed, e.detail.new).then(() => this.reset());
  } else {
    // Rafraîchissement de la ligne
    e.target.text = e.detail.old; 
    this.sendEvent('alert-danger', { error: result });
  }
}

Là on procède à la validation de la saisie et si c'est bon on appelle la méthode update du modèle. La suite est exactement la même que ci-dessus pour le changement d'état.

La purge des tâches finies

On dispose d'un bouton pour supprimer de façon globale toutes les tâches finies dans le composant todo-footer : Quand on clique on envoie un événement purge :
this.querySelector('div').addEventListener('click', e => {
  if(e.target.matches('button')) {
    this.dispatchEvent(new CustomEvent('purge', { bubbles: true }));       
  }
})
Cet événement est intercepté par le composant todo-list :
this.addEventListener('purge', () => this.handlePurge());
On appelle la méthode handlePurge :
handlePurge() 
{
  this.loader(true);
  tasks.clean().then(() => this.reset());
}
On active le loader et on appelle la méthode clean du modèle :
const clean = async () => {
  await fetch(baseUrl + '/clean?api_token=' + key, { method: 'DELETE' });
  tasks = tasks.filter(task => !task.completed);
};
Là on utilise encore Fetch avec le verbe DELETE, par exemple :
http://monapi.oo/api/todos/clean?api_token=464JYhZGFXX5hehkQhquIwvhgJakeMPmSnRBAKHF3PKMhl5JVpJ1j5VG3O87enZ8DshoTSDCgAt4aLfu
Côté API on a cette route :
Route::middleware('auth:api')->prefix('todos')->group(function () {
  ...
  Route::delete('clean', 'TodoController@clean'); 
  ...
});
Et dans le contrôleur on supprime les enregistrements concernés et on renvoie une réponse vide :
public function clean(Request $request)
{
  $request->user()->todos()->whereCompleted(true)->delete();
  return response()->json();
}
Dans le modèle on met à jour tasks :
tasks = tasks.filter(task => !task.completed);
Et au retour du modèle on actualise la liste (reset) :
tasks.clean().then(() => this.reset());

Conclusion

On a vu dans cet article un exemple complet de réalisation d'une API avec Laravel et du client consommateur de cette API. j'ai utilisé une authentification basique pour ne pas alourdir le code. Il serait évidemment plis judicieux d'utiliser par exemple JWT pour sécuriser l'API.



Par bestmomo

Aucun commentaire