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, et pour le chargement des providers, consultez Découverte automatique des packages.
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.
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 unhasMany 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 deconfig/acme-order-notes.php est null. La simple installation n’enregistre donc aucune relation sur les modèles de l’application.
src/Models/Note.php du package est le modèle lié.
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.
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. 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
Danssrc/OrderNotesServiceProvider.php, fusionnez la configuration, puis enregistrez le callback sur le modèle cible dans boot().
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.
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.
Utiliser comme une relation classique
Sur le modèle de commande configuré, l’appel de méthode retourne unHasMany, et l’accès par propriété retourne la collection des résultats récupérés.
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 deget() 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.
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.
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.
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.
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.
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, 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 detests/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.
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_modelvautnull. - 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
- Documentation officielle de Laravel : Package Development
- HasRelationships : enregistrement, recherche par héritage et gestion des clés
- Model : appels dynamiques et effacement de l’état de boot
- HasAttributes : priorité des attributs et conservation des résultats de relation
- Builder : construction des relations lors de l’Eager loading
- QueriesRelationships : conditions d’existence des relations
- HasOneOrMany : définition de la clé étrangère lors de create
- Tests officiels de Laravel : DatabaseEloquentDynamicRelationsTest