reedware/laravel-composite-relations
Adds composite foreign key support to Eloquent relationships. Define Belongs To, Has One, and Has Many relations using multiple columns, with full eager loading and "where has" query support through a simple trait on your base model.
Architecture fit. The package is a single trait (HasCompositeRelations) applied to the application's base Eloquent model. There is no service provider, facade, config file, or migration — it adds three methods (compositeBelongsTo, compositeHasOne, compositeHasMany) that return genuine Relation subclasses. That means with(), whereHas, doesntHave, whereRelation, and withCount all flow through Laravel's standard existence-query machinery rather than a parallel query path. This is the right shape for the problem: the gap is in Eloquent's relation layer, and the fix is confined to that layer. The deliberate scope limit (three relation types, no morph/through/pivot variants) is a feature for evaluation purposes — the surface area we'd be betting on is small and readable in an afternoon.
Integration feasibility. Two steps: composer require reedware/laravel-composite-relations, then use HasCompositeRelations; on the abstract base model. Effort is trivial; blast radius is not. Because it lands on the base class, every model in the application inherits the capability and the version constraint. This should be treated as a reviewed, single-owner dependency decision rather than a routine composer update.
Technical risk. Ranked:
'or'. Composite relations default to ("fk1" = ? or "fk2" = ?), which matches any row sharing either column. This is a silent over-match that produces plausible-looking wrong data — the worst failure class. 'and' must be passed explicitly on every relation and enforced by code review, a helper wrapper, and a test asserting row identity against fixtures that share one key column and differ on the other.compositeBelongsTo($related, $foreign, $local, $relation = null, $glue = 'or') — the fourth parameter is the relation name, not the glue. compositeHasOne/compositeHasMany take ($related, $foreign, $local, $glue = 'or'). Passing 'and' as the fourth argument to compositeBelongsTo silently names the relation and. This is a first-hour mistake every implementer will make once.The mitigating factor for 4 and 5: the package is small enough to vendor or fork. Budget for that as the fallback rather than treating it as an emergency.
Key questions.
reedware/laravel-relation-joins for reporting surfaces, or does with() cover it? Every added dependency adds a version matrix.withCount and whereRelation verified against seeded fixtures, or assumed?Stack fit. Laravel 12.x/13.x on PHP 8.2–8.5+ for package v6. Older Laravel versions require older package majors — pin deliberately and confirm the matrix row matches our actual runtime, not our intended one.
Migration path. This is a capability adoption, not a data migration. Nothing changes in the schema, and no existing relation breaks by adding the trait. That said, adopt incrementally rather than installing and immediately rewriting queries: the risk here is correctness of individual relations, not the install itself.
Compatibility. Low conflict surface because there is no service provider and no container binding. The one theoretical conflict is another package that also overrides relation methods on the base model — check the base class before and after. The optional companion (laravel-relation-joins) carries its own version matrix and should be evaluated as a separate dependency decision, not bundled in.
Sequencing.
'and' glue — no $primaryKeys convention shortcuts yet.toSql() shows (a = ? and b = ?) and write a test seeding two parents that share one key column and differ on the other. If the SQL shows or, stop.'and' and correct argument position per relation type, so implementers never pass the glue by hand.$primaryKeys convention to drop boilerplate arrays.Maintenance. The package tracks Laravel's support policy rather than offering an LTS line. Each Laravel major triggers a package major, which means this dependency gates our upgrades until a compatible release lands. Given the maintainer's single-person cadence, assume we may need to publish a fork patch on our own timeline for any non-trivial upgrade. Because the shipped surface is essentially one trait, that patch is cheap — budget it as a known contingency, not a crisis.
Support. No vendor, no SLA. GitHub issues on a 22-star repo. Internal expertise required: at least one engineer must read the trait source, not just the README. The README is good but does not adequately flag the 'or' default — §4 is buried and reads as an advanced option when it is in fact the primary correctness lever.
Scaling. All risk is at the query layer, and it is index-shaped. Composite joins need composite indexes on both sides of the pair or eager loading degrades to table scans; verify with EXPLAIN on the largest candidate table before committing to a dashboard or report built on the relation. with() eliminates N+1 as usual. The 'or' glue failure mode is worse than slow — it inflates result sets and will silently corrupt aggregate reports (withCount, revenue rollups, dedupe counts). Prioritize the SQL-assertion test on any relation feeding a number that a human reads.
Failure modes to monitor.
'or' glue — highest severity, lowest detection probability.and from the compositeBelongsTo fourth-argument trap.withCount / whereRelation edge cases — unverified paths in a low-adoption package.Ramp-up. Low for a competent Laravel engineer — the API mirrors native Eloquent closely and the trait is short. Realistic estimate: half a day to first working relation including the test and SQL verification. The cost is not learning the API; it is institutionalizing the 'and' rule and the argument-position rule so that the sixth engineer to write a composite relation doesn't reintroduce the over-match. Encode both in a helper and a review checklist, and this package is a reasonable bet; leave them to convention, and it becomes a recurring data-integrity incident stream.
How can I help you explore Laravel packages today?