mpociot/laravel-composite-key
Adds support for composite primary keys in Laravel Eloquent models. This package lets models with multi-column keys work with common Eloquent operations like saving, updating, deleting, and finding records, without hacking around the framework's single-key assumptions.
id. The first PM decision is upstream of any code: for tables we control, do we mandate a surrogate id plus UNIQUE(order_id, line_no) — same integrity, zero friction with bindings, relations, caching, and third-party tooling — or do we accept tuple identity? This package only becomes a candidate after that call is made in favor of composite keys, and only for tables where the tuple is a fact of the data rather than a modeling choice.find()/save() such models cleanly determines whether we ship a full CRUD surface for them or settle for read-only reporting — a scoping decision that changes the shape of a sprint, not just the code.id or use implicit route model binding. That forces an explicit, greppable contract (nested routes like /orders/{orderId}/lines/{lineNo}, or a composed route key) and forbids 'id' => $model->getKey() in responses. This is a public-facing decision that should be settled before the first endpoint ships, not retrofitted.cache()->remember($model->getKey(), …), session keys, queue payloads, log correlation IDs — misbehaves silently. Choosing an explicit composite cache key convention (e.g. order_line:{order_id}:{line_no}) is a small but load-bearing platform standard.Consider it when all of the following hold:
SHOW CREATE TABLE … PRIMARY KEY (a, b), not merely a unique index).find, save, update, delete), not just reporting reads.hasMany whose foreign key is a single column. They must not be the parent of a hasMany, a belongsTo target, or a morphMany target.$class::find($ids). Note that under this trait, find([1,2,3]) means "the tuple (1,2,3)", not "three records" — a genuine footgun in shared code.packages/, and back it with a canary test covering the four CRUD paths that runs on every Laravel/PHP upgrade.Look elsewhere when any of these are true:
id plus a unique composite index. Same integrity guarantees, no friction with binding, relations, caching, or third-party packages. This is the default answer and should be beaten deliberately, not skipped.awobaz/compoships. Do not stack the two packages.fico7489/laravel-composite-key is the more actively maintained package in this niche and should be benchmarked before committing. Choose one, pin it, and isolate it behind a single base model so the swap is one file.fresh()/refresh() guarantees, is() comparisons, or generic queue/cache keying. All of these are unproven or broken territory; the workarounds are explicit and must be budgeted.For executives (one paragraph): This is a small, tactically useful enabler, not a strategic dependency. It lets our Laravel application read and write records in systems that identify rows by two or more columns — common in ERP, warehouse, and vendor-owned data — using the ORM's normal find/save/delete API instead of hand-written SQL for every touch point. The relevant decision is not "should we depend on this abandoned 2016 package" (we should not) but "should we fork roughly thirty lines of MIT-licensed code and own it," which is cheaper than building it ourselves and far cheaper than the schema change that vendors and legacy systems will not permit. We should approve it only for a narrow, named set of models, with a designated owner and an upgrade canary test, and we should prefer a surrogate key on every table we actually control.
For engineering: Treat it as vendored source you own, not a dependency that will fix itself. The entire package is a single trait; the one real failure mode is signature drift on setKeysForSaveQuery(), which fails either loudly (PHP fatal on incompatible declaration — fine) or silently (parent method wins and save() returns truthy while updating zero rows — dangerous). Guard that with a query-log assertion in CI. Non-negotiables on the model: $primaryKey is an array, $incrementing = false, $keyType set correctly, and the trait applied on a shared abstract base model so replacement is a one-file change. Prefer query-builder bulk operations and scoped where chains for read paths — they never touch getKeyName() and behave normally — and verify write paths explicitly rather than assuming updateOrCreate, fresh, or refresh work. Benchmark fico7489/laravel-composite-key and a plain surrogate-key redesign before we commit; both are credible and one of them is probably cheaper.
How can I help you explore Laravel packages today?