babr / method-wrapper
Method wrapper generator with aspect-oriented programming support
Requires
- php: >=7.4
- psr/container: ^1.0|^2.0
Requires (Dev)
- phpunit/phpunit: ^9.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-10 13:34:10 UTC
README
Library for generating method wrapper proxies with AOP-style interceptors. Wraps public and protected methods of a class, delegating all calls through an interceptor that can observe and modify behavior.
PHP >= 7.4 | PSR-11 Container
How it works
- Mark a class
#[Wrapable]— the library will generate a proxy for it on demand. - Mark individual methods
#[Wrap('interceptor-name')]— those methods will be intercepted. - Calls go through
before()→ original method →after(). Exceptions go throughonException(). - Generated proxy classes are cached to disk (by default in
sys_get_temp_dir()).
Service instance → Proxy class (generated at runtime) → Target instance
↓
Interceptor (before / after / onException)
Installation
composer require babr/method-wrapper
Quick start
use MethodWrapper\ProxyFactory; use MethodWrapper\Interceptor\Factory; use MethodWrapper\Contract\MethodInvocationInterceptor; // 1. Implement the interceptor class LoggingInterceptor implements MethodInvocationInterceptor { public function before(object $target, string $method, array $args): void { file_put_contents('/tmp/log.txt', "{$method}() called\n", FILE_APPEND); } public function after(object $target, string $method, array $args, $result): void { file_put_contents('/tmp/log.txt', "{$method}() = {$result}\n", FILE_APPEND); } public function onException(object $target, string $method, array $args, \Throwable $e): void { file_put_contents('/tmp/log.txt', "{$method}() threw {$e->getMessage()}\n", FILE_APPEND); } } // 2. Register it in the factory $factory = new Factory(['logging' => new LoggingInterceptor()]); // 3. Create the proxy factory $config = new \MethodWrapper\Config($factory, '/tmp/proxies'); $proxyFactory = new ProxyFactory($config, new \MethodWrapper\Generator\ProxyClassGenerator('/tmp/proxies')); // 4. Wrap any Wrapable class $service = $proxyFactory->create(new MyService()); $service->doSomething(); // intercepted
With PSR-11 Container
use MethodWrapper\ContainerProxy; use MethodWrapper\ProxyFactory; use MethodWrapper\Interceptor\Factory; $factory = new Factory([' interceptor => new MyInterceptor()]); $config = new \MethodWrapper\Config($factory, '/tmp/proxies'); $proxyFactory = new ProxyFactory($config, new \MethodWrapper\Generator\ProxyClassGenerator('/tmp/proxies')); // Wrap any PSR-11 container — every fetched service is automatically proxied $container = new ContainerProxy($myPsrContainer, $proxyFactory); $service = $container->get(MyService::class); // already wrapped
Attributes
#[Wrapable]
Applied to a class. Marks it as a candidate for proxy generation.
use MethodWrapper\Attribute\Wrapable; #[Wrapable] class MyService { // ... }
Classes without #[Wrapable] are returned as-is (no proxy generated). Classes with #[Wrapable] but no #[Wrap] methods are also returned as-is.
#[Wrap]
Applied to individual methods. Each call to the method is routed through an interceptor.
use MethodWrapper\Attribute\Wrap; use MethodWrapper\Attribute\Wrapable; #[Wrapable] class MyService { #[Wrap('my-interceptor', options: ['key' => 'value'])] public function doSomething(): string { return 'original'; } protected function helper(): void { // protected methods are accessible through the proxy, // but are NOT intercepted (no #[Wrap] attribute) } }
Constructor parameters:
| Parameter | Type | Description |
|---|---|---|
wrapper |
string | Interceptor name, passed to InterceptorFactory::create() |
options |
array | Optional key-value data forwarded to the interceptor |
Interfaces
MethodInvocationInterceptor
Implement this to define what happens around a method call.
use MethodWrapper\Contract\MethodInvocationInterceptor; interface MethodInvocationInterceptor { /** Called before the original method. */ public function before(object $target, string $method, array $args): void; /** Called after a successful result. */ public function after(object $target, string $method, array $args, $result); /** Called when the original method throws an exception. */ public function onException(object $target, string $method, array $args, \Throwable $e); }
All three methods are always called. before is called before the original method. after is called after a successful result. onException is called when the original method throws.
InterceptorFactory
Create interceptors by name. Factory is a built-in implementation that holds a map of name → instance.
use MethodWrapper\Contract\InterceptorFactory; interface InterceptorFactory { public function create(string $name, array $options): MethodInvocationInterceptor; }
Caching
Proxy classes are written as .php files to the configured cache directory:
/tmp/proxies/
├── MethodWrapper/Tests/Proxy_WrappedService_hash.php ← generated proxy class
└── ...
The cache is not invalidated automatically — clear it manually when source classes change.
Limitations
finalmethods are not intercepted — the generated proxy does not override them, so the original behavior is preserved.finalclasses are returned as-is — no proxy is generated for them.- Private constructors are not handled specially — the proxy uses
extends, so the original constructor is called. - Static methods marked with
#[Wrap]are scanned but delegate without triggering interceptors in the current implementation. - Cache directory must be writable. The library creates it with
0755if it doesn't exist.
File structure
src/
├── Attribute/
│ ├── Wrap.php ← #[Wrap] attribute
│ └── Wrapable.php ← #[Wrapable] attribute
├── Contract/
│ ├── InterceptorFactory.php
│ ├── MethodInvocationInterceptor.php
│ ├── ProxyClassGenerator.php
│ ├── ProxyFactory.php
│ └── ProxyMapBuilder.php
├── Generator/
│ └── ProxyClassGenerator.php ← generates and writes proxy class files
├── Interceptor/
│ ├── Factory.php ← map-based InterceptorFactory implementation
│ └── NullInterceptor.php ← no-op interceptor (default for unknown names)
├── Mapping/
│ └── ProxyMapBuilder.php ← persistent class → proxy file mapping
├── Config.php ← value object: interceptorFactory + cacheDirectory
├── ContainerProxy.php ← PSR-11 container decorator (auto-wraps every get())
├── CachedProxyFactory.php ← ProxyFactory with persistent ProxyMapBuilder
├── Proxy.php ← value object: proxy class name + file path
└── ProxyFactory.php ← public API: create() wraps instances
Running tests
php phpunit.phar --bootstrap tests/bootstrap.php tests/