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->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

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy