Skip to content

Commit 03e909b

Browse files
committed
[Decorator] Add new component for function decoration
1 parent c3bb47a commit 03e909b

23 files changed

+821
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"symfony/config": "self.version",
6767
"symfony/console": "self.version",
6868
"symfony/css-selector": "self.version",
69+
"symfony/decorator": "self.version",
6970
"symfony/dependency-injection": "self.version",
7071
"symfony/debug-bundle": "self.version",
7172
"symfony/doctrine-bridge": "self.version",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.git* export-ignore
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Decorator\Attribute;
13+
14+
/**
15+
* An attribute that specifies under which decorator a function or method should be wrapped.
16+
*
17+
* @author Yonel Ceruto <open@yceruto.dev>
18+
*
19+
* @experimental
20+
*/
21+
#[\Attribute(\Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
22+
class Decorate
23+
{
24+
public function __construct(
25+
public string $id,
26+
public array $options = [],
27+
) {
28+
}
29+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
7.2
5+
---
6+
7+
* Add the component
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Decorator;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\Decorator\Attribute\Decorate;
16+
use Symfony\Contracts\Service\ServiceLocatorTrait;
17+
18+
/**
19+
* @author Yonel Ceruto <open@yceruto.dev>
20+
*/
21+
readonly class DecoratorChain implements DecoratorInterface
22+
{
23+
/**
24+
* @param ContainerInterface $decorators Locator of decorators, keyed by service id
25+
*/
26+
public function __construct(
27+
private ContainerInterface $decorators,
28+
) {
29+
}
30+
31+
/**
32+
* @param array<string, callable(): DecoratorInterface> $decorators
33+
*/
34+
public static function from(array $decorators): self
35+
{
36+
return new self(new class($decorators) implements ContainerInterface {
37+
use ServiceLocatorTrait;
38+
});
39+
}
40+
41+
public function call(callable $callable, mixed ...$args): mixed
42+
{
43+
return $this->decorate($callable(...))(...$args);
44+
}
45+
46+
public function decorate(\Closure $func): \Closure
47+
{
48+
foreach ($this->getAttributes($func) as $attribute) {
49+
$func = $this->decorators->get($attribute->id)->decorate($func, ...$attribute->options);
50+
}
51+
52+
return $func;
53+
}
54+
55+
/**
56+
* @return iterable<Decorate>
57+
*/
58+
private function getAttributes(\Closure $func): iterable
59+
{
60+
$attributes = (new \ReflectionFunction($func))->getAttributes(Decorate::class, \ReflectionAttribute::IS_INSTANCEOF);
61+
62+
foreach (array_reverse($attributes) as $attribute) {
63+
yield $attribute->newInstance();
64+
}
65+
}
66+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Decorator;
13+
14+
/**
15+
* Decorates the functionality of other function.
16+
*
17+
* @author Yonel Ceruto <open@yceruto.dev>
18+
*
19+
* @experimental
20+
*/
21+
interface DecoratorInterface
22+
{
23+
public function decorate(\Closure $func): \Closure;
24+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Decorator\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
15+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
20+
/**
21+
* @author Yonel Ceruto <open@yceruto.dev>
22+
*/
23+
final readonly class DecoratorsPass implements CompilerPassInterface
24+
{
25+
use PriorityTaggedServiceTrait;
26+
27+
public function process(ContainerBuilder $container): void
28+
{
29+
if (!$container->hasDefinition('decorator.chain')) {
30+
return;
31+
}
32+
33+
$tagName = new TaggedIteratorArgument('decorator', needsIndexes: true);
34+
$decorators = $this->findAndSortTaggedServices($tagName, $container);
35+
36+
$decoratorChain = $container->getDefinition('decorator.chain');
37+
$decoratorChain->replaceArgument(0, new ServiceLocatorArgument($decorators));
38+
}
39+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2024-present Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
Clock Component
2+
===============
3+
4+
Symfony Decorator modify the functionality of other functions or methods.
5+
6+
**This Component is experimental**.
7+
[Experimental features](https://symfony.com/doc/current/contributing/code/experimental.html)
8+
are not covered by Symfony's
9+
[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html).
10+
11+
Getting Started
12+
---------------
13+
14+
```bash
15+
composer require symfony/decorator
16+
```
17+
18+
```php
19+
use Symfony\Component\Decorator\Attribute\Decorate;
20+
use Symfony\Component\Decorator\DecoratorChain;
21+
use Symfony\Component\Decorator\DecoratorInterface;
22+
23+
class DebugDecorator implements DecoratorInterface
24+
{
25+
public function decorate(\Closure $func): \Closure
26+
{
27+
return function (mixed ...$args) use ($func): mixed {
28+
echo "Do something before\n";
29+
30+
$result = $func(...$args);
31+
32+
echo "Do something after\n";
33+
34+
return $result;
35+
}
36+
}
37+
}
38+
39+
$decorator = DecoratorChain::from([
40+
DebugDecorator::class => fn () => new DebugDecorator(),
41+
]);
42+
43+
#[\Attribute(\Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
44+
class Debug extends Decorate
45+
{
46+
public function __construct()
47+
{
48+
parent::__construct(DebugDecorator::class);
49+
}
50+
}
51+
52+
class Greeting
53+
{
54+
#[Debug]
55+
public function hello(string $name): void
56+
{
57+
echo "Hello $name\n"
58+
}
59+
}
60+
61+
$greeting = new Greeting();
62+
$decorator->call($greeting->hello(...), 'Fabien');
63+
```
64+
65+
Resources
66+
---------
67+
68+
* [Documentation](https://symfony.com/doc/current/components/decorator.html)
69+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
70+
* [Report issues](https://github.com/symfony/symfony/issues) and
71+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
72+
in the [main Symfony repository](https://github.com/symfony/symfony)

0 commit comments

Comments
 (0)