> ## 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 Paketen und Kompatibilität gespeicherter Daten

> Anhand der Morph Maps von Laravel 13: paketspezifische Aliase registrieren, Zusammenführung und Konflikte, Auswirkungen des strikten Modus sowie Migration und Tests bestehender Daten.

Wenn Sie ein Paketmodell verschieben oder umbenennen und die gespeicherten Daten polymorpher Beziehungen weiterhin lesen möchten, **trennen Sie den Klassennamen des Modells vom Typnamen, der in der Datenbank gespeichert wird**. Ein Morph-Map-Alias ist weniger eine Kurzschreibweise als ein Vertrag über das Speicherformat, den Sie über Releases hinweg einhalten.

Diese Seite bezieht sich auf die offizielle Implementierung von Laravel Framework `v13.42.0`. Die Grundlagen von Beziehungen finden Sie in der [Einführung in Eloquent-Relationen](https://kawax.biz/de/eloquent-relationships.md). Wie Sie den Modellen der nutzenden Anwendung von außen Beziehungen hinzufügen, erklärt [Dynamische Relationen in Paketen registrieren](https://kawax.biz/de/advanced/package-dynamic-relations.md).

## Typnamen und Beziehungsnamen trennen

Eine polymorphe Beziehung kombiniert den Primärschlüssel des verknüpften Modells mit einem Typnamen und kann so auf mehrere Modellarten verweisen. Wenn Kommentare etwa zu Artikeln oder Videos gehören können, brauchen Sie neben `commentable_id` auch `commentable_type`.

| Name | Beispiel auf dieser Seite | Aufgabe |
| - | - | - |
| Beziehungsmethode | `commentable()` | Liefert das verknüpfte Modell eines Kommentars |
| Spalte für den Typ | `commentable_type` | Speichert die Art des verknüpften Modells |
| Schlüssel der Morph Map | `acme-publishing.article` | Stabiler Alias, der in der Datenbank gespeichert wird |
| Wert der Morph Map | `Article::class` | Aktuelle Klasse des verknüpften Modells |

Ohne Registrierung wird normalerweise ein vollqualifizierter Klassenname wie `Acme\Publishing\Models\Article` als Typ verwendet. Nach der Registrierung eines Alias wird `acme-publishing.article` verwendet, etwa beim Anlegen neuer Beziehungen. **Die Werte bestehender Zeilen werden durch die Registrierung allein nicht umgeschrieben.**

```mermaid theme={null}
flowchart LR
    A["Article-Modell"] -->|"getMorphClass()"| B["acme-publishing.article"]
    B --> C["In commentable_type speichern"]
    C --> D["Mit morphTo laden"]
    D -->|"Über die Morph Map auflösen"| A
```

Eine Morph Map erstellt weder Beziehungsmethoden noch Tabellen. Modelle, Spalten und Indizes legen Sie wie gewohnt an.

## Nur paketspezifische Aliase registrieren

Im Beispiel ist `Acme\Publishing\` per Composer-PSR-4 auf `src/` abgebildet. Definieren Sie zuerst `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` definieren Sie die umgekehrte Beziehung.

```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` erhält `id`, `title` und Zeitstempel, `acme_comments` erhält `id`, `body`, `commentable_type`, `commentable_id` und Zeitstempel. Die Primärschlüssel in diesem Beispiel sind Ganzzahlen. Sie können die Spalten und den zusammengesetzten Index aus `$table->morphs('commentable')` verwenden. Bei UUIDs oder ähnlichen Setups passen Sie die Spaltentypen an das verknüpfte Modell an.

Registrieren Sie anschließend den Alias in `boot()` eines normalen, nicht verzögerten Service-Providers `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,
        ]);
    }
}
```

Das Laden des Providers konfigurieren Sie über die [Paket-Auto-Discovery](https://kawax.biz/de/advanced/package-discovery.md). Während der Registrierung müssen Sie weder Modelle speichern noch Typnamen in der Datenbank aktualisieren. Die Migration bestehender Daten ist eine separate Aufgabe.

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

$article = Article::create(['title' => 'Öffentlicher Vertrag des Pakets']);
$comment = $article->comments()->create(['body' => 'Typnamen festlegen']);

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

`$alias` ist `acme-publishing.article`, `$class` ist `Article::class`. Solange `Comment` selbst nicht das verknüpfte Modell einer anderen polymorphen Beziehung wird, müssen Sie für dieses Beispiel keinen Alias für `Comment` registrieren.

## Regeln für Zusammenführung und Namenskonflikte verstehen

`Relation::morphMap()` arbeitet auf einer statischen Zuordnungstabelle. Bei der standardmäßigen Zusammenführung werden das neue und das bestehende Array mit **`$map + static::$morphMap`** kombiniert.

| Operation oder Zustand | Verhalten in `v13.42.0` |
| - | - |
| `morphMap()` ohne Argumente | Liefert die aktuelle Zuordnung |
| Einen unbenutzten Alias registrieren | Wird hinzugefügt, bestehende Aliase bleiben erhalten |
| Für denselben Alias eine andere Klasse registrieren | Die neu übergebene Klasse hat Vorrang |
| `morphMap($map, false)` | Ersetzt die gesamte bestehende Zuordnung |
| `morphMap([])` | Wird zusammengeführt, die bestehende Zuordnung bleibt erhalten |
| Mehrere Aliase für dieselbe Klasse | Das Standard-`getMorphClass()` liefert den ersten in der Zuordnung gefundenen Alias |

Übergibt ein Paket `false` als zweites Argument, löscht es die Registrierungen anderer Pakete und der nutzenden Anwendung. Behalten Sie bei normaler Registrierung die bestehende Zuordnung bei und wählen Sie einen Alias, der Herausgeber und Paket erkennen lässt, statt eines generischen Namens wie `article`. Verwenden Sie denselben Alias nicht für eine andere Bedeutung.

Wenn Sie nur eine Liste von Klassennamen übergeben, instanziiert Laravel jedes Modell und verwendet den Tabellennamen als Schlüssel. Pakete, die das Speicherformat auch von Änderungen des Tabellennamens entkoppeln möchten, **geben die Zuordnung von Alias und Klasse explizit an**, wie im obigen Beispiel.

> [!WARNING]
>
> Fügt die nutzende Anwendung für dieselbe Klasse einen anderen Alias hinzu, kann die Registrierungsreihenfolge die künftig gespeicherten Werte und die Bedingungen von Beziehungsabfragen ändern. Dass beide Namen registriert sind, bedeutet nicht zwingend, dass die Kompatibilität erhalten bleibt. Auch wenn Sie das Ersetzen der Klasse erlauben, stellen Sie als Teil des öffentlichen Vertrags sicher, dass gespeicherte Daten mit demselben Alias lesbar bleiben.

Das Standard-`getMorphClass()` sucht nach der konkreten Modellklasse. Wenn Sie eine Unterklasse von `Article` verwenden, wird der Alias der Elternklasse nicht automatisch geerbt. Pakete, die das Ersetzen von Modellen unterstützen, brauchen ein Design, das die tatsächlich verwendeten Klassen in der Zuordnung registriert.

## Den strikten Modus zur Richtlinie der gesamten Anwendung machen

`enforceMorphMap()` aus der offiziellen Dokumentation registriert Aliase und ruft `requireMorphMap(true)` in einem Schritt auf. **Diese Erzwingung ist nicht auf das Paket beschränkt.**

| API | Aufgabe |
| - | - |
| `morphMap([...])` | Registriert die Zuordnung. Ändert die aktuelle Einstellung des strikten Modus nicht |
| `requireMorphMap(true)` | Verlangt explizite Zuordnungen |
| `enforceMorphMap([...])` | Aktiviert die Anforderung und registriert die Zuordnung |
| `getMorphedModel($alias)` | Liest die Zuordnung direkt. Liefert `null`, wenn nicht registriert |

In der Standardimplementierung von `v13.42.0` wirkt der strikte Modus in beide Richtungen.

* **Vom Modell zum Typnamen**: `getMorphClass()` wirft für nicht registrierte Modelle eine `ClassMorphViolationException`. Für die eingebaute generische Klasse `Pivot` gibt es eine Ausnahme, für normale Modelle und eigene Pivots jedoch nicht.
* **Vom gespeicherten Typnamen zur Klasse**: `getActualClassNameForMorph()` wirft dieselbe Ausnahme für Werte, die kein Schlüssel der Zuordnung sind. Diese Methode wird auch beim normalen Abruf über `morphTo()` und beim Eager Loading verwendet. Selbst wenn die Klasse als „Wert“ in der Zuordnung steht, kann ein alter vollqualifizierter Klassenname, der kein „Schlüssel“ ist, nicht geladen werden.

Ist der strikte Modus deaktiviert, werden Typnamen, die nicht in der Zuordnung stehen, als Klassennamen behandelt. Das garantiert jedoch weder, dass die Klasse existiert, noch, dass es sich um das erwartete verknüpfte Modell handelt.

Pakete registrieren in der Regel nur ihre eigenen Modelle mit `morphMap()` und überlassen es der nutzenden Anwendung, ob die Zuordnung anwendungsweit erzwungen wird. Erzwingt die nutzende Anwendung sie bereits, setzen Sie sie vom Paket aus nicht mit `requireMorphMap(false)` zurück.

## Bestehende Daten migrieren, bevor Sie die Kompatibilität beurteilen

Für Neuinstallationen und für Upgrades bestehender Anwendungen gelten bei der Einführung der Alias-Registrierung unterschiedliche Voraussetzungen. Auch die offizielle Dokumentation weist darauf hin, dass bei der Einführung bestehende vollqualifizierte Klassennamen in Aliase umgewandelt werden müssen.

### Die Prüfung der umgekehrten Abfrage allein reicht nicht

Ist der strikte Modus deaktiviert, können Sie den Artikel eines Kommentars möglicherweise weiterhin per `morphTo()` abrufen, solange die Klasse im alten `commentable_type` existiert. Das `morphMany()` auf der Artikelseite filtert jedoch nach dem Wert von `getMorphClass()`. Nach der Registrierung des Alias sucht es nach `acme-publishing.article`, daher **fallen Zeilen mit dem alten Klassennamen aus der Kommentarliste des Artikels heraus**.

`MorphOneOrMany` hält den Typnamen bei der Erzeugung fest und verwendet ihn sowohl für Abfragebedingungen als auch beim Anlegen neuer Datensätze. Auch wenn alte und neue Typnamen gemischt vorkommen, sucht es nicht automatisch per OR nach allen Werten.

### Das Upgrade-Verfahren in das Release aufnehmen

Das folgende Fragment wandelt den alten Klassennamen in den Alias um. Implementieren Sie es als Datenmigration für die Zielanwendung und führen Sie es nicht automatisch aus einem Service-Provider aus.

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

Mit diesem Schritt allein ist das Deployment nicht abgeschlossen.

1. Untersuchen Sie die betroffenen `*_type`-Spalten und gespeicherten Werte und bereiten Sie vor dem Update ein Backup, eine Zeilenzählung und ein Wiederherstellungsverfahren vor. Wenn andere Tabellen auf dasselbe Modell verweisen, gehören sie ebenfalls dazu.
2. Legen Sie für die erste Einführung ein Umstellungsverfahren fest, einschließlich Schreibstopp oder Wartungsmodus, damit Prozesse, die den alten Klassennamen schreiben, und Prozesse, die den Alias schreiben, nicht gleichzeitig laufen. Ziehen Sie bei großen Tabellen auch Update-Sperren und Batch-Verarbeitung in Betracht.
3. Stellen Sie Zuordnung und Daten um und wandeln Sie die Typnamen um, ohne die Werte der Primärschlüssel zu ändern. Legen Sie den alten Klassennamen im Migrationscode als **die Zeichenkette fest, die tatsächlich gespeichert wurde**, nicht als `Article::class`, dessen Bedeutung sich bei künftigen Umbenennungen ändert.
4. Prüfen Sie die Anzahl verbleibender alter Werte, neue Speichervorgänge, Beziehungsabfragen in beide Richtungen und Eager Loading. Aktualisieren Sie langlebige Worker, bevor Sie Schreibvorgänge wieder zulassen.
5. Prüfen Sie für ein Rollback, ob der zurückgerollte Code den gespeicherten Alias auflösen kann. Das Zurücksetzen der Composer-Version allein setzt die Typnamen in der Datenbank nicht zurück.

Wenn Sie einen bereits festgelegten Alias beibehalten und nur den Namespace des Modells ändern, ändern Sie den Wert in der Zuordnung auf die neue Klasse. Die Typnamen funktionieren dann unverändert weiter. Eine Änderung des Alias selbst behandeln Sie dagegen als Änderung, die eine Datenmigration erfordert. Stimmen Sie das Upgrade-Verfahren auch mit [Versionskompatibilität von Paketen verwalten](https://kawax.biz/de/advanced/package-versioning.md) und [Paket-Migrationen veröffentlichen und aktualisieren](https://kawax.biz/de/advanced/package-migrations.md) ab.

## Gespeicherte Werte und Abfragen in beide Richtungen testen

Prüfen Sie mit [Orchestra Testbench](https://kawax.biz/de/advanced/package-testing.md) nicht nur die Zuordnung, sondern auch die in der Datenbank gespeicherten Werte und Abfragen in beide Richtungen. Das folgende Beispiel setzt voraus, dass das Paket über Composer automatisch geladen wird und der PDO-SQLite-Treiber verfügbar ist. Der Provider wird im Test explizit registriert, getrennt von der Prüfung der Auto-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' => 'Öffentlicher Vertrag']);
        $comment = $article->comments()->create(['body' => 'Mit dem Alias speichern']);

        $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
{
}
```

Zuordnung und Erzwingungs-Flag sind getrennte statische Zustände. Gehen Sie daher nicht davon aus, dass ein Neuaufbau der Anwendung sie zurücksetzt. Dieses Beispiel sichert und stellt beide wieder her. Schreiben Sie sie auch in Produktion nicht pro Request oder Mandant um, sondern beschränken Sie sich auf eine stabile Registrierung beim Start. Bereits erzeugte Beziehungen halten ihren Typnamen fest, daher wirkt sich eine zwischenzeitliche Änderung der Registrierung auch nicht einheitlich auf bestehende Instanzen aus.

Ergänzen Sie die CI des Pakets außerdem um folgende Regressionsfälle.

* Die Registrierung des Providers deaktiviert den strikten Modus nicht, wenn die nutzende Anwendung ihn bereits aktiviert hat.
* Bei Konflikten um denselben Alias, mehreren Aliasen für dieselbe Klasse und einem ersetzten konkreten Modell bleibt das veröffentlichte Speicherformat unverändert.
* Mit Fixtures, die mit dem Klassennamen vor dem Upgrade gespeichert wurden, sind nach der Migration keine alten Werte mehr vorhanden, und die Abfragen in beide Richtungen sowie die Anzahl stimmen überein.
* Das tatsächliche Upgrade-Verfahren einschließlich Queue-Workern lässt sich mit der Kombination aus zurückgerolltem Code und zurückgerollten Daten wiederherstellen.

## Herangezogene Primärquellen

* [Offizielle Laravel-Dokumentation: Custom Polymorphic Types](https://github.com/laravel/docs/blob/13.x/eloquent-relationships.md#custom-polymorphic-types)
* [Offizielle Laravel-Dokumentation: Package Development](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Relation: Zusammenführen der Zuordnung und strikter Modus](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/Relation.php)
* [HasRelationships: Typnamen ermitteln und Klassen auflösen](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [MorphOneOrMany: Typnamen festhalten, Abfragebedingungen und Speichern](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php)
* [MorphTo: Typauflösung beim Eager Loading](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/MorphTo.php)
* [Offizielle Laravel-Tests: Registrieren und Auflösen von Morph Maps](https://github.com/laravel/framework/blob/v13.42.0/tests/Database/DatabaseEloquentRelationTest.php)
* [Offizielle Laravel-Tests: Lesen und Schreiben im strikten Modus](https://github.com/laravel/framework/blob/v13.42.0/tests/Database/DatabaseEloquentStrictMorphsTest.php)


## Related topics

- [Dynamische Relationen in Paketen registrieren](https://kawax.biz/de/advanced/package-dynamic-relations.md)
- [Weiterführende Themen](https://kawax.biz/de/advanced/index.md)
- [Eigene Session-Treiber in Paketen](https://kawax.biz/de/advanced/package-session-drivers.md)
- [Paket-Views überschreiben und aktualisieren](https://kawax.biz/de/advanced/package-views.md)
- [Datenbankverbindungen in Paketen erweitern](https://kawax.biz/de/advanced/package-database-connections.md)


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