laravel.png

Laravel est un framework PHP assez sympathique qui permet de réaliser très proprement des applications web. Parmi ses fonctionnalités, on trouve un moteur de validation proposant pleins de règles dont l'unicité d'un attribut d'instance de modèle. Mais s'il fonctionne parfaitement pour la création d'une nouvelle instance de modèle, il provoque une erreur si on veut modifier une instance existante sans modifier l'attribut déclaré comme devant être unique. Voici donc la façon que j'ai trouvée (un peu difficilement d'où ce billet) pour y remédier.

Je précise que je suis loin d'être un expert PHP ou Laravel, donc s'il y a une meilleure façon de faire telle ou telle chose, je suis bien évidemment preneur.

Plantons le décors

Disons que nous avons un modèle User doté d'un attribut name tel que généré par

 artisan make:auth

Pour le gérer, nous avons généré notre contrôleur avec:

 artisan make:controller UserController

contrôleur que nous adaptons pour gérer le CRUD des formulaires en passant le modèle. Dans routes/web.php, nous aurons donc:

...
Route::model('users', 'User');
Route::resource('users', 'UserController');
...

Dans notre contrôleur, nous aurons notamment les méthodes (actions) store et update destinées à traiter les créations et les mises à jour:

 <?php
 namespace poum\Http\Controllers;
 
 use DB;
 use poum\User;
 use Input;
 use Redirect;
  
  ....
 
 /** 
 * Création de l'utilisateur en base de données
 * @return Response
 */
 public function store() {
   $input = Input::all();
   User::create($input);
 
   return Redirect::route('users.index')
                ->with('message', 'Utilisateur créé');
 
 }
 
 ....
 
  /** 
  * Mise à jour de l'utilisateur
  * @User $user
  * @return Response
  */
  public function update(User $user) {
 
     $input = array_except(Input::all(), '_method');
     $user->update($input);
 
     return Redirect::route('users.show', $user->id)
                   ->with('message', 'Utilisateur mis à jour');
  }
 
   ...

La validation bête et méchante

Nous allons implémenter une validation basique en créant une instance dédiée de FormRequest. Le principe est simple: en injectant cette FormRequest dans les actions store et update de notre contrôleur, cette dernière effectuera une validation en s'appuyant sur les règles qu'on aura définies dans sa méthodes rules. Au passage, on personnalisera les messages d'erreurs via la méthode messages.

Pour créer la requête, on appelle artisan à la rescousse via :

 artisan make:request UserRequest

puis on modifie le résultat dans app/Http/Requests/UserRequest.ph. On n'oublie pas de modifier au passage la méthode authorize pour qu'elle ne bloque pas les requêtes en lui faisant retourner true (vous adapterez en fonction de vos besoins mais ceci n'est pas l'objet de ce billet):

 ....
 
 /**
  * On laisse passer tout le monde
  */
   public function authorize() {
     return true;
   }
 
  /**
   * Les règles de validation
   * @return array
   */
   public function rules() {
   
     return [
       'name' => 'required|unique:users',
       'email'  => 'required|email|unique:users',
     ];
   }
 
  /**
   * Les messages personnalisés
   * @return array
   */
   public function messages() {
 
     return [
       'name.required' => 'Vous devez saisir un nom d\'utilisateur',
       
       ...
     ];
   }
 
   ....

Si vous testez la création ou la modification à cet instant, vous verrez que votre application va superbement ignorer nos règles de validation. Et pour cause: personne n'a dit aux méthodes de notre contrôleur d'utiliser notre UserRequest. En revanche, si vous avez proprement défini/modifié votre table dans database/migrations avec la règle d'unicité, vous aurez un beau message d'exception du à la violation de cette unicité en base de données (un peu tard, donc, et peu convivial pour les utilisateurs).

 $table->string('name')->unique();
 $table->string('email')->unique();

Le problème vient de ce que l'application utilise toujours FormRequest et n'a donc cure de nos règles définies dans UserRequest. Pour changer cela, il suffit d'injecter via annotation notre UserRequest dans nos actions store et update:

  /** 
   * Création de l'utilisateur en base de données
   * @param UserRequest $userRequest
   * @return Response
   */
 public function store() {
   $input = Input::all();
   User::create($input);
 
   return Redirect::route('users.index')
                ->with('message', 'Utilisateur créé');
 
 }
 
 ..
 
  /**
   * Mise à jour de l'utilisateur
   * @User $user
   * @param UserRequest $userRequest
    * @return Response
    */
   public function update(User $user) {
 
     $input = array_except(Input::all(), '_method');
     $user->update($input);
 
     return Redirect::route('users.show', $user->id)
                   ->with('message', 'Utilisateur mis à jour');
   }

Maintenant, la création d'un nouvel utilisateur va parfaitement se passer, tout comme la modification simultanée des attributs name et email. Mais si l'un de ces attributs conserve sa valeur, la modification va provoquer un message d'erreur car la nouvelle valeur est identique à la précédente, donc existe déjà en base de données, ce qui viole encore la contrainte d'intégrité.

Mise en place de l'exception d'unicité

Nous allons donc mettre en place une exception à la règle d'unicité afin de ne pas tenir compte de la valeur précédemment saisie pour l'instance de modèle en cours de modification. Ceci se fait dans notre méthode rules de notre UserRequest:

 ...
 
 use Illuminate\Validation\Rules;
  
 ...
 
  /**
   * Les règles de validation
   * @return array
   */
   public function rules() {
   
     $user = $this->route()->getParameter('user');
     $id = $user ? $user->id : '';
 
     return [
       'name' =>   'required', Rule::unique('users')...,
      'email'  => 'required', 'email', Rule::unique('...,
     ];
   }

La première partie récupère dans la route de la requête ($this) le modèle passé via user. Pour trouver ce nom d'attribut, vous pouvez utiliser:

 artisan route::list

Ensuite, si on a bien un tel modèle (donc pas en création), on récupère son attribut id. Dans les règles, on remplace la chaîne par un tableau et unique devient une Rule unique cherchant dans la table users mais en ignorant le tuple dont l'identifant est $id (à condition qu'il existe, donc ce sera transparent lors de la création).

Et là, toute fonctionne enfin comme attendu.

Bonus

A tout moment, pour voir le contenu d'une variable d'une manière conviviale (dans le cas des structures notamment), on peut utiliser le die & debug. Par exemple, pour savoir ce que contient $this dans la méthode rules de UserRequest, vous n'avez qu'à y ajouter la ligne:

 dd($his);

Ceci provoquera l'arrêt de l'exécution et l'affichage dans la page web d'une structure de données interarctive dans laquelle vous pourrez naviguer.

En complément, il y a bien sûr la documentation de Laravel.