ejsmont-artur/php-proxy-builder
Runtime proxy builder for PHP that wraps any object with Advice (AOP-style) to add cross-cutting behavior like caching. Proxies transparently delegate method calls to the target while keeping proxy, advice, and client code fully decoupled.
Installation Add the package via Composer:
composer require ejsmont-artur/php-proxy-builder
Register the service provider in config/app.php:
'providers' => [
// ...
ArturEjsmont\ProxyBuilder\ProxyBuilderServiceProvider::class,
],
Basic Usage
Create a simple proxy for a class (e.g., UserRepository):
use ArturEjsmont\ProxyBuilder\ProxyBuilder;
$proxyBuilder = app(ProxyBuilder::class);
$proxy = $proxyBuilder->build(UserRepository::class, [
'method' => 'findById',
'interceptor' => function ($method, $args) {
// Pre-processing logic (e.g., logging)
$result = $method(...$args);
// Post-processing logic
return $result;
}
]);
First Use Case: Logging Wrap a service method to log inputs/outputs:
$loggerProxy = $proxyBuilder->build(UserService::class, [
'method' => 'createUser',
'interceptor' => function ($method, $args) use ($logger) {
$logger->info('Creating user with args:', $args);
$result = $method(...$args);
$logger->info('User created:', $result);
return $result;
}
]);
build() to generate proxies dynamically in service containers or middleware.Cache facade for analytics:
$cacheProxy = $proxyBuilder->build(\Illuminate\Cache\CacheManager::class, [
'method' => 'get',
'interceptor' => function ($method, $args) {
$key = $args[0];
$hit = $method(...$args);
app('analytics')->track('cache_hit', ['key' => $key]);
return $hit;
}
]);
*ById).save* methods:
$proxy = $proxyBuilder->build(UserRepository::class, [
'pattern' => '/^save/',
'interceptor' => function ($method, $args) {
if (empty($args[0]->email)) {
throw new \InvalidArgumentException('Email required!');
}
return $method(...$args);
}
]);
$app->bind(UserRepository::class, function ($app) {
return $app->make(ProxyBuilder::class)
->build(UserRepository::class, [
'method' => 'delete',
'interceptor' => function ($method, $args) {
// Soft delete logic
$method(...$args);
app('log')->info('Soft-deleted user ID: ' . $args[0]);
}
]);
});
$proxy = $proxyBuilder->build(OrderService::class);
$proxy->addInterceptor('placeOrder', function ($method, $args) {
if (!auth()->check()) throw new \UnauthorizedHttpException;
return $method(...$args);
});
$proxy->addInterceptor('placeOrder', function ($method, $args) {
app('log')->info('Order placed:', $args);
return $method(...$args);
});
$clientProxy = $proxyBuilder->build(\GuzzleHttp\Client::class, [
'method' => '__call',
'interceptor' => function ($method, $args) {
try {
return $method(...$args);
} catch (\GuzzleHttp\Exception\RequestException $e) {
return $method(...$args); // Retry
}
}
]);
Method Signature Mismatches
&$ for references).var_dump(func_get_args()) to debug argument structures.Proxy Lifetime
$callCount = 0;
$proxy = $proxyBuilder->build(Service::class, [
'method' => 'doWork',
'interceptor' => function () use (&$callCount) {
$callCount++;
// ...
}
]);
Performance Overhead
Circular Dependencies
A proxies B, which proxies A).instance() in bindings or lazy-load proxies.var_dump or dd() to interceptors to inspect calls:
'interceptor' => function ($method, $args) {
error_log("Method: {$method}, Args: " . print_r($args, true));
return $method(...$args);
}
get_class($proxy) to verify the generated proxy class name (e.g., ProxyBuilder_Proxy_123).Custom Proxy Classes
ArturEjsmont\ProxyBuilder\ProxyBuilder to add default interceptors:
class CustomProxyBuilder extends ProxyBuilder {
public function __construct() {
$this->addDefaultInterceptor('*', function ($method, $args) {
// Global logging
});
}
}
Interceptor Priorities
$proxy->addInterceptor('method', $interceptor, 'auth'); // Lower priority
$proxy->addInterceptor('method', $interceptor, 'log'); // Higher priority
Proxy Caching
$cacheKey = 'proxy_' . md5(UserRepository::class);
return Cache::remember($cacheKey, 3600, function () use ($proxyBuilder) {
return $proxyBuilder->build(UserRepository::class, [...]);
});
boot()).ProxyBuilder constructor or using unique prefixes.How can I help you explore Laravel packages today?