Relations personnalisées avec Laravel

Bien que non indiqué dans la documentation officielle, il est possible de créer des relations personnalisées entre les modèles.

Classiquement, 2 modèles sont liées par une relation OneToOne, OneToMany, HasOne… Mais il peut arriver, en particulier dans le cas d’une base de données pour une application legacy, que les relations ne soient pas faites sur un champ simple.

Prenons le cas simplifié, mais étrange, suivant :

<?php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

/**
 * @property int $id
 * @property string $title
 * @property string $tagId
 */
final class Post extends Model
{
}
<?php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

/**
 * @property int $id
 * @property string $name
 */
final class Tag extends Model
{
}

La propriété Post.tagID est une chaîne de caractère, par exemple tag14 pour indiquer que l’instance de Post est liée à l’instance de Tag avec l’id 14.

Pour obtenir l’instance de Tag depuis une instance de Post, impossible d’ajouter une relation simple OneToOne. C’est ici que les relations personnalisées entrent en jeu.

Pour créer une relation personnalisée, on crée la classe suivante :

<?php
declare(strict_types=1);

namespace App\Relations;

use App\Models\Post;
use App\Models\Tag;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;

/**
 * @property Post $parent
 * @property Tag|Builder $query
 */
final class Taggable extends Relation
{
    private const PREFIX = 'Tag';

    public function __construct(Post $parent)
    {
        parent::__construct(Tag::query(), $parent);
    }

    public function addConstraints(): void
    {
        // Nothing to do here
    }

    /**
     * @param Post[] $models
     */
    public function addEagerConstraints(array $models): void
    {
        $prefixLength = strlen(self::PREFIX);
        $ids = [];
        foreach ($models as $model) {
            if (!str_starts_with($model->tagId, self::PREFIX)) {
                continue;
            }
            $ids[] = (int)substr($model->tagId, $prefixLength);
        }
        $this->query->whereIn('ID', $ids);
    }

    /**
     * @param Post[] $models
     * @param string $relation
     * @return Post[]
     */
    public function initRelation(array $models, $relation): array
    {
        foreach ($models as $model) {
            $model->setRelation($relation, $this->related);
        }

        return $models;
    }

    /**
     * @param Post[] $models
     * @param Collection<Tag> $results
     * @param string $relation
     * @return Post[]
     */
    public function match(array $models, Collection $results, $relation): array
    {
        foreach ($models as $post) {
            $post->setRelation(
                $relation,
                $results->filter(
                    static fn(Model $model): bool => $model instanceof Tag
                        && $post->tagId === self::PREFIX.$model->id
                )->first()
            );
        }

        return $models;
    }

    public function getResults(): ?Tag
    {
        return $this->query->first();
    }
}

Puis on utilise la relation dans le modèle Post :

<?php
declare(strict_types=1);

namespace App\Models;

use App\Relations\Taggable;
use Illuminate\Database\Eloquent\Model;

/**
 * @property int $id
 * @property string $title
 * @property string $tagId
 * @property ?Tag $tag
 */
final class Post extends Model
{
    public function tag(): Taggable {
        return new Taggable($this);
    }
}

Il est maintenant possible d’obtenir d’obtenir l’instance de Tag depuis une instance de Post :

$post = Post::find(1);
echo $post->tag->name;

// Il est même possible de faire ceci :
$post = Post::find(1)->with('tag');

Code disponible ici : https://github.com/thomasage/laravel-custom-relation/tree/main

Inspiré par https://stitcher.io/blog/laravel-custom-relation-classes

Points d’arrêt en Javascript

Une application legacy, le code HTML et JavaScript sont tellement mélangés qu’il devient difficile de savoir quand un élément du DOM a changé et pourquoi.
Grace aux outils de développement de Google Chrome ou Mozilla Firefox, il est possible de poser un point d’arrêt conditionnel.

Voici le code suivant, volontairement très simplifié :

<body>
<input type="text" id="name">
<script>
    setTimeout(function () {
        document.getElementById('name').disabled = 'disabled';
    }, 2000);
</script>
</body>

Avec ce code, 2 secondes après l’affichage de la page, le champ « name » va être désactivé.

Pour poser un point d’arrêt lorsque l’attribut « disabled » va changer, il faut :

  1. Ouvrir les outils de développements sur l’onglet « Inspecteur » (Firefox) ou « Éléments » (Chrome).
  2. Cliquer droit sur l’élément du DOM à surveiller (ici la balise « input »).
  3. Sélectionner « Point d’arrêt sur… » (Firefox) ou « Arrêt activé » (Chrome).
  4. Sélectionner « Une modification d’attribut » (Firefox) ou « modifications d’attribut » (Chrome).
  5. Recharger la page.