> ## 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 Relationen in Paketen registrieren

> Analysiert resolveRelationUsing in Laravel 13 und erläutert das Hinzufügen von Relationen ohne Änderung der Modelle, die Angabe von Schlüsseln, Namenskonflikte und Vererbung, die Lebensdauer des Registrierungszustands sowie Tests.

Um den Modellen der nutzenden Anwendung aus einem Paket heraus Relationen hinzuzufügen, können Sie mit `resolveRelationUsing()` registrieren, wie die Relation erzeugt wird. Ohne die Modelldateien direkt zu ändern, lassen sich Abrufen, Eager Loading und Abfragen nach Existenzbedingungen genauso wie bei gewöhnlichen Relationen nutzen.

Diese Seite basiert auf der offiziellen Implementierung von Laravel Framework `v13.40.0`. Die Grundlagen von Relationen finden Sie unter [Einführung in Eloquent-Relationen](https://kawax.biz/de/eloquent-relationships.md), das Laden von Providern unter [Paket-Discovery](https://kawax.biz/de/advanced/package-discovery.md).

## Von gewöhnlichen Definitionen abgrenzen

Die offizielle Dokumentation empfiehlt für die übliche Anwendungsentwicklung gewöhnliche Relationsmethoden und weist darauf hin, dass dynamische Definitionen etwa bei der Paketentwicklung nützlich sein können.

| Situation | Richtschnur für die Wahl |
| - | - |
| Einem selbst verwalteten Modell eine Relation hinzufügen | Eine gewöhnliche Relationsmethode im Modell schreiben |
| Ein verteiltes Paket stellt eine Relation für Modelle der Nutzer bereit | Das Zielmodell per Konfiguration entgegennehmen und `resolveRelationUsing()` in Betracht ziehen |
| Verwandte Daten nur als Property abrufen | `$order->notes`, das auch mit gewöhnlichen Relationen funktioniert, genügt |

**Dynamische Relations-Properties** sind etwas anderes als die Funktion, Relationsdefinitionen von außerhalb des Modells zu registrieren. Außerdem ist `resolveRelationUsing()` kein Macro, mit dem sich beliebige Methoden hinzufügen lassen. Gestalten Sie den Callback so, dass er eine Eloquent-`Relation`-Instanz zurückgibt.

## Für das vom Nutzer gewählte Modell registrieren

Als Beispiel fügen wir dem Bestellmodell der nutzenden Anwendung eine `hasMany`-Relation namens `acmeNotes` hinzu. Angenommen wird, dass der Paket-Namespace `Acme\OrderNotes\` per Composer-PSR-4 auf `src/` abgebildet ist und das Paket als gewöhnlicher, nicht verzögerter Service-Provider geladen wird.

### Konfiguration und verknüpftes Modell vorbereiten

Setzen Sie den Standardwert in `config/acme-order-notes.php` auf `null`. Die bloße Installation registriert dann keine Relation an den Modellen der Anwendung.

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

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

`src/Models/Note.php` auf Paketseite ist das verknüpfte Modell.

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

Im Beispiel wird angenommen, dass die Bestelltabelle einen ganzzahligen Primärschlüssel `id` hat und die Tabelle `acme_order_notes` die Spalten `id`, `order_id`, `body` sowie Zeitstempel besitzt. Legen Sie einen Index auf `order_id` an. Wenn Sie etwa UUIDs verwenden, müssen auch die Typen der zugehörigen Spalten übereinstimmen.

> [!NOTE]
>
> Die Registrierung einer dynamischen Relation erstellt weder Tabellen noch Fremdschlüssel-Constraints. Sie ist ein von [Paket-Migrationen veröffentlichen und aktualisieren](https://kawax.biz/de/advanced/package-migrations.md) getrennter Vorgang. Auch der folgende Provider enthält keine DB-Zugriffe und führt keine Migrationen aus.

### Die Erzeugungslogik in boot registrieren

Führen Sie in `src/OrderNotesServiceProvider.php` die Konfiguration zusammen und registrieren Sie in `boot()` den Callback am Zielmodell.

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

In der nutzenden Anwendung wird die Konfiguration veröffentlicht und `order_model` auf das tatsächlich verwendete konkrete Modell gesetzt, etwa `\App\Models\Order::class`. Das Laden des Providers wird über [Discovery](https://kawax.biz/de/advanced/package-discovery.md) konfiguriert.

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

Bei `hasMany()` werden der Fremdschlüssel `order_id` und der Schlüssel auf Elternseite explizit angegeben. So wird auch dann, wenn die Klasse des Elternmodells `Purchase` heißt, nicht `purchase_id` abgeleitet, und die Definition bleibt mit der Tabellendefinition des Pakets konsistent.

> [!WARNING]
>
> Das Zielmodell wird von den Administratoren der Anwendung konfiguriert. Entwerfen Sie das Paket nicht so, dass Klassen- oder Relationsnamen direkt aus HTTP-Requests o. Ä. übernommen und pro Request neu registriert werden. Eine Registrierung am Eloquent-Basismodell `Model` wirkt sich zudem auf alle davon abgeleiteten Modelle aus.

## Wie eine gewöhnliche Relation verwenden

Beim konfigurierten Bestellmodell liefert der Methodenaufruf eine `HasMany`, der Property-Zugriff die Collection der abgerufenen Ergebnisse.

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

Bei `create()` setzt `hasMany` die Spalte `order_id`, sodass der Fremdschlüssel nicht in das vom Nutzer übergebene Array eingefügt werden muss. Die umgekehrte Relation wird nicht automatisch hinzugefügt; definieren Sie sie bei Bedarf separat.

### Im Callback keine Daten abrufen

Gibt der Callback das Ergebnis von `get()` oder ein einzelnes Modell zurück, kann dies etwa beim Property-Zugriff nicht als gültige Relation behandelt werden. Trennen Sie den Aufbau der Relation vom Abrufen der Daten.

`Builder::getRelation()` beim Eager Loading baut die Relation mit einer neuen Modellinstanz auf. Vermeiden Sie daher Entwürfe, bei denen der Callback voraussetzt, dass bereits eine abgerufene Bestell-ID vorhanden ist, oder abhängig von dieser ID eine andere Relationsdefinition zurückgibt.

Auch eine dynamische Registrierung beseitigt weder das N+1-Problem noch die Einschränkungen für Lazy Loading. Wenn Sie mit mehreren Modellen arbeiten, laden Sie Relationen wie gewohnt mit `with()` o. Ä.

### Bei belongsTo auf die Ableitung des Relationsnamens achten

Wird bei `belongsTo()` der Relationsname weggelassen, leitet die Methode ihn per Backtrace aus dem Aufrufer ab. Verlassen Sie sich bei einer Definition aus einer Closure nicht auf dieselbe Ableitung wie bei gewöhnlichen Modellmethoden, sondern geben Sie neben Fremdschlüssel und Owner-Schlüssel auch den Relationsnamen als viertes Argument explizit an.

Das folgende eigenständige Beispiel setzt `customer_id` auf Bestellseite und `id` auf Kundenseite voraus.

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

## Die Regeln für Namenskonflikte und Vererbung verstehen

`HasRelationships` speichert die Closure in einer statischen Property, mit **Klassenname des Modells und Relationsname** als Schlüssel. `Model::__call()`, das Aufrufe nicht existierender Methoden empfängt, sucht den Resolver und führt ihn mit der Zielmodellinstanz aus.

```mermaid theme={null}
flowchart TD
    A["Aufruf einer nicht existierenden Relationsmethode"] --> B["Model::__call"]
    B --> C["Registrierten Resolver suchen:<br>eigene Klasse → Elternklassen"]
    C --> D["Closure mit dem Zielmodell<br>ausführen"]
    D --> E["Relation-Instanz zurückgeben"]
```

Das Diagramm zeigt den Pfad, wenn ein Resolver gefunden wird. Beachten Sie, dass bei Konflikten mit bestehenden Methoden oder Attributen ein anderer Pfad genommen wird.

| Situation | Verhalten |
| - | - |
| Gleiche Klasse und gleicher Relationsname werden erneut registriert | Die spätere Closure überschreibt die frühere |
| Nur das Elternmodell hat eine Registrierung | Auch Kindmodelle können den Resolver des Elternmodells nutzen |
| Auch das Kindmodell hat eine gleichnamige Registrierung | Die Registrierung des Kindes hat Vorrang. Die des Elternmodells bleibt unverändert |
| Gleichnamige Registrierung an Modellen ohne Vererbungsbeziehung | Wird pro Klasse als separate Definition gehalten |
| Eine gleichnamige public-Methode existiert im Modell oder in einem Trait | PHP ruft die bestehende Methode auf. Sie lässt sich durch dynamische Registrierung nicht ersetzen |
| Es gibt ein gleichnamiges geladenes Attribut, einen Cast oder einen Accessor | Beim Property-Zugriff wird zuerst die Attributseite ausgewertet |

Verwenden Sie eindeutige Relationsnamen wie `acmeNotes` und prüfen Sie Konflikte mit Methoden und Attributen der Nutzer sowie mit anderen Paketen. Mit `method_exists()` allein lässt sich eine dynamische Registrierung nicht erkennen. Eloquents `isRelation()` prüft zwar auch Resolver, garantiert aber nicht, dass kein Konflikt mit Attributen besteht.

## Lebensdauer von Registrierungszustand und abgerufenen Ergebnissen trennen

Die Registrierung eines Resolvers und die geladenen Relationsdaten, die jedes Modell hält, sind zwei verschiedene Dinge.

| Operation | Ziel |
| - | - |
| `resolveRelationUsing()` | Registriert oder überschreibt die Erzeugungslogik pro Klasse |
| `$order->unsetRelation('acmeNotes')` | Entfernt das geladene Ergebnis dieses Modells. Der Resolver bleibt erhalten |
| `Model::clearBootedModels()` | Löscht u. a. den boot-bezogenen Zustand von Modellen. Resolver-Registrierungen werden nicht gelöscht |

Wird der Resolver überschrieben, nachdem `$order->acmeNotes` bereits geladen wurde, aktualisiert sich das von diesem Modell gehaltene Ergebnis nicht automatisch. Wenn Sie neu abrufen müssen, entfernen Sie das Relationsergebnis und verwenden Sie dann etwa `load('acmeNotes')`.

> [!WARNING]
>
> Registriert wird nicht im Service-Container, sondern in einer statischen Property. Wenn die Closure Requests, authentifizierte Nutzer, Tenants o. Ä. einfängt, können diese in langlebigen Prozessen erhalten bleiben. Beschränken Sie Registrierungen auf stabile Definitionen beim Start und aktualisieren Sie bei Konfigurations- oder Codeänderungen auch Prozesse wie Queue-Worker oder Octane. Siehe auch [Lebenszyklus-Design für Paket-Services](https://kawax.biz/de/advanced/package-service-lifetimes.md).

Ebenso sollten Sie nicht davon ausgehen, dass Resolver verschwinden, nur weil Testbench die Anwendung neu erzeugt. Für Tests, die den aktivierten und deaktivierten Zustand vergleichen, verwenden Sie separate Modell-Fixtures oder die Prozessisolation von PHPUnit, um Abhängigkeiten von früheren Registrierungen zu vermeiden.

## Sowohl Registrierung als auch SQL testen

Prüfen Sie mit [Orchestra Testbench](https://kawax.biz/de/advanced/package-testing.md) nicht nur die Registrierung durch den Provider, sondern auch die Fremdschlüssel und das tatsächliche Abrufen der Relation. Das folgende Beispiel ist `tests/DynamicRelationsTest.php`. Sorgen Sie dafür, dass die Paketklassen und der Test-Namespace per Composer geladen werden können, und stellen Sie die PDO- und SQLite-Treiber bereit.

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

Das Testmodell heißt `OrderFixture`, der Fremdschlüssel bleibt jedoch `order_id` statt `order_fixture_id`. So lässt sich bestätigen, dass keine Abhängigkeit von der Ableitung aus dem Klassennamen besteht.

Prüfen Sie in der CI des Pakets zusätzlich folgende Grenzfälle:

* Mit einem anderen, nicht registrierten Fixture wird keine Relation hinzugefügt, wenn `order_model` `null` ist.
* Vererbung an Kindmodelle und die Priorität, wenn nur am Kind ein gleichnamiger Resolver registriert ist.
* Bei Konflikten mit bestehenden Methoden oder Attributen ist die beabsichtigte API nutzbar.
* Der Unterschied zwischen einem Modell, dessen Relation bereits einmal geladen wurde, und einem neuen Modell nach erneuter Registrierung des Resolvers.
* Spaltentypen, Indizes und Fremdschlüssel-Constraints in den unterstützten Datenbanken sowie vom Nutzer geänderte Primärschlüssel.

## Herangezogene Primärquellen

* [Offizielle Laravel-Dokumentation: Dynamic Relationships](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/eloquent-relationships.md#dynamic-relationships)
* [Offizielle Laravel-Dokumentation: Package Development](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [HasRelationships: Registrierung, Suche über die Vererbung und Behandlung von Schlüsseln](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [Model: Dynamische Aufrufe und Löschen des boot-Zustands](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Model.php)
* [HasAttributes: Priorität von Attributen und Halten von Relationsergebnissen](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php)
* [Builder: Aufbau von Relationen beim Eager Loading](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Builder.php)
* [QueriesRelationships: Existenzbedingungen für Relationen](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php)
* [HasOneOrMany: Setzen des Fremdschlüssels bei create](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php)
* [Offizielle Laravel-Tests: DatabaseEloquentDynamicRelationsTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Database/DatabaseEloquentDynamicRelationsTest.php)


## Related topics

- [Einführung in Eloquent-Relationen](https://kawax.biz/de/eloquent-relationships.md)
- [Weiterführende Themen](https://kawax.biz/de/advanced/index.md)
- [Event-Listener in Paketen registrieren und cachen](https://kawax.biz/de/advanced/package-events.md)
- [Geplante Aufgaben in Paketen registrieren und pflegen](https://kawax.biz/de/advanced/package-scheduling.md)
- [Mail-Transports in Paketen registrieren und testen](https://kawax.biz/de/advanced/package-mail-transports.md)


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