korridor/laravel-has-many-merged
Adds a hasManyMerged relationship to Laravel Eloquent, letting you combine several hasMany relations across models into one query-backed collection with sorting, limits, and eager loading. Ideal for unified feeds, timelines, and activity lists.
Architecture fit
korridor/laravel-has-many-merged contributes one relationship type (HasManyMerged) that presents N hasMany sources as a single Eloquent relation. No service provider, no config file, no facade, no global query scope, no schema change. From a governance standpoint this is the cheapest class of dependency to approve: narrow blast radius, confined to the model layer, removable without a data-migration project.with(), whereHas, withCount, withSum, pagination, ordering, where constraints — it slots into existing controllers, API resources, and reporting queries. The alternative (hand-rolled collect()->merge()->sortBy() scattered across services, or raw UNION SQL) is exactly the duplicated, inconsistently tested logic that degrades correctness over time. The package's value is standardization, not novelty.whereHas test coverage as far back as 0.0.3. For code that sits inside the query layer, static-analysis friendliness and aggregate correctness matter more than 90 stars.hasMany only — no belongsToMany, no hasOne, no hasManyThrough chains, no field-level fusion (you cannot derive an effective price across two tables). Any roadmap item assuming general relational merging is out of scope and must be labeled as custom work up front, or the first engineer to hit the wall will file it as a bug.Integration feasibility
composer require, one relation method on the model, done. No infrastructure, no backfill, no environment configuration, no data migration.toSql() and a debug toolbar confirm the generated union within minutes. "Does it do what it says" is answered in a spike, not discovered in production.Technical risk
| Risk | Likelihood | Impact | TPM mitigation |
|---|---|---|---|
| Single-maintainer, near-zero adoption (90 stars, 0 dependents, one primary author) | Medium | Medium | Treat as vendored-by-default: pin the exact version, name a fork owner, budget ~1 day/quarter to review upstream activity. The codebase is small enough that forking is a realistic fallback, not a crisis. |
| Abandonment / framework-version gap | Medium (low frequency) | Medium | Tie the review trigger to our framework upgrade ticket, not a calendar date. Exit criterion: no matching major-version release within 60 days of our upgrade → fork or inline. |
| No published benchmarks | High | Medium if placed on a hot path | Require our own measurement on production-shaped data before this touches any endpoint with a latency SLO. |
| In-memory merge at scale | Medium | Medium–High | Mandate DB-level constraints (where, whereHas, limits) plus pagination for any merged set expected to exceed roughly 10K rows. Enforce in code review, not in a wiki. |
| Aggregate and pagination semantics over a union | Medium | Medium | withCount / withSum and pagination over a merged set do not always mean what a reviewer assumes (deduplication, ordering, count semantics). Per-relation test coverage is a merge gate, not a nice-to-have. |
| Non-standard polymorphic or custom foreign keys | Low–Medium | Low | Confirm key naming during the spike. If it does not fit natively, that is a stop signal, not something to work around. |
| Misread as a general relationship-merge tool | Medium | Low | Maintain a "supported / not supported" section in the internal conventions doc and link it from the PR template. |
Net: technical risk is low; supply-chain risk is the one a TPM actually owns — and it is manageable precisely because the dependency is small, MIT-licensed, shallow, and has a clean removal path.
Key questions
hasManyThrough? (Owner: backend.) Decides whether the package fits natively or whether we stop.Stack fit
belongsToMany/hasOne/hasManyThrough; anything requiring derived values across tables. Say no early and in writing.Migration path
hasManyMerged on one model alongside the existing hand-rolled merge, route a single non-critical read-only surface through it, compare outputs (ordering, counts, dedup) against the incumbent, then cut over. Repeat per surface.Compatibility
whereHas behave differently enough to warrant explicit tests. These were fixed and covered as early as 0.0.3, but the semantics of counting and filtering across a union are the single most likely source of subtle regressions. Require per-relation test coverage.Sequencing
toSql(), confirm ordering and dedup match the incumbent, capture latency on production-shaped data.Maintenance
Support
hasMany at call sites, which is the point.Scaling
hasMany would not incur.where, whereHas, and explicit limits must accompany any merged relation. Pagination is mandatory above roughly 10K expected rows. A wiki page will not hold this line; a PR checklist will.withCount and withSum over a union are cheap; hydrating the full merged set is not. Push features toward aggregates where the product only needs a number.Failure modes
| Failure mode | Symptom | Detection | Response |
|---|---|---|---|
| Upstream goes quiet | No release within 60 days of our framework upgrade | Tracked via upgrade ticket, not a monitor | Fork or inline; already-budgeted fallback |
| Memory/latency blowup | Slow or OOM on an endpoint with a large merged set | APM latency alert; load test in spike | Add DB-side filters and pagination; if it still fails, replace with an explicit union query |
| Aggregate/count mismatch | Dashboard numbers disagree with detail views | Per-relation tests; cross-check in pilot | Fix query constraints; do not "adjust" the number downstream |
| Ordering inconsistency across surfaces | Same records in different orders on two screens | Output comparison against incumbent during migration | Standardize ordering in the relation definition, not in each controller |
| Silent version drift | Behavior change after a routine composer update |
Exact version pin; lockfile review | Revert the bump; re-evaluate in isolation |
| Scope creep into unsupported relations | Engineer attempts belongsToMany or derived fields through the merged relation |
Code review against the conventions doc | Stop; route to custom work; log as a scope gap, not a bug |
Ramp-up
toSql(). The generics support means static analysis stays green without extra annotations.How can I help you explore Laravel packages today?