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

Peek And Poke Laravel Package

sebastian/peek-and-poke

sebastian/peek-and-poke is a small PHP utility that uses reflection to peek at and poke into objects. It lets you read and write private and protected properties, making it handy for tests that need to inspect or set up object state without changing production code.

View on GitHub
Deep Wiki
Context7

Getting Started

sebastian/peek-and-poke is a ~150-line micro-library that wraps an object and exposes its non-public properties and methods through PHP's magic methods. For a Laravel developer it is almost exclusively a testing tool: it lets you assert on and manipulate private state without rewriting production code to be "testable."

Install it as a dev dependency only:

composer require --dev sebastian/peek-and-poke

Before you commit to it, check two things:

  1. Archived status. The repository is archived and carries no license declaration (NOASSERTION). There will be no PHP 8.x compatibility fixes, no security patches, and no support. Treat it as a temporary convenience, not a foundation. If your composer.json already pins a modern PHP version, run composer require and immediately run your test suite to confirm it autoloads and reflects correctly.
  2. Duplicate class risk. Older PHPUnit versions shipped or depended on this same namespace (SebastianBergmann\PeekAndPoke\Proxy). If your project uses an old PHPUnit, Composer may resolve a version you don't expect. composer show sebastian/peek-and-poke should show exactly one version.

Where to look first: the single source file, src/Proxy.php. That's the whole API. There are no config files, service providers, or facades — it is not a Laravel package in that sense, it's a plain PHP utility you new up inside a test.

First use case — inspecting a private collaborator on a service:

use SebastianBergmann\PeekAndPoke\Proxy;

final class InvoiceCalculatorTest extends TestCase
{
    public function test_uses_the_injected_tax_resolver(): void
    {
        $calculator = $this->app->make(InvoiceCalculator::class);

        $proxy = new Proxy($calculator);

        $this->assertInstanceOf(TaxResolver::class, $proxy->taxResolver);
    }
}

The mental model is simple: new Proxy($obj) returns something whose __get, __set, __isset, __unset, and __call all resolve against $obj via Reflection, bypassing visibility.

Implementation Patterns

1. The base-test-case helper

Because you'll wrap objects constantly, put a tiny helper on your TestCase (or a trait) so tests stay readable and you keep a single place to swap the implementation later if you migrate off the archived package:

trait PeeksAndPokes
{
    protected function peek(object $object): Proxy
    {
        return new Proxy($object);
    }
}

Then $this->peek($service)->somePrivateValue. When you eventually replace it with ReflectionProperty or Closure::bind, you change one method.

2. Asserting on private state after an action

The highest-value pattern in a Laravel app: call the public entry point, then verify the private bookkeeping.

$command = new SendDailyDigest();
(new Proxy($command))->handle($this->app->make(DigestService::class));

$this->assertTrue((new Proxy($command))->hasRun);
$this->assertSame(42, (new Proxy($command))->processedCount);

This is the pattern that lets you keep private on implementation details you don't want to expose as public API just for tests.

3. Swapping a private collaborator for a mock

When a class news up or holds its dependency privately and you cannot (or will not) refactor to constructor injection right now:

$service = new ReportBuilder();
$proxy   = new Proxy($service);

$proxy->exporter = Mockery::mock(Exporter::class)
    ->shouldReceive('export')->once()->andReturn(true);

$this->assertTrue($service->build());

Use this as a pressure valve, not a habit. In Laravel the better answer is almost always $this->app->instance(Exporter::class, $mock) against constructor injection — reach for the proxy only for legacy classes or framework internals you can't rebind.

4. Resetting private caches between tests

Singletons and memoized private arrays are a classic source of order-dependent test failures. The proxy gives you a surgical reset:

$proxy = new Proxy($this->app->make(GeoLookup::class));
$proxy->cache = [];

Prefer this over rebuilding the container when the cost of a fresh instance is high.

5. Exercising protected/private methods in isolation

$proxy = new Proxy($normalizer);
$this->assertSame('ACME-001', $proxy->normalizeSku(' acme 001 '));

This is genuinely useful when a private method has ten branches and the public wrapper only exposes two of them. Keep the number of such tests small — they are the most refactor-fragile tests you can write.

6. Inspecting framework internals

Occasionally you want to assert on something Laravel keeps private, e.g. the recorded state inside a Illuminate\Http\Client\Factory fake, the queued connections on a PendingDispatch, or the resolved handlers on a console command. The proxy gives you read access without ReflectionProperty boilerplate. Keep these assertions close to the integration boundary and don't build your test suite on framework private APIs — they move between minor versions.

7. One-off debugging scripts and tinker

php artisan tinker is fine for public API, but when you need to inspect a private field during an incident:

$p = new \SebastianBergmann\PeekAndPoke\Proxy($suspiciousService);
dump($p->internalState, $p->retryCount);

Don't leave this in application code; it's a debugging scalpel, not a production instrument.

Gotchas and Tips

Arrays are read-by-value — indirect modification silently fails. __get returns a copy for array properties, so:

$proxy->items['new'] = 'value';   // "Indirect modification of overloaded property has no effect"

Always read, mutate, write back:

$items = $proxy->items;
$items['new'] = 'value';
$proxy->items = $items;

Object properties are handles, so $proxy->someObject->foo = 'bar' works as expected.

No by-reference arguments or returns. __call cannot pass arguments by reference, so any private method with a &$param signature will misbehave (typically the by-ref variable stays unchanged rather than erroring loudly). This bites on recursive collectors and accumulator-style helpers.

Private members of parent classes are not found. ReflectionClass::getMethod()/getProperty() return the current class's private members but not a parent's private ones. If the value lives one level up the hierarchy, the proxy throws. Walk the hierarchy with ReflectionClass::getParentClass() yourself, or use Closure::bind($fn, null, ParentClass::class).

Uninitialized typed properties throw. Reading a private ?Foo $bar; before it's assigned raises Error: Typed property must not be accessed before initialization — the same as direct access. Guard with (new ReflectionProperty(...))->isInitialized($obj) or wrap in a try/catch, and avoid asserting on properties that are legitimately nullable-but-unset.

Read-only and readonly-property limits apply. PHP 8.1+ readonly properties cannot be written from the proxy's calling scope; PHP 8.2+ rejects writes to already-initialized readonly properties entirely. If you need to seed a readonly property, that's a design signal — inject the value instead.

Unset/uninitialized distinctions. isset($proxy->prop) maps to __isset, which respects visibility but also returns false for a set-but-null property (matching PHP's normal isset semantics). Use $proxy->prop === null or reflection if you need to distinguish "never set" from "set to null."

Property named object is shadowed. The wrapper stores the target in a private $object; __get('object') will look through to the wrapped object rather than return the target itself, and some collisions around internal names are possible. Extremely unlikely in practice, but don't name test fixtures with properties that collide with the proxy's internals.

Static members are out of scope. The constructor takes an object, so public static $foo needs plain ReflectionClass::getStaticPropertyValue().

No IDE or static-analysis support. $proxy->whatever is invisible to PHPStan/Psalm and autocomplete. Add @var docblocks on the property you read, or a /** @var SomeType $value */ inline annotation:

/** @var TaxResolver $resolver */
$resolver = $proxy->taxResolver;

Without this, level-5 PHPStan runs will report Access to an undefined property SebastianBergmann\PeekAndPoke\Proxy::$taxResolver.

The class is final — no extension point. You cannot subclass Proxy to add caching or logging. If you need that, write your own thin wrapper or a ReflectionProperty-based helper; the package's only extension surface is composition.

Debugging failures. The two errors you'll actually see:

  • ReflectionException: Property X does not exist — typo, or the member is on a parent class (see above).
  • Error: Cannot access private property ... — this means the magic method wasn't triggered, usually because you're accessing the member via a typed property or a self::/$this-> path, not through the proxy. Re-check you're operating on the Proxy instance, not the raw object.

Alternatives worth considering before you adopt it. For a one-off, Closure::bind(function () { return $this->secret; }, $object, $object)() needs no dependency at all. For structural access, ReflectionProperty::setAccessible(true) (no-op since PHP 8.1) is more verbose but permanent and supported. For collaborators, Laravel's container binding ($this->app->instance(), $this->mock(), $this->fake()) is nearly always the right answer. The proxy's only real advantage is ergonomics: $proxy->name beats three lines of reflection every time.

Pin it and fence it in. If you keep it, add it to require-dev, keep it out of any published package's require, and leave a TODO on the trait helper so a future refactor to ReflectionProperty is a single-file change. Given the archived repo, no license metadata, and zero dependents, treat any production exposure as a defect.

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.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json