spatie/invade
Spatie's invade function lets you read, write, and call private properties and methods of any PHP object, including static ones, through a simple proxy—handy for testing or working with legacy code.
invade() lets a test read private state and call private helpers directly, which converts "untestable" modules into candidates for characterization tests — and therefore makes the refactor-vs-rewrite-vs-leave-alone call on legacy modules an evidence-based decision rather than a gut call.composer require spatie/invade) gets you the PHPStan extension that suppresses false "accessing private property" errors, an existing test suite, and Spatie's maintenance — at zero license cost. Building gets you no new dependency and total control. Either answer is defensible; the package makes "buy" cheap enough that it rarely warrants a design meeting.tests/, forbidden in app/ and runtime service code. That rule can be enforced mechanically (PHPStan rules, architecture tests, namespace restrictions in CI), which makes it a policy decision with a cheap enforcement mechanism rather than a documentation-only guideline.@internal accessors, or widened visibility purely so tests can assert on state. Deciding to allow invade() in tests removes that pressure and keeps production APIs honest — a real API-surface decision that affects backward-compatibility obligations later.invade() is a fast inspection primitive. That supports tooling decisions such as whether to ship it in dev-only dependencies and expose it through an internal debug command or Tinker workflow.Consider it when:
require-dev.Look elsewhere (or don't adopt) when:
app/" as a signal of a missing abstraction, not a tooling gap.uopz/refactor territory; invade() reads state, it doesn't fake collaborators.tests/Support/invade.php is the lower-friction answer.invade($obj)->property trades type safety for ergonomics; heavy reliance will erode static-analysis value across a large test suite.Adoption signals worth weighing: 341 stars and a reported zero dependents indicate a small, niche footprint — this is a convenience utility, not an ecosystem commitment. Maturity signals (README, changelog, releases, PHPStan extension, dependabot upkeep, MIT license) suggest low but non-zero abandonment risk; Spatie's track record mitigates it, and the fallback is a self-authored helper. Note also the global-namespace invade() declaration: the changelog confirms it is guarded against redeclaration, but in a large monolith with many dev dependencies, namespace collisions are a real (if small) integration risk worth a one-line smoke test in CI.
For executives — one paragraph: spatie/invade is a free, MIT-licensed, single-function PHP utility that lets our tests inspect and exercise the internals of code we don't control — vendor SDKs, framework internals, and legacy modules with no public accessors. The business value is test coverage velocity on legacy surfaces: it turns "we can't safely test or refactor this" into "we can pin current behavior with tests this sprint," which de-risks modernization work and shortens the path from untested code to safely refactorable code. Cost is a Composer line item in require-dev and roughly an hour of setup; the downside is bounded because the entire API is one function we could reimplement ourselves in a few dozen lines if the package were ever abandoned. The one non-negotiable is scope: tests only, never production runtime code, enforced in CI. Approve as a low-cost, low-risk developer-productivity dependency with a defined guardrail, not as an architectural commitment.
For engineering — the practical case: Instead of hand-rolling ReflectionClass/Closure::bind helpers in every project (and then fighting PHPStan's private-access errors), we standardize on one maintained, tested implementation with a PHPStan extension that keeps static analysis green. Usage is invade($obj)->privateProperty, invade($obj)->privateMethod(), plus static equivalents via invade(MyClass::class)->get()/set()/method()->call(). It was extracted from Livewire's own internals, so the pattern is already proven in production-grade PHP. Constraints we accept: it breaks encapsulation by design, so it lives behind a tests/ namespace restriction and a review rule; it does not substitute for mocking frameworks; and it should be a signal to add a seam, not a permanent crutch. If dependency review objects, the fallback is a ~20-line internal helper with an identical call signature — zero migration risk either way.
Recommended next step: one spike in a single service — adopt in require-dev, restrict to tests/, add an architecture test that fails on any invade() reference under app/, and measure whether the first two legacy modules reach "covered and refactorable" faster than the current approach.
How can I help you explore Laravel packages today?