Lazy loading and interception without a framework
A dynamic proxy class stands in for a real object and intercepts method calls before they reach the actual object. With __call, eval()-based code generation and reflection, you can build proxy classes in PHP that enable lazy loading, logging and access control, without touching the class itself or pulling in a complete ORM.
Table of Contents
- 1. What a proxy in PHP is and why you need one
- 2. Hand-written proxy classes: where the limits lie
- 3. Interception with __call and __get without code generation
- 4. Generating proxy classes at runtime with eval()
- 5. Lazy loading proxies: deferring initialization
- 6. A generic proxy generator for arbitrary interfaces
- 7. Caching proxy classes and persisting them to files
- 8. Pitfalls: final classes, private methods, constructors
- 9. Proxy approaches compared
- 10. Summary
- 11. FAQ
1. What a proxy in PHP is and why you need one
A proxy class is a stand-in that offers the same interface as a real object, but intercepts method calls before they actually reach the underlying object. Between the caller and the real object, the proxy can insert additional logic: logging, access control, caching of return values, or exactly deferred initialization. Ideally the caller notices nothing, because the proxy satisfies the same type declaration as the original.
In PHP, dynamic proxy classes mainly show up in two contexts: ORMs like Doctrine generate proxy entities that only load a database row once a property is actually accessed, and dependency injection containers generate proxies so that expensive services are only instantiated on actual use. Both cases share the fact that the proxy class cannot be hand-written for every possible target class, it has to be generated at runtime.
The difference from a classic, hand-written decorator lies exactly in this genericity: a decorator is written and maintained for one concrete class, while a dynamically generated proxy class works for arbitrary target classes that share a common structure like an interface, without new code having to be written for every new target class.
2. Hand-written proxy classes: where the limits lie
The simplest entry point into the proxy pattern is a hand-written class that implements an interface and forwards every method call to an internal, real object. This works well as long as there are only a few target classes and their interface rarely changes. The downside shows up as soon as an interface gains a new method: every hand-written proxy class must be updated in lockstep, or type compatibility breaks.
With ten or twenty target classes of similar structure, this quickly becomes boilerplate that has to be maintained in multiple places on every interface change. This is exactly where the idea of a dynamic proxy class comes in: instead of maintaining a separate proxy file for every target class, the proxy code is automatically derived from the target class's structure at runtime or at build time.
<?php
declare(strict_types=1);
interface PaymentGateway
{
public function charge(int $amountCents): bool;
}
// Hand-written proxy: works, but must be updated whenever the interface changes
final class LoggingPaymentGatewayProxy implements PaymentGateway
{
public function __construct(private readonly PaymentGateway $inner)
{
}
public function charge(int $amountCents): bool
{
$start = microtime(true);
$result = $this->inner->charge($amountCents);
$duration = microtime(true) - $start;
error_log(sprintf('charge(%d) took %.4fs, result=%s', $amountCents, $duration, $result ? 'true' : 'false'));
return $result;
}
}
3. Interception with __call and __get without code generation
The lightest weight path to a dynamic proxy class uses PHP's magic methods __call() and __get(). Instead of writing a dedicated implementation for every method of the interface, a single __call() method intercepts every method call that is not directly present, forwards it to the real object via call_user_func_array(), and can run arbitrary logic before or after. The benefit: no code generation, no eval() usage, works immediately with any target class.
The downside of this approach is equally real: without concrete method declarations, neither the IDE nor PHPStan recognize which methods the proxy class actually offers, autocompletion and static analysis fail entirely. For internal tools and prototypes that is an acceptable tradeoff, for public library APIs one of the following, code-generating techniques is almost always the better choice.
<?php
declare(strict_types=1);
final class GenericLoggingProxy
{
public function __construct(private readonly object $target)
{
}
// Intercepts every method call without knowing the target's interface upfront
public function __call(string $name, array $arguments): mixed
{
$start = microtime(true);
$result = $this->target->{$name}(...$arguments);
$duration = microtime(true) - $start;
error_log(sprintf('%s() took %.4fs', $name, $duration));
return $result;
}
}
$gateway = new GenericLoggingProxy(new StripePaymentGateway());
$gateway->charge(1999); // dispatched dynamically to StripePaymentGateway::charge()
4. Generating proxy classes at runtime with eval()
Anyone who wants to avoid the downsides of magic methods, but still doesn't want to maintain a file per target class, generates real PHP code as a string and loads it at runtime with eval(). The Reflection API inspects the target's interface, assembles a matching method signature as a string for each method, and compiles the entire class code in a single eval() call at the end. The result is a real class with real method declarations that IDEs and PHPStan understand like any other class.
eval() has a bad reputation in PHP, mostly deserved when it comes to executing user input. For generated, controlled code from known interface definitions, the situation is different: it is not foreign input, but code the application itself constructed. Still, any generated code should be logged before compiling, or at least reviewed in a test environment, to catch errors in the generation logic early.
<?php
declare(strict_types=1);
function generateProxyClass(string $interfaceName, string $proxyClassName): void
{
$reflection = new ReflectionClass($interfaceName);
$methodBodies = [];
foreach ($reflection->getMethods() as $method) {
$params = implode(', ', array_map(
static fn (ReflectionParameter $p): string => '$' . $p->getName(),
$method->getParameters()
));
$methodBodies[] = sprintf(
'public function %s(%s) { return $this->target->%s(%s); }',
$method->getName(),
$params,
$method->getName(),
$params
);
}
$code = sprintf(
'final class %s implements %s {
public function __construct(private readonly %s $target) {}
%s
}',
$proxyClassName,
$interfaceName,
$interfaceName,
implode(' ', $methodBodies)
);
eval($code);
}
generateProxyClass(PaymentGateway::class, 'GeneratedPaymentGatewayProxy');
$proxy = new GeneratedPaymentGatewayProxy(new StripePaymentGateway());
5. Lazy loading proxies: deferring initialization
A lazy loading proxy is a specialized form of the dynamic proxy class, where the real object is not created immediately, but only on the first actual method call. This is especially valuable for expensive resources like database connections, remote API clients, or large object graphs that are not needed on every request. Instead of creating the real object in the constructor, the proxy only stores a closure that creates the object on demand, and calls that closure exactly once on first access.
PHP 8.4 further simplifies this use case with native lazy objects via ReflectionClass::newLazyGhost(), which implement exactly this behavior at the language level without having to write a custom proxy class. For PHP versions before 8.4, or for cases where additional interception logic is needed alongside plain lazy loading, the manual closure-based solution remains relevant.
<?php
declare(strict_types=1);
final class LazyPaymentGatewayProxy implements PaymentGateway
{
private ?PaymentGateway $resolved = null;
/** @param Closure(): PaymentGateway $factory */
public function __construct(private readonly Closure $factory)
{
}
private function resolve(): PaymentGateway
{
// Instantiate the expensive real object only on first actual use
return $this->resolved ??= ($this->factory)();
}
public function charge(int $amountCents): bool
{
return $this->resolve()->charge($amountCents);
}
}
$proxy = new LazyPaymentGatewayProxy(static fn (): PaymentGateway => new StripePaymentGateway());
// StripePaymentGateway is only constructed here, not a moment earlier
$proxy->charge(1999);
6. A generic proxy generator for arbitrary interfaces
In larger applications, a central proxy generator pays off, one that generates a matching proxy class for any given interface on demand, instead of writing a dedicated generation function for every use case. Such a generator combines the eval() technique shown in the previous section with a registry that remembers already generated class names per interface, so the same interface is not generated multiple times in the same request.
For interception logic that should be identical across all generated proxies, such as uniform logging or access checks, it pays off to not forward the generated method directly to the target object, but to route it through a central invoke() method of a base class. That way the amount of generated code stays minimal, while the actual logic is maintained in a single place.
7. Caching proxy classes and persisting them to files
Repeatedly generating the same proxy class with eval() on every request is unnecessary overhead. The common solution: write generated code to a regular PHP file in a cache directory the first time it's needed, and simply require it on subsequent requests instead of calling eval() again. This has the added benefit that OPcache can compile and cache these generated files like any other PHP file, which is not the case to the same degree with plain eval().
Similar to the reflection caching strategy from the article on the Reflection API in detail, development and production should be distinguished here as well: during development you ideally check whether the source interface has changed since the last generation, in production you fully trust the cache built once at deployment time.
<?php
declare(strict_types=1);
final class ProxyClassCache
{
public function __construct(private readonly string $cacheDir)
{
}
public function loadOrGenerate(string $interfaceName, string $proxyClassName): string
{
$file = $this->cacheDir . '/' . $proxyClassName . '.php';
if (is_file($file)) {
require_once $file;
return $proxyClassName;
}
$code = $this->buildProxyCode($interfaceName, $proxyClassName);
file_put_contents($file, "<?php\n\n" . $code, LOCK_EX);
require_once $file;
return $proxyClassName;
}
private function buildProxyCode(string $interfaceName, string $proxyClassName): string
{
// Reuse the generation logic from the eval()-based approach
$reflection = new ReflectionClass($interfaceName);
$methods = [];
foreach ($reflection->getMethods() as $method) {
$methods[] = sprintf('public function %s() {}', $method->getName());
}
return sprintf(
'final class %s implements %s { %s }',
$proxyClassName,
$interfaceName,
implode(' ', $methods)
);
}
}
8. Pitfalls: final classes, private methods, constructors
Not every class can be wrapped in a dynamic proxy class. Classes declared final cannot be overridden through inheritance, which is not a problem for composition-based proxies like the ones shown here, but is a problem for inheritance-based approaches, as used by some ORMs for lazy loading. Private methods of a target class are fundamentally not interceptable via __call(), because they are not visible outside the class at all, regardless of whether a proxy is placed in between.
Another common pitfall involves constructors with mandatory, complex dependencies. A generated proxy that extends the target class through inheritance must either call its constructor or bypass it with newInstanceWithoutConstructor(), which in turn means every property of the base class must be manually populated afterward. Composition-based proxies, as shown in this article, elegantly avoid this problem because they receive the real object already fully instantiated, instead of constructing it themselves through inheritance.
9. Proxy approaches compared
The presented techniques differ significantly in type safety, performance and implementation effort. The following table summarizes the key differences.
| Approach | Type safety | Performance | When suitable |
|---|---|---|---|
| Hand-written proxy | Full | Optimal | Few, stable interfaces |
| __call interception | None (blind to IDE, PHPStan) | Good | Internal tools, prototypes |
| eval() code generation | Full | Good (with caching) | Many changing interfaces |
| Closure-based lazy loading | Full | Very good | Expensive, rarely used objects |
| File cache of generated classes | Full | Optimal (OPcache-capable) | Production frameworks, many proxies |
In practice, production systems usually combine several of these techniques: eval() or reflection-based code generation for the structure, file caching for performance, and closures for the special case of lazy loading. The choice mainly depends on how stable the target interfaces are and how many different target classes need to be wrapped in proxies.
Mironsoft
PHP architecture, lazy loading and code generation
Decoupling expensive objects with a lazy loading proxy?
We design dynamic proxy classes for lazy loading, logging and access control, including a caching strategy for production PHP applications.
Proxy generators
Generic proxy generation for arbitrary interfaces
Lazy loading
Initializing expensive resources only on actual demand
Code generation
Safe eval() alternatives and file caching for generated classes
10. Summary
A dynamic proxy class in PHP stands in for a real object and intercepts method calls to insert logging, access control or lazy loading. __call()-based proxies are the fastest to write but sacrifice type safety. Code generated with the Reflection API and eval() produces real method declarations and stays visible to IDEs and PHPStan.
Lazy loading proxies defer creating expensive objects until the first actual access, either through a closure or, since PHP 8.4, through native lazy objects. Generated proxy classes should be cached as files so OPcache can treat them like regular code, instead of calling eval() again on every request. Classes declared final and private methods remain fundamental limits of every proxy technique.
Generating Dynamic Proxy Classes in PHP — Key Takeaways
__call interception
Fastest to build, but no type safety for IDE and PHPStan.
eval() code generation
Reflection reads the interface, real methods are compiled at runtime.
Lazy loading
Closure-based, or natively since PHP 8.4 with newLazyGhost().
Caching
Persist generated classes as files so OPcache can compile them.