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

# Registro dinámico de relaciones en paquetes

> Analiza resolveRelationUsing de Laravel 13 y explica cómo añadir relaciones sin modificar los modelos, cómo indicar las claves, los conflictos de nombres y la herencia, la vida útil del estado registrado y las pruebas.

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](https://kawax.biz/es/eloquent-relationships.md), y para la carga de providers, [Detección automática de paquetes](https://kawax.biz/es/advanced/package-discovery.md).

## 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.

| Situación | Criterio de elección |
| - | - |
| Añadir una relación a un modelo que gestionas tú | Escribe un método de relación normal en el modelo |
| Un paquete distribuido ofrece una relación en los modelos del usuario | Recibe el modelo objetivo mediante configuración y considera `resolveRelationUsing()` |
| Solo quieres obtener los datos relacionados como propiedad | Basta con `$order->notes`, que también funciona con las relaciones normales |

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.

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

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

El archivo `src/Models/Note.php` del paquete es el modelo relacionado.

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

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.

> [!NOTE]
>
> 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](https://kawax.biz/es/advanced/package-migrations.md). 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()`.

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

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](https://kawax.biz/es/advanced/package-discovery.md).

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

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.

> [!WARNING]
>
> 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.

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

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`.

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

## 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.

```mermaid theme={null}
flowchart TD
    A["Llamada a un método de relación inexistente"] --> B["Model::__call"]
    B --> C["Buscar el resolver registrado<br>en la propia clase y luego en la clase padre"]
    C --> D["Ejecutar el closure<br>pasándole el modelo objetivo"]
    D --> E["Devolver una instancia de Relation"]
```

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.

| Situación | Comportamiento |
| - | - |
| Volver a registrar la misma clase con el mismo nombre de relación | El closure posterior sobrescribe al anterior |
| Solo el modelo padre tiene un registro | El modelo hijo también puede usar el resolver del padre |
| El modelo hijo también tiene un registro con el mismo nombre | Tiene prioridad el registro del hijo. El registro del padre no cambia |
| Registrar el mismo nombre en modelos sin relación de herencia | Se conservan como definiciones independientes por clase |
| Existe un método public con el mismo nombre en el modelo o en un trait | PHP llama al método existente. El registro dinámico no lo reemplaza |
| Existe un atributo obtenido, cast o accessor con el mismo nombre | En el acceso como propiedad, se evalúa primero el atributo |

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.

| Operación | Objetivo |
| - | - |
| `resolveRelationUsing()` | Registra o sobrescribe la lógica de creación por clase |
| `$order->unsetRelation('acmeNotes')` | Elimina los resultados cargados de ese modelo. El resolver se mantiene |
| `Model::clearBootedModels()` | Borra el estado relacionado con el boot de los modelos, entre otros. No borra el registro de resolvers |

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.

> [!WARNING]
>
> 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](https://kawax.biz/es/advanced/package-service-lifetimes.md).

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](https://kawax.biz/es/advanced/package-testing.md), 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.

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

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

* [Documentación oficial de Laravel: Dynamic Relationships](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/eloquent-relationships.md#dynamic-relationships)
* [Documentación oficial de Laravel: Package Development](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [HasRelationships: registro, búsqueda por herencia y tratamiento de claves](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [Model: llamadas dinámicas y borrado del estado de boot](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Model.php)
* [HasAttributes: prioridad de los atributos y conservación de los resultados de relaciones](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php)
* [Builder: construcción de relaciones en la carga Eager](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Builder.php)
* [QueriesRelationships: condiciones de existencia de relaciones](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php)
* [HasOneOrMany: asignación de la clave foránea en create](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php)
* [Pruebas oficiales de Laravel: DatabaseEloquentDynamicRelationsTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Database/DatabaseEloquentDynamicRelationsTest.php)


## Related topics

- [Introducción a las relaciones de Eloquent](https://kawax.biz/es/eloquent-relationships.md)
- [Temas avanzados](https://kawax.biz/es/advanced/index.md)
- [Registro de rutas y caché en paquetes](https://kawax.biz/es/advanced/package-routes.md)
- [Registro y caché de listeners de eventos en paquetes](https://kawax.biz/es/advanced/package-events.md)
- [Registro y mantenimiento de tareas programadas en paquetes](https://kawax.biz/es/advanced/package-scheduling.md)


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