sebastian/object-enumerator
Traverses arrays and object graphs to enumerate every referenced object, including nested structures and circular references. A small utility from Sebastian Bergmann's PHP testing stack, commonly pulled in by PHPUnit to walk object graphs safely.
Architecture fit — a utility, not a component. sebastian/object-enumerator is one final class with one cycle-safe method: enumerate() walks arrays and object graphs, reflects into non-public properties, dedupes by object identity, and returns a flat array of reachable objects. There is no service provider, no config, no facade, no Laravel coupling. That makes it perfectly compatible and almost entirely irrelevant to application architecture. Its true position in our dependency graph is as a transitive artifact: PHPUnit pulls it in via sebastian/comparator to find mocks and walk test arguments. The 6,527 stars and the Dependents: 0 figure should both be read correctly — the former reflects reflected glory from the PHPUnit ecosystem, the latter is a Packagist metadata artifact, not an adoption signal.
Integration feasibility — mechanically trivial, strategically thin. composer require --dev succeeds in seconds and resolves to a version we already have. So the TPM question is never "can we install this," it's "is there a named caller?" Value is only realized at a direct call site, and in a Laravel codebase that means exactly two shapes: custom test assertions (graph-shape, retained-reference, leak checks) or an internal, non-production inspector command. Absent one of those, this is capability inventory, not a project. The 42.3 score and 56.2 opportunity score corroborate that read: high utility, low leverage — infrastructure to consume, not an investment to champion.
Technical risk — low at runtime, moderate at the version boundary. Three items belong on the risk register before this becomes a decision:
sebastian/* family this phrasing conventionally signals a minimum-version bump, but it is genuinely ambiguous in this text. Do not plan a PHP upgrade on either interpretation — verify against the installed require constraint in composer.json first. This is a one-command check and a hard gate.Behavioral note: 8.1.0 fixed infinite recursion on self-referencing arrays and materially improved large-graph performance. Anything pinned below that inherits a known flakiness class.
Key questions
composer show sebastian/object-enumerator already report, and which PHPUnit major is resolving it?Arr:: helpers, json_encode, or Symfony VarDumper genuinely cannot provide? (Usually the answer is no; when it's yes, it's decisive.)tests/ or app/? The answer must be tests/ unless a non-production-only guard exists.Stack fit — dev-only, deliberately. require-dev, consumed from a test helper trait or a local/CI-gated artisan command. It should never appear in a production code path. Be explicit with stakeholders about what it is not: not a replacement for dump()/VarDumper (which produces human output), not a deep-clone or serialization mechanism (native or dedicated serializers), not an ORM or container introspection substitute. Teams routinely reach for graph enumeration expecting value comparison or deep-copy semantics and get burned; write that line down.
Migration path — inventory before install. There is no migration, only a two-step promotion. Step one is an audit, not a change: confirm transitive presence and record the resolved version from composer.lock. Step two, triggered only by a direct call site, is promoting it to an explicit require-dev entry. Promotion is worth doing at that point because it converts an implicit dependency into a declared one and protects us if PHPUnit restructures its tree — but doing it speculatively just buys us a constraint we have to maintain.
Compatibility
require constraint. Verify per CI matrix entry; re-verify on every PHP minor and major upgrade. This is the recurring tax, and it is small but non-zero.phpunit/phpunit loosely, pin this package to whatever PHPUnit already resolves, and never pin it independently. Independent pinning is the only way to manufacture a conflict here.Sequencing
spl_object_id set comparison, Arr:: helpers, assertSame on identity) are insufficient. If step 2 is unconvincing, stop here and record the outcome. Most teams should stop here.Tests\Support\ObjectGraphAssertions trait or a Support\ObjectGraph helper with intent-named methods (assertGraphHasOnly, assertNoLeakedObjects). Wrap before first use, not after. The API is one method today, but the wrapper is our exit ramp and our documentation, because upstream offers only a README and release notes.require-dev at the version PHPUnit already resolves; add CI coverage at our minimum and maximum PHP versions.Deliverable framing for planning: steps 1–2 are an afternoon of engineering judgment; steps 3–5 are one to two days including review. Size accordingly and do not open a multi-sprint epic.
Maintenance — near-zero for the package itself. It is a leaf utility released in lockstep with PHPUnit majors, and we inherit PHPUnit's testing of it as a downstream consumer. The residual cost is ours and is confined to two things: the wrapper, and a small review task at each PHPUnit major bump confirming the transitive version still satisfies both our constraint and our PHP floor. Budget effectively zero ongoing effort; call it an hour per test-framework major.
Support — no first-party channel, no SLA, effectively a single maintainer. That is acceptable precisely because we never depend on it in production, and because PHPUnit exercises it heavily upstream — it is crowd-tested by the ecosystem at large. The wrapper converts any upstream behavioral shift from an investigation into a one-file change. I would not sign off on this dependency for a production path; for tests/, the support profile is fine.
Scaling — negligible for ordinary suites, with two identifiable cliffs. Graph size: a fully hydrated Eloquent aggregate or a thousand-entry service container will burn time and memory, and lazy-loaded proxies and "ghost" models will silently inflate what gets enumerated. Call frequency: per-assertion enumeration across a large suite multiplies the cost invisibly. Mitigations are scope-limiting the input before enumeration, keeping calls out of hot loops, and — because the failure looks like random CI flakiness rather than a graph problem — having the wrapper name its call site in any error path.
Failure modes
dump() would have answered the question in five seconds. Reinforce in the note; this is the most likely actual cost of adoption.Ramp-up — trivial: one class, one method, roughly fifteen minutes for a PHP-fluent developer. Skip formal training entirely. The real cost is discoverability, since there is nothing beyond a README; recommend a single 30-minute pairing session when the first real use case lands, and budget it against that use case rather than as general enablement.
TPM recommendation: Do not adopt as a direct dependency now. Record it as a pre-approved, already-installed, dev-only capability gated on a named caller in test tooling or internal inspection. Revisit only if we commit to custom graph assertions or an object-graph inspector — at which point the wrapper (step 3) is the deliverable and the dependency is a formality. Success metric is not adoption; it's whether we ever needed to write custom recursive traversal code. If we didn't, this package did its job from vendor/ without us.
How can I help you explore Laravel packages today?