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

Proxy Laravel Package

yiisoft/proxy

yiisoft/proxy provides lightweight proxy objects for PHP that defer instantiation until first use, enabling lazy service loading and cleaner dependency injection without changing your application code. Part of the Yii 3 ecosystem.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer:

composer require yiisoft/proxy --dev

It is a small, single-purpose package (BSD-3-Clause, actively released), so treat it as a tooling dependency rather than runtime infrastructure — most teams will keep it in require-dev.

Where to look first

  1. src/Proxy.php — the whole public surface is essentially here. You do not need to read the docs twice; the class is short.
  2. Proxy::create($object) — returns a wrapper that lets you call non-public methods and read/write non-public properties on the wrapped instance.
  3. The static helpers (Proxy::get(), Proxy::set(), and friends) — one-shot access without constructing a wrapper, useful inside assertions.

The implementation is built on Closure::bind() scope rebinding, which is why it can reach private/protected members without a full reflection boilerplate.

First use case — testing a private collaborator method

use Yiisoft\Proxy\Proxy;

final class InvoiceCalculator
{
    private function applyDiscount(int $cents): int
    {
        return (int) ($cents * 0.9);
    }
}

$calc = new InvoiceCalculator();
$proxy = Proxy::create($calc);

$this->assertSame(900, $proxy->applyDiscount(1000));

That is the 80% case: you have a legacy or third-party class with the logic you need to test, and splitting the method out would mean a large, risky refactor. The proxy gets you a passing test today and a seam to refactor tomorrow.

If you only need one property read in an assertion, the static form is shorter:

$state = Proxy::get($service, 'internalState');

Implementation Patterns

Pattern 1 — Central test helper, not scattered calls. Do not let Proxy::create() leak into every test. Wrap it once:

trait ProxiesInternals
{
    protected function proxy(object $target): object
    {
        return Proxy::create($target);
    }
}

When the package is later removed (and it should be, eventually), you change one trait instead of 200 test files.

Pattern 2 — Guard rails around assertion-only access. Proxy access is a smell in production code but legitimate in tests. Enforce that with architecture tests (phpstan, deptrac, or a custom Pest expectation) that fail if Yiisoft\Proxy is referenced outside tests/.

Pattern 3 — Legacy adapter seam. When integrating a vendor SDK that hides its HTTP client behind a private property, use the proxy to swap in a fake rather than standing up a mock server:

$client = new VendorSdkClient();
Proxy::set($client, 'transport', $fakeTransport);

Prefer this only while you build a proper adapter interface. It is a bridge, not a home.

Pattern 4 — Asserting on framework-internal state. Yii3 components often keep resolved state in protected properties. Instead of re-deriving expected values by hand in the test, read the actual internal value and assert it matches what the public API implied:

$proxy = Proxy::create($resolver);
$this->assertSame(
    [$expectedRoute],
    Proxy::get($resolver, 'resolved'),
);

Pattern 5 — Wrapping callable access. Because binding happens through closures, you can also invoke private methods that take by-reference arguments and closures; pass them normally through the proxy call. This is handy for testing callback registration paths.

Pattern 6 — CI hygiene. Keep the dependency in require-dev and run one job with the minimum PHP version you support. The package is tiny but it does depend on closure binding semantics that have shifted subtly across PHP minors.

Gotchas and Tips

Scope, not magic. The proxy rebinds a closure to the target's class scope. That means it can see private members of the class that declares them, but a private property declared on a parent class is invisible from a child class scope. If you get "cannot access", confirm which class actually declares the member — you may need to proxy a value typed as the declaring parent.

Readonly and typed properties. Proxy::set() on a readonly property will throw. Setting a typed property to null when the type does not allow it likewise throws — the proxy bypasses visibility, not the type system. Uninitialized typed properties read as errors, so initialise or assert first.

Static properties are a different scope. Rebinding to static context versus instance context matters. Reading a static private property from an instance-scoped closure can fail in ways that look random; use the static accessor form and be explicit.

Do not benchmark it. Each Proxy::create() builds closure bindings. It is cheap enough for tests, irrelevant for a one-off migration script, and wrong for a hot loop or a request-scoped middleware. If you find yourself reaching for it in src/, that is the signal to introduce a real interface.

Debugging is awkward. Stack traces inside a bound closure show the proxy file, not your test. When a proxied call fails, log the target class and method name explicitly before invoking, or temporarily add a debug_backtrace() inside the bound closure via a custom wrapper.

Error messages lie about visibility. A TypeError from a proxied call frequently means "wrong number of arguments" or "argument is a different type than the private signature expects" — not a visibility problem. Re-check the actual signature in the class, since your IDE will not always surface private parameter types in autocomplete.

Extension point. There is no plugin system. The intended extension is composition: build your own thin facade class around Proxy that encodes your team's rules (read-only access, allow-list of members, logging). That facade is the only thing your tests should import.

Version pinning. With a single small maintainer surface and a 2026 release cadence, pin the major version in composer.json and note the upgrade in your changelog. The blast radius of a behaviour change is small, but it will land in your test suite, not production — which is exactly where you want it.

Escape hatch. Before adding another proxy access, ask whether the member should be protected or exposed via a narrow method. Every Proxy::get() is a future refactor ticket; the package is most valuable as a temporary pressure valve, not a permanent architecture layer.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core