yceruto/decorator

PHP function decorators

v1.3.3 2024-12-15 21:21 UTC

This package is auto-updated.

Last update: 2024-12-15 21:22:45 UTC


README

GitHub Actions Workflow Status Version PHP GitHub License

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

Installation

composer require yceruto/decorator

Usage

Example 1 - Minimalistic

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

Example 2 - Split decorator metadata from its implementation

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.

Example 3 - Decorating invokable objects

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.

Example 4 - Adding configuration to your decorator

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

Frameworks Integration

Decorators

Compound

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.

License

This software is published under the MIT License