Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eloquent Param Limit Fix X Laravel Adjacency List Laravel Package

staudenmeir/eloquent-param-limit-fix-x-laravel-adjacency-list

Bridge package that lets staudenmeir/laravel-adjacency-list work with the eloquent-param-limit-fix, avoiding database parameter limits when recursive adjacency list queries generate large numbers of bindings.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a bridge, not a feature. It only earns its place if you already have both parents in the graph and a tree query that crosses a driver's bind-parameter ceiling. Start by proving that, not by installing.

1. Confirm the conjunction.

composer why staudenmeir/laravel-adjacency-list
composer why staudenmeir/eloquent-param-limit-fix

If either comes back empty, stop — the bridge is dead weight. If both are present, find the models using HasAdjacencyList:

grep -rn "HasAdjacencyList" app/ database/ tests/

2. Install and resolve all three together.

composer require staudenmeir/eloquent-param-limit-fix-x-laravel-adjacency-list
composer update staudenmeir/eloquent-param-limit-fix-x-laravel-adjacency-list \
  staudenmeir/laravel-adjacency-list \
  staudenmeir/eloquent-param-limit-fix -W

The -W (--with-all-dependencies) is not optional here. A partial upgrade that advances the bridge but leaves a parent behind is the most common way to end up with an unsatisfiable resolution. Then read the resolved constraints rather than trusting release notes:

composer show staudenmeir/eloquent-param-limit-fix-x-laravel-adjacency-list

3. Look at the source first. It's a very small package — one shim, essentially. Open vendor/staudenmeir/eloquent-param-limit-fix-x-laravel-adjacency-list/src/ and find the exported trait. The integration surface is a single trait swap: the bridge exports a trait with the same role as HasAdjacencyList, in the bridge's own namespace. Do not guess the FQCN from blog posts — read the file, then use that one. There is no service provider, no migration, no publishable config, no container binding.

4. Reproduce the failure before you claim the fix. The best harness is SQLite in a test, because the ceiling is easy to hit and the failure mode is unambiguous.

public function test_deep_tree_descendants_exceed_param_limit(): void
{
    // SQLite's SQLITE_MAX_VARIABLE_NUMBER is 999 on older builds,
    // 32766 on 3.32+. Check yours before assuming a threshold.
    $root = Category::factory()->create();

    $parents = [$root->id];
    for ($depth = 0; $depth < 1200; $depth++) {
        $parents = [Category::factory()->create(['parent_id' => $parents[0]])->id];
    }

    $this->assertSame(1200, $root->descendants()->count());
}

Run it against the un-swapped model first. You want to see too many SQL variables (SQLite) or the 2,100-parameter error (SQL Server) — otherwise you're installing test-fidelity insurance and should say so in the PR description.

5. First real use case. The canonical one: a Category/Comment/OrgUnit model with parent_id, where a subtree endpoint or a whereDescendantOfMany() filter blows up on a parameter-constrained driver. Swap the trait, keep everything else identical, re-run the suite.

Implementation Patterns

Keep the trait swap boring and explicit. Put a comment on the model so the next developer knows why the namespace looks odd:

use Staudenmeir\EloquentParamLimitFixXLaravelAdjacencyList\Concerns\HasAdjacencyList; // verified FQCN

class Category extends Model
{
    use HasAdjacencyList;

    public function getParentKeyName(): string { return 'parent_id'; }
}

If the package's trait name differs from HasAdjacencyList, alias it on import so call sites and muscle memory stay intact.

Subtree endpoints — eager load, then assemble in PHP.

Route::get('/catalog/{category}', function (Category $category) {
    return CategoryResource::collection(
        $category->descendantsAndSelf()
            ->withDepth()
            ->orderBy('path')      // always explicit — see Gotchas
            ->get()
            ->toTree()
    );
});

toTree() builds the hierarchy in memory, so it needs the complete contiguous set. That's fine for one subtree; it's wrong for "the whole taxonomy" once the table grows.

Bulk filtering with a large ID set — the case the package exists for.

$allowedRoots = $user->permittedCategoryIds();   // may be thousands of IDs

$visible = Category::query()
    ->whereDescendantOfMany($allowedRoots)
    ->whereIsLeaf()
    ->paginate(50);

This is exactly where the chunking kicks in: the seed whereIn gets split into multiple statements under the driver's ceiling. Keep the set as IDs, not models — passing a large array of hydrated models through the same path multiplies the bind count.

Breadcrumbs via ancestors. Ancestor sets are tiny (bounded by depth), so these never need the fix — but they flow through the same trait, which is the point of using the bridge rather than hand-patching one method:

$trail = $page->ancestorsAndSelf()->orderByDepth()->get();

Dashboard / reporting counts. Prefer a single aggregate over withCount per node:

// Good: one extra query for the whole page.
$roots = Category::whereIsRoot()
    ->withCount('descendants')
    ->get();

// Avoid on tree pages: N+1 across every rendered node.

Testing pattern. Centralise fixture building so the deep/wide tree is one call, and always assert both correctness and query volume:

DB::enableQueryLog();
$results = Category::whereDescendantOfMany($ids)->get();
$this->assertLessThan(20, count(DB::getQueryLog())); // chunk count is visible here

The fix converts a hard failure into extra round trips. Make the round trips a number in a test, not a surprise in a dashboard.

Upgrade workflow. Treat bridge + laravel-adjacency-list + eloquent-param-limit-fix as a single upgrade unit. Group them in Renovate/Dependabot so a bot PR can't advance one without the others. On a Laravel major, bump the framework and this bridge in the same changeset — a Laravel 13 upgrade without a v1.4-equivalent bridge fails resolution, and if either parent lacks a matching release, Composer silently resolves the bridge back and the upgrade stalls.

Gotchas and Tips

The trait conflict is the whole reason this exists — don't defeat it. Using the upstream HasAdjacencyList alongside the upstream param-limit traits reintroduces the bug: adjacency-list's custom query builder path bypasses the chunking traits. Use the bridge's trait on every hierarchy model, and remove the raw upstream import so nobody re-adds it.

Chunking changes ordering, not just query count. Once a whereIn is split into N statements, result order across chunks is only as deterministic as your ORDER BY. Any endpoint whose order matters — menus, nested comment threads, sitemaps — needs an explicit orderBy. If your model exposes a materialised path column, sort by it; otherwise sort by parent_id, id and build the tree in PHP.

A recursive CTE is not chunked — only the seed is. The parameter fix helps the initial ID list. Deep recursion is still deep recursion: PostgreSQL and MySQL will happily recurse but slowly, and a missing index on parent_id turns a 5-level tree into a full scan per level. Add the index before blaming the package:

$table->foreignId('parent_id')->nullable()->index();

Soft deletes orphan subtrees. If Category uses SoftDeletes, a descendants() call rooted at a soft-deleted node behaves differently from .withTrashed() on the relationship. Decide explicitly whether a deleted parent's children stay reachable, and test it — this is the most common correctness bug in adjacency-list applications, and the bridge does nothing to help.

SQLite's ceiling is version-dependent, so CI and local can disagree. Older SQLite builds cap at 999 bind parameters; 3.32+ raises it to 32,766. If your local SQLite passes but CI fails, the difference is the binary, not your code. Pin the extension version in the test image, or run the reproduction test against a deliberately small chunk threshold.

Only SQLite and SQL Server are genuinely constrained. MySQL/Postgres ceilings are high enough (thousands to ~65k placeholders, plus packet-size limits) that most trees never hit them. Keep that in mind when explaining the dependency: for a Postgres-only production setup, this is a test-fidelity package, and its value is "CI matches production behaviour," not "the app gets faster."

Zero dependents and 11 stars is not a quality signal here — it's a scoping signal. This is a single-maintainer "x" metapackage; the maintainer is the same person who owns both parents. Read the source once (it's one class), pin it deliberately, and keep a documented fork contingency, because the worst-case remediation is vendoring a shim you could rewrite in an afternoon.

Don't add a fourth staudenmeir package. The package name encodes exactly two parents. Introducing a third (e.g. a belongs-to-through variant) can render resolution unsatisfiable — a dead-end that looks like a Composer bug and isn't.

Verify widening vs. pinning in composer.json, every release. Check whether the bridge permits a range of parent versions (safe, flexible) or pins a specific pair (forces lockstep upgrades for everyone). The answer changes your upgrade policy, and release notes never state it.

Watch for retirement. If a future upstream major absorbs the parameter-chunking behaviour into adjacency-list itself, this dependency should be removed rather than carried indefinitely. Put that question on the same checklist as each Laravel major bump.

Cache aggressively, but invalidate on any tree mutation. Tree endpoints are query-heavy by construction, and the fix makes them more round-trip-heavy, not less. Cache::remember on the assembled tree, and flush the key on create/update/delete of any node in that root's subtree — not just the node itself.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource