> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Enregistrement de relations dynamiques pour les packages

> Analysez resolveRelationUsing de Laravel 13 pour ajouter des relations sans modifier les modèles, préciser les clés, gérer les collisions de noms et l'héritage, et comprendre la durée de vie de l'enregistrement ainsi que les tests.

Pour ajouter une relation aux modèles de l'application consommatrice depuis un package, vous pouvez enregistrer la façon de créer la relation avec `resolveRelationUsing()`. Sans modifier directement le fichier du modèle, vous pouvez récupérer les données, utiliser l'Eager loading et effectuer des recherches par condition d'existence comme avec une relation classique.

Cette page a été vérifiée sur l'implémentation officielle de Laravel Framework `v13.40.0`. Pour les bases des relations, consultez [Introduction aux relations Eloquent](https://kawax.biz/fr/eloquent-relationships.md), et pour le chargement des providers, consultez [Découverte automatique des packages](https://kawax.biz/fr/advanced/package-discovery.md).

## Choisir entre définition classique et dynamique

La documentation officielle recommande les méthodes de relation classiques pour le développement d'applications en général, et indique que la définition dynamique peut être utile notamment pour le développement de packages.

| Situation | Choix recommandé |
| - | - |
| Ajouter une relation à un modèle que vous gérez vous-même | Écrire une méthode de relation classique dans le modèle |
| Un package distribué fournit une relation aux modèles de l'utilisateur | Recevoir le modèle cible via la configuration et envisager `resolveRelationUsing()` |
| Vous voulez seulement récupérer les données liées comme une propriété | `$order->notes`, utilisable aussi avec une relation classique, suffit |

Les **propriétés de relation dynamiques** et la fonctionnalité d'enregistrement d'une définition de relation depuis l'extérieur du modèle sont deux choses distinctes. De plus, `resolveRelationUsing()` n'est pas une Macro permettant d'ajouter n'importe quelle méthode. Concevez le callback pour qu'il retourne une instance `Relation` d'Eloquent.

## Enregistrer sur le modèle choisi par l'utilisateur

À titre d'exemple, nous ajoutons un `hasMany` nommé `acmeNotes` au modèle de commande de l'application consommatrice. On suppose une structure où l'espace de noms du package `Acme\OrderNotes\` correspond à `src/` via le PSR-4 de Composer, et où il est chargé comme un service provider classique non différé.

### Préparer la configuration et le modèle lié

La valeur initiale de `config/acme-order-notes.php` est `null`. La simple installation n'enregistre donc aucune relation sur les modèles de l'application.

```php theme={null}
<?php

return [
    'order_model' => null,
];
```

Le fichier `src/Models/Note.php` du package est le modèle lié.

```php theme={null}
<?php

namespace Acme\OrderNotes\Models;

use Illuminate\Database\Eloquent\Model;

final class Note extends Model
{
    protected $table = 'acme_order_notes';

    protected $fillable = ['body'];
}
```

Dans cet exemple, on suppose que la table des commandes possède une clé primaire entière `id`, et que la table `acme_order_notes` contient `id`, `order_id`, `body` et les timestamps. Prévoyez un index sur `order_id`. Si vous utilisez des UUID ou autres, faites aussi correspondre le type des colonnes liées.

> [!NOTE]
>
> L'enregistrement d'une relation dynamique ne crée ni table ni contrainte de clé étrangère. C'est un traitement distinct de la [publication et mise à jour des migrations de package](https://kawax.biz/fr/advanced/package-migrations.md). Le provider ci-dessous n'inclut pas non plus d'accès à la base de données ni d'exécution de migrations.

### Enregistrer la méthode de création dans boot

Dans `src/OrderNotesServiceProvider.php`, fusionnez la configuration, puis enregistrez le callback sur le modèle cible dans `boot()`.

```php theme={null}
<?php

namespace Acme\OrderNotes;

use Acme\OrderNotes\Models\Note;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;

final class OrderNotesServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/acme-order-notes.php',
            'acme-order-notes',
        );
    }

    public function boot(): void
    {
        $this->publishes([
            __DIR__.'/../config/acme-order-notes.php' => config_path('acme-order-notes.php'),
        ], 'acme-order-notes-config');

        $model = $this->app['config']->get('acme-order-notes.order_model');

        if ($model === null) {
            return;
        }

        if (! is_string($model) || ! is_subclass_of($model, Model::class)) {
            throw new InvalidArgumentException('order_model must be an Eloquent model class.');
        }

        $model::resolveRelationUsing(
            'acmeNotes',
            static fn (Model $order): HasMany => $order->hasMany(
                Note::class,
                'order_id',
                $order->getKeyName(),
            ),
        );
    }
}
```

Dans l'application consommatrice, publiez la configuration et remplacez `order_model` par le modèle concret réellement utilisé, par exemple `\App\Models\Order::class`. Le chargement du provider se configure via la [découverte automatique](https://kawax.biz/fr/advanced/package-discovery.md).

```bash theme={null}
php artisan vendor:publish --tag=acme-order-notes-config
```

La clé étrangère `order_id` et la clé côté parent sont indiquées explicitement à `hasMany()`. Ainsi, même si la classe du modèle parent s'appelle `Purchase`, Laravel ne devine pas `purchase_id`, et la relation reste cohérente avec la définition de table du package.

> [!WARNING]
>
> La configuration du modèle cible est effectuée par l'administrateur de l'application. Ne concevez pas un système qui reçoit directement un nom de classe ou de relation depuis une requête HTTP ou autre et réenregistre la relation à chaque requête. De plus, un enregistrement sur le `Model` de base d'Eloquent affecte tous ses modèles descendants.

## Utiliser comme une relation classique

Sur le modèle de commande configuré, l'appel de méthode retourne un `HasMany`, et l'accès par propriété retourne la collection des résultats récupérés.

```php theme={null}
use App\Models\Order;

$order = Order::findOrFail($id);

$relation = $order->acmeNotes();
$note = $order->acmeNotes()->create(['body' => '配送日時を確認する']);
$notes = $order->acmeNotes;

$orders = Order::with('acmeNotes')->get();
$ordersWithNotes = Order::whereHas('acmeNotes')->get();
```

Avec `create()`, `hasMany` renseigne `order_id`, il n'est donc pas nécessaire d'ajouter la clé étrangère au tableau fourni par l'utilisateur. La relation inverse n'est pas ajoutée automatiquement : définissez-la séparément si nécessaire.

### Ne pas exécuter la récupération dans le callback

Si le callback retourne le résultat de `get()` ou un modèle unique, il ne pourra pas être traité comme une relation valide, par exemple lors de l'accès par propriété. Séparez la construction de la relation de la récupération des données.

Le `Builder::getRelation()` de l'Eager loading construit la relation avec une nouvelle instance de modèle. Évitez donc les conceptions où le callback suppose « qu'un ID de commande déjà récupéré existe », ou retourne une définition de relation différente selon cet ID.

L'enregistrement dynamique ne supprime ni le problème N+1 ni les restrictions du Lazy Loading. Lorsque vous manipulez plusieurs modèles, chargez les relations comme d'habitude avec `with()` ou autre.

### Avec belongsTo, attention aussi à la déduction du nom de relation

Lorsque le nom de relation est omis, `belongsTo()` devine l'appelant via la backtrace. Pour une définition depuis une closure, au lieu de vous fier à la même déduction que pour une méthode de modèle classique, vous pouvez indiquer explicitement la clé étrangère, la clé propriétaire, ainsi que le nom de relation en quatrième argument.

Voici un exemple indépendant où la commande possède `customer_id` et le client possède `id`.

```php theme={null}
use App\Models\Customer;
use App\Models\Order;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

Order::resolveRelationUsing(
    'acmeCustomer',
    static fn (Order $order): BelongsTo => $order->belongsTo(
        Customer::class,
        'customer_id',
        'id',
        'acmeCustomer',
    ),
);
```

## Comprendre les règles de collision de noms et d'héritage

`HasRelationships` stocke la closure dans une propriété statique, avec pour clé **le nom de classe du modèle et le nom de relation**. `Model::__call()`, qui reçoit l'appel d'une méthode inexistante, recherche le resolver et l'exécute en lui passant l'instance du modèle cible.

```mermaid theme={null}
flowchart TD
    A["Appel d'une méthode de relation inexistante"] --> B["Model::__call"]
    B --> C["Recherche du resolver enregistré<br>dans la classe elle-même puis les classes parentes"]
    C --> D["Exécution de la closure<br>en passant le modèle cible"]
    D --> E["Retour d'une instance Relation"]
```

Le diagramme montre le chemin lorsqu'un resolver est trouvé. Notez qu'en cas de collision avec une méthode ou un attribut existant, le chemin est différent.

| Situation | Comportement |
| - | - |
| Réenregistrer la même classe avec le même nom de relation | La closure la plus récente écrase la précédente |
| Seul le modèle parent possède un enregistrement | Le modèle enfant peut aussi utiliser le resolver du parent |
| Le modèle enfant possède aussi un enregistrement du même nom | L'enregistrement de l'enfant est prioritaire. Celui du parent reste inchangé |
| Enregistrer le même nom sur des modèles sans lien d'héritage | Chaque classe conserve sa propre définition |
| Une méthode public du même nom existe dans le modèle ou un trait | PHP appelle la méthode existante. L'enregistrement dynamique ne la remplace pas |
| Un attribut déjà récupéré, un cast ou un accesseur du même nom existe | Lors de l'accès par propriété, l'attribut est évalué en premier |

Utilisez un nom de relation propre comme `acmeNotes`, et vérifiez l'absence de collision avec les méthodes et attributs de l'utilisateur ainsi qu'avec d'autres packages. `method_exists()` seul ne permet pas de vérifier l'existence d'un enregistrement dynamique. Le `isRelation()` d'Eloquent vérifie aussi les resolvers, mais ne garantit pas l'absence de collision avec les attributs.

## Distinguer la durée de vie de l'enregistrement et celle des résultats

L'enregistrement du resolver et les données de relation chargées conservées par chaque modèle sont deux choses distinctes.

| Opération | Cible |
| - | - |
| `resolveRelationUsing()` | Enregistre ou écrase la méthode de création au niveau de la classe |
| `$order->unsetRelation('acmeNotes')` | Retire les résultats chargés de ce modèle. Le resolver reste |
| `Model::clearBootedModels()` | Efface notamment l'état lié au boot des modèles. N'efface pas l'enregistrement des resolvers |

Si vous écrasez le resolver après avoir déjà chargé `$order->acmeNotes`, les résultats conservés par ce modèle ne sont pas mis à jour automatiquement. Si une nouvelle récupération est nécessaire, retirez les résultats de la relation puis utilisez `load('acmeNotes')` ou autre.

> [!WARNING]
>
> L'enregistrement est stocké non pas dans le conteneur de services, mais dans une propriété statique. Si la closure capture la requête, l'utilisateur authentifié, le tenant, etc., ceux-ci risquent d'être conservés dans les processus de longue durée. Limitez l'enregistrement à des définitions stables au démarrage, et lors de modifications de configuration ou de code, redémarrez aussi les processus tels que les workers de file d'attente ou Octane. Consultez aussi [Conception de la durée de vie des services](https://kawax.biz/fr/advanced/package-service-lifetimes.md).

De même, ne supposez pas que le simple fait de recréer l'application avec Testbench efface les resolvers. Pour les tests qui comparent l'état activé et désactivé, utilisez une fixture de modèle distincte ou l'isolation de processus de PHPUnit, afin d'éviter toute dépendance à un enregistrement précédent.

## Tester à la fois l'enregistrement et le SQL

Avec [Orchestra Testbench](https://kawax.biz/fr/advanced/package-testing.md), vérifiez non seulement l'enregistrement du provider, mais aussi la clé étrangère et la récupération réelle de la relation. Voici un exemple de `tests/DynamicRelationsTest.php`. Rendez les classes du package et l'espace de noms des tests chargeables via Composer, et préparez les drivers PDO et SQLite.

```php theme={null}
<?php

namespace Acme\OrderNotes\Tests;

use Acme\OrderNotes\Models\Note;
use Acme\OrderNotes\OrderNotesServiceProvider;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Orchestra\Testbench\TestCase;

final class DynamicRelationsTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [OrderNotesServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('acme-order-notes.order_model', OrderFixture::class);
        $app['config']->set('database.default', 'testing');
        $app['config']->set('database.connections.testing', [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ]);
    }

    protected function setUp(): void
    {
        parent::setUp();

        Schema::create('orders', function (Blueprint $table): void {
            $table->id();
        });

        Schema::create('acme_order_notes', function (Blueprint $table): void {
            $table->id();
            $table->unsignedBigInteger('order_id')->index();
            $table->text('body');
            $table->timestamps();
        });
    }

    public function test_provider_registers_relation_with_explicit_keys(): void
    {
        $order = new OrderFixture;
        $relation = $order->acmeNotes();

        $this->assertInstanceOf(HasMany::class, $relation);
        $this->assertInstanceOf(Note::class, $relation->getRelated());
        $this->assertSame('acme_order_notes.order_id', $relation->getQualifiedForeignKeyName());
        $this->assertSame('id', $relation->getLocalKeyName());
        $this->assertFalse($order->relationLoaded('acmeNotes'));
    }

    public function test_relation_supports_creation_eager_loading_and_existence_queries(): void
    {
        $order = OrderFixture::query()->create();
        OrderFixture::query()->create();

        $note = $order->acmeNotes()->create(['body' => '配送日時を確認する']);

        $this->assertSame($order->getKey(), $note->order_id);

        $orders = OrderFixture::with('acmeNotes')->orderBy('id')->get();

        $this->assertTrue($orders[0]->relationLoaded('acmeNotes'));
        $this->assertTrue($note->is($orders[0]->acmeNotes->sole()));
        $this->assertCount(0, $orders[1]->acmeNotes);
        $this->assertSame(
            [$order->getKey()],
            OrderFixture::whereHas('acmeNotes')->pluck('id')->all(),
        );
    }
}

final class OrderFixture extends Model
{
    protected $table = 'orders';

    public $timestamps = false;
}
```

Le modèle de test s'appelle `OrderFixture`, mais la clé étrangère reste `order_id` et non `order_fixture_id`. Cela permet de confirmer que la relation ne dépend pas d'une déduction à partir du nom de classe.

Dans la CI du package, vérifiez également les cas limites suivants.

* Avec une autre fixture non enregistrée, aucune relation n'est ajoutée lorsque `order_model` vaut `null`.
* L'héritage vers les modèles enfants, et l'ordre de priorité lorsqu'un resolver du même nom est enregistré uniquement sur l'enfant.
* L'API prévue reste utilisable en cas de collision avec une méthode ou un attribut existant.
* La différence, après réenregistrement du resolver, entre un modèle ayant déjà chargé la relation et un nouveau modèle.
* Les types de colonnes, index et contraintes de clé étrangère sur les bases de données prises en charge, ainsi qu'une clé primaire modifiée par l'utilisateur.

## Sources primaires consultées

* [Documentation officielle de Laravel : Dynamic Relationships](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/eloquent-relationships.md#dynamic-relationships)
* [Documentation officielle de Laravel : Package Development](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [HasRelationships : enregistrement, recherche par héritage et gestion des clés](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [Model : appels dynamiques et effacement de l'état de boot](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Model.php)
* [HasAttributes : priorité des attributs et conservation des résultats de relation](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php)
* [Builder : construction des relations lors de l'Eager loading](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Builder.php)
* [QueriesRelationships : conditions d'existence des relations](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php)
* [HasOneOrMany : définition de la clé étrangère lors de create](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php)
* [Tests officiels de Laravel : DatabaseEloquentDynamicRelationsTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Database/DatabaseEloquentDynamicRelationsTest.php)


## Related topics

- [Introduction aux relations Eloquent](https://kawax.biz/fr/eloquent-relationships.md)
- [Sujets avancés](https://kawax.biz/fr/advanced/index.md)
- [Drivers de session personnalisés pour les packages](https://kawax.biz/fr/advanced/package-session-drivers.md)
- [Extension des connexions de base de données pour les packages](https://kawax.biz/fr/advanced/package-database-connections.md)
- [Stores de cache personnalisés et contrats pour les packages](https://kawax.biz/fr/advanced/package-cache-drivers.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.