Search by

babr / method-wrapper

kotovaz

There is no license information available for the latest version (1.1.5) of this package.

Method wrapper generator with aspect-oriented programming support

Package info

github.com/KotovaZ/method-wrapper

pkg:composer/babr/method-wrapper

Statistics

Installs: 102

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.5 2026-08-13 13:11 UTC

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

  1. Mark a class #[Wrapable] — the library will generate a proxy for it on demand.
  2. Mark individual methods #[Wrap('interceptor-name')] — those methods will be intercepted.
  3. Calls go through before() → original method → after(). Exceptions go through onException().
  4. 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

  • final methods are not intercepted — the generated proxy does not override them, so the original behavior is preserved.
  • final classes 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 0755 if 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/