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

# Morph maps in packages en compatibiliteit van opgeslagen data

> Leest de morph maps van Laravel 13 en behandelt het registreren van package-specifieke aliassen, samenvoegen en conflicten, de impact van de strikte modus en het migreren en testen van bestaande data.

Wil je de opgeslagen data van polymorfe relaties blijven lezen nadat je een model van je package hebt verplaatst of hernoemd, **scheid dan de klassenaam van het model van de typenaam die in de database wordt opgeslagen**. Een morph map-alias is minder een afkorting dan een contract over het opslagformaat dat je over releases heen onderhoudt.

Deze pagina is gebaseerd op de officiële implementatie van Laravel Framework `v13.42.0`. De basis van relaties vind je in de [Introductie tot Eloquent-relaties](https://kawax.biz/nl/eloquent-relationships.md). Hoe je van buitenaf relaties toevoegt aan de modellen van de gebruikende app, lees je in [Dynamische relaties registreren vanuit packages](https://kawax.biz/nl/advanced/package-dynamic-relations.md).

## Typenamen en relatienamen scheiden

Een polymorfe relatie combineert de primaire sleutel van het gerelateerde model met een typenaam om naar meerdere soorten modellen te verwijzen. Als reacties bijvoorbeeld bij artikelen of video's kunnen horen, heb je naast `commentable_id` ook `commentable_type` nodig.

| Naam | Voorbeeld op deze pagina | Rol |
| - | - | - |
| Relatiemethode | `commentable()` | Haalt het gerelateerde model op vanuit een reactie |
| Kolom die het type opslaat | `commentable_type` | Slaat de soort gerelateerd model op |
| Sleutel van de morph map | `acme-publishing.article` | Stabiele alias die in de database wordt opgeslagen |
| Waarde van de morph map | `Article::class` | Huidige klasse van het gerelateerde model |

Zonder registratie wordt normaal gesproken een volledig gekwalificeerde klassenaam zoals `Acme\Publishing\Models\Article` als type gebruikt. Na het registreren van een alias wordt `acme-publishing.article` gebruikt, bijvoorbeeld bij het aanmaken van nieuwe relaties. **Registratie alleen herschrijft de waarden in bestaande rijen niet.**

```mermaid theme={null}
flowchart LR
    A["Article-model"] -->|"getMorphClass()"| B["acme-publishing.article"]
    B --> C["Opslaan in commentable_type"]
    C --> D["Laden met morphTo"]
    D -->|"Oplossen via de morph map"| A
```

Een morph map maakt geen relatiemethoden of tabellen aan. Modellen, kolommen en indexen maak je zoals gewoonlijk aan.

## Alleen package-specifieke aliassen registreren

In het voorbeeld is `Acme\Publishing\` via Composer PSR-4 gekoppeld aan `src/`. Definieer eerst `src/Models/Article.php`.

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

namespace Acme\Publishing\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Article extends Model
{
    protected $table = 'acme_articles';

    protected $fillable = ['title'];

    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
```

In `src/Models/Comment.php` definieer je de omgekeerde relatie.

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

namespace Acme\Publishing\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Comment extends Model
{
    protected $table = 'acme_comments';

    protected $fillable = ['body'];

    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}
```

`acme_articles` krijgt `id`, `title` en timestamps, `acme_comments` krijgt `id`, `body`, `commentable_type`, `commentable_id` en timestamps. De primaire sleutels in dit voorbeeld zijn integers. Je kunt de kolommen en de samengestelde index van `$table->morphs('commentable')` gebruiken, maar stem bij UUID's en vergelijkbare opzetten de kolomtypes af op het gerelateerde model.

Registreer daarna de alias in `boot()` van een gewone, niet-uitgestelde service provider, `src/PublishingServiceProvider.php`.

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

namespace Acme\Publishing;

use Acme\Publishing\Models\Article;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider;

class PublishingServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Relation::morphMap([
            'acme-publishing.article' => Article::class,
        ]);
    }
}
```

Het laden van de provider configureer je met [package discovery](https://kawax.biz/nl/advanced/package-discovery.md). Tijdens de registratie hoef je geen modellen op te slaan of typenamen in de database bij te werken. Het migreren van bestaande data is een aparte taak.

```php theme={null}
use Acme\Publishing\Models\Article;
use Illuminate\Database\Eloquent\Relations\Relation;

$article = Article::create(['title' => 'Het publieke contract van het package']);
$comment = $article->comments()->create(['body' => 'De typenaam vastleggen']);

$alias = $article->getMorphClass();
$class = Relation::getMorphedModel('acme-publishing.article');
$parent = $comment->fresh()->commentable;
```

`$alias` is `acme-publishing.article` en `$class` is `Article::class`. Zolang `Comment` zelf niet het gerelateerde model van een andere polymorfe relatie wordt, hoef je voor dit voorbeeld geen alias voor `Comment` te registreren.

## De regels voor samenvoegen en naamconflicten begrijpen

`Relation::morphMap()` werkt op een statische map. Bij het standaard samenvoegen worden de nieuwe en de bestaande array gecombineerd met **`$map + static::$morphMap`**.

| Bewerking of toestand | Gedrag in `v13.42.0` |
| - | - |
| `morphMap()` zonder argumenten | Geeft de huidige map terug |
| Een ongebruikte alias registreren | Wordt toegevoegd, bestaande aliassen blijven behouden |
| Een andere klasse registreren voor dezelfde alias | De nieuw meegegeven klasse krijgt voorrang |
| `morphMap($map, false)` | Vervangt de hele bestaande map |
| `morphMap([])` | Is een samenvoeging, dus de bestaande map wordt niet gewist |
| Meerdere aliassen voor dezelfde klasse | De standaard `getMorphClass()` geeft de eerste alias terug die in de map wordt gevonden |

Geeft een package `false` mee als tweede argument, dan wist het de registraties van andere packages en van de gebruikende app. Behoud bij normale registratie de bestaande map en kies een alias die de vendor en het package herkenbaar maakt, in plaats van een generieke naam zoals `article`. Gebruik dezelfde alias niet opnieuw voor een andere betekenis.

Geef je alleen een lijst klassenamen mee, dan instantieert Laravel elk model en gebruikt het de tabelnaam als sleutel. Packages die het opslagformaat ook los willen koppelen van wijzigingen in de tabelnaam, **declareren de koppeling tussen alias en klasse expliciet**, zoals in het voorbeeld hierboven.

> [!WARNING]
>
> Voegt de gebruikende app een andere alias toe voor dezelfde klasse, dan kan de registratievolgorde bepalen welke waarden voortaan worden opgeslagen en welke voorwaarden relatiequery's gebruiken. Dat beide namen geregistreerd zijn, betekent niet automatisch dat de compatibiliteit behouden blijft. Ook als je het vervangen van de klasse toestaat, controleer je als onderdeel van het publieke contract dat opgeslagen data met dezelfde alias leesbaar blijft.

De standaard `getMorphClass()` zoekt naar de concrete modelklasse. Een subklasse van `Article` erft niet automatisch de alias van de bovenliggende klasse. Packages die het vervangen van modellen ondersteunen, hebben een ontwerp nodig dat de daadwerkelijk gebruikte klassen in de map registreert.

## Maak de strikte modus een beleid voor de hele app

`enforceMorphMap()` uit de officiële documentatie registreert aliassen en roept `requireMorphMap(true)` in één stap aan. **Het is geen verplichting die alleen voor het package geldt.**

| API | Rol |
| - | - |
| `morphMap([...])` | Registreert de map. Wijzigt de huidige instelling van de strikte modus niet |
| `requireMorphMap(true)` | Vereist expliciete koppelingen |
| `enforceMorphMap([...])` | Schakelt de vereiste in en registreert de map |
| `getMorphedModel($alias)` | Leest de map direct uit. Geeft `null` terug als de alias niet geregistreerd is |

In de standaardimplementatie van `v13.42.0` werkt de strikte modus in beide richtingen.

* **Van model naar typenaam**: `getMorphClass()` gooit een `ClassMorphViolationException` voor niet-geregistreerde modellen. De ingebouwde generieke klasse `Pivot` heeft een uitzondering, maar gewone modellen en eigen pivots krijgen die vrijstelling niet.
* **Van opgeslagen typenaam naar klasse**: `getActualClassNameForMorph()` gooit dezelfde exception voor waarden die geen sleutel in de map zijn. Dit wordt ook gebruikt bij het gewone ophalen via `morphTo()` en bij eager loading. Ook als de klasse als "waarde" in de map staat, kan een oude volledig gekwalificeerde klassenaam die geen "sleutel" is niet worden geladen.

Is de strikte modus uitgeschakeld, dan worden typenamen die niet in de map staan als klassenamen behandeld. Dat garandeert echter niet dat de klasse bestaat of dat het om het verwachte gerelateerde model gaat.

Een package registreert meestal alleen zijn eigen modellen met `morphMap()` en laat de keuze om de map app-breed af te dwingen aan de gebruikende app over. Dwingt de gebruikende app die al af, zet hem dan niet vanuit het package terug met `requireMorphMap(false)`.

## Migreer bestaande data voordat je de compatibiliteit beoordeelt

Voor nieuwe installaties en voor upgrades van bestaande apps gelden andere voorwaarden bij het invoeren van aliasregistratie. Ook de officiële documentatie waarschuwt dat je bij de invoering bestaande volledig gekwalificeerde klassenamen naar aliassen moet omzetten.

### Alleen de omgekeerde opvraging controleren is niet genoeg

Met de strikte modus uitgeschakeld kun je het artikel van een reactie mogelijk nog steeds ophalen met `morphTo()`, zolang de klasse in de oude `commentable_type` bestaat. Maar `morphMany()` aan de kant van het artikel filtert op de waarde van `getMorphClass()`. Na het registreren van de alias zoekt het naar `acme-publishing.article`, dus **rijen met de oude klassenaam verdwijnen uit de lijst met reacties van het artikel**.

`MorphOneOrMany` legt de typenaam vast bij het aanmaken en gebruikt die zowel in de queryvoorwaarden als bij het aanmaken van nieuwe records. Ook als oude en nieuwe typenamen door elkaar voorkomen, zoekt het niet automatisch met OR naar alle waarden.

### Neem de upgradeprocedure op in de release

Het volgende fragment zet de oude klassenaam om naar de alias. Implementeer het als datamigratie voor de betreffende app en voer het niet automatisch uit vanuit een service provider.

```php theme={null}
use Illuminate\Support\Facades\DB;

DB::table('acme_comments')
    ->where('commentable_type', 'Acme\\Publishing\\Models\\Article')
    ->update(['commentable_type' => 'acme-publishing.article']);
```

Met deze stap alleen is de deployment nog niet klaar.

1. Breng de betreffende `*_type`-kolommen en opgeslagen waarden in kaart en zorg vóór de update voor een back-up, een telling van de rijen en een herstelprocedure. Verwijzen andere tabellen naar hetzelfde model, neem die dan ook mee.
2. Leg voor de eerste invoering een overstapprocedure vast, inclusief het stoppen van schrijfacties of onderhoud, zodat processen die de oude klassenaam schrijven en processen die de alias schrijven niet tegelijk draaien. Overweeg bij grote tabellen ook update-locks en verwerking in batches.
3. Schakel de map en de data om en zet de typenamen om zonder de waarden van de primaire sleutels te wijzigen. Leg de oude klassenaam in de migratiecode vast als **de string die in het verleden daadwerkelijk is opgeslagen**, niet als `Article::class`, waarvan de betekenis verandert bij toekomstige hernoemingen.
4. Controleer het aantal resterende oude waarden, nieuwe opslagacties, opvragingen in beide richtingen en eager loading, en werk langlopende workers bij voordat je de schrijfacties hervat.
5. Controleer bij een rollback of de teruggezette code de opgeslagen alias kan oplossen. Alleen de Composer-versie terugzetten zet de typenamen in de database niet terug.

Behoud je een alias die al vastligt en wijzig je alleen de namespace van het model, pas dan de waarde in de map aan naar de nieuwe klasse. De typenamen blijven dan gewoon werken. Het wijzigen van de alias zelf behandel je daarentegen als een wijziging die een datamigratie vereist. Stem de upgradeprocedure ook af op [Versiecompatibiliteit van packages beheren](https://kawax.biz/nl/advanced/package-versioning.md) en [Migrations van packages publiceren en bijwerken](https://kawax.biz/nl/advanced/package-migrations.md).

## Opgeslagen waarden en opvragingen in beide richtingen testen

Controleer met [Orchestra Testbench](https://kawax.biz/nl/advanced/package-testing.md) niet alleen de map, maar ook de in de database opgeslagen waarden en opvragingen in beide richtingen. Het volgende voorbeeld gaat ervan uit dat het package via Composer wordt geautoload en dat de PDO SQLite-driver beschikbaar is. De provider wordt expliciet in de test geregistreerd, los van het controleren van package discovery.

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

namespace Acme\Publishing\Tests;

use Acme\Publishing\Models\Article;
use Acme\Publishing\PublishingServiceProvider;
use Illuminate\Database\ClassMorphViolationException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Orchestra\Testbench\TestCase;

final class MorphMapTest extends TestCase
{
    private array $originalMap;

    private bool $originalRequirement;

    protected function defineEnvironment($app): void
    {
        $app['config']->set('database.default', 'testing');
        $app['config']->set('database.connections.testing', [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ]);
    }

    protected function setUp(): void
    {
        parent::setUp();

        $this->originalMap = Relation::morphMap();
        $this->originalRequirement = Relation::requiresMorphMap();

        Relation::morphMap(['app.record' => UnrelatedRecord::class], false);
        Relation::requireMorphMap(false);

        $this->app->register(PublishingServiceProvider::class);

        Schema::create('acme_articles', function (Blueprint $table): void {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });

        Schema::create('acme_comments', function (Blueprint $table): void {
            $table->id();
            $table->text('body');
            $table->morphs('commentable');
            $table->timestamps();
        });
    }

    protected function tearDown(): void
    {
        try {
            parent::tearDown();
        } finally {
            Relation::morphMap($this->originalMap, false);
            Relation::requireMorphMap($this->originalRequirement);
        }
    }

    public function test_registration_preserves_other_mappings_and_strictness(): void
    {
        $this->assertSame(UnrelatedRecord::class, Relation::getMorphedModel('app.record'));
        $this->assertSame('acme-publishing.article', (new Article)->getMorphClass());
        $this->assertFalse(Relation::requiresMorphMap());
    }

    public function test_alias_is_stored_and_both_directions_can_be_loaded(): void
    {
        $article = Article::create(['title' => 'Publiek contract']);
        $comment = $article->comments()->create(['body' => 'Opslaan met de alias']);

        $this->assertSame(
            'acme-publishing.article',
            DB::table('acme_comments')->where('id', $comment->getKey())->value('commentable_type'),
        );
        $this->assertTrue($comment->fresh()->commentable->is($article));
        $this->assertTrue(Article::with('comments')->findOrFail($article->getKey())->comments->sole()->is($comment));
    }

    public function test_strict_read_does_not_accept_an_unmapped_raw_class_name(): void
    {
        Relation::requireMorphMap();

        $this->expectException(ClassMorphViolationException::class);

        Model::getActualClassNameForMorph(Article::class);
    }
}

class UnrelatedRecord extends Model
{
}
```

De map en de vlag voor afdwingen zijn aparte statische toestanden, dus ga er niet van uit dat het opnieuw opbouwen van de applicatie ze reset. Dit voorbeeld bewaart en herstelt beide. Herschrijf ze ook in productie niet per request of per tenant, en beperk je tot een stabiele registratie bij het opstarten. Relaties die al zijn aangemaakt houden hun typenaam vast, dus een tussentijdse wijziging van de registratie wordt ook niet uniform doorgevoerd in bestaande instanties.

Voeg daarnaast de volgende regressiegevallen toe aan de CI van het package.

* Het registreren van de provider schakelt de strikte modus niet uit wanneer de gebruikende app die al heeft ingeschakeld.
* Het gepubliceerde opslagformaat verandert niet bij conflicten rond dezelfde alias, meerdere aliassen voor dezelfde klasse en een vervangen concreet model.
* Met fixtures die zijn opgeslagen met de klassenaam van vóór de upgrade blijven na de migratie geen oude waarden over, en kloppen de opvragingen in beide richtingen en de aantallen.
* De echte upgradeprocedure, inclusief queue workers, kan worden hersteld met de combinatie van teruggezette code en data.

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: Custom Polymorphic Types](https://github.com/laravel/docs/blob/13.x/eloquent-relationships.md#custom-polymorphic-types)
* [Officiële Laravel-documentatie: Package Development](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Relation: samenvoegen van de map en strikte modus](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/Relation.php)
* [HasRelationships: typenamen ophalen en klassen oplossen](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [MorphOneOrMany: typenaam vasthouden, queryvoorwaarden en opslaan](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php)
* [MorphTo: typeresolutie bij eager loading](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/MorphTo.php)
* [Officiële Laravel-tests: morph maps registreren en oplossen](https://github.com/laravel/framework/blob/v13.42.0/tests/Database/DatabaseEloquentRelationTest.php)
* [Officiële Laravel-tests: lezen en schrijven in de strikte modus](https://github.com/laravel/framework/blob/v13.42.0/tests/Database/DatabaseEloquentStrictMorphsTest.php)


## Related topics

- [Dynamische relaties registreren vanuit packages](https://kawax.biz/nl/advanced/package-dynamic-relations.md)
- [Geavanceerde onderwerpen](https://kawax.biz/nl/advanced/index.md)
- [Eigen cachestores en contracten voor packages](https://kawax.biz/nl/advanced/package-cache-drivers.md)
- [Views van packages overschrijven en bijwerken](https://kawax.biz/nl/advanced/package-views.md)
- [Eigen sessiedrivers voor packages](https://kawax.biz/nl/advanced/package-session-drivers.md)


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