php-cs-fixer/accessible-object
AccessibleObject is a small PHP utility for reading and writing private or protected object properties and invoking non-public methods through a simple wrapper. It helps tests and tooling inspect internals without writing reflection boilerplate.
Install as a dev-only dependency — this package has no runtime place in a Laravel app:
composer require --dev php-cs-fixer/accessible-object
There is nothing to register. No service provider, no facade, no config file. The entire surface is one wrapper class (PhpCsFixer\AccessibleObject\AccessibleObject) that uses reflection to read private/protected properties and invoke private/protected methods on any object.
Where to look first: the README (it is short — the whole library is ~3 methods). The core API you'll actually use:
use PhpCsFixer\AccessibleObject\AccessibleObject;
$obj = AccessibleObject::create($someService);
$obj->getProperty('secret'); // read private/protected
$obj->setProperty('secret', 'changed'); // write private/protected
$obj->callMethod('internalThing', $a, $b); // call private/protected
AccessibleObject::createFromClass(Expensive::class); // skip the constructor
First use case — a Pest/PHPUnit test against a service whose internals you can't observe through its public API:
it('resets the internal cursor after failure', function () {
$service = app(BatchImporter::class);
$service->import([/* bad rows */]);
$acc = AccessibleObject::create($service);
expect($acc->getProperty('cursor'))->toBe(0)
->and($acc->getProperty('retryBuffer'))->toBeEmpty();
});
That's the 80% case: assert on state you deliberately kept private. Everything else in this assessment is variations on it.
Inspecting Laravel framework internals. The most genuinely useful application is peering into framework classes that store important state in protected properties — the container's $bindings/$instances, Illuminate\Cache\Repository's $store, queue workers' $options, the Illuminate\Log\Logger's handlers, Application::$terminatingCallbacks. This is invaluable when writing integration tests that need to assert how a feature wired something up, not just that it returns a result:
$app = AccessibleObject::create(app());
expect($app->getProperty('resolved'))
->toHaveKey(MyService::class); // was it a singleton or a fresh resolve?
Driving private collaborators that have no public setter. When a class accepts no dependency injection point for a clock, a random generator, or a HTTP client, wrap the instance and swap the property. This is the "poor man's seam" and is far cleaner than reflecting inline in ten different tests:
$report = app(SalesReport::class);
AccessibleObject::create($report)->setProperty('clock', $fakeClock);
expect($report->total())->toBe(4200);
Calling protected template methods directly. If a base class exposes protected function buildQuery() and you want to test that method in isolation without spinning up the whole public entry point:
$query = AccessibleObject::create(new UserReport())
->callMethod('buildQuery', ['active' => true]);
Faster, more focused tests — but see the gotchas about coupling to method names.
Bypassing constructors for fixtures. AccessibleObject::createFromClass() lets you instantiate a class without running __construct. Handy when a constructor reaches for the DB, Redis, or a network client but the class under test only uses a single method that touches none of it:
$job = AccessibleObject::createFromClass(SyncInventoryJob::class);
AccessibleObject::create($job)->setProperty('sku', 'ABC-1');
Wrap it in a helper. Because the raw API returns mixed and reads like reflection, define a tiny test trait so intent stays readable and one place absorbs any future API churn:
trait RevealsInternals
{
protected function internals(object $o): AccessibleObject
{
return AccessibleObject::create($o);
}
}
Then $this->internals($service)->getProperty('cacheKey') reads as a deliberate test choice rather than raw reflection.
Pair the reads with Pest expectations. expect($acc->getProperty('state'))->toBe(...) keeps the assertion API consistent with the rest of your suite — the accessibility layer disappears into the flow of the test.
Uninitialized typed properties throw. Reading a private ?string $foo; (declared but never assigned) via reflection yields Error: must not be accessed before initialization. Seed it with setProperty() first, especially after createFromClass() skips the constructor.
readonly properties cannot be written. PHP 8.1+ readonly props are reflection-write-protected; setProperty() will fail. Treat this as a signal: if the class is designed to be immutable, test through the public read path instead.
Private properties on parent classes are invisible by name alone. Native reflection needs the declaring class to reach a parent's private property. Verify the package version you have walks the hierarchy; if not, resolve the parent class explicitly. Classically bites when testing subclasses of framework base classes.
Static members need a different handle. AccessibleObject wraps instances. For static properties/methods use ReflectionClass directly — this package doesn't cover them.
By-reference parameters break silently. callMethod() forwards args through ReflectionMethod::invokeArgs(), which cannot pass by reference. If the private method takes &$x and mutates it, you'll never see the change in the caller. Refactor or drop to invokeArgs with an array of references yourself.
Reflection is a compatibility risk. Every getProperty('resolved') against a Laravel class is a promise that Laravel won't rename that property in a minor release. Pin your test suite to known framework versions, and prefer this technique for your code over framework internals where a public accessor exists. If you catch yourself reflecting into a third-party package, ask whether a ->getX() already exists.
This defeats encapsulation by design — keep it in tests/. It is a test/debug helper. Don't ship it in app/, don't use it in production hot paths (reflection is slow and it's a dev dependency — it won't even be installed with --no-dev). For a one-off in tinker debugging, Closure::bind() may be simpler; this library earns its keep only when you're doing it repeatedly and want the readability.
No type safety. getProperty() returns mixed; static analysis and IDEs will not help you if you typo a property name — you'll get a reflection exception at runtime, not at lint time. Assert the value immediately with Pest/PHPUnit so a bad property name fails loudly in the right test.
License is NOASSERTION. The repo ships a license file but Composer couldn't identify it. That's fine for a dev-only test dependency in most shops, but check with whoever owns OSS compliance before it lands in a lockfile for a commercial product.
Check the release date. The 2025-08-20 release is recent, but with 11 stars and 0 dependents this is an internal PHP-CS-Fixer utility that just happens to be public. Assume no community support and read the source (it's a few hundred lines) before relying on edge-case behavior — particularly around inherited properties and static members, where reflection libraries commonly have gaps.
How can I help you explore Laravel packages today?