jiaxincui/closure-table
Closure table implementation for Laravel Eloquent models, adding fast hierarchical tree queries and operations—ancestors, descendants, siblings, moving nodes—without recursive queries. Simple trait integration with support for multiple trees.
Architecture fit The package implements the closure-table pattern as an Eloquent trait plus a companion table. From a program standpoint this is a build-vs-buy decision on commodity plumbing: hierarchy (categories, org charts, menu/permission trees, comment threads, location pickers) recurs across roadmaps, and each occurrence otherwise triggers a design spike on recursive queries, cycle prevention, and subtree moves. Adopting the package converts that into a migration plus a trait, and collapses "all descendants of X" / "path to root" into a single indexed join rather than recursive CTEs or N+1 walks — a p95 and database-CPU commitment, not an implementation detail. The counterweight is architectural, not cosmetic: every insert, move, and delete rewrites ancestor–descendant rows for the affected subtree. Adoption is therefore a read/write ratio decision that must be made with product input before commitment. It fits read-heavy, moderate-depth, occasionally-restructured trees; it does not fit high-frequency drag-and-drop reorg, bulk taxonomy imports, or true graph problems (multi-parent DAGs, dependency resolution).
Integration feasibility Low ceremony and small blast radius: one migration, one trait, configurable table/column names, and opted-in models remain ordinary Eloquent models for every other concern. Existing relationships and query-builder usage keep working, so integration risk is confined to models we deliberately enroll. Multi-tree support (several independent roots in one table, keyed by tenant or context) is a genuine product enabler — frequently the difference between shipping to all customers now versus a schema migration in six months. Friction to plan for: the trait only applies to models we control, so legacy or vendor tables need adapter models; there is no first-class caching, no bulk-mutation API, and no admin-panel or API-resource integration — all of that becomes our scope. Documentation beyond the README is thin and release notes are partly in Chinese, so internal onboarding material is a deliverable, not an afterthought.
Technical risk Ranked for the program risk register:
getTree() output for multi-tree, v3.0.0 "standardized method signatures and return values," plus repeated fixes for string/UUID IDs (3.0.2, 3.0.5), toTree() returning empty (3.0.4), getRoot() ordering nondeterminism (2.1.7), and table-prefix handling (2.1.8). Plan for minor upgrades to require code changes; pin exact versions.Key questions to resolve before commitment
Stack fit
Laravel + Eloquent, PHP 8.x, Laravel 9–13. Pin v3.0.8 or later — do not adopt pre-3.0.2 (string/UUID primary key support landed there), and toTree() was returning empty as recently as 3.0.4. String/char IDs matter for our conventions, so the floor is firm. Confirm the package's Laravel support matrix against our own CI before scheduling, and treat the 3.0.8/Laravel 13 claim as unverified until proven in our pipeline.
Migration path Two tracks, sequenced deliberately:
parent_id column in a batched, idempotent job, run in a maintenance window or behind a feature flag with dual-read verification. The same script that backfills is the rebuild-from-adjacency-list repair tool — build it once, keep it in the runbook, and it doubles as the exit path if we ever leave the package.Compatibility The package's shape is a trait plus a table; our compatibility surface is (a) the Eloquent/Laravel versions we run, (b) our primary key strategy (auto-increment vs. UUID), and (c) our column-naming conventions, since ancestor/descendant columns become part of our schema. Declare compatibility explicitly in an internal ADR and add the package to the dependency-review process so upgrades are gated, not automatic.
Sequencing (recommended)
(ancestor, descendant) on day one, a uniqueness constraint on the pair, transaction wrapping for all mutations, and circular-reference validation at the service boundary.Non-goals to state up front: no bulk tree surgery inside a request cycle (route through queued jobs), no graph/DAG use cases, no vendor or API exposure of closure columns.
Maintenance We are the de facto maintainers. Budget: a named owner, a pinned version, periodic review of upstream releases, and a fork-on-demand capability. Keep a "rebuild the tree from the adjacency list" script in the runbook — it is both the corruption recovery tool and the migration-off path. Hide the package behind an internal repository/service interface so a fork or replacement touches one layer, not the application.
Support
There is no vendor, no SLA, and no security review trail from upstream. Support plan is literally "read the source." Escalation path is internal: the owning engineer, then the fork. Add the dependency to our OSS inventory with an explicit risk acceptance note rather than letting it arrive quietly in a composer require.
Scaling Reads are the strength — single indexed joins, no recursive CTEs, which keeps drill-down screens inside latency targets without bolting on a cache tier. Writes are the constraint: subtree moves and deletes scale with subtree size, and storage scales with nodes × depth. Operating thresholds to agree with engineering before launch:
Failure modes
Ramp-up Low-to-moderate. The API is fluent and the pattern is well understood; the cost is not learning the package but learning the failure modes, which are undocumented upstream. Ramp plan: internal onboarding doc (README is thin, notes are partly Chinese), the integration test suite as executable documentation, one engineer trained as the tree owner, and a review checklist for any PR touching hierarchy (transactions, cycle check, no raw deletes, no synchronous bulk moves).
Product-facing note for stakeholders: no new UI, no user-visible behavior change — this makes deep category trees, org drill-downs, and comment threads load reliably fast and unblocks deferred nested-content roadmap items. The one visible change: reorganizing very large trees becomes asynchronous, so bulk moves will feel slower than a single drag-and-drop.
How can I help you explore Laravel packages today?