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

# Registering dynamic relationships in packages

> Examine Laravel 13's resolveRelationUsing to understand adding relationships without modifying models, specifying keys, name collisions and inheritance, the lifetime of registered state, and testing.

To add relationships to a consuming application's models from a package, you can register how the relationship is created with `resolveRelationUsing()`. Without editing the model file directly, you can retrieve, eager load, and query by relationship existence just as you would with a regular relationship.

This page is based on the official source of Laravel Framework `v13.40.0`. For relationship basics, see [Eloquent relationships](https://kawax.biz/en/eloquent-relationships.md). For provider loading, see [Package auto-discovery](https://kawax.biz/en/advanced/package-discovery.md).

## Choose between this and regular definitions

The official documentation recommends regular relationship methods for typical application development, and notes that dynamic definitions may be useful in cases such as package development.

| Situation | Guideline |
| - | - |
| Adding a relationship to a model you maintain yourself | Write a regular relationship method on the model |
| A distributed package provides a relationship on the user's model | Receive the target model through configuration and consider `resolveRelationUsing()` |
| You just want to retrieve related data as a property | `$order->notes`, which also works with regular relationships, is sufficient |

**Dynamic relationship properties** are different from the feature that registers relationship definitions from outside the model. Also, `resolveRelationUsing()` is not a macro that adds arbitrary methods. Design the callback to return an Eloquent `Relation` instance.

## Register on the model chosen by the user

As an example, we add a `hasMany` named `acmeNotes` to the consuming application's order model. This assumes the package namespace `Acme\OrderNotes\` maps to `src/` via Composer PSR-4 and is loaded as a regular non-deferred service provider.

### Prepare the configuration and the related model

Set the default value in `config/acme-order-notes.php` to `null`. Installing the package alone does not register a relationship on the application's models.

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

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

The package's `src/Models/Note.php` is the related model.

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

The example assumes the orders table has an integer primary key `id`, and the `acme_order_notes` table has `id`, `order_id`, `body`, and timestamps. Add an index on `order_id`. If you use UUIDs or similar, make the types of the related columns match as well.

> [!NOTE]
>
> Registering a dynamic relationship does not create tables or foreign key constraints. It is a separate process from [Publishing and updating package migrations](https://kawax.biz/en/advanced/package-migrations.md). The provider below also does not include database access or migration runs.

### Register the creation logic in boot

In `src/OrderNotesServiceProvider.php`, merge the configuration and register the callback on the target model in `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(),
            ),
        );
    }
}
```

In the consuming application, publish the configuration and change `order_model` to the concrete model you actually use, such as `\App\Models\Order::class`. Configure provider loading with [auto-discovery](https://kawax.biz/en/advanced/package-discovery.md).

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

The foreign key `order_id` and the parent key are passed explicitly to `hasMany()`. This prevents Eloquent from guessing `purchase_id` even if the parent model's class name is `Purchase`, and keeps it consistent with the package's table definition.

> [!WARNING]
>
> The target model is configured by the application's administrator. Do not design the package to receive class names or relationship names directly from HTTP requests or re-register them on every request. Also, registering on Eloquent's base `Model` affects all of its descendant models.

## Use it like a regular relationship

On the configured order model, calling the method returns a `HasMany`, and accessing the property returns the collection of results.

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

With `create()`, `hasMany` sets `order_id`, so you do not need to mix the foreign key into the array passed by the user. The inverse relationship is not added automatically, so define it separately if needed.

### Do not run retrieval in the callback

If the callback returns the result of `get()` or a single model, it cannot be treated as a valid relationship, for example when accessed as a property. Keep building the relationship separate from retrieving data.

`Builder::getRelation()` for eager loading builds the relationship on a new model instance. Therefore, avoid designs where the callback assumes "a retrieved order ID exists" or returns a different relationship definition depending on that ID.

Dynamic registration does not eliminate the N+1 problem or lazy loading restrictions. When working with multiple models, load relationships with `with()` and the like as usual.

### Be careful with relationship name guessing in belongsTo

When the relationship name is omitted, `belongsTo()` guesses it from the caller using a backtrace. When defining it from a closure, rather than relying on the same guessing as a regular model method, you can explicitly specify the relationship name as the fourth argument in addition to the foreign key and owner key.

The following is a standalone example where the order has `customer_id` and the customer has `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',
    ),
);
```

## Understand the rules for name collisions and inheritance

`HasRelationships` stores closures in a static property keyed by **the model's class name and the relationship name**. `Model::__call()`, which receives calls to nonexistent methods, looks up the resolver and runs it, passing the target model instance.

```mermaid theme={null}
flowchart TD
    A["Call to a nonexistent relationship method"] --> B["Model::__call"]
    B --> C["Search registered resolvers<br>in its own class, then parent classes"]
    C --> D["Run the closure<br>passing the target model"]
    D --> E["Return a Relation instance"]
```

The diagram shows the path when a resolver is found. Note that collisions with existing methods or attributes follow a different path.

| Situation | Behavior |
| - | - |
| Re-registering the same class and relationship name | The later closure overwrites it |
| Only the parent model has a registration | Child models can also use the parent's resolver |
| The child model also has a registration with the same name | The child's registration takes precedence. The parent's registration is unchanged |
| Registering the same name on models without an inheritance relationship | Kept as separate definitions per class |
| A public method with the same name exists on the model or a trait | PHP calls the existing method. Dynamic registration cannot replace it |
| A retrieved attribute, cast, or accessor with the same name exists | For property access, the attribute is evaluated first |

Use a unique name such as `acmeNotes` for the relationship and check for collisions with the user's methods, attributes, and other packages. `method_exists()` alone cannot detect dynamic registrations. Eloquent's `isRelation()` also checks resolvers, but it does not guarantee that there are no collisions with attributes.

## Separate the lifetime of registered state from retrieved results

Resolver registrations and the loaded relationship data held by each model are different things.

| Operation | Target |
| - | - |
| `resolveRelationUsing()` | Registers or overwrites the creation logic per class |
| `$order->unsetRelation('acmeNotes')` | Removes the loaded result from that model. The resolver remains |
| `Model::clearBootedModels()` | Clears model boot-related state and similar. Does not clear resolver registrations |

Overwriting a resolver after `$order->acmeNotes` has already been loaded does not automatically update the result held by that model. If you need to retrieve it again, remove the relationship result and then use `load('acmeNotes')` or similar.

> [!WARNING]
>
> Registrations are stored in a static property, not the service container. If the closure captures the request, the authenticated user, a tenant, or similar, it may be retained in long-lived processes. Limit registrations to stable definitions at boot time, and restart processes such as queue workers and Octane when configuration or code changes. See also [Package service lifetime design](https://kawax.biz/en/advanced/package-service-lifetimes.md).

Similarly, do not assume that recreating the application in Testbench clears resolvers. For tests that compare enabled and disabled states, use separate model fixtures or PHPUnit process isolation to avoid depending on earlier registrations.

## Test both registration and SQL

With [Orchestra Testbench](https://kawax.biz/en/advanced/package-testing.md), verify not only provider registration but also the foreign key and actual relationship retrieval. The following is an example of `tests/DynamicRelationsTest.php`. Make the package classes and test namespace loadable via Composer, and have the PDO and SQLite drivers available.

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

The test model is named `OrderFixture`, but the foreign key remains `order_id` rather than `order_fixture_id`. This confirms that it does not depend on guessing from the class name.

In the package's CI, also verify the following boundaries.

* With a separate unregistered fixture, no relationship is added when `order_model` is `null`.
* Inheritance to child models, and precedence when a resolver with the same name is registered only on the child.
* The intended API is available when colliding with existing methods or attributes.
* The difference between a model that has already loaded the relationship and a new model after the resolver is re-registered.
* Column types, indexes, and foreign key constraints on supported databases, as well as primary keys changed by the user.

## Primary sources

* [Laravel official documentation: Dynamic Relationships](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/eloquent-relationships.md#dynamic-relationships)
* [Laravel official documentation: Package Development](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [HasRelationships: Registration, inheritance lookup, and key handling](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [Model: Dynamic calls and clearing boot state](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Model.php)
* [HasAttributes: Attribute precedence and retaining relationship results](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php)
* [Builder: Building relationships during eager loading](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Builder.php)
* [QueriesRelationships: Relationship existence conditions](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php)
* [HasOneOrMany: Setting the foreign key on create](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php)
* [Laravel official tests: DatabaseEloquentDynamicRelationsTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Database/DatabaseEloquentDynamicRelationsTest.php)


## Related topics

- [Eloquent relationships](https://kawax.biz/en/eloquent-relationships.md)
- [Advanced Topics](https://kawax.biz/en/advanced/index.md)
- [Registering and re-resolving package Storage drivers](https://kawax.biz/en/advanced/package-filesystem-drivers.md)
- [Registering and maintaining scheduled tasks in packages](https://kawax.biz/en/advanced/package-scheduling.md)
- [Registering and caching package event listeners](https://kawax.biz/en/advanced/package-events.md)


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