8000 [DependencyInjection] Configure service tags via attributes. · symfony/symfony@fac3e51 · GitHub
[go: up one dir, main page]

Skip to content

Commit fac3e51

Browse files
committed
[DependencyInjection] Configure service tags via attributes.
1 parent 76f02fd commit fac3e51

File tree

31 files changed

+567
-22
lines changed

31 files changed

+567
-22
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Contracts\Service\Attribute\TagInterface;
16+
17+
final class AttributeAutoconfigurationPass implements CompilerPassInterface
18+
{
19+
public function process(ContainerBuilder $container): void
20+
{
21+
if (80000 > \PHP_VERSION_ID || !\interface_exists(TagInterface::class)) {
22+
return;
23+
}
24+
25+
foreach ($container->getDefinitions() as $definition) {
26+
if (!$definition->isAutoconfigured()) {
27+
continue;
28+
}
29+
30+
if (!$class = $container->getParameterBag()->resolveValue($definition->getClass())) {
31+
continue;
32+
}
33+
34+
try {
35+
$reflector = new \ReflectionClass($class);
36+
} catch (\ReflectionException $e) {
37+
continue;
38+
}
39+
40+
foreach ($reflector->getAttributes(TagInterface::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
41+
/** @var TagInterface $tag */
42+
$tag = $attribute->newInstance();
43+
$definition->addTag($tag->getName(), $tag->getAttributes());
44+
}
45+
}
46+
}
47+
}

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function __construct()
4242
$this->beforeOptimizationPasses = [
4343
100 => [
4444
new ResolveClassPass(),
45+
new AttributeAutoconfigurationPass(),
4546
new ResolveInstanceofConditionalsPass(),
4647
new RegisterEnvVarProcessorsPass(),
4748
],

src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\DependencyInjection\Alias;
1717
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1818
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
19+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1920
use Symfony\Component\DependencyInjection\ContainerBuilder;
2021
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
2122
use Symfony\Component\DependencyInjection\Reference;
@@ -24,6 +25,9 @@
2425
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
2526
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
2627
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooTagClass;
28+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService1;
29+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
30+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
2731
use Symfony\Contracts\Service\ServiceProviderInterface;
2832
use Symfony\Contracts\Service\ServiceSubscriberInterface;
2933

@@ -506,6 +510,41 @@ public function testTaggedServiceLocatorWithDefaultIndex()
506510
];
507511
$this->assertSame($expected, ['baz' => $serviceLocator->get('baz')]);
508512
}
513+
514+
/**
515+
* @requires PHP 8
516+
*/
517+
public function testTagsViaAttribute()
518+
{
519+
$container = new ContainerBuilder();
520+
$container->register('one', TaggedService1::class)
521+
->setPublic(true)
522+
->setAutoconfigured(true);
523+
$container->register('two', TaggedService2::class)
524+
->setPublic(true)
525+
->setAutoconfigured(true);
526+
$container->register('three', TaggedService3::class)
527+
->setPublic(true)
528+
->setAutoconfigured(true);
529+
530+
$collector = new TagCollector();
531+
$container->addCompilerPass($collector);
532+
533+
$container->compile();
534+
535+
self::assertSame([
536+
'one' => [
537+
['foo' => 'bar', 'priority' => 0],
538+
['bar' => 'baz', 'priority' => 0],
539+
],
540+
'two' => [
541+
['someAttribute' => 'prio 100', 'priority' => 100],
542+
],
543+
'three' => [
544+
['someAttribute' => 'custom_tag_class'],
545+
]
546+
], $collector->collectedTags);
547+
}
509548
}
510549

511550
class ServiceSubscriberStub implements ServiceSubscriberInterface
@@ -566,3 +605,13 @@ public function setSunshine($type)
566605
{
567606
}
568607
}
608+
609+
final class TagCollector implements CompilerPassInterface
610+
{
611+
public $collectedTags;
612+
613+
public function process(ContainerBuilder $container): void
614+
{
615+
$this->collectedTags = $container->findTaggedServiceIds('app.custom_tag');
616+
}
617+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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\DependencyInjection\Tests\Fixtures\Attribute;
13+
14+
use Symfony\Contracts\Service\Attribute\TagInterface;
15+
16+
#[\Attribute(\Attribute::TARGET_CLASS)]
17+
final class CustomTag implements TagInterface
18+
{
19+
public function getName(): string
20+
{
21+
return 'app.custom_tag';
22+
}
23+
24+
public function getAttributes(): array
25+
{
26+
return ['someAttribute' => 'custom_tag_class'];
27+
}
28+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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\DependencyInjection\Tests\Fixtures;
13+
14+
use Symfony\Contracts\Service\Attribute\Tag;
15+
16+
#[Tag(name: 'app.custom_tag', attributes: ['foo' => 'bar'])]
17+
#[Tag(name: 'app.custom_tag', attributes: ['bar' => 'baz'])]
18+
final class TaggedService1
19+
{
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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\DependencyInjection\Tests\Fixtures;
13+
14+
use Symfony\Contracts\Service\Attribute\Tag;
15+
16+
#[Tag(name: 'app.custom_tag', priority: 100, attributes: ['someAttribute' => 'prio 100'])]
17+
final class TaggedService2
18+
{
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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\DependencyInjection\Tests\Fixtures;
13+
14+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomTag;
15+
16+
#[CustomTag]
17+
final class TaggedService3
18+
{
19+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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\EventDispatcher\Attribute;
13+
14+
use Symfony\Contracts\Service\Attribute\TagInterface;
15+
16+
/**
17+
* Service tag to autoconfigure event listeners.
18+
*
19+
* @author Alexander M. Turek <me@derrabus.de>
20+
*/
21+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
22+
class EventListener implements TagInterface
23+
{
24+
public function __construct(
25+
private ?string $event = null,
26+
private ?string $method = null,
27+
private int $priority = 0
28+
) {
29+
}
30+
31+
public function getName(): string
32+
{
33+
return 'kernel.event_listener';
34+
}
35+
36+
public function getAttributes(): array
37+
{
38+
return [
39+
'event' => $this->event,
40+
'method' => $this->method,
41+
'priority' => $this->priority,
42+
];
43+
}
44+
}

src/Symfony/Component/EventDispatcher/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.3.0
5+
-----
6+
7+
* Added the `EventListener` service tag attribute for PHP 8.
8+
49
5.1.0
510
-----
611

src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
16+
use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
1718
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1819
use Symfony\Component\DependencyInjection\Reference;
1920
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
2021
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
2122
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
23+
use Symfony\Component\EventDispatcher\Tests\Fixtures\CustomEvent;
24+
use Symfony\Component\EventDispatcher\Tests\Fixtures\TaggedInvokableListener;
25+
use Symfony\Component\EventDispatcher\Tests\Fixtures\TaggedMultiListener;
2226

2327
class RegisterListenersPassTest extends TestCase
2428
{
@@ -231,6 +235,74 @@ public function testInvokableEventListener()
231235
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
232236
}
233237

238+
/**
239+
* @requires PHP 8
240+
*/
241+
public function testTaggedInvokableEventListener()
242+
{
243+
$container = new ContainerBuilder();
244+
$container->register('foo', TaggedInvokableListener::class)->setAutoconfigured(true);
245+
$container->register('event_dispatcher', \stdClass::class);
246+
247+
(new AttributeAutoconfigurationPass())->process($container);
248+
(new RegisterListenersPass())->process($container);
249+
250+
$definition = $container->getDefinition('event_dispatcher');
251+
$expectedCalls = [
252+
[
253+
'addListener',
254+
[
255+
CustomEvent::class,
256+
[new ServiceClosureArgument(new Reference('foo')), '__invoke'],
257+
0,
258+
],
259+
],
260+
];
261+
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
262+
}
263+
264+
/**
265+
* @requires PHP 8
266+
*/
267+
public function testTaggedMultiEventListener()
268+
{
269+
$container = new ContainerBuilder();
270+
$container->register('foo', TaggedMultiListener::class)->setAutoconfigured(true);
271+
$container-&g 97AE t;register('event_dispatcher', \stdClass::class);
272+
273+
(new AttributeAutoconfigurationPass())->process($container);
274+
(new RegisterListenersPass())->process($container);
275+
276+
$definition = $container->getDefinition('event_dispatcher');
277+
$expectedCalls = [
278+
[
279+
'addListener',
280+
[
281+
CustomEvent::class,
282+
[new ServiceClosureArgument(new Reference('foo')), 'onCustomEvent'],
283+
0,
284+
],
285+
],
286+
[
287+
'addListener',
288+
[
289+
'foo',
290+
[new ServiceClosureArgument(new Reference('foo')), 'onFoo'],
291+
42,
292+
],
293+
],
294+
[
295+
'addListener',
296+
[
297+
'bar',
298+
[new ServiceClosureArgument(new Reference('foo')), 'onBarEvent'],
299+
0,
300+
],
301+
],
302+
];
303+
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
304+
}
305+
234306
public function testAliasedEventListener()
235307
{
236308
$container = new ContainerBuilder();
@@ -419,10 +491,6 @@ final class AliasedEvent
419491
{
420492
}
421493

422-
final class CustomEvent
423-
{
424-
}
425-
426494
final class TypedListener
427495
{
428496
public function __invoke(AliasedEvent $event): void
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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\EventDispatcher\Tests\Fixtures;
13+
14+
final class CustomEvent
15+
{
16+
}

0 commit comments

Comments
 (0)
0