v13.42.0. Para los fundamentos de las relaciones, consulta la Introducción a las relaciones de Eloquent. 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.
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, necesitascommentable_type además de commentable_id.
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.
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.
src/Models/Comment.php defines la relación inversa.
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.
$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.
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.
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.
En la implementación estándar de
v13.42.0, el modo estricto afecta en ambos sentidos.
- Del modelo al nombre de tipo:
getMorphClass()lanzaClassMorphViolationExceptionpara modelos no registrados. La clase genéricaPivotincorporada 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 conmorphTo()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.
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 conmorphTo() 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.- Revisa las columnas
*_typeafectadas 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. - 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.
- 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. - 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.
- 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.
Probar los valores guardados y las consultas en ambos sentidos
Con Orchestra Testbench, 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.- 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
- Documentación oficial de Laravel: Package Development
- Relation: fusión de la tabla y modo estricto
- HasRelationships: obtención de nombres de tipo y resolución de clases
- MorphOneOrMany: conservación del nombre de tipo, condiciones de consulta y guardado
- MorphTo: resolución de tipos en la carga anticipada
- Pruebas oficiales de Laravel: registro y resolución de morph maps
- Pruebas oficiales de Laravel: lectura y escritura en modo estricto