Photo d'illustration
Code

Générer des UUIDs dans Laravel avec un trait

Utiliser des UUIDs quand il s'agit de faire de l'obfuscation est souvent l'option la plus simple et la plus efficace à mettre en place. Mais est-ce qu'utiliser un package dédié en vaut vraiment la peine?

Pour celles et ceux qui l'ignore encore, l'obfuscation est une méthode qui permet de "cacher" simplement une donnée sensible par une autre, pour des raisons de sécurité ou de vie privée. Dans notre métier de dev, on l'utilise assez souvent pour masquer les IDs issus de nos bases de données qui sont souvent utilisés très généralement comme clé primaire.

Si Laravel te permet de créer aisément dans tes migrations un champ de type UUID, côté Model il n'existe encore rien dans ce framework qui pourrait te généréer un UUID automatiquement et le stocker dans le champ approprié. Pour pallier à cette carence, je vois encore trop souvent une peletée de "tutoriels" (ndr: où plutôt des articles pompés et sur-repompés d'autres articles totalement outdatés) sur le net qui recommande l'utilisation de package, comme ceux-ci par exemple :

https://github.com/ramsey/uuid

https://github.com/michaeldyrynda/laravel-model-uuid

En soit, je n'ai strictement aucun reproche à faire à ces packages parce qu'ils peuvent très bien répondre au besoin principal, mais c'est un poil exagéré de s'ajouter des dépendances dans un projet pour générer un truc aussi simple qu'une chaine de caractère unique... surtout quand on sait que sur le plan technique c'est en réalité très simple. Bouge pas, je te montre.

Générer un UUID dans un Model en quelques lignes

Il est possible dans un Model d'exécuter des actions spécifiques en écoutant ses events. Si on reprend notre exemple d'obfuscation, tu voudrais donc générer un UUID pour ton Model avant de le sauver dans ta base de données et pour ce faire tu pourrais procéder comme suit :

<?php

namespace App\Models;

use Illuminate\Support\Str;

class Post extends Model
{
    public static function boot()
    {
        self::saving(function($model) { 
            $model->uuid = (string) Str::uuid(); 
        });
    }

    // ...
}

Comme tu peux le constater, j'utilise la méthode statique boot() du Model pour lui indiquer qu'au moment de son enregistrement (l'event saving de la ligne 11) nous générons pour le champ nommé uuid un UUID v4 via le helper Str. Résultat : l'UUID est créé et sauvé.

Plus simple, tu meurs pas vrai? Ouaip, mais ce n'est pas encore parfait: si c'est diablement efficace, il te faudra dupliquer ces lignes si tu veux faire la même chose pour un autre Model. C'est clairement fastidieux, en plus de rendre ta maintenabilité plus difficile...

Pour la réusabilité, un Trait suffit

Les traits sont apparus avec la version 5.4 de PHP (sortie en 2012) et nous permettent de réutiliser facilement des méthodes dans des classes sans avoir à jouer avec de l'héritage. C'est donc la réponse parfaite à notre besoin de réusabilité pour notre exemple d'UUID.

Laravel n'ayant pas de réel guideline spécifique sur le sujet "où stocker mes traits?", je t'invite à créer un ton fichier en utilisant le path suivant : app/Traits/HasUUID.php et dans ce même fichier tu vas y placer ces quelques lignes :

<?php

namespace App\Traits;

use Illuminate\Support\Str;

trait HasUUID
{
    public static function bootHasUUID()
    {
        static::saving(function ($model) {
            $model->uuid = (string) Str::uuid();
        });
    }
}

La seule différence avec ce que l'on a inscrit dans ton Model tout à l'heure, c'est la ligne 9 : nous ne faisons plus appel à la méthode boot() mais bootHasUUID(). En effet, utiliser simplement la méthode boot() dans un trait ne fonctionnera pas dans Laravel, ton model sera totalement incapable de l'interpréter.

Pour remédier à cela, le framework a inventé une méthode "magique": boot[nom de ton trait]. Donc si demain tu nous inventes par exemple un traits HasSlug, ta méthode boot se nommera... bootHasSlug() (c'est bien, y en a qui suivent) !

Grâce à ce magnifique trait, il ne te reste plus désormais qu'à retourner dans ton Model et de faire un peu de nettoyage :

<?php

namespace App\Models;

use App\Traits\HasUUid;

class Post extends Model
{
    use HasUUID;
    
    // ...
}

Et voilà. Désormais, tu peux réutiliser au besoin ce trait dans tes Models et générer à la volée des UUIDs sans jamais dépendre d'un package tiers!

Bonus : utiliser l'UUID automatiquement comme clé pour faire du Route Model Binding

Alexis, le génial auteur derrière le compte de @parfaitementweb, m'a fait cette suggestion intéressante sur Twitter :

Tu le sais peux être déjà, il est possible avec Laravel de faire du Route Model Binding très facilement depuis le fichier routes/web.php, comme ceci par exemple:

Route::get('/posts/{post}', function (Post $post) {
    return $post->title;
});

Tu me diras très justement que cet exemple ne fonctionne que dans le cas où l'on utilise la clé primaire, en l'occurence un ID, alors qu'on cherche à utiliser depuis le début de cet article un UUID pour faire de l'obfuscation. Tu as raison et pour arriver à tes fins Laravel t'offre non pas une mais deux possibilités :

  • soit tu spécifies la clé à utiliser directement dans ta route, via ton fichier routes/web.php (tu noteras le 'post:uuid' dans l'url à la ligne 3)
use App\Models\Post;

Route::get('/posts/{post:uuid}', function (Post $post) {
    dd($post);
});
  • soit tu spécifie dans directement dans ton Model la méthode suivante
public function getRouteKeyName()
{
    return 'uuid';
}

Mais comme nous utilisons un trait pour nous faciliter la vie, en terme de réusabilité et d'implémentation (ouais, et de maintenabilité aussi), il te suffit en fait tout simplement d'ajouter cette dernière méthode dans ton fichier HasUUID.php :

<?php

namespace App\Traits;

use Illuminate\Support\Str;

trait HasUUID
{
    public function getRouteKeyName()
    {
        return 'uuid';
    }

    public static function bootHasUUID()
    {        
        static::saving(function ($model) {
            $model->uuid = (string) Str::uuid();
        });
    }
}

Et voilà, c'est plié, désormais tout Model qui fera appel à ce trait te génèrera un UUID v4 tout propre et utilisera ce dernier comme clé pour le Route Model Binding!