> ## 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 de paquetes y compatibilidad de los datos guardados

> Analiza los morph maps de Laravel 13 y explica el registro de alias propios del paquete, la fusión y los conflictos, el efecto del modo estricto, y la migración y las pruebas de los datos existentes.

Si quieres seguir leyendo los datos guardados de relaciones polimórficas después de mover o renombrar un modelo de tu paquete, **separa el nombre de clase del modelo del nombre de tipo que se guarda en la base de datos**. Un alias del morph map no es tanto una abreviatura como un contrato sobre el formato de almacenamiento que mantienes entre versiones.

Esta página se basa en la implementación oficial de Laravel Framework `v13.42.0`. Para los fundamentos de las relaciones, consulta la [Introducción a las relaciones de Eloquent](https://kawax.biz/es/eloquent-relationships.md). Para añadir relaciones desde fuera a los modelos de la aplicación que usa el paquete, consulta el [Registro dinámico de relaciones en paquetes](https://kawax.biz/es/advanced/package-dynamic-relations.md).

## Separar los nombres de tipo de los nombres de relación

Una relación polimórfica combina la clave primaria del modelo relacionado con un nombre de tipo para referenciar varios tipos de modelos. Por ejemplo, si los comentarios pueden pertenecer a artículos o vídeos, necesitas `commentable_type` además de `commentable_id`.

| Nombre | Ejemplo en esta página | Función |
| - | - | - |
| Método de relación | `commentable()` | Obtiene el modelo relacionado desde un comentario |
| Columna que guarda el tipo | `commentable_type` | Guarda el tipo de modelo relacionado |
| Clave del morph map | `acme-publishing.article` | Alias estable que se guarda en la base de datos |
| Valor del morph map | `Article::class` | Clase actual del modelo relacionado |

Si no hay registro, normalmente se usa como tipo un nombre de clase completamente cualificado como `Acme\Publishing\Models\Article`. Al registrar un alias, se usa `acme-publishing.article`, por ejemplo al crear nuevas relaciones. **Registrarlo no reescribe los valores de las filas existentes.**

```mermaid theme={null}
flowchart LR
    A["Modelo Article"] -->|"getMorphClass()"| B["acme-publishing.article"]
    B --> C["Guardar en commentable_type"]
    C --> D["Cargar con morphTo"]
    D -->|"Resolver con el morph map"| A
```

Un morph map no crea métodos de relación ni tablas. Prepara los modelos, las columnas y los índices como de costumbre.

## Registrar solo alias propios del paquete

En el ejemplo, `Acme\Publishing\` corresponde a `src/` mediante PSR-4 de Composer. Primero, define `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');
    }
}
```

En `src/Models/Comment.php` defines la relación inversa.

```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` tiene `id`, `title` y marcas de tiempo, y `acme_comments` tiene `id`, `body`, `commentable_type`, `commentable_id` y marcas de tiempo. Las claves primarias de este ejemplo son enteros. Puedes usar las columnas y el índice compuesto que crea `$table->morphs('commentable')`, pero si usas UUID u otras configuraciones, haz coincidir los tipos de columna con el modelo relacionado.

A continuación, registra el alias en `boot()` de un service provider normal, no diferido, `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,
        ]);
    }
}
```

La carga del provider se configura con la [detección automática de paquetes](https://kawax.biz/es/advanced/package-discovery.md). Durante el registro no necesitas guardar modelos ni actualizar los nombres de tipo en la base de datos. La migración de los datos existentes es una tarea aparte.

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

$article = Article::create(['title' => 'Contrato público del paquete']);
$comment = $article->comments()->create(['body' => 'Fijar el nombre de tipo']);

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

`$alias` es `acme-publishing.article` y `$class` es `Article::class`. Salvo que `Comment` pase a ser el modelo relacionado de otra relación polimórfica, no necesitas registrar un alias para `Comment` en este ejemplo.

## Entender las reglas de fusión y colisión de nombres

`Relation::morphMap()` opera sobre una tabla de correspondencias estática. La fusión predeterminada combina el array nuevo y el existente con **`$map + static::$morphMap`**.

| Operación o estado | Comportamiento en `v13.42.0` |
| - | - |
| `morphMap()` sin argumentos | Devuelve la tabla actual |
| Registrar un alias sin usar | Se añade y se conservan los alias existentes |
| Registrar otra clase para el mismo alias | Prevalece la clase recién pasada |
| `morphMap($map, false)` | Reemplaza toda la tabla existente |
| `morphMap([])` | Es una fusión, así que la tabla existente no se borra |
| Varios alias para la misma clase | El `getMorphClass()` estándar devuelve el primer alias que encuentra en la tabla |

Si un paquete pasa `false` como segundo argumento, borra los registros de otros paquetes y de la aplicación. En un registro normal, conserva la tabla existente y elige un alias que identifique al proveedor y al paquete, en lugar de un nombre genérico como `article`. No reutilices el mismo alias con otro significado.

Si pasas solo una lista de nombres de clase, Laravel instancia cada modelo y usa el nombre de su tabla como clave. Los paquetes que quieran desacoplar el formato de almacenamiento también de los cambios de nombre de tabla deben **declarar explícitamente la correspondencia entre alias y clase**, como en el ejemplo anterior.

> [!WARNING]
>
> Si la aplicación añade otro alias para la misma clase, el orden de registro puede cambiar los valores que se guarden a partir de entonces y las condiciones de las consultas de relación. Registrar ambos nombres no garantiza que se mantenga la compatibilidad. Incluso si permites sustituir la clase, comprueba como parte del contrato público que los datos guardados se pueden leer con el mismo alias.

El `getMorphClass()` estándar busca la clase concreta del modelo. Usar una subclase de `Article` no hereda automáticamente el alias de la clase padre. Los paquetes que admiten sustituir modelos necesitan un diseño que registre en la tabla las clases que realmente se usan.

## Convertir el modo estricto en una política de toda la aplicación

`enforceMorphMap()`, de la documentación oficial, registra los alias y llama a `requireMorphMap(true)` en un solo paso. **No es una obligación limitada al paquete.**

| API | Función |
| - | - |
| `morphMap([...])` | Registra la tabla. No cambia el valor actual del modo estricto |
| `requireMorphMap(true)` | Exige correspondencias explícitas |
| `enforceMorphMap([...])` | Activa la exigencia y registra la tabla |
| `getMorphedModel($alias)` | Consulta la tabla directamente. Devuelve `null` si no está registrado |

En la implementación estándar de `v13.42.0`, el modo estricto afecta en ambos sentidos.

* **Del modelo al nombre de tipo**: `getMorphClass()` lanza `ClassMorphViolationException` para modelos no registrados. La clase genérica `Pivot` incorporada tiene una excepción, pero los modelos normales y los pivots propios no tienen esa exención.
* **Del nombre de tipo guardado a la clase**: `getActualClassNameForMorph()` lanza la misma excepción para valores que no son claves de la tabla. También se usa en la obtención normal con `morphTo()` y en la carga anticipada. Aunque la clase figure como "valor" en la tabla, un nombre de clase completamente cualificado antiguo que no sea "clave" no se puede cargar.

Si el modo estricto está desactivado, los nombres de tipo que no están en la tabla se tratan como nombres de clase. Sin embargo, eso no garantiza que la clase exista ni que sea el modelo relacionado esperado.

Normalmente, el paquete registra solo sus propios modelos con `morphMap()` y deja a la aplicación la decisión de imponerlo en toda la aplicación. Si la aplicación ya lo impone, no lo desactives desde el paquete con `requireMorphMap(false)`.

## Migrar los datos existentes antes de evaluar la compatibilidad

Introducir el registro de alias tiene condiciones distintas para instalaciones nuevas y para actualizaciones de aplicaciones existentes. La documentación oficial también advierte que, al introducirlo, hay que convertir los nombres de clase completamente cualificados existentes en alias.

### Comprobar solo la consulta inversa no basta

Con el modo estricto desactivado, es posible que puedas seguir obteniendo el artículo desde un comentario con `morphTo()` mientras exista la clase del `commentable_type` antiguo. Sin embargo, el `morphMany()` del lado del artículo filtra por el valor de `getMorphClass()`. Tras registrar el alias busca `acme-publishing.article`, así que **las filas con el nombre de clase antiguo desaparecen de la lista de comentarios del artículo**.

`MorphOneOrMany` conserva el nombre de tipo al crearse y lo usa tanto en las condiciones de consulta como al crear registros nuevos. Aunque convivan nombres de tipo antiguos y nuevos, no busca automáticamente todos los valores con OR.

### Incluir el procedimiento de actualización en la versión

El siguiente fragmento convierte el nombre de clase antiguo en el alias. Impleméntalo como una migración de datos de la aplicación destino y no lo ejecutes automáticamente desde 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']);
```

Este paso por sí solo no completa el despliegue.

1. Revisa las columnas `*_type` afectadas y los valores guardados, y prepara una copia de seguridad, un recuento de filas y un procedimiento de restauración antes de actualizar. Si otras tablas referencian el mismo modelo, inclúyelas también.
2. En la primera implantación, define un procedimiento de cambio, incluida la detención de escrituras o el mantenimiento, para que no convivan procesos que escriben el nombre de clase antiguo y procesos que escriben el alias. En tablas grandes, considera también los bloqueos de actualización y el procesamiento por lotes.
3. Cambia la tabla y los datos, convirtiendo los nombres de tipo sin modificar los valores de las claves primarias. Fija el nombre de clase antiguo en el código de migración como **la cadena que realmente se guardó en el pasado**, no como `Article::class`, cuyo significado cambia con futuros renombrados.
4. Comprueba el número de valores antiguos restantes, los nuevos guardados, las consultas de relación en ambos sentidos y la carga anticipada, y actualiza los workers de larga duración antes de reanudar las escrituras.
5. Para revertir, comprueba que el código al que vuelves puede resolver el alias guardado. Volver a la versión anterior de Composer no revierte los nombres de tipo de la base de datos.

Si mantienes un alias ya fijado y solo cambias el namespace del modelo, cambia el valor de la tabla a la nueva clase y los nombres de tipo siguen funcionando tal cual. En cambio, modificar el propio alias se trata como un cambio que requiere migrar datos. Alinea también el procedimiento de actualización con la [Gestión de la compatibilidad de versiones de paquetes](https://kawax.biz/es/advanced/package-versioning.md) y la [Publicación y actualización de migraciones de paquetes](https://kawax.biz/es/advanced/package-migrations.md).

## Probar los valores guardados y las consultas en ambos sentidos

Con [Orchestra Testbench](https://kawax.biz/es/advanced/package-testing.md), verifica no solo la tabla, sino también los valores guardados en la base de datos y las consultas en ambos sentidos. El siguiente ejemplo supone que el paquete se carga automáticamente con Composer y que el driver PDO de SQLite está disponible. Registra el provider explícitamente en la prueba, separado de la verificación de la detección automática.

```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' => 'Contrato público']);
        $comment = $article->comments()->create(['body' => 'Guardar con el 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 tabla y el indicador de obligación son estados estáticos independientes, así que no supongas que reconstruir la aplicación los reinicia. Este ejemplo guarda y restaura ambos. En producción tampoco los reescribas por petición ni por tenant, y limítate a un registro estable durante el arranque. Las relaciones ya creadas conservan su nombre de tipo, por lo que un cambio de registro a mitad de camino tampoco se refleja de forma uniforme en las instancias existentes.

Añade además los siguientes casos de regresión a la CI del paquete.

* Registrar el provider no desactiva el modo estricto cuando la aplicación ya lo ha activado.
* El formato de almacenamiento publicado no cambia con conflictos del mismo alias, varios alias para la misma clase o un modelo concreto sustituido.
* Con fixtures guardados con el nombre de clase anterior a la actualización, tras la migración no quedan valores antiguos y las consultas en ambos sentidos y los recuentos coinciden.
* El procedimiento real de actualización, incluidos los workers de colas, se puede restaurar con la combinación de código y datos revertidos.

## Fuentes primarias consultadas

* [Documentación oficial de Laravel: Custom Polymorphic Types](https://github.com/laravel/docs/blob/13.x/eloquent-relationships.md#custom-polymorphic-types)
* [Documentación oficial de Laravel: Package Development](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Relation: fusión de la tabla y modo estricto](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/Relation.php)
* [HasRelationships: obtención de nombres de tipo y resolución de clases](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php)
* [MorphOneOrMany: conservación del nombre de tipo, condiciones de consulta y guardado](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php)
* [MorphTo: resolución de tipos en la carga anticipada](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Database/Eloquent/Relations/MorphTo.php)
* [Pruebas oficiales de Laravel: registro y resolución de morph maps](https://github.com/laravel/framework/blob/v13.42.0/tests/Database/DatabaseEloquentRelationTest.php)
* [Pruebas oficiales de Laravel: lectura y escritura en modo estricto](https://github.com/laravel/framework/blob/v13.42.0/tests/Database/DatabaseEloquentStrictMorphsTest.php)


## Related topics

- [Registro dinámico de relaciones en paquetes](https://kawax.biz/es/advanced/package-dynamic-relations.md)
- [Temas avanzados](https://kawax.biz/es/advanced/index.md)
- [Gestión de la compatibilidad de versiones de paquetes](https://kawax.biz/es/advanced/package-versioning.md)
- [Publicación y actualización de migraciones de paquetes](https://kawax.biz/es/advanced/package-migrations.md)
- [Stores de caché personalizados y contratos en paquetes](https://kawax.biz/es/advanced/package-cache-drivers.md)


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