Skip to main content
To keep reading the stored data of polymorphic relations after you move or rename a package model, separate the model class name from the type name stored in the database. A morph map alias is less a shorthand than a storage-format contract you maintain across releases. This page checks the official implementation of Laravel Framework 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 need commentable_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.
Configure provider loading with package auto-discovery. You don’t need to save models or update type names in the database during registration. Migrating existing data is a separate task.
$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.
If the consuming app adds another alias for the same class, the registration order can change the values stored from then on and the conditions of relation queries. Registering both names does not necessarily keep compatibility. Even when you allow replacing the class, verify as part of the public contract that stored data can be read with the same alias.
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() throws ClassMorphViolationException for unregistered models. The built-in generic Pivot class 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 regular morphTo() 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.
When strict mode is disabled, type names not in the map are treated as class names. That does not guarantee the class exists or that it is the expected related model. Packages usually register only their own models with 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 with morphTo() 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.
This step alone does not complete the deployment.
  1. Survey the target *_type columns 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
If you keep an alias you already pinned and only change the model namespace, change the map value to the new class and the type names keep working as they are. Changing the alias itself, on the other hand, should be treated as a change that requires a data migration. Align the upgrade procedure with Version compatibility management and Publishing and updating migrations.

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.
The map and the enforcement flag are separate static state, so don’t assume rebuilding the application resets them. This example saves and restores both. In production too, don’t rewrite them per request or per tenant, and limit registration to stable registration at boot. Relations that were already constructed keep their type name, so changing registration midway is not uniformly reflected in existing instances either. Add the following regression cases to the package CI as well.
  • 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

Last modified on November 11, 2026