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

Invade Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (test-time dependency, usually):

composer require --dev spatie/invade

There is no service provider, no config file, no publishable assets, no facade. Composer autoloads a single global invade() function (guarded by function_exists, so it won't explode if you ever define your own). If you only poke at objects from tests, Tinker, or a local-only debug command, keep it in require-dev; if you ship a debug route, move it to require.

Where to look first:

  1. The README "Usage" block — the entire public API is four lines long.
  2. src/ — an invade() function plus an instance Invader (property read/write, method call via magic __get/__set/__call) and a StaticInvader (get() / set() / method()->call()).
  3. CHANGELOG — v2.0.0 replaced the Reflection implementation with bound closures, and v2.1.0 added static support. Anything below ^2.1 won't have invade(Class::class).
  4. The bundled PHPStan extension (see Gotchas — it matters more than you'd think).

First use case: you have a Laravel service with no getter, and a test that needs to prove the side effect happened.

final class ImportReport
{
    private array $failures = [];   // no accessor on purpose

    public function run(array $rows): void { /* ... */ }
}

it('records failures without throwing', function () {
    $report = new ImportReport();
    $report->run([['id' => null], ['id' => 5]]);

    expect(invade($report)->failures)->toHaveCount(1);
});

That's the sweet spot: one line, no ReflectionClass, no setAccessible(true) boilerplate, and the intent reads clearly in a diff.

Implementation Patterns

1. Testing state, not internals, as the default. Use invade() for assertions on state that has no public surface, and keep using real public APIs to arrange state. Reaching in to set up and then asserting via the public API is the more robust order — it fails for the right reason when someone refactors.

2. Seeding a hard-to-reach state instead of mocking the world. Classic case: a client object whose private $attempts counter or private $token you can't set. One assignment beats a five-line Mockery chain and keeps the real code path under test.

$client = new ApiClient($config);
invade($client)->token = 'expired-token';   // force the refresh branch
expect($client->get('/me'))->toBeArray();

3. Calling a private method to unit-test extracted logic. Legal, occasionally useful when a big public method fans out into a private one, but treat it as a smell alarm: if you're calling invade($x)->parseRow(...) in many tests, parseRow wants to be its own class with a public method. Invade makes that pressure invisible, so review it in PRs.

4. Static state (2.1+). Registries, singletons, and static caches are common in older Laravel code:

invade(CacheRegistry::class)->get('drivers');
invade(CacheRegistry::class)->set('drivers', []);
invade(CacheRegistry::class)->method('resolveDriver')->call('redis');

Note the asymmetry: statics take a class-string and use get()/set()/method()->call(); instances take an object and use property syntax. Don't expect the instance style to work on a class-string or vice versa.

5. Laravel framework / package introspection. The highest-value everyday use is post-upgrade smoke tests against framework or third-party internals you don't control: assert a Spatie package still populates its private cache, that a queued job's private payload survived serialization, that Livewire-style components still hold the internal state you depend on. This is the "canary" pattern — one invade assertion catches a silent breaking change that a green test suite would otherwise miss.

6. Tinker / debug commands. In php artisan tinker, invade($container->make(Foo::class))->cache is faster than writing (fn () => $this->cache)->call($obj). If you do this often, wrap it: a tiny App\Support\Peek helper keeps invade out of your domain code and greppable in one place.

7. Tidy assertion helper. Avoid sprinkling raw property syntax through a suite:

function privateState(object $object, string $property): mixed
{
    return invade($object)->{$property};
}

expect(privateState($report, 'failures'))->toHaveCount(1);

8. Always reset static state. Any test that calls invade(SomeClass::class)->set(...) must restore the original in afterEach/tearDown. Static mutations leak across tests in the same PHP process, producing the worst kind of order-dependent failures.

Gotchas and Tips

  • Protected members are incidental, not contract. The instance invader's closure is bound to the object's own class scope, so protected (and that class's own private) members are reachable in practice. Don't build on it — the documented surface is private, and the internals have changed once already (Reflection → closures in 2.0).
  • Parent-class privates are out of scope. Binding happens against the concrete class, so a private property declared in a base class isn't accessible from a subclass instance. Symptom: a confusing "cannot access private property" error even though the property clearly exists one extends up. Workaround is a small ReflectionProperty against the declaring class — at which point, consider whether the design wants a getter.
  • Typed and readonly properties bite. Writing a string-typed property with an int throws TypeError; reading a typed property that was never initialized throws Error: must not be accessed before initialization; writing a PHP 8.1 readonly property throws. None of these are invade-specific, but they surface as raw PHP errors rather than friendly test failures, so wrap or assert deliberately.
  • Arrays come back by value. invade($o)->items[] = 'x'; silently does nothing useful — the array is a copy. Read it, mutate it, assign it back: $items = invade($o)->items; $items[] = 'x'; invade($o)->items = $items;. Objects inside the array are still handles, so mutating those works.
  • Missing property names fail quietly when __get exists. On a class with a magic getter, invade($o)->itemz won't error — it'll return whatever __get decides (often null). A typo then produces an assert-null false negative. Keep property names in one place, or assert property_exists() in a helper.
  • Wrong receiver, unhelpful error. invade($instance)->get('prop') fails with "call to undefined method" because get() lives on the static invader. Conversely, an object passed where a class-string is expected is rejected (2.1.1 tightened this validation) — the error message is clear, but only if you know the two APIs are separate.
  • Enough with the magic. invade() returns a proxy object, so var_dump(invade($o)) shows the wrapper, not the target. Debug the target directly; for uninitialized-property questions use (new ReflectionProperty($o, 'x'))->isInitialized($o).
  • PHPStan needs a hand. The package ships a PHPStan extension so static analysis doesn't scream about your private access. It's registered via extra.phpstan.includes, which is only picked up automatically if phpstan/extension-installer is present — usual with Larastan, but if you get "Access to private property" errors in CI, add the include manually to phpstan.neon rather than littering @phpstan-ignore.
  • Keep it out of production paths. Invade is a seam for tests and debugging. If it appears in a controller or a domain service, it's hiding a missing accessor or a leaked responsibility — flag it in review. If you genuinely need it at runtime, gate it (app()->isLocal()) so it can't be used to bypass invariants in production.
  • Extension points are DIY. There's no config and no container binding to override. If you need "invade with a fallback", "invade an array of objects", or protected/parent-private reach, write a thin wrapper on the same technique — Closure::bind(fn () => $this->x, $object, $object::class) — and keep the wrapper tested once instead of re-deriving it per test.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource