alama@world: ~/
./toggle-theme
cat blog/oss-adrs-spatie-invade.md

OSS-ADR: Spatie Invade

· post · energy

A masterclass in modern PHP: how Spatie uses Closure Binding to bypass Reflection and modify private object state.

1. Context & Decision

The Problem: Testing or modifying the internal state of third-party classes can be a nightmare in PHP. Sometimes you need to assert that a private property was set correctly, or you need to call a protected method during a unit test without having to mock the entire world. In older versions of PHP, you would have to write verbose Reflection boilerplate ($reflectionClass->getProperty('name')->setAccessible(true)).

The Alternatives: - Writing verbose ReflectionClass boilerplate every time you need to touch private state. - Extending the third-party class just to expose a protected method (which pollutes your codebase with "Testable..." wrapper classes). - Heavy mocking using Mockery.

The Decision: We chose spatie/invade because it turns what used to be 5 lines of ugly Reflection code into a beautiful, single-line helper function. It’s perfect for tests and saves countless hours of boilerplate writing.

2. The Integration

The Quick Win: You literally just wrap the object in the invade() function, and you instantly have read/write access to its private and protected members:

class Order {
    private string $status = 'pending';

    private function calculateSecretDiscount(): int {
        return 50;
    }
}

$order = new Order();

// Read a private property!
echo invade($order)->status; // "pending"

// Write to a private property!
invade($order)->status = 'shipped';

// Call a private method!
$discount = invade($order)->calculateSecretDiscount(); // 50

Practical Usage: This is most frequently used inside PHPUnit or Pest tests where you need to artificially manipulate the state of a Service class before calling the method you actually want to test.

3. Under the Hood

Architecture & Design Patterns: The architecture here is fundamentally a Proxy Pattern leveraging PHP's magic methods (__get, __set, __call). When you call invade($object), it returns a new Invader class that holds a reference to your original object.

But here is where it gets absolutely brilliant. You might assume the Invader class is full of ReflectionClass logic under the hood. It isn't.

Code Masterclass: Closure Binding instead of Reflection Look at the entire source code of the Invader class:

class Invader
{
    public function __construct(public object $obj) {}

    public function __get(string $name): mixed
    {
        return (fn () => $this->{$name})->call($this->obj);
    }

    public function __set(string $name, mixed $value): void
    {
        (fn () => $this->{$name} = $value)->call($this->obj);
    }

    public function __call(string $name, array $params = []): mixed
    {
        return (fn () => $this->{$name}(...$params))->call($this->obj);
    }
}

Spatie completely bypassed the Reflection API! Instead, they used PHP Closure Binding (->call()). In PHP, when you bind a closure to an object using ->call($this->obj), that closure executes inside the scope of that object. This means the closure has full access to the object's private and protected properties. It's incredibly fast, requires practically zero memory overhead, and is only 34 lines of code.

4. Consequences & Trade-offs

The Good: - Drastically reduces test boilerplate: One function call replaces paragraphs of Reflection. - Elegant syntax: invade($obj)->method() looks and feels like native PHP.

The Gotchas: - Dangerous in Production: You should almost never use this in production code. Breaking encapsulation in production means you are tightly coupling your application to the private, undocumented internals of a third-party package, which will likely break on their next minor update. - IDE Autocomplete: Since you are interacting with a proxy class utilizing magic methods, your IDE (like PhpStorm) won't auto-complete the private methods or properties of the invaded object.

#oss #architecture #spatie #laravel