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, das Laden von Providern unter Paket-Discovery.
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.
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 einehasMany-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 inconfig/acme-order-notes.php auf null. Die bloße Installation registriert dann keine Relation an den Modellen der Anwendung.
src/Models/Note.php auf Paketseite ist das verknüpfte Modell.
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.
Die Registrierung einer dynamischen Relation erstellt weder Tabellen noch Fremdschlüssel-Constraints. Sie ist ein von Paket-Migrationen veröffentlichen und aktualisieren 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 insrc/OrderNotesServiceProvider.php die Konfiguration zusammen und registrieren Sie in boot() den Callback am Zielmodell.
order_model auf das tatsächlich verwendete konkrete Modell gesetzt, etwa \App\Models\Order::class. Das Laden des Providers wird über Discovery konfiguriert.
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.
Wie eine gewöhnliche Relation verwenden
Beim konfigurierten Bestellmodell liefert der Methodenaufruf eineHasMany, der Property-Zugriff die Collection der abgerufenen Ergebnisse.
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 vonget() 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 beibelongsTo() 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.
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.
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.
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.
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').
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 nicht nur die Registrierung durch den Provider, sondern auch die Fremdschlüssel und das tatsächliche Abrufen der Relation. Das folgende Beispiel isttests/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.
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_modelnullist. - 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
- Offizielle Laravel-Dokumentation: Package Development
- HasRelationships: Registrierung, Suche über die Vererbung und Behandlung von Schlüsseln
- Model: Dynamische Aufrufe und Löschen des boot-Zustands
- HasAttributes: Priorität von Attributen und Halten von Relationsergebnissen
- Builder: Aufbau von Relationen beim Eager Loading
- QueriesRelationships: Existenzbedingungen für Relationen
- HasOneOrMany: Setzen des Fremdschlüssels bei create
- Offizielle Laravel-Tests: DatabaseEloquentDynamicRelationsTest