spatie/laravel-morph-map-generator
Stop forgetting to register models in your Laravel morph map. This package auto-generates morph maps at boot, letting each model register itself via a simple getMorphClass method. Includes configurable autogeneration and a base directory setting for non-default model locations.
Architecture fit. This is a narrow, single-purpose package: a service provider that scans a configured set of directories for Eloquent models, calls getMorphClass() on each, and registers the resulting alias→class pairs with Laravel's morph map. It sits at the framework boot layer, not in the domain layer, so it does not constrain our application architecture. The real architectural commitment is not the package — it is the convention it enforces: every persisted polymorphic model must declare a stable, explicit alias. That convention is individually valuable regardless of whether we adopt this package, which makes the package optional-to-swap (we could drop it and hand-write the map with a CI test to keep it honest). That is a favorable risk profile for a utility of this size.
Integration feasibility. Low-to-moderate. Mechanically it is a composer require, a publishable config, and one method on a base model. The work is in the codebase audit, not the install:
*_type column today and record its current stored value.getMorphClass() throws by default, so omissions fail loudly rather than silently resolving to a FQCN.Relation::enforceMorphMap() calls — both write to the same registry, so ordering and precedence must be verified, not assumed.Technical risk. Moderate, concentrated in two places rather than spread across the integration:
autogenerate on, the map is rebuilt at boot; in production it is expected to come from morph-map:cache. If a deploy ships a new or renamed polymorphic model without regenerating the cache, the map is stale and polymorphic relations resolve incorrectly or throw — a data-integrity-shaped bug, not a crash-shaped one. This must be bolted into the deploy pipeline, not left to developer discipline. Laravel 11+ optimize/optimize:clear hooks exist (added in 1.3.0) but only help if our pipeline actually calls them.*_type row. The README warns about this explicitly. As TPM I would gate resolver usage behind an ADR and require a documented backfill plan; the default of explicit per-model getMorphClass() literals is far safer because the diff is reviewable in the PR.Secondary risks: boot-time reflection cost if autogenerate is left enabled in production (measurable, usually small, entirely avoidable); alias collisions between two models returning the same string, which I do not see explicitly guarded in the documented surface and should be verified with a test; and the fact that getMorphClass() throwing during boot fails the entire application, which is the right behavior for CI and a sharp edge for a bad hotfix deploy.
Key questions to resolve before a decision:
*_type rows exist across how many tables?morph-map:cache runs on every deploy, including hotfix and rollback paths? What is the rollback story if the cached map and the DB disagree?getMorphClass()" as a deliberate guardrail, and does our on-call rotation understand that failure signature?Verdict: assess/trial, do not standardize. Pilot on one service with an existing polymorphic relation, prove the cache-invalidation path end to end, and keep the manual-map fallback documented as the exit.
Stack fit. Laravel 9 through 13, PHP 8.1 through 8.4, per the changelog trail (1.0.7 → 1.3.1 → 1.4.0/1.4.1). This aligns with a modern Laravel estate and, notably, the package has kept pace with framework majors rather than lagging — the Laravel 13 support line landing in 1.4.0 and the class-loading-error fix in 1.4.1 are both signals of active upkeep. It requires no queue worker, no scheduler, no external service, and no schema change of its own. That means it can be scoped to a single application without any shared-infrastructure work.
Two fit notes worth flagging: the release dates in the changelog read ahead of the current ecosystem, so verify the version we pin actually resolves against our Laravel constraint in a scratch install before committing to it in a plan; and the package is opinionated about models living under a single base_directory, which fits a conventional app/ layout and needs explicit config for domain-oriented layouts (src/Domain).
Migration path. Phased, low-blast-radius, and reversible at every step:
*_type column. This is the artifact that determines whether the migration is a half-day or a quarter — if existing values are already short aliases, the work is convention-only; if they are FQCNs, there is a data backfill.getMorphClass() and implement explicit literal aliases matching the existing stored values. Verify with the audit from step 1 that the generated map is byte-for-byte equivalent to today's behavior. This is the checkpoint that makes the rest safe.autogenerate in local and staging only. Diff the generated map against the enforced map. Any difference is a bug in step 2, not in the package.App namespaced models — via standard Relation::enforceMorphMap() and confirm precedence with the generated map.morph-map:cache into the deploy script as a hard gate, and wire morph-map:clear into the pre-deploy/rollback path.Compatibility. The main compatibility surface is not Laravel versioning — it is our own codebase's existing relationship to the morph map. Specifically: any pre-existing enforceMorphMap call, any test that asserts on a FQCN in a *_type column, any factory or seeder that writes a raw class string, and any queue payload or serialized model reference that embeds a morph class. The last one is easy to miss and only fails on in-flight jobs during the deploy window — sequence the cutover accordingly (drain or accept a short queue quiesce).
Sequencing recommendation. Steps 1–2 are pure upside and should proceed independently of the package decision, because explicit aliases are good practice on their own. Steps 3–5 constitute the actual package adoption and should be a single tracked change with a named owner. Step 6 is a separate change with its own risk review. Do not collapse 4 and 6.
Maintenance. Low and largely externalized. The package's own code surface is small, and the maintenance history is a steady cadence of framework-compatibility bumps rather than feature churn — the pattern you want in a dependency you rarely think about. Our ongoing obligation is the convention itself: every new polymorphic model must declare an alias, and the throwing base model converts that from a code-review hope into a build failure. That is the right trade — a small, permanent tax on model authors in exchange for eliminating the "forgot to add it to the morph map" class of production bug.
Support. Community-supported, MIT, no SLA. Spatie's responsiveness is historically good but contractual guarantees are zero, and with 0 dependents we have no peer cohort to lean on when something breaks. Mitigation: pin the version, vendor the fallback (a hand-written map plus a CI test asserting it matches the discovered models), and treat any upgrade as a normal dependency change requiring the same map-diff check as the initial rollout. For a boot-layer component this is acceptable because the exit is cheap; it would not be acceptable for something holding state.
Scaling. Not a scaling concern in the conventional sense — the morph map is a static in-memory registry, and once cached there is no per-request or per-entity cost. The only scaling-adjacent item is the number of discovered models: with autogenerate on, boot cost grows with reflection over the model directory. This is fine in development and should be eliminated in production by caching. If a service has hundreds of models, measure boot time before and after in staging rather than assuming.
Failure modes, ranked by what actually costs us:
getMorphClass() — the whole app fails to start. Loud, cheap to diagnose once the on-call runbook names it, but a genuine hotfix footgun; the throwing base model is the right guardrail only if the failure signature is documented.Ramp-up. A senior Laravel engineer can complete the install and config in under a day; the audit and convention layer are where the time goes — budget roughly 1–2 engineering days for a mid-sized application with no existing morph map, more if step 6 backfill is required. Team-wide ramp is effectively zero: contributors do not need to know the package exists, only that their model needs a getMorphClass() literal, and the base model teaches that at the moment they hit it. On-call ramp is the one real gap — the runbook must cover the stale-cache and boot-failure signatures before production cutover, or the first incident will be diagnosed from scratch.
Bottom line for the operating model: this package reduces a recurring class of defects for a small, bounded, reversible cost. Its risk is not technical complexity but the discipline of the deploy pipeline — specifically the guarantee that the cached map is never older than the code. If that guarantee can be made mechanical, adopt it on one service and revisit; if it would rely on developer memory, defer, because a hand-written map with a CI test delivers most of the benefit with none of the boot-layer coupling.
How can I help you explore Laravel packages today?