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.
withCount) are easy to get 80% right and expensive to debug at 100%. Buying this for a trait include is materially cheaper than owning custom Relation subclasses, and the cost of being wrong is one composer line.with() / whereHas() / whereDoesntHave replace those one-off queries — a real maintainability and correctness win for admin/reporting surfaces, and it makes "does this record have a matching source record" answerable in one place.Adopt when all of the following hold:
whereHas, whereDoesntHave, withCount) are the actual requirement. If you only need a join in one report, write the join.'or' between composite keys, which silently over-matches rows sharing either column. Mitigable, but it is a standing correctness risk that requires a convention (always pass 'and', encode it in a base-model helper) rather than a config flag.compositeBelongsTo's fourth argument is the relation name, not the glue, so passing 'and' there silently names the relation and. Again manageable, but it is a footgun a new hire will hit.Look elsewhere or hold when:
For executives: We occasionally have to work with data tables where a record is identified by two columns rather than one — typically partner feeds or vendor-owned systems we don't control. Laravel's ORM can't express that relationship natively, so today those cases turn into hand-written database queries scattered across the app: slower to build, harder to change, and a source of quiet data bugs. This is a small, MIT-licensed package that closes that gap with a one-line setup, letting those cases use the same standard patterns as the rest of the codebase. The decision is narrow and reversible: it applies to a specific, short list of tables, it carries no licensing cost, and removing it is a one-line revert. The trade-off we're accepting is reliance on a small open-source project — which is why we're scoping it to non-critical paths and documenting it, rather than standardizing on it.
For engineering: Trait on the base model gives every model compositeBelongsTo, compositeHasOne, compositeHasMany, all participating in standard eager loading and existence queries. Three things to internalize before writing a line: (1) the default glue is 'or' and will over-match — always pass 'and', enforced via a base-model helper; (2) compositeBelongsTo's fourth parameter is the relation name, not the glue; (3) key-array order is positional and first-array semantics differ between belongs-to and has-one/has-many. Pair it with reedware/laravel-relation-joins only if we need flat reporting rows. Treat the initial install as a reviewed, single-owner change since it lands on the base class and binds the whole app's version constraint. Add one test per composite relation that seeds two parents sharing one key column and differing on the other — that test is the thing that catches the 'or' mistake in CI rather than in a dashboard.
How can I help you explore Laravel packages today?