staudenmeir/eloquent-eager-limit-x-laravel-adjacency-list
Compatibility package combining staudenmeir/laravel-adjacency-list with eloquent-eager-limit, letting Laravel apps cap eager-loaded relations on recursive adjacency list queries. Adds the missing trait support so hierarchical models can limit eager loads per parent without N+1 queries.
Architecture fit. This is a bridge, not a framework: it makes staudenmeir/eloquent-eager-limit (per-parent LIMIT via ROW_NUMBER() OVER (PARTITION BY ...)) compose with staudenmeir/laravel-adjacency-list (recursive relationships over parent_id). The fit is high and narrow — Laravel applications that already store hierarchies as adjacency lists and need bounded, presentational reads: latest N replies per thread, N featured children per category, capped org reporting lines, two-level menus. The fit is zero if the hierarchy is a nested set, closure table, or materialized path, or if it hasn't been modelled yet.
The real architectural fact for a TPM is the coupling: domain model → bridge → two upstream packages → Laravel version → database engine with window functions. That is a five-way compatibility matrix behind a single composer require, and tracking it — not reviewing trait syntax — is the TPM job here.
Integration feasibility. Code effort is genuinely low: composer require, one trait on the target model, standard eager-load closures with limit(). No schema migration, no backfill, no dual-write. The work is validation, and it should be treated as a spike with explicit exit criteria rather than an implementation task.
Technical risk, ranked by likelihood × blast radius:
composer.json for the bridge and both upstream packages. A stale bridge either blocks our next framework upgrade or forces us to fork and own it.Key questions to close before speccing the feature:
limit() compose with recursive relationships at depth 2+, or only for direct children? Test and document the depth ceiling.EXPLAIN show index usage on the parent column?Stack fit. Adopt only where three conditions hold simultaneously: the tree is already an adjacency list; the requirement is bounded and presentational (latest N, first N, two-to-three-level preview); and the database supports window functions. If any of the three fails, stop — the package buys nothing and adds a dependency.
Migration path. There is no data migration. The path is: add the trait to one model in one bounded context, keep the existing query path intact behind a thin internal wrapper (a repository method or Eloquent scope), and switch via feature flag. The wrapper is the important engineering instruction — it makes swapping in a hand-rolled ROW_NUMBER() scope a one-file change instead of a call-site sweep, and it converts an invasive trait dependency into a reversible one.
Compatibility sequencing. Do these in order; each can kill the initiative cheaply:
composer.json for bridge and both upstreams; map Laravel/PHP constraints against our target version. If Laravel 11/12 is unsupported, decide now between staying pinned, forking, or writing the scope ourselves — that decision belongs before the sprint, not in it.Compatibility risk to accept explicitly. Three packages must move in lockstep against a moving framework. If we adopt, we accept an obligation to re-evaluate at every Laravel major. Write that into the team's upgrade checklist the day we merge, or it becomes an emergency later.
Maintenance. Low ongoing code maintenance, non-trivial upgrade maintenance. The dependency is small and stable, but it is pinned and pre-2.0, and the bridge trails upstream releases. Assign a named engineer accountable for the pinned version and for the Laravel-major re-check. Budget a fork contingency: the fallback is a hand-written ROW_NUMBER() OVER (PARTITION BY parent_id) scope — roughly the same effort as the integration, minus the dependency. That fallback should be documented before adoption, not after abandonment.
Support. No SLA, no security policy, no commercial backing. Support model is "read the upstream source" — the bridge's own release notes are thin or inherited. That's acceptable for a small team absorbing the dependency; it is not acceptable where a published stability guarantee is a hard requirement. Escalation path is internal ownership, not vendor contact.
Scaling. Two pressure points. First, fan-out: a hub node with thousands of children under a recursive CTE plus window function is where latency appears; benchmark that case specifically. Second, depth: cost compounds with recursion depth, and limit() may not compose below direct children. Both should be encoded as guardrails — a documented depth ceiling and a monitored query-count/p95 budget per request — rather than discovered in production.
Failure modes, in order of how quietly they fail:
limit() not applying below direct children, producing over-fetch that looks like working output. Mitigate with explicit depth-2+ tests.EXPLAIN review pre-merge.Ramp-up. Low: an Eloquent-fluent engineer can integrate in hours, and the mental model (traits + eager-load closures) is familiar. The learning cost sits in the validation, not the API — window-function SQL, recursive CTE query plans, and scope interaction. Plan a short time-boxed spike on one model in one bounded context, behind a flag, with query count and p95 measured against realistic data. If the spike passes, widen deliberately. Note the scoping line for roadmap reviews: this supports bounded-depth, bounded-fan-out presentation. It does not support hierarchy analytics, shortest-path, cycle detection, per-tree paginated totals, or recommendation traversal — those need their own evaluation and should stay off the near-term roadmap regardless of how well the pilot goes.
How can I help you explore Laravel packages today?