Saltar al contenido principal
Para añadir relaciones a los modelos de la aplicación consumidora desde un paquete, puedes registrar la forma de crear la relación con resolveRelationUsing(). Sin modificar directamente el archivo del modelo, puedes obtener los datos, usar la carga Eager y buscar por condiciones de existencia igual que con una relación normal. Esta página se basa en la revisión del código fuente oficial de Laravel Framework v13.40.0. Para lo básico de las relaciones, consulta Introducción a las relaciones de Eloquent, y para la carga de providers, Detección automática de paquetes.

Distinguirlo de la definición normal

La documentación oficial recomienda los métodos de relación normales para el desarrollo habitual de aplicaciones e indica que las definiciones dinámicas pueden ser útiles en casos como el desarrollo de paquetes. Las propiedades de relación dinámicas y la función para registrar definiciones de relaciones desde fuera del modelo son cosas distintas. Además, resolveRelationUsing() no es una Macro que añada métodos arbitrarios. Diseña el callback para que devuelva una instancia de Relation de Eloquent.

Registrar en el modelo elegido por el usuario

Como ejemplo, añadiremos un hasMany llamado acmeNotes al modelo de pedidos de la aplicación consumidora. Se asume una estructura en la que el espacio de nombres del paquete Acme\OrderNotes\ corresponde a src/ mediante PSR-4 de Composer y se carga como un service provider normal no diferido.

Preparar la configuración y el modelo relacionado

El valor inicial de config/acme-order-notes.php es null. La simple instalación no registra ninguna relación en los modelos de la aplicación.
El archivo src/Models/Note.php del paquete es el modelo relacionado.
En el ejemplo, se asume que la tabla de pedidos tiene una clave primaria entera id y que la tabla acme_order_notes tiene id, order_id, body y timestamps. Prepara un índice para order_id. Si usas UUID u otros tipos, haz que también coincidan los tipos de las columnas relacionadas.
El registro de relaciones dinámicas no crea tablas ni restricciones de clave foránea. Es un proceso distinto de la Publicación y actualización de migraciones de paquetes. El provider que se muestra a continuación tampoco incluye accesos a la BD ni la ejecución de migraciones.

Registrar la lógica de creación en boot

En src/OrderNotesServiceProvider.php, combina la configuración y registra el callback en el modelo objetivo dentro de boot().
En la aplicación consumidora, publica la configuración y cambia order_model al modelo concreto que realmente uses, como \App\Models\Order::class. La carga del provider se configura con la detección automática.
En hasMany() se indican explícitamente la clave foránea order_id y la clave del lado padre. Así, aunque la clase del modelo padre se llame Purchase, no se deduce purchase_id y se mantiene la coincidencia con la definición de tablas del paquete.
La configuración del modelo objetivo la realiza el administrador de la aplicación. No diseñes el sistema para recibir directamente nombres de clase o de relación desde peticiones HTTP u otras fuentes y volver a registrarlos en cada petición. Además, si registras la relación en el Model base de Eloquent, afectará a todos sus modelos descendientes.

Usarla igual que una relación normal

En el modelo de pedidos configurado, la llamada al método devuelve un HasMany y el acceso como propiedad devuelve la colección con los resultados obtenidos.
En create(), hasMany asigna order_id, por lo que no es necesario mezclar la clave foránea en el array recibido del usuario. La relación inversa no se añade automáticamente; si la necesitas, defínela por separado.

No ejecutar la obtención de datos en el callback

Si el valor devuelto por el callback es el resultado de get() o un único modelo, no podrá tratarse como una relación válida, por ejemplo al acceder como propiedad. Separa la construcción de la relación de la obtención de datos. Builder::getRelation() de la carga Eager construye la relación con una nueva instancia del modelo. Por tanto, evita diseños en los que el callback dé por supuesto que “ya existe el ID del pedido obtenido” o devuelva una definición de relación distinta según ese ID. El registro dinámico no elimina el problema N+1 ni las restricciones de Lazy Loading. Cuando trabajes con varios modelos, carga las relaciones con with() u otros métodos, como de costumbre.

En belongsTo, presta atención también a la deducción del nombre de la relación

Si omites el nombre de la relación, belongsTo() deduce el llamador mediante un backtrace. En una definición desde un closure, en lugar de depender de la misma deducción que en un método normal del modelo, puedes indicar explícitamente, además de la clave foránea y la clave del propietario, el nombre de la relación como cuarto argumento. El siguiente es un ejemplo independiente para el caso en que el pedido tiene customer_id y el cliente tiene id.

Comprender las reglas de conflictos de nombres y herencia

HasRelationships guarda el closure en una propiedad static usando como clave el nombre de clase del modelo y el nombre de la relación. Model::__call(), que recibe la llamada a un método inexistente, busca el resolver y lo ejecuta pasándole la instancia del modelo objetivo. El diagrama muestra el recorrido cuando se encuentra un resolver. Ten en cuenta que, en caso de conflicto con métodos o atributos existentes, el recorrido es distinto. Usa un nombre de relación propio como acmeNotes y comprueba que no haya conflictos con los métodos y atributos del usuario ni con otros paquetes. Con method_exists() por sí solo no se puede comprobar la existencia de un registro dinámico. isRelation() de Eloquent también comprueba los resolvers, pero no garantiza que no haya conflictos con atributos.

Separar la vida útil del estado registrado y de los resultados obtenidos

El registro del resolver y los datos de relación ya cargados que conserva cada modelo son cosas distintas. Aunque sobrescribas el resolver después de haber cargado $order->acmeNotes, los resultados que conserva ese modelo no se actualizan automáticamente. Si necesitas volver a obtenerlos, elimina los resultados de la relación y después usa load('acmeNotes') u otro método similar.
El registro no se guarda en el contenedor de servicios, sino en una propiedad static. Si el closure captura la petición, el usuario autenticado, el tenant u otros datos, pueden quedar retenidos en procesos de larga duración. Limita el registro a definiciones estables en el arranque y, cuando cambies la configuración o el código, actualiza también procesos como los workers de colas u Octane. Consulta también Diseño del ciclo de vida de los servicios de paquetes.
Del mismo modo, no des por hecho que los resolvers desaparecen solo por volver a crear la aplicación en Testbench. En las pruebas que comparan el estado activado y desactivado, usa otro fixture de modelo o el aislamiento de procesos de PHPUnit para evitar depender de registros anteriores.

Probar tanto el registro como el SQL

Con Orchestra Testbench, verifica no solo el registro del provider, sino también la clave foránea y la obtención real de la relación. El siguiente es un ejemplo de tests/DynamicRelationsTest.php. Haz que Composer pueda cargar las clases del paquete y el espacio de nombres de las pruebas, y prepara los drivers de PDO y SQLite.
El modelo de prueba se llama OrderFixture, pero la clave foránea sigue siendo order_id y no order_fixture_id. Así puedes confirmar que no se depende de la deducción a partir del nombre de clase. En la CI del paquete, verifica además los siguientes límites.
  • Que, con otro fixture no registrado, no se añada la relación si order_model es null.
  • La herencia hacia los modelos hijos y el orden de prioridad cuando se registra un resolver con el mismo nombre solo en el hijo.
  • Que la API prevista esté disponible cuando haya conflictos con métodos o atributos existentes.
  • La diferencia, tras volver a registrar el resolver, entre un modelo que ya cargó la relación y un modelo nuevo.
  • Los tipos de columna, índices y restricciones de clave foránea en las BD compatibles, así como las claves primarias modificadas por el usuario.

Fuentes primarias consultadas

Última modificación el 4 de noviembre de 2026