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