Note
Inspired by Python's decorator
This library implements the Decorator Pattern around any PHP callable, allowing you to:
- Execute logic before or after a callable is executed
- Skip the execution of a callable by returning earlier
- Modify the result of a callable
composer require yceruto/decorator
use Yceruto\Decorator\Attribute\DecoratorAttribute;
use Yceruto\Decorator\CallableDecorator;
use Yceruto\Decorator\DecoratorInterface;
#[\Attribute(\Attribute::TARGET_METHOD)]
class Debug extends DecoratorAttribute implements DecoratorInterface
{
public function decorate(\Closure $func): \Closure
{
return function (mixed ...$args) use ($func): mixed
{
echo "Do something before\n";
$result = $func(...$args);
echo "Do something after\n";
return $result;
};
}
}
class Greeting
{
#[Debug]
public function sayHello(string $name): void
{
echo "Hello $name!\n";
}
}
$greeting = new Greeting();
$decorator = new CallableDecorator();
$decorator->call($greeting->sayHello(...), 'John');
Output:
Do something before
Hello John!
Do something after
You can separate the decorator attribute from its implementation whenever necessary:
use Yceruto\Decorator\Attribute\DecoratorAttribute;
use Yceruto\Decorator\DecoratorInterface;
#[\Attribute(\Attribute::TARGET_METHOD)]
class Debug extends DecoratorAttribute
{
}
class DebugDecorator implements DecoratorInterface
{
public function decorate(\Closure $func): \Closure
{
return function (mixed ...$args) use ($func): mixed
{
echo "Do something before\n";
$result = $func(...$args);
echo "Do something after\n";
return $result;
};
}
}
The DecoratorAttribute
automatically links to its corresponding decorator class if the decorator is in the same
directory, shares the same base name, and ends with the *Decorator
suffix. If these conditions are not met, you must
define the decoratedBy()
method to establish the link manually.
In this example, the Debug
attribute is used to decorate an invokable object. The decorator logic will be applied
when the __invoke()
method is called.
#[Debug]
class Greeting
{
public function __invoke(string $name): void
{
echo "Hello $name!\n";
}
}
Important
The attribute will only be collected if there are no other decorator attributes defined on the __invoke
method.
Add options through your attribute __construct()
method, and pass them at the end of your decorate()
method:
use Yceruto\Decorator\Attribute\DecoratorAttribute;
use Yceruto\Decorator\DecoratorInterface;
#[\Attribute(\Attribute::TARGET_METHOD)]
class Debug extends DecoratorAttribute implements DecoratorInterface
{
public function __construct(
private readonly string $prefix = '',
) {
}
public function decorate(\Closure $func, self $debug = new self()): \Closure
{
return function (mixed ...$args) use ($func, $debug): mixed
{
echo $debug->prefix."Do something before\n";
$result = $func(...$args);
echo $debug->prefix."Do something after\n";
return $result;
};
}
}
class Greeting
{
#[Debug(prefix: 'Greeting: ')]
public function sayHello(string $name): void
{
echo "Hello $name!\n";
}
}
Output:
Greeting: Do something before
Hello John!
Greeting: Do something after
To create a reusable set of decorators, extend the Compound
class:
use Yceruto\Decorator\Attribute\Compound;
#[\Attribute(\Attribute::TARGET_METHOD)]
class Greetings extends Compound
{
/**
* @return array<DecoratorAttribute>
*/
public function getDecorators(array $options): array
{
return [
new Hello(),
new Welcome(),
// ...
];
}
}
class Greeting
{
#[Greetings]
public function __invoke(): void
{
// ...
}
}
When the Greeting::__invoke()
method is decorated, the Hello
and Welcome
decorator attributes will be applied
in the specified order. This is equivalent to directly defining #[Hello]
and #[Welcome]
on this method.
This software is published under the MIT License