lukascivil/treewalker
TreeWalker is a lightweight PHP 8.1+ library for manipulating nested arrays, objects, and JSON strings interchangeably. It offers diffing, recursive walking with edits/deletion, merging, and dynamic key-path get/set/create operations, with configurable array, object, or JSON output.
Architecture fit. TreeWalker is a single-class, dependency-free utility that normalizes three representations — array, object, and JSON string — behind six methods: getdiff, walker, structMerge, createDynamicallyObjects, getDynamicallyValue, setDynamicallyValue. There is no service provider, facade, config file, or Laravel coupling, so it slots in as a pure library in an application or domain layer rather than as framework infrastructure. The genuinely useful surface for a Laravel product is narrow and opinionated: getdiff() for audit/change tracking, walker() for recursive normalization or redaction of nested payloads, and the dynamic path accessors for form/JSON-schema style editors. structMerge() overlaps with array_replace_recursive, so its marginal value is low unless precedence semantics matter.
Integration feasibility. composer require lukascivil/treewalker and new TreeWalker([...]) is the entire integration. Constructor options (debug, returntype) are per-instance, so a container binding is straightforward but carries a state caveat: returntype is instance-level, meaning a shared singleton forces every caller to the same output type. Feasibility is high; effort is measured in hours, not sprints.
Technical risk. This is a low-adoption package — 73 stars, 0 dependents, a single maintainer, and a 1.0.0 tag published only in April 2026 after a long unversioned 0.x lineage. Combined with the low composite score (13.05), the correct TPM posture is "useful but unconsumed": treat it as convenience code, not as a de facto standard. Specific risks worth validating in a spike:
$struct1 (b, oi) are reported under new, while oi2 — present only in $struct2 — is reported under removed; edited.casa shows oldvalue: 2 (struct2) and newvalue: 1 (struct1). If this reflects real behavior, "new" and "removed" are inverted relative to the argument order, which is the single highest-consequence defect for an audit-log use case. Must be characterized against real fixtures before adoption, not assumed from the README.edited/new/removed output rather than an LCS-style move/edit narrative. Unacceptable for diffing collections or ordered JSON arrays without a normalization layer."5" vs 5, 0 vs null, and int/float drift produce phantom edits. No strict-mode flag is documented.cafeina/ss); JSON keys containing / will collide and produce ambiguous audit records.walker() passes the parent and value by reference — powerful, but a caller-side footgun that can mutate structures outside the intended scope.{} vs [] collapsing) are unresolved without reading source.Key questions for the maintainer spike.
getdiff($a, $b), which argument is authoritative — is new derived from $a or $b? Can we get a documented contract plus a fixture test we control?returntype: "object" return stdClass recursively, and are numeric keys preserved?Stack fit. Compatible with any Laravel application on PHP 8.1+ (Laravel 9+ on 8.1, Laravel 10/11/12 comfortably). No conflicting Composer constraints, no framework bootstrapping required. It belongs behind an application-owned interface — PayloadDiffer / NestedStructureWriter — with TreeWalker as the implementation, not as a type-hinted dependency sprinkled through controllers and jobs.
Recommended binding pattern.
TreeWalker as a singleton only for read-only use, or bind a factory that returns a fresh instance per call site so returntype never leaks across consumers. Prefer the factory.app/Contracts, implement it with TreeWalker in app/Support, and expose a normalizer that resolves argument order into unambiguous added/removed/changed keys. This is one small class and it is what converts an inverted-semantics risk into a non-event.new/removed/edited shape to API responses or downstream systems — re-key it at the boundary.Migration path. No migration is required for greenfield use. If replacing an existing custom recursive diff (very common in Laravel codebases for audit trails and webhook comparison), the path is characterization-first:
null values, reordered lists, numeric-keyed maps, keys containing /, nested depth ≥ 8.new/removed and normalizes list handling.Compatibility and sequencing.
^1.0 in composer.json; the 1.0.0 semver cut is fresh and the 0.x history shows frequent behavioral commits, so avoid dev-master and avoid *.symfony/serializer normalization plus a hand-rolled recursive differ, or sebastian/diff on canonicalized JSON strings, are credible exits. The wrapper interface is what makes that exit a one-file change.Maintenance. Zero runtime dependencies means a shallow dependency graph and near-zero transitive vulnerability exposure — a genuine operational plus for a platform already carrying dozens of libraries. The counterweight is bus factor: one maintainer, 73 stars, 0 dependents, and a first stable release only just shipped. CI, Codecov, and semantic-release are now in place, which is a positive maturity signal, but "has CI" and "is battle-tested" are different things. Budget: near-zero ongoing effort; pin and let Dependabot surface releases. Because the package is one class, vendoring or forking is cheap insurance and should be pre-approved as a fallback so an abandoned upstream is a same-day fix rather than a scramble.
Support. Community issues only, no SLA, no commercial backing. Internalize the API contract in your own tests so maintainer silence does not block you. The absence of dependents also means fewer people hitting and reporting edge cases — expect to be the one discovering them.
Scaling. Recursive, fully in-memory traversal; cost is proportional to payload size, not diff size. Practical guardrails: reject or offload payloads above a threshold (start at ~256 KB for synchronous requests, queue anything larger) and never run diffs on user-supplied unbounded JSON inside a web request. Flat slash-delimited output grows with the number of differences and can explode for list-heavy structures — cap emitted entries and truncate for UI. Diffing is CPU-light relative to memory, so horizontal scaling is not the constraint; per-request memory ceilings are. If diffs become hot-path (e.g., every model save), compute asynchronously and persist the diff rather than recomputing.
Failure modes to monitor.
added/removed re-keying layer."5" → 5), which quietly poisons audit fidelity./, merging distinct paths in flat output.returntype: "jsonstring" and a later consumer receives a string it expected to be an array — a subtle, environment-dependent class of bug that tests often miss.{} vs []) breaking downstream schema validation.Instrument the diff feature with structured logging (payload size, node count, elapsed time, emitted diff counts) so the first two failure modes are visible in production rather than discovered by an auditor.
Ramp-up. Very low — an engineer is productive in under a day from the README alone, and the method table is genuinely complete. The ramp-up cost is not learning the API, it is learning the semantics: direction of getdiff, list behavior, and walker() mutation scope. Mitigate by writing a one-page internal reference with the characterized behavior and golden fixtures, and require it be consulted before any new call site. Total onboarding for the team: one short walkthrough plus the adapter's contract tests. The real organizational cost is governance, not learning — keeping an unconsumed, single-maintainer library behind a seam so that adopting it stays a convenience decision rather than an architectural commitment.
How can I help you explore Laravel packages today?