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

# Registrazione di relazioni dinamiche nei pacchetti

> Analizzare resolveRelationUsing di Laravel 13 per aggiungere relazioni senza modificare i modelli, specificare le chiavi, gestire collisioni di nomi ed ereditarietà, la durata dello stato di registrazione e i test.

Per aggiungere una relazione ai modelli dell'applicazione che usa il pacchetto, puoi registrare il metodo di creazione della relazione con `resolveRelationUsing()`. Senza modificare direttamente i file dei modelli, puoi recuperare i dati, usare l'Eager loading ed eseguire query sull'esistenza della relazione come con una relazione normale.

Questa pagina esamina l'implementazione ufficiale di Laravel Framework `v13.40.0`. Per le basi delle relazioni consulta [Introduzione alle relazioni Eloquent](https://kawax.biz/it/eloquent-relationships.md), per il caricamento dei provider consulta [Package auto-discovery](https://kawax.biz/it/advanced/package-discovery.md).

## Scegliere tra definizione normale e dinamica

La documentazione ufficiale raccomanda i normali metodi di relazione per il comune sviluppo di applicazioni, e indica che la definizione dinamica può essere utile in casi come lo sviluppo di pacchetti.

| Situazione | Scelta consigliata |
| - | - |
| Aggiungere una relazione a un modello che gestisci tu | Scrivere un normale metodo di relazione nel modello |
| Un pacchetto distribuito fornisce una relazione ai modelli dell'utente | Ricevere il modello di destinazione tramite configurazione e valutare `resolveRelationUsing()` |
| Vuoi solo ottenere i dati correlati come proprietà | È sufficiente `$order->notes`, disponibile anche con una relazione normale |

Le **proprietà di relazione dinamiche** sono una cosa diversa dalla funzionalità che registra la definizione di una relazione dall'esterno del modello. Inoltre, `resolveRelationUsing()` non è una Macro per aggiungere metodi arbitrari. Progetta il callback in modo che restituisca un'istanza `Relation` di Eloquent.

## Registrare la relazione sul modello scelto dall'utente

Come esempio, aggiungiamo una relazione `hasMany` chiamata `acmeNotes` al modello degli ordini dell'applicazione. Si presuppone una struttura in cui il namespace del pacchetto `Acme\OrderNotes\` corrisponde a `src/` tramite PSR-4 di Composer e viene caricato come normale service provider non differito.

### Preparare la configurazione e il modello correlato

Il valore iniziale in `config/acme-order-notes.php` è `null`. La semplice installazione non registra alcuna relazione sui modelli dell'applicazione.

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

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

`src/Models/Note.php` nel pacchetto è il modello correlato.

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

Nell'esempio, si presuppone che la tabella degli ordini abbia una chiave primaria intera `id` e che la tabella `acme_order_notes` abbia `id`, `order_id`, `body` e i timestamp. Prepara un indice su `order_id`. Se usi UUID o simili, fai corrispondere anche il tipo delle colonne correlate.

> [!NOTE]
>
> La registrazione di una relazione dinamica non crea tabelle né vincoli di chiave esterna. È un processo separato dalla [Pubblicazione e aggiornamento delle migrazioni di un pacchetto](https://kawax.biz/it/advanced/package-migrations.md). Anche il provider seguente non include accessi al DB né l'esecuzione di migrazioni.

### Registrare il metodo di creazione in boot

In `src/OrderNotesServiceProvider.php` unisci la configurazione e registra il callback sul modello di destinazione 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(),
            ),
        );
    }
}
```

Nell'applicazione, pubblica la configurazione e imposta `order_model` sul modello concreto effettivamente usato, ad esempio `\App\Models\Order::class`. Il caricamento del provider si configura con l'[auto-discovery](https://kawax.biz/it/advanced/package-discovery.md).

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

In `hasMany()` sono indicati esplicitamente la chiave esterna `order_id` e la chiave del modello padre. In questo modo, anche se la classe del modello padre si chiama `Purchase`, non viene dedotto `purchase_id` e la relazione resta coerente con la definizione della tabella del pacchetto.

> [!WARNING]
>
> La configurazione del modello di destinazione spetta all'amministratore dell'applicazione. Non progettare il pacchetto in modo da ricevere nomi di classe o nomi di relazione direttamente da richieste HTTP o simili, né da registrarli di nuovo a ogni richiesta. Inoltre, registrare la relazione sul `Model` base di Eloquent influisce su tutti i modelli discendenti.

## Usarla come una relazione normale

Sul modello degli ordini configurato, la chiamata al metodo restituisce `HasMany`, mentre l'accesso alla proprietà restituisce la collection dei risultati.

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

Con `create()`, `hasMany` imposta `order_id`, quindi non è necessario aggiungere la chiave esterna all'array passato dall'utente. La relazione inversa non viene aggiunta automaticamente: se serve, definiscila separatamente.

### Non eseguire il recupero dei dati nel callback

Se il valore restituito dal callback è il risultato di `get()` o un singolo modello, non può essere trattato come una relazione valida, ad esempio durante l'accesso alla proprietà. Separa la costruzione della relazione dal recupero dei dati.

Durante l'Eager loading, `Builder::getRelation()` costruisce la relazione su una nuova istanza del modello. Evita quindi progetti in cui il callback presuppone che "esista l'ID di un ordine già recuperato" o restituisce definizioni di relazione diverse a seconda di quell'ID.

Anche con la registrazione dinamica il problema N+1 e i vincoli del Lazy Loading non scompaiono. Quando gestisci più modelli, carica le relazioni come di consueto con `with()` o simili.

### Con belongsTo, attenzione anche alla deduzione del nome della relazione

Se il nome della relazione viene omesso, `belongsTo()` deduce il chiamante tramite il backtrace. Con una definizione tramite closure, invece di affidarti alla stessa deduzione di un normale metodo del modello, puoi indicare esplicitamente, oltre alla chiave esterna e alla chiave del proprietario, anche il nome della relazione come quarto argomento.

Di seguito un esempio indipendente in cui l'ordine ha `customer_id` e il cliente ha `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',
    ),
);
```

## Comprendere le regole su collisioni di nomi ed ereditarietà

`HasRelationships` salva la closure in una proprietà static usando come chiave **il nome della classe del modello e il nome della relazione**. `Model::__call()`, quando riceve la chiamata a un metodo inesistente, cerca il resolver e lo esegue passandogli l'istanza del modello di destinazione.

```mermaid theme={null}
flowchart TD
    A["Chiamata a un metodo di relazione inesistente"] --> B["Model::__call"]
    B --> C["Cercare il resolver registrato<br>nella classe stessa, poi nelle classi padre"]
    C --> D["Eseguire la closure<br>passando il modello di destinazione"]
    D --> E["Restituire un'istanza Relation"]
```

Il diagramma mostra il percorso quando il resolver viene trovato. Tieni presente che in caso di collisione con metodi o attributi esistenti il percorso è diverso.

| Situazione | Comportamento |
| - | - |
| Registrare di nuovo la stessa classe con lo stesso nome di relazione | La closure successiva sovrascrive la precedente |
| La registrazione esiste solo sul modello padre | Anche il modello figlio può usare il resolver del padre |
| Esiste una registrazione con lo stesso nome anche sul modello figlio | Ha la precedenza la registrazione del figlio. Quella del padre non cambia |
| Registrare lo stesso nome su modelli senza relazione di ereditarietà | Ogni classe conserva una definizione separata |
| Esiste un metodo public con lo stesso nome nel modello o in un trait | PHP chiama il metodo esistente. Non può essere sostituito dalla registrazione dinamica |
| Esiste un attributo già recuperato, un cast o un accessor con lo stesso nome | Nell'accesso alla proprietà viene valutato prima l'attributo |

Usa per la relazione un nome univoco come `acmeNotes` e verifica che non collida con metodi e attributi dell'utente né con altri pacchetti. `method_exists()` da solo non permette di verificare l'esistenza di una registrazione dinamica. `isRelation()` di Eloquent verifica anche i resolver, ma non garantisce l'assenza di collisioni con gli attributi.

## Separare la durata dello stato di registrazione e dei risultati recuperati

La registrazione del resolver e i dati di relazione già caricati conservati da ciascun modello sono cose diverse.

| Operazione | Oggetto |
| - | - |
| `resolveRelationUsing()` | Registra o sovrascrive il metodo di creazione per classe |
| `$order->unsetRelation('acmeNotes')` | Rimuove il risultato caricato di quel modello. Il resolver rimane |
| `Model::clearBootedModels()` | Cancella lo stato relativo al boot dei modelli e simili. Non cancella la registrazione dei resolver |

Se sovrascrivi il resolver dopo aver già caricato `$order->acmeNotes`, il risultato conservato da quel modello non viene aggiornato automaticamente. Se devi recuperarlo di nuovo, rimuovi il risultato della relazione e poi usa ad esempio `load('acmeNotes')`.

> [!WARNING]
>
> La registrazione avviene in una proprietà static, non nel service container. Se la closure cattura la richiesta, l'utente autenticato, il tenant o simili, questi possono essere conservati nei processi a lunga durata. Limita la registrazione a definizioni stabili al momento dell'avvio e, quando cambi configurazione o codice, aggiorna anche processi come i queue worker e Octane. Consulta anche [Progettare il ciclo di vita dei servizi di un pacchetto](https://kawax.biz/it/advanced/package-service-lifetimes.md).

Allo stesso modo, non presumere che ricreare l'applicazione in Testbench sia sufficiente a eliminare i resolver. Per i test che confrontano lo stato abilitato e disabilitato, usa una fixture di modello diversa o l'isolamento dei processi di PHPUnit, così da non dipendere da registrazioni precedenti.

## Testare sia la registrazione sia l'SQL

Con [Orchestra Testbench](https://kawax.biz/it/advanced/package-testing.md) verifica non solo la registrazione del provider, ma anche la chiave esterna e il recupero effettivo della relazione. Di seguito un esempio di `tests/DynamicRelationsTest.php`. Rendi caricabili tramite Composer le classi del pacchetto e il namespace dei test, e prepara i driver PDO e 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;
}
```

Il modello di test si chiama `OrderFixture`, ma la chiave esterna resta `order_id` e non `order_fixture_id`. Questo permette di verificare che la relazione non dipenda dalla deduzione basata sul nome della classe.

Nella CI del pacchetto, verifica anche i seguenti casi limite.

* Con un'altra fixture non registrata, se `order_model` è `null` la relazione non viene aggiunta.
* L'ereditarietà verso i modelli figli e l'ordine di precedenza quando un resolver con lo stesso nome è registrato solo sul figlio.
* In caso di collisione con metodi o attributi esistenti, l'API prevista resta utilizzabile.
* La differenza, dopo una nuova registrazione del resolver, tra un modello che ha già caricato la relazione e un modello nuovo.
* Tipi di colonna, indici e vincoli di chiave esterna sui DB supportati, e chiavi primarie modificate dall'utente.

## Fonti primarie consultate

* [Documentazione ufficiale di Laravel: Dynamic Relationships](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/eloquent-relationships.md#dynamic-relationships)
* [Documentazione ufficiale di Laravel: Package Development](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [HasRelationships: registrazione, ricerca nell'ereditarietà e gestione delle chiavi](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [Model: chiamate dinamiche e cancellazione dello stato di boot](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Model.php)
* [HasAttributes: priorità degli attributi e conservazione dei risultati delle relazioni](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php)
* [Builder: costruzione delle relazioni durante l'Eager loading](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Builder.php)
* [QueriesRelationships: condizioni di esistenza delle relazioni](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php)
* [HasOneOrMany: impostazione della chiave esterna in create](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php)
* [Test ufficiali di Laravel: DatabaseEloquentDynamicRelationsTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Database/DatabaseEloquentDynamicRelationsTest.php)


## Related topics

- [Introduzione alle relazioni Eloquent](https://kawax.biz/it/eloquent-relationships.md)
- [Argomenti avanzati](https://kawax.biz/it/advanced/index.md)
- [Registrazione e test dei transport di posta nei pacchetti](https://kawax.biz/it/advanced/package-mail-transports.md)
- [Analisi statica dei pacchetti (PHPStan / Larastan)](https://kawax.biz/it/advanced/package-static-analysis.md)
- [Hook di risoluzione dei servizi nei pacchetti](https://kawax.biz/it/advanced/package-service-resolution.md)


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