v13.42.0. For relationship basics, see Introduction to Eloquent relationships. To add relations to the consuming app’s models from outside, see Registering dynamic relationships.
Separate type names from relation names
A polymorphic relation combines the related primary key and a type name to reference several kinds of models. For example, when comments can belong to articles or videos, you needcommentable_type in addition to commentable_id.
Without registration, a fully qualified class name such as
Acme\Publishing\Models\Article is normally used as the type. Once you register an alias, acme-publishing.article is used, for example when creating new relations. Registering it does not rewrite the values in existing rows.
A morph map does not create relation methods or tables. Prepare models, columns and indexes as usual.
Register only package-specific aliases
In this example,Acme\Publishing\ maps to src/ through Composer PSR-4. First, define src/Models/Article.php.
src/Models/Comment.php defines the inverse relation.
acme_articles has id, title and timestamps. acme_comments has id, body, commentable_type, commentable_id and timestamps. Primary keys in this example are integers. You can use the columns and composite index created by $table->morphs('commentable'), but with UUIDs or similar setups, match the column types to the related model.
Next, register the alias in boot() of a regular, non-deferred service provider, src/PublishingServiceProvider.php.
$alias is acme-publishing.article and $class is Article::class. Unless Comment itself becomes the related model of another polymorphic relation, you don’t need to register an alias for Comment for this example.
Understand merging and name collision rules
Relation::morphMap() operates on a static map. The default merge combines the new array and the existing array with $map + static::$morphMap.
Passing
false as the second argument from a package erases registrations made by other packages and the consuming app. For normal registration, keep the existing map and choose an alias that identifies the vendor and package rather than a generic name such as article. Don’t reuse the same alias for a different meaning.
When you pass only a list of class names, Laravel instantiates each model and uses its table name as the key. Packages that want to decouple the storage format from table name changes too should declare the alias-to-class mapping explicitly, as in the example above.
The default getMorphClass() looks up the concrete model class. Using a subclass of Article does not automatically inherit the parent class’s alias. Packages that support replacing models need a design that registers the classes actually in use in the map.
Make strict mode an app-wide policy
enforceMorphMap() in the official documentation registers aliases and calls requireMorphMap(true) in one step. It is not enforcement limited to the package.
In the default implementation of
v13.42.0, strict mode affects both directions.
- From model to type name:
getMorphClass()throwsClassMorphViolationExceptionfor unregistered models. The built-in genericPivotclass is exempt, but regular models and custom pivots get no such exemption. - From stored type name to class:
getActualClassNameForMorph()throws the same exception for values that are not keys in the map. It is also used by regularmorphTo()retrieval and eager loading. Even if the class appears as a “value” in the map, an old fully qualified class name that is not a “key” cannot be loaded.
morphMap() and leave app-wide enforcement to the consuming app. If the consuming app already enforces it, don’t switch it back with requireMorphMap(false) from the package.
Migrate existing data before judging compatibility
Introducing alias registration has different conditions for new installs and for upgrading existing apps. The official documentation also notes that when you introduce it, you need to convert existing fully qualified class names to aliases.Checking the inverse lookup alone is not enough
When strict mode is disabled, you may still be able to retrieve the article from a comment withmorphTo() as long as the class in the old commentable_type exists. However, morphMany() on the article side filters by the value of getMorphClass(). After the alias is registered it looks for acme-publishing.article, so rows with the old class name drop out of the article’s comment list.
MorphOneOrMany keeps the type name when it is constructed and uses it for both query conditions and creating new records. Even when old and new type names are mixed, it does not automatically search all of them with OR.
Include the upgrade procedure in the release
The following is a fragment that converts the old class name to the alias. Implement it as a data migration for the target app, and don’t run it automatically from a service provider.- Survey the target
*_typecolumns and stored values, and prepare a backup, row counts and a restore procedure before updating. If other tables reference the same model, include them too. - For the first rollout, decide on a cutover procedure, including stopping writes or maintenance, so that processes writing the old class name and processes writing the alias do not run at the same time. For large tables, also consider update locks and batching.
- Switch the map and the data, converting type names without changing primary key values. Pin the old class name in the migration code as the string that was actually stored in the past, not
Article::class, whose meaning changes with future renames. - Check the remaining count of old values, new saves, relation lookups in both directions and eager loading, then update long-running workers before resuming writes.
- To roll back, check that the code you roll back to can resolve the stored alias. Reverting the Composer version alone does not revert type names in the database.
Test stored values and lookups in both directions
With Orchestra Testbench, verify not only the map but also the values stored in the database and lookups in both directions. The following example assumes the package is autoloaded through Composer and the PDO SQLite driver is available. It registers the provider explicitly in the test, keeping it separate from verifying auto-discovery.- Registering the provider does not disable strict mode when the consuming app has already enabled it.
- The published storage format stays the same with conflicting aliases, several aliases for the same class, and a replaced concrete model.
- Prepare fixtures stored with the pre-upgrade class name, and after migration no old values remain and lookups in both directions and counts match.
- The actual upgrade procedure, including queue workers, can be restored with the rolled-back code and data combination.
Primary sources
- Laravel official documentation: Custom Polymorphic Types
- Laravel official documentation: Package Development
- Relation: merging the map and strict mode
- HasRelationships: getting type names and resolving classes
- MorphOneOrMany: keeping type names, query conditions and saving
- MorphTo: type resolution during eager loading
- Laravel official tests: registering and resolving morph maps
- Laravel official tests: strict reads and writes