Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Custom Relation Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

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.

  • License ambiguity is a hard gate. The repository reports 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.
  • Maintainer and adoption risk. 18 stars, zero dependents, effectively single-maintainer, no vendor, no SLA, no documented security disclosure path. Mitigating factor: the package is small enough that a vendored fork is a realistic contingency rather than a fantasy.
  • Framework-internal coupling. Custom relation classes hook into Laravel's internal relation lifecycle. Those seams have been stable for years but are not a public API contract, so a future refactor of the relation system could break subclasses. The release history (v1.3 → v1.5 adding Laravel 11, 12, 13 in sequence) shows the maintainer tracks majors — but also proves we will always lag a new Laravel release momentarily.
  • The reference implementation is an adoption hazard. The documented 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.
  • Ergonomic gaps versus native relations. Constraint closures in 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.

  1. What license actually applies, and does it clear our OSS/commercial-redistribution policy? Nothing proceeds until this is answered in writing.
  2. Do we need 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.
  3. Are we replacing hasManyThrough/pivot relations, or covering only the residue they cannot express? Only the latter is defensible.
  4. How many relations would we realistically implement — one, or fifteen? At one, a ~30-line query scope wins on total cost of ownership.
  5. Who owns a vendored fork if upstream goes quiet, and is that person named on the roadmap?

Integration Approach

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.

  1. License determination — blocking gate; no engineering time booked until legal signs off in writing.
  2. Compatibility check — version matrix verified; if our app is outside the supported window, stop here.
  3. Timeboxed PoC (≤3 days) — one branch, one model, one relation, using our reviewed implementation rather than the README's. Benchmark eager-load at realistic parent counts (production-scale, not 10 rows) and capture query count, memory, and wall time.
  4. Go/no-go decision on three options — adopt narrow (one to three relations, ACL only), fork-and-own (~small surface, removes license and abandonment risk), or build the same ~200 lines in-house behind a first-party API. Present all three with TCO, not just the adoption case.
  5. If adopt: standardize — an approved template relation class, a code-review checklist item for match() correctness and eager-load hygiene, and a test pattern every new relation must follow.
  6. Register for upgrade tracking — package owner and upgrade-check step added to the framework major-upgrade runbook.

Operational Impact

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.

  • Silent wrong results. 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.
  • Feature-expectation gap. A developer adds 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.
  • N+1 regression. Someone reads the relation via dynamic property inside a loop; performance degrades gradually and is caught late.
  • Framework upgrade breakage. A Laravel major changes relation internals; the package lags; the app is blocked on a third party.
  • Legal reversal. An unclear license surfaces post-adoption and forces emergency removal. This is why the gate in step 1 is non-negotiable and why the internal seam is worth designing in from day one.

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log