Skip to content

Commit d5d49c2

Browse files
committed
[DependencyInjection] Configure service tags via attributes.
1 parent d093475 commit d5d49c2

File tree

31 files changed

+581
-22
lines changed

31 files changed

+581
-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: 54 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,10 @@
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;
31+
use Symfony\Contracts\Service\Attribute\TagInterface;
2732
use Symfony\Contracts\Service\ServiceProviderInterface;
2833
use Symfony\Contracts\Service\ServiceSubscriberInterface;
2934

@@ -506,6 +511,45 @@ public function testTaggedServiceLocatorWithDefaultIndex()
506511
];
507512
$this->assertSame($expected, ['baz' => $serviceLocator->get('baz')]);
508513
}
514+
515+
/**
516+
* @requires PHP 8
517+
*/
518+
public function testTagsViaAttribute()
519+
{
520+
if (!\interface_exists(TagInterface::class)) {
521+
self::markTestSkipped('This test requires symfony/service-contracts 2.4 or newer.');
522+
}
523+
524+
$container = new ContainerBuilder();
525+
$container->register('one', TaggedService1::class)
526+
->setPublic(true)
527+
->setAutoconfigured(true);
528+
$container->register('two', TaggedService2::class)
529+
->setPublic(true)
530+
->setAutoconfigured(true);
531+
$container->register('three', TaggedService3::class)
532+
->setPublic(true)
533+
->setAutoconfigured(true);
534+
535+
$collector = new TagCollector();
536+
$container->addCompilerPass($collector);
537+
538+
$container->compile();
539+
540+
self::assertSame([
541+
'one' => [
542+
['foo' => 'bar', 'priority' => 0],
543+
['bar' => 'baz', 'priority' => 0],
544+
],
545+
'two' => [
546+
['someAttribute' => 'prio 100', 'priority' => 100],
547+
],
548+
'three' => [
549+
['someAttribute' => 'custom_tag_class'],
550+
],
551+
], $collector->collectedTags);
552+
}
509553
}
510554

511555
class ServiceSubscriberStub implements ServiceSubscriberInterface
@@ -566,3 +610,13 @@ public function setSunshine($type)
566610
{
567611
}
568612
}
613+
614+
final class TagCollector implements CompilerPassInterface
615+
{
616+
public $collectedTags;
617+
618+
public function process(ContainerBuilder $container): void
619+
{
620+
$this->collectedTags = $container->findTaggedServiceIds('app.custom_tag');
621+
}
622+
}
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: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
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;
26+
use Symfony\Contracts\Service\Attribute\TagInterface;
2227

2328
class RegisterListenersPassTest extends TestCase
2429
{
@@ -231,6 +236,82 @@ public function testInvokableEventListener()
231236
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
232237
}
233238

239+
/**
240+
* @requires PHP 8
241+
*/
242+
public function testTaggedInvokableEventListener()
243+
{
244+
if (!\interface_exists(TagInterface::class)) {
245+
self::markTestSkipped('This test requires symfony/service-contracts 2.4 or newer.');
246+
}
247+
248+
$container = new ContainerBuilder();
249+
$container->register('foo', TaggedInvokableListener::class)->setAutoconfigured(true);
250+
$container->register('event_dispatcher', \stdClass::class);
251+
252+
(new AttributeAutoconfigurationPass())->process($container);
253+
(new RegisterListenersPass())->process($container);
254+
255+
$definition = $container->getDefinition('event_dispatcher');
256+
$expectedCalls = [
257+
[
258+
'addListener',
259+
[
260+
CustomEvent::class,
261+
[new ServiceClosureArgument(new Reference('foo')), '__invoke'],
262+
0,
263+
],
264+
],
265+
];
266+
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
267+
}
268+
269+
/**
270+
* @requires PHP 8
271+
*/
272+
public function testTaggedMultiEventListener()
273+
{
274+
if (!\interface_exists(TagInterface::class)) {
275+
self::markTestSkipped('This test requires symfony/service-contracts 2.4 or newer.');
276+
}
277+
278+
$container = new ContainerBuilder();
279+
$container->register('foo', TaggedMultiListener::class)->setAutoconfigured(true);
280+
$container->register('event_dispatcher', \stdClass::class);
281+
282+
(new AttributeAutoconfigurationPass())->process($container);
283+
(new RegisterListenersPass())->process($container);
284+
285+
$definition = $container->getDefinition('event_dispatcher');
286+
$expectedCalls = [
287+
[
288+
'addListener',
289+
[
290+
CustomEvent::class,
291+
[new ServiceClosureArgument(new Reference('foo')), 'onCustomEvent'],
292+
0,
293+
],
294+
],
295+
[
296+
'addListener',
297+
[
298+
'foo',
299+
[new ServiceClosureArgument(new Reference('foo')), 'onFoo'],
300+
42,
301+
],
302+
],
303+
[
304+
'addListener',
305+
[
306+
'bar',
307+
[new ServiceClosureArgument(new Reference('foo')), 'onBarEvent'],
308+
0,
309+
],
310+
],
311+
];
312+
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
313+
}
314+
234315
public function testAliasedEventListener()
235316
{
236317
$container = new ContainerBuilder();
@@ -416,10 +497,6 @@ final class AliasedEvent
416497
{
417498
}
418499

419-
final class CustomEvent
420-
{
421-
}
422-
423500
final class TypedListener
424501
{
425502
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