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.

Liaison avec un automate industriel

Contexte : La société Faurecia est le leader technologique de l’industrie automobile. Au sein de leurs usines, les agents ont besoin d’un outil pour leur permettre d’ordonner les pièces suivant les priorités de la chaîne de montage.

Réalisation : Développement d’un serveur HTTP accessible par des tablettes. Développement d’un serveur TCP appelé par le serveur HTTP et l’automate industriel pour échanger.

Contraintes : Les dispositifs utilisés ont une résolution très réduite (800×480) pour pouvoir être attachés à un bras. Le code est déposé dans un serveur Windows intégré à l’automate, verrouillé en lecture seule après déploiement et sans accès Internet.

Technologies : NodeJS

Application de télé-relève

Contexte : Une société spécialisée dans la commercialisation de services de gestion du cycle de l’eau (*) intervient sur plusieurs sites pour relever des indicateurs et effectuer des entretiens.

Réalisation : Développement d’une application web responsive accessible via mobile. L’application se décompose en 2 parties : la saisie et la consultation. L’administration des sites est prévue pour être la plus flexible possible et permettre l’ajout de nouveaux sites sans développement ultérieurs. Un export Excel est réalisé pour effectuer le lien avec le système interne.

Contraintes : La flotte des dispositifs utilisés peut comprendre Android & iOS. Certains sites sont parfois à la limite du réseau mobile ou complètement sans connectivité : l’application doit permettre la saisie hors-ligne.

Technologies : PHP, Symfony Full-Stack, VueJS, HTML5, CSS3, Bootstrap CSS, MySQL, LocalStorage

(*) La société n’a pas donné son accord pour apparaître ici.