pindab0ter/constrained-morph-to-for-laravel
Type-constrained polymorphic relationships for Laravel Eloquent. Extend morphTo to accept only specified model types, ensuring type safety and data integrity while keeping the flexibility of a polymorphic database structure.
Architecture fit. The package is additive and shallow: a HasConstrainedMorphTo trait plus a MorphTo subclass, surfaced through constrainedMorphTo($types, $typeColumn, $idColumn). It reuses the existing *_type / *_id columns, so there is no schema change, no new table, and no change to how data is persisted. This matters because polymorphic columns are exactly where relational integrity cannot be enforced by the database — there is no foreign key to hang a rule on. A read-path allowlist is one of the few places a type contract can live without reshaping the schema. From an architecture standpoint, this is a narrowly scoped, well-bounded extension rather than a framework-level commitment.
Integration feasibility. High on the happy path. One composer require, one trait per model, one method swap per relation. No service provider to register, no configuration file, no global behavior change — meaning adoption is genuinely incremental and reversible, relation by relation. The real cost is not integration but verification: a TPM should budget for a spike that exercises the Eloquent surfaces the package has not visibly hardened.
Technical risk. Three items dominate the risk register.
null, which is indistinguishable from "parent deleted," "record orphaned," or "query returned nothing." That is correct behavior for a filter and dangerous for anything where a missing relation must be loud — payments, entitlements, permissions, audit subjects, moderation state. Decide per-relation whether silence is acceptable, and record that decision.whereMorphedTo() / orWhereMorphedTo() qualifying morph columns against the first constrained type instead of the parent table, producing an unknown-column error — is the tell. Young packages surface edge cases in patch releases. Any Eloquent surface not covered by the test suite should be assumed unverified, not assumed working.Key questions to close before a go decision.
with(), load(), and whereHas() on mixed-type sets, and whether disallowed rows leak into eager-loaded collections.exists(), associate(), dissociate(), touch(), has(), and cascading deletes when the stored type is disallowed?commentable_type = Image::class? The README implies it does not — only the read path is constrained. If confirmed, plan a validation rule, observer, or check constraint to close the gap.Stack fit. Eloquent-only, PHP 8.2+, Laravel 10.48+/11/12/13. If the team is not already using morph maps, fix that first. Constraining to raw class strings without a morph map relocates the brittleness (namespace refactors still break stored data) rather than removing it; v1.3.0 added morph-map compatibility, which is the configuration worth standardizing on.
Migration path. No data migration, no schema change, no backfill. The unit of change is a single relation method, and rollback is reverting that method. Adopt first on low-blast-radius read paths — comment targets, attachments, reactions, taggables — and explicitly not on money, permissions, or anything carrying a compliance narrative. Where write-side integrity matters, ship the guard in the same release as the constraint so the claim becomes "this association is rejected on write and filtered on read," not "this association can never happen."
Compatibility. Inventory every package and internal abstraction that touches polymorphic behavior before rollout. Pay specific attention to existing whereMorphedTo() / orWhereMorphedTo() usage and to morph-map aliases, since those are the surfaces with the most recent defect history.
Sequencing.
1.3.1), not a caret range — the package is pre-2.0 and still correcting query-builder edge cases.whereHas, exists(), soft deletes, and morph maps.Maintenance. Low, with one caveat. The code is small, MIT, legible, and forkable; if the maintainer stops, a mid-level engineer can own it permanently. Budget for that contingency explicitly rather than assuming upstream support. Pin versions and treat upgrades as reviewed changes — the v1.3.1 column-qualification fix is precisely the kind of patch you want to take deliberately, not automatically.
Support. The package improves debuggability of type errors: instead of an unexpected model class appearing in a collection, an out-of-contract relation resolves to null, which narrows the search space. The counterweight is that "why is this relation null?" becomes a new question class — teams will need a quick way to distinguish "type disallowed" from "parent gone." Add a logging or assertion hook for relations where silence is unacceptable, and document the diagnostic path in the runbook.
Scaling. The performance story is entirely dependent on the unanswered query-level question. If the constraint is pushed into the SQL (where type in (...)), the cost is a marginally more selective index scan on the polymorphic type column and eager loading gets cheaper on mixed-type sets. If it is post-hydration filtering, eager loading mixed-type collections fetches rows it then discards, and the package becomes a small negative on hot read paths. Measure before scaling adoption to high-traffic endpoints.
Failure modes. Enumerate them in the runbook: (a) silent null on a relation the business treats as required; (b) allowlist drift — a new model type added to the schema but not to the constraint, producing invisible nulls; (c) query-builder regressions of the v1.3.1 class surfacing under new Eloquent usages; (d) whereHas returning incomplete result sets without error; (e) package abandonment leaving a pinned version blocking a future Laravel major.
Ramp-up. Roughly an afternoon for a senior Laravel engineer to read the source and internalize the semantics; the trait and subclass are small enough to audit in full. The larger ramp-up cost is organizational: getting teams to understand that this is a read-side filter with a write-side gap, and that the allowlist is now domain-owned code that must be updated whenever a model type joins or leaves the polymorphic family. That ownership discipline, not the package, is the thing most likely to fail.
How can I help you explore Laravel packages today?