> ## 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 d'un package et compatibilité des données stockées

> Analyse les morph maps de Laravel 13 et explique l'enregistrement d'alias propres au package, la fusion et les conflits, l'impact du mode strict, ainsi que la migration et les tests des données existantes.

Pour continuer à lire les données stockées des relations polymorphes après avoir déplacé ou renommé un modèle de votre package, **séparez le nom de classe du modèle du nom de type stocké en base de données**. Un alias de morph map est moins un raccourci qu'un contrat sur le format de stockage que vous maintenez d'une version à l'autre.

Cette page s'appuie sur l'implémentation officielle de Laravel Framework `v13.42.0`. Pour les bases des relations, consultez l'[Introduction aux relations Eloquent](https://kawax.biz/fr/eloquent-relationships.md). Pour ajouter des relations depuis l'extérieur aux modèles de l'application cliente, consultez l'[Enregistrement de relations dynamiques pour les packages](https://kawax.biz/fr/advanced/package-dynamic-relations.md).

## Séparer les noms de type des noms de relation

Une relation polymorphe combine la clé primaire du modèle lié et un nom de type pour référencer plusieurs sortes de modèles. Par exemple, si les commentaires peuvent appartenir à des articles ou à des vidéos, vous avez besoin de `commentable_type` en plus de `commentable_id`.

| Nom | Exemple de cette page | Rôle |
| - | - | - |
| Méthode de relation | `commentable()` | Récupère le modèle lié depuis un commentaire |
| Colonne stockant le type | `commentable_type` | Stocke la sorte de modèle lié |
| Clé de la morph map | `acme-publishing.article` | Alias stable stocké en base de données |
| Valeur de la morph map | `Article::class` | Classe actuelle du modèle lié |

Sans enregistrement, un nom de classe pleinement qualifié comme `Acme\Publishing\Models\Article` est normalement utilisé comme type. Une fois un alias enregistré, `acme-publishing.article` est utilisé, par exemple lors de la création de nouvelles relations. **L'enregistrement seul ne réécrit pas les valeurs des lignes existantes.**

```mermaid theme={null}
flowchart LR
    A["Modèle Article"] -->|"getMorphClass()"| B["acme-publishing.article"]
    B --> C["Stocker dans commentable_type"]
    C --> D["Charger avec morphTo"]
    D -->|"Résoudre via la morph map"| A
```

Une morph map ne crée ni méthodes de relation ni tables. Préparez les modèles, colonnes et index comme d'habitude.

## N'enregistrer que des alias propres au package

Dans l'exemple, `Acme\Publishing\` correspond à `src/` via le PSR-4 de Composer. Commencez par définir `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');
    }
}
```

`src/Models/Comment.php` définit la relation inverse.

```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` contient `id`, `title` et les horodatages, et `acme_comments` contient `id`, `body`, `commentable_type`, `commentable_id` et les horodatages. Les clés primaires de cet exemple sont des entiers. Vous pouvez utiliser les colonnes et l'index composite créés par `$table->morphs('commentable')`, mais avec des UUID ou des configurations similaires, alignez les types de colonnes sur le modèle lié.

Ensuite, enregistrez l'alias dans `boot()` d'un service provider ordinaire, non différé, `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,
        ]);
    }
}
```

Le chargement du provider se configure avec la [découverte automatique des packages](https://kawax.biz/fr/advanced/package-discovery.md). Pendant l'enregistrement, vous n'avez pas besoin de sauvegarder des modèles ni de mettre à jour les noms de type en base. La migration des données existantes est une tâche distincte.

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

$article = Article::create(['title' => 'Contrat public du package']);
$comment = $article->comments()->create(['body' => 'Figer le nom de type']);

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

`$alias` vaut `acme-publishing.article` et `$class` vaut `Article::class`. Tant que `Comment` ne devient pas lui-même le modèle lié d'une autre relation polymorphe, vous n'avez pas besoin d'enregistrer d'alias pour `Comment` dans cet exemple.

## Comprendre les règles de fusion et de collision de noms

`Relation::morphMap()` agit sur une table de correspondance statique. La fusion par défaut combine le nouveau tableau et le tableau existant avec **`$map + static::$morphMap`**.

| Opération ou état | Comportement en `v13.42.0` |
| - | - |
| `morphMap()` sans argument | Renvoie la table actuelle |
| Enregistrer un alias inutilisé | Ajouté en conservant les alias existants |
| Enregistrer une autre classe pour le même alias | La classe nouvellement passée l'emporte |
| `morphMap($map, false)` | Remplace toute la table existante |
| `morphMap([])` | C'est une fusion, la table existante n'est donc pas effacée |
| Plusieurs alias pour la même classe | Le `getMorphClass()` par défaut renvoie le premier alias trouvé dans la table |

Si un package passe `false` en second argument, il efface les enregistrements des autres packages et de l'application cliente. Pour un enregistrement normal, conservez la table existante et choisissez un alias qui identifie l'éditeur et le package plutôt qu'un nom générique comme `article`. Ne réutilisez pas le même alias pour une autre signification.

Si vous ne passez qu'une liste de noms de classes, Laravel instancie chaque modèle et utilise son nom de table comme clé. Les packages qui veulent aussi découpler le format de stockage des changements de nom de table doivent **déclarer explicitement la correspondance entre alias et classe**, comme dans l'exemple ci-dessus.

> [!WARNING]
>
> Si l'application cliente ajoute un autre alias pour la même classe, l'ordre d'enregistrement peut changer les valeurs stockées par la suite et les conditions des requêtes de relation. Enregistrer les deux noms ne garantit pas que la compatibilité soit préservée. Même si vous autorisez le remplacement de la classe, vérifiez dans le cadre du contrat public que les données stockées restent lisibles avec le même alias.

Le `getMorphClass()` par défaut recherche la classe concrète du modèle. Utiliser une sous-classe d'`Article` n'hérite pas automatiquement de l'alias de la classe parente. Les packages qui permettent de remplacer les modèles ont besoin d'une conception qui enregistre dans la table les classes réellement utilisées.

## Faire du mode strict une politique de toute l'application

`enforceMorphMap()`, dans la documentation officielle, enregistre les alias et appelle `requireMorphMap(true)` en une seule étape. **Ce n'est pas une contrainte limitée au package.**

| API | Rôle |
| - | - |
| `morphMap([...])` | Enregistre la table. Ne modifie pas la valeur actuelle du mode strict |
| `requireMorphMap(true)` | Exige des correspondances explicites |
| `enforceMorphMap([...])` | Active l'exigence et enregistre la table |
| `getMorphedModel($alias)` | Consulte directement la table. Renvoie `null` si l'alias n'est pas enregistré |

Dans l'implémentation par défaut de `v13.42.0`, le mode strict agit dans les deux sens.

* **Du modèle vers le nom de type** : `getMorphClass()` lève une `ClassMorphViolationException` pour les modèles non enregistrés. La classe générique intégrée `Pivot` bénéficie d'une exception, mais les modèles ordinaires et les pivots personnalisés n'ont pas cette dérogation.
* **Du nom de type stocké vers la classe** : `getActualClassNameForMorph()` lève la même exception pour les valeurs qui ne sont pas des clés de la table. Elle est aussi utilisée par la récupération normale via `morphTo()` et par l'eager loading. Même si la classe figure comme « valeur » dans la table, un ancien nom de classe pleinement qualifié qui n'est pas une « clé » ne peut pas être chargé.

Lorsque le mode strict est désactivé, les noms de type absents de la table sont traités comme des noms de classe. Cela ne garantit toutefois ni que la classe existe ni qu'il s'agit du modèle lié attendu.

Côté package, on enregistre généralement uniquement ses propres modèles avec `morphMap()` et on laisse l'application cliente décider de l'imposer à toute l'application. Si l'application cliente l'impose déjà, ne la désactivez pas depuis le package avec `requireMorphMap(false)`.

## Migrer les données existantes avant de juger la compatibilité

L'introduction de l'enregistrement d'alias n'implique pas les mêmes conditions pour une nouvelle installation et pour la mise à jour d'une application existante. La documentation officielle signale aussi qu'il faut, lors de l'introduction, convertir les noms de classe pleinement qualifiés existants en alias.

### Vérifier uniquement la récupération inverse ne suffit pas

Lorsque le mode strict est désactivé, il est possible de récupérer encore l'article d'un commentaire avec `morphTo()` tant que la classe de l'ancien `commentable_type` existe. Mais le `morphMany()` côté article filtre selon la valeur de `getMorphClass()`. Après l'enregistrement de l'alias, il recherche `acme-publishing.article`, si bien que **les lignes portant l'ancien nom de classe disparaissent de la liste des commentaires de l'article**.

`MorphOneOrMany` conserve le nom de type à sa création et l'utilise à la fois dans les conditions de requête et lors de la création de nouveaux enregistrements. Même si anciens et nouveaux noms de type coexistent, il ne recherche pas automatiquement toutes les valeurs avec un OR.

### Inclure la procédure de mise à jour dans la version

Voici un fragment qui convertit l'ancien nom de classe en alias. Implémentez-le comme une migration de données pour l'application cible et ne l'exécutez pas automatiquement depuis un 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']);
```

Cette étape seule ne suffit pas à terminer le déploiement.

1. Inventoriez les colonnes `*_type` concernées et les valeurs stockées, et préparez une sauvegarde, un comptage des lignes et une procédure de restauration avant la mise à jour. Si d'autres tables référencent le même modèle, incluez-les aussi.
2. Pour la première mise en place, définissez une procédure de bascule, y compris l'arrêt des écritures ou une maintenance, afin que des processus écrivant l'ancien nom de classe et des processus écrivant l'alias ne coexistent pas. Pour les grandes tables, envisagez aussi les verrous de mise à jour et le traitement par lots.
3. Basculez la table et les données en convertissant les noms de type sans modifier les valeurs des clés primaires. Figez l'ancien nom de classe dans le code de migration comme **la chaîne réellement stockée par le passé**, et non `Article::class`, dont le sens change lors de futurs renommages.
4. Vérifiez le nombre d'anciennes valeurs restantes, les nouveaux enregistrements, les récupérations dans les deux sens et l'eager loading, puis mettez à jour les workers de longue durée avant de reprendre les écritures.
5. Pour revenir en arrière, vérifiez que le code restauré peut résoudre l'alias stocké. Revenir à l'ancienne version Composer ne rétablit pas les noms de type en base.

Si vous conservez un alias déjà figé et ne changez que le namespace du modèle, modifiez la valeur de la table vers la nouvelle classe : les noms de type restent utilisables tels quels. En revanche, modifier l'alias lui-même est un changement qui implique une migration de données. Alignez aussi la procédure de mise à jour sur la [Gestion de la compatibilité de versions de package](https://kawax.biz/fr/advanced/package-versioning.md) et [Publier et mettre à jour les migrations d'un package](https://kawax.biz/fr/advanced/package-migrations.md).

## Tester les valeurs stockées et les récupérations dans les deux sens

Avec [Orchestra Testbench](https://kawax.biz/fr/advanced/package-testing.md), vérifiez non seulement la table, mais aussi les valeurs stockées en base et les récupérations dans les deux sens. L'exemple suivant suppose que le package est chargé automatiquement par Composer et que le pilote PDO SQLite est disponible. Il enregistre le provider explicitement dans le test, séparément de la vérification de la découverte automatique.

```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' => 'Contrat public']);
        $comment = $article->comments()->create(['body' => 'Stocker avec un 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
{
}
```

La table et l'indicateur de contrainte sont des états statiques distincts : ne supposez pas que reconstruire l'application les réinitialise. Cet exemple sauvegarde et restaure les deux. En production non plus, ne les réécrivez pas par requête ou par tenant, et limitez-vous à un enregistrement stable au démarrage. Les relations déjà créées conservent leur nom de type, si bien qu'un changement d'enregistrement en cours de route ne se répercute pas uniformément sur les instances existantes.

Ajoutez aussi les cas de régression suivants à la CI du package.

* Enregistrer le provider ne désactive pas le mode strict lorsque l'application cliente l'a déjà activé.
* Le format de stockage publié ne change pas en cas de conflit sur un même alias, de plusieurs alias pour une même classe ou de modèle concret remplacé.
* Avec des fixtures stockées sous le nom de classe antérieur à la mise à jour, aucune ancienne valeur ne subsiste après la migration, et les récupérations dans les deux sens ainsi que les comptages concordent.
* La procédure réelle de mise à jour, workers de file d'attente compris, peut être restaurée avec la combinaison du code et des données restaurés.

## Sources primaires consultées

* [Documentation officielle de Laravel : Custom Polymorphic Types](https://github.com/laravel/docs/blob/13.x/eloquent-relationships.md#custom-polymorphic-types)
* [Documentation officielle de Laravel : Package Development](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Relation : fusion de la table et mode strict](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/Relation.php)
* [HasRelationships : obtention des noms de type et résolution des classes](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [MorphOneOrMany : conservation du nom de type, conditions de requête et enregistrement](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php)
* [MorphTo : résolution des types lors de l'eager loading](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/MorphTo.php)
* [Tests officiels de Laravel : enregistrement et résolution des morph maps](https://github.com/laravel/framework/blob/v13.42.0/tests/Database/DatabaseEloquentRelationTest.php)
* [Tests officiels de Laravel : lecture et écriture en mode strict](https://github.com/laravel/framework/blob/v13.42.0/tests/Database/DatabaseEloquentStrictMorphsTest.php)


## Related topics

- [Enregistrement de relations dynamiques pour les packages](https://kawax.biz/fr/advanced/package-dynamic-relations.md)
- [Sujets avancés](https://kawax.biz/fr/advanced/index.md)
- [Publier et mettre à jour les migrations d'un package](https://kawax.biz/fr/advanced/package-migrations.md)
- [Surcharger et mettre à jour les vues d'un package](https://kawax.biz/fr/advanced/package-views.md)
- [Stores de cache personnalisés et contrats pour les packages](https://kawax.biz/fr/advanced/package-cache-drivers.md)


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