culturegr/custom-relation
Easily define custom relationships between Eloquent models, like connecting users to permissions through roles. Includes an Artisan generator and full support for eager-loading, making complex multi-table joins clean and reusable.
Architecture fit. The package fills a narrow but real gap in Eloquent: relationships requiring a multi-hop join with bespoke filtering that neither hasManyThrough nor a pivot belongsToMany expresses cleanly. The README's ACL traversal (User → Role → Permission) is the canonical case and, from a TPM standpoint, the only one I would sponsor. Architecturally it is a thin convention layer, not a framework: HasCustomRelation::relatesTo() returns a CustomRelation subclass that mirrors Laravel's lifecycle (addConstraints, addEagerConstraints, match), so the result behaves like a native relation for method calls, dynamic properties, with(), and load(). The integration surface is a trait on one model plus one class per relationship — a small, reversible blast radius. That is the strongest argument in its favour: the cost of being wrong is low.
Integration feasibility. Composer-only, service provider auto-discovered on Laravel 5.5+, one Artisan generator (make:relation) scaffolding into app/Eloquent/CustomRelations. No config file, no migrations, no schema coupling, no queue/cache/worker dependency, no meaningful transitive dependency risk. A single engineer can spike the ACL example in under a day. The caveat is that feasibility is per-relation, not per-install: effort scales linearly with the number of custom relations, because each one is hand-written join and matching logic that we own and must test.
Technical risk.
NOASSERTION and the README defers to LICENSE.md rather than naming a license. For a commercial or redistributed product this must clear legal review before code lands. Stop-the-line item, not a footnote.match() does per-parent filtering over a fully hydrated Collection, and the sample addEagerConstraints() chains ->with('roles.users'). Copied verbatim into production that is O(parents × results) in PHP plus deep hydration. Developers will cargo-cult the README, so adoption must ship with an internal, reviewed reference relation of our own.with(['x' => fn ($q) => ...]), whereHas/has, withCount, and relation pagination are unsupported or untested. This is the most likely source of developer friction and the thing that most often turns a "nice idea" into a rewrite.Key questions.
whereHas, withCount, or constrained eager loading on these relations? If yes, the right answer is a scoped query object or repository method, not this package.hasManyThrough/pivot relations, or covering only the residue they cannot express? Only the latter is defensible.Stack fit. Good for a Laravel 10–13 monolith or modular app with a first-party relational ACL layer. Poor fit for API-only services with flat schemas, for teams standardized on a permissions vendor package (e.g. Spatie Laravel-Permission), and for any relation query on a hot request path. The trivial dependency footprint makes the trial cheap, which lowers the cost of being wrong — the main reason to permit a timeboxed spike at all.
Migration path. Do not retrofit working queries. Go additive: put HasCustomRelation on one model, implement one relation class, and route new call sites through it while existing manual joins stay behind their current service/repository methods. Once proven, replace legacy query bodies one at a time — each replacement a small revertible PR with an obvious test target: assert the new relation's output equals the old query's output over a fixed fixture set. Keep an internal seam (a repository method or model accessor wrapping the relation) so swapping to a hand-rolled implementation later is a one-file change rather than a codebase sweep.
Compatibility. Diff composer.json constraints against our exact Laravel/PHP versions before anything else. The release history shows rolling drops of PHP and Laravel majors (PHP 8.0 removed in v1.2; Laravel 7 and 8 dropped across v1.1–v1.2), so an older LTS application may already be out of support. Pin explicitly ("culturegr/custom-relation": "1.5.*"), commit the lockfile, and add the package to the dependency-review checklist for every framework upgrade.
Sequencing.
match() correctness and eager-load hygiene, and a test pattern every new relation must follow.Maintenance. We own the relation logic regardless of upstream health. Upstream releases have been regular and major-tracking, but there is no stated deprecation policy and no guarantee of a fast turnaround after a new Laravel release. Treat the package as a tracked dependency with a named internal owner and a documented fork procedure. Budget a few hours per Laravel major to verify subclasses still behave.
Support. No vendor, no SLA, no security disclosure channel, no commercial backing. Internal first-line support must be a specific engineer, not "the team." Escalation path for a production bug is: patch the relation class locally, then decide whether to vendor the package outright. Document that path before adoption, not during the incident.
Scaling. The performance profile is the operational risk that will actually page someone. Eager-loaded custom relations do their matching in PHP, per parent, over a hydrated result set — cost grows with parent count × result count, not just row count. There is no withCount, so any UI needing counts or pagination over a custom relation requires a separate query. Guidance to enforce: never eager-load a custom relation across an unbounded parent set; always eager-load explicitly (lazy dynamic-property access silently reintroduces N+1); cap parent batches in reporting and export jobs; add query-count and peak-memory assertions to the test suite for any relation on a request path.
Failure modes.
match() is hand-written; a subtle mismatch (deduplication, missing filter, stale join) returns plausible-but-wrong data with no error. Mitigate with fixture-based equivalence tests against the legacy query.whereHas or a with() constraint and gets unfiltered data instead of an exception. This is the most likely production incident and should be called out explicitly in onboarding docs.Ramp-up. Low technical ramp — a competent Laravel engineer is productive in half a day. The real ramp is judgment: knowing when a relation is the wrong tool and why match() is where bugs hide. One well-documented reference relation in the codebase, plus a short README section on the supported and unsupported Eloquent surface, is the minimum viable enablement. Plan for that documentation as a deliverable of the pilot, not an afterthought.
Overall TPM read. Opportunity is low (15.74) and the package sits at 18 stars with zero dependents and an unresolved license. This is a narrow, contingent adoption at best: one to three ACL-style relations, behind an internal seam, with legal clearance in writing and a named owner. If the license does not clear, or if the team needs whereHas/withCount/constrained eager loading, the correct decision is to build the equivalent in-house — the surface is small enough that the custom-relation pattern, not the package, is the thing worth taking.
How can I help you explore Laravel packages today?