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

# Dynamische relaties registreren vanuit packages

> Ontleed resolveRelationUsing van Laravel 13 en leer relaties toevoegen zonder het model te wijzigen, sleutels opgeven, naamconflicten en overerving, de levensduur van de registratie en testen.

Om vanuit een package relaties toe te voegen aan de modellen van de gebruikende applicatie, kun je met `resolveRelationUsing()` registreren hoe de relatie wordt aangemaakt. Zonder het modelbestand rechtstreeks te wijzigen, kun je net als bij gewone relaties ophalen, eager loaden en zoeken op het bestaan van relaties.

Deze pagina is gecontroleerd aan de hand van de officiële broncode van Laravel Framework `v13.40.0`. Zie [Inleiding tot Eloquent-relaties](https://kawax.biz/nl/eloquent-relationships.md) voor de basis van relaties en [Package discovery](https://kawax.biz/nl/advanced/package-discovery.md) voor het laden van providers.

## Onderscheid met gewone definities

De officiële documentatie raadt voor gewone applicatieontwikkeling normale relatiemethoden aan en geeft aan dat dynamische definities nuttig kunnen zijn bij bijvoorbeeld packageontwikkeling.

| Situatie | Richtlijn |
| - | - |
| Een relatie toevoegen aan een model dat je zelf beheert | Schrijf een gewone relatiemethode in het model |
| Een gedistribueerde package biedt een relatie aan op het model van de gebruiker | Ontvang het doelmodel via configuratie en overweeg `resolveRelationUsing()` |
| Je wilt gerelateerde data alleen als property ophalen | `$order->notes`, dat ook bij gewone relaties werkt, volstaat |

**Dynamische relatieproperties** zijn iets anders dan de functie om relatiedefinities van buiten het model te registreren. Bovendien is `resolveRelationUsing()` geen macro die willekeurige methoden toevoegt. Ontwerp de callback zo dat hij een Eloquent `Relation`-instantie retourneert.

## Registreren op het model dat de gebruiker kiest

Als voorbeeld voegen we een `hasMany` met de naam `acmeNotes` toe aan het ordermodel van de gebruikende applicatie. We gaan uit van een opzet waarin de namespace `Acme\OrderNotes\` van de package via Composer-PSR-4 aan `src/` is gekoppeld en als gewone, niet-uitgestelde service provider wordt geladen.

### Configuratie en gerelateerd model voorbereiden

Zet de standaardwaarde in `config/acme-order-notes.php` op `null`. Alleen het installeren registreert dan nog geen relatie op de modellen van de applicatie.

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

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

`src/Models/Note.php` aan de kant van de package is het gerelateerde model.

```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'];
}
```

In het voorbeeld gaan we ervan uit dat de ordertabel een integer primaire sleutel `id` heeft en dat de tabel `acme_order_notes` de kolommen `id`, `order_id`, `body` en timestamps heeft. Voorzie `order_id` van een index. Gebruik je bijvoorbeeld UUID's, zorg dan dat ook de types van de gerelateerde kolommen overeenkomen.

> [!NOTE]
>
> Het registreren van een dynamische relatie maakt geen tabellen of foreign key-constraints aan. Dat is een ander proces dan [Migraties van packages publiceren en bijwerken](https://kawax.biz/nl/advanced/package-migrations.md). Ook de provider hieronder bevat geen databasetoegang of het uitvoeren van migraties.

### De aanmaakmethode registreren in boot

Merge in `src/OrderNotesServiceProvider.php` de configuratie en registreer in `boot()` de callback op het doelmodel.

```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(),
            ),
        );
    }
}
```

Publiceer in de gebruikende applicatie de configuratie en wijzig `order_model` naar het concrete model dat je daadwerkelijk gebruikt, zoals `\App\Models\Order::class`. Het laden van de provider stel je in via [package discovery](https://kawax.biz/nl/advanced/package-discovery.md).

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

Bij `hasMany()` worden de foreign key `order_id` en de sleutel aan de ouderkant expliciet opgegeven. Zo wordt, ook als de klassenaam van het oudermodel `Purchase` is, niet `purchase_id` afgeleid en komt het overeen met de tabeldefinitie van de package.

> [!WARNING]
>
> Het doelmodel wordt ingesteld door de beheerder van de applicatie. Ontwerp het niet zo dat klassenamen of relatienamen rechtstreeks uit bijvoorbeeld HTTP-requests worden overgenomen en per request opnieuw worden geregistreerd. Bovendien heeft registreren op de Eloquent-basisklasse `Model` invloed op alle afgeleide modellen.

## Gebruiken zoals een gewone relatie

Op het geconfigureerde ordermodel retourneert een methodeaanroep een `HasMany` en een property-toegang de collectie met opgehaalde resultaten.

```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();
```

Bij `create()` stelt `hasMany` `order_id` in, dus je hoeft de foreign key niet toe te voegen aan de array die de gebruiker doorgeeft. De omgekeerde relatie wordt niet automatisch toegevoegd; definieer die indien nodig apart.

### Voer in de callback geen ophaalactie uit

Als de retourwaarde van de callback het resultaat van `get()` of een enkel model is, kan deze bijvoorbeeld bij property-toegang niet als geldige relatie worden behandeld. Scheid het opbouwen van de relatie van het ophalen van data.

`Builder::getRelation()` bij eager loading bouwt de relatie op met een nieuwe modelinstantie. Vermijd daarom ontwerpen waarin de callback ervan uitgaat dat er al een opgehaald order-ID is, of afhankelijk van dat ID een andere relatiedefinitie retourneert.

Ook met dynamische registratie verdwijnen het N+1-probleem en de beperkingen van lazy loading niet. Laad relaties bij het werken met meerdere modellen zoals gebruikelijk met bijvoorbeeld `with()`.

### Let bij belongsTo ook op het afleiden van de relatienaam

Als je de relatienaam weglaat, leidt `belongsTo()` de aanroeper af via een backtrace. Bij definities vanuit een closure kun je, in plaats van te vertrouwen op dezelfde afleiding als bij een gewone modelmethode, naast de foreign key en de owner key ook de relatienaam als vierde argument expliciet opgeven.

Hieronder een losstaand voorbeeld waarbij de orderkant `customer_id` heeft en de klantkant `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',
    ),
);
```

## De regels voor naamconflicten en overerving begrijpen

`HasRelationships` slaat de closure op in een static property met **de klassenaam van het model en de relatienaam** als sleutel. `Model::__call()`, dat de aanroep van een niet-bestaande methode ontvangt, zoekt de resolver op en voert die uit met de betreffende modelinstantie.

```mermaid theme={null}
flowchart TD
    A["Aanroep van een niet-bestaande relatiemethode"] --> B["Model::__call"]
    B --> C["Geregistreerde resolver zoeken<br>eerst in eigen klasse, dan in ouderklassen"]
    C --> D["Closure uitvoeren<br>met het betreffende model"]
    D --> E["Relation-instantie retourneren"]
```

Het diagram toont het pad wanneer een resolver wordt gevonden. Let op dat conflicten met bestaande methoden of attributen een ander pad volgen.

| Situatie | Gedrag |
| - | - |
| Dezelfde klasse en dezelfde relatienaam opnieuw registreren | De latere closure overschrijft de eerdere |
| Alleen het oudermodel heeft een registratie | Het kindmodel kan ook de resolver van de ouder gebruiken |
| Het kindmodel heeft ook een registratie met dezelfde naam | De registratie van het kind heeft voorrang. De registratie van de ouder verandert niet |
| Registreren met dezelfde naam op modellen zonder overervingsrelatie | Wordt per klasse als aparte definitie bewaard |
| Er bestaat een public methode met dezelfde naam in het model of een trait | PHP roept de bestaande methode aan. Dynamische registratie kan deze niet vervangen |
| Er bestaat een opgehaald attribuut, cast of accessor met dezelfde naam | Bij property-toegang wordt eerst het attribuut geëvalueerd |

Gebruik een unieke relatienaam zoals `acmeNotes` en controleer op conflicten met methoden en attributen van de gebruiker en met andere packages. Met alleen `method_exists()` kun je het bestaan van een dynamische registratie niet controleren. Eloquents `isRelation()` controleert ook resolvers, maar garandeert niet dat er geen conflict met attributen is.

## De levensduur van registratie en opgehaalde resultaten scheiden

De registratie van resolvers en de geladen relatiedata die elk model vasthoudt, zijn twee verschillende dingen.

| Bewerking | Doel |
| - | - |
| `resolveRelationUsing()` | Registreert of overschrijft de aanmaakmethode per klasse |
| `$order->unsetRelation('acmeNotes')` | Verwijdert de geladen resultaten van dat model. De resolver blijft bestaan |
| `Model::clearBootedModels()` | Wist onder meer de boot-gerelateerde status van modellen. De registratie van resolvers wordt niet gewist |

Als je een resolver overschrijft nadat `$order->acmeNotes` al is geladen, worden de resultaten die dat model vasthoudt niet automatisch bijgewerkt. Moet je opnieuw ophalen, verwijder dan eerst de relatieresultaten en gebruik daarna bijvoorbeeld `load('acmeNotes')`.

> [!WARNING]
>
> De registratie wordt niet in de service container opgeslagen, maar in een static property. Als je requests, de geauthenticeerde gebruiker, tenants en dergelijke in de closure opneemt, kunnen deze in langlopende processen worden vastgehouden. Beperk registraties tot stabiele definities bij het opstarten en werk bij wijzigingen in configuratie of code ook processen zoals queue workers en Octane bij. Zie ook [De levensduur van services ontwerpen](https://kawax.biz/nl/advanced/package-service-lifetimes.md).

Ga er evenmin van uit dat resolvers verdwijnen door de applicatie in Testbench opnieuw op te bouwen. Gebruik voor tests die de ingeschakelde en uitgeschakelde situatie vergelijken een aparte model-fixture of de procesisolatie van PHPUnit, zodat je niet afhankelijk bent van eerdere registraties.

## Zowel de registratie als de SQL testen

Controleer met [Orchestra Testbench](https://kawax.biz/nl/advanced/package-testing.md) niet alleen de registratie door de provider, maar ook de foreign key en het daadwerkelijk ophalen van de relatie. Hieronder een voorbeeld van `tests/DynamicRelationsTest.php`. Zorg dat de klassen van de package en de testnamespace via Composer geladen kunnen worden, en voorzie in PDO en de SQLite-driver.

```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;
}
```

De naam van het testmodel is `OrderFixture`, maar de foreign key blijft `order_id` in plaats van `order_fixture_id`. Zo kun je controleren dat er niet wordt vertrouwd op afleiding uit de klassenaam.

Controleer in de CI van de package daarnaast de volgende grensgevallen.

* Bij een aparte, niet-geregistreerde fixture wordt geen relatie toegevoegd als `order_model` `null` is.
* Overerving naar kindmodellen, en de prioriteit wanneer alleen op het kind een resolver met dezelfde naam is geregistreerd.
* Bij een conflict met bestaande methoden of attributen is de beoogde API bruikbaar.
* Het verschil na het opnieuw registreren van de resolver tussen een model waarop de relatie al eens is geladen en een nieuw model.
* Kolomtypes, indexen en foreign key-constraints op de ondersteunde databases, en primaire sleutels die door de gebruiker zijn gewijzigd.

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: Dynamic Relationships](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/eloquent-relationships.md#dynamic-relationships)
* [Officiële Laravel-documentatie: Package Development](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [HasRelationships: registratie, zoeken via overerving en sleutelafhandeling](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [Model: dynamische aanroepen en het wissen van de boot-status](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Model.php)
* [HasAttributes: prioriteit van attributen en het vasthouden van relatieresultaten](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php)
* [Builder: relaties opbouwen bij eager loading](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Builder.php)
* [QueriesRelationships: voorwaarden op het bestaan van relaties](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php)
* [HasOneOrMany: foreign key instellen bij create](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php)
* [Officiële Laravel-tests: DatabaseEloquentDynamicRelationsTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Database/DatabaseEloquentDynamicRelationsTest.php)


## Related topics

- [Introductie tot Eloquent-relaties](https://kawax.biz/nl/eloquent-relationships.md)
- [Geavanceerde onderwerpen](https://kawax.biz/nl/advanced/index.md)
- [Databaseverbindingen uitbreiden vanuit packages](https://kawax.biz/nl/advanced/package-database-connections.md)
- [Statische analyse van packages (PHPStan / Larastan)](https://kawax.biz/nl/advanced/package-static-analysis.md)
- [Artisan-commands van een package registreren en afhankelijkheden resolven](https://kawax.biz/nl/advanced/package-commands.md)


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