Skip to content

Commit 635fee7

Browse files
[DI] Allow defining service ids, named autowiring aliases, index and priority via the #[Service] attribute on PHP 8
1 parent 8aaa152 commit 635fee7

11 files changed

+191
-16
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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\Attribute;
13+
14+
/**
15+
* An attribute to tell under which id a class should be registered as a service.
16+
*
17+
* Declare a type and optionnaly a name to create a named autowiring alias for the class.
18+
* The name and the priority also tell how tagged iterators and locators should be indexed and ordered.
19+
*
20+
* @author Nicolas Grekas <p@tchwork.com>
21+
*/
22+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
23+
class Service
24+
{
25+
public function __construct(
26+
public ?string $id = null,
27+
public ?string $type = null,
28+
public ?string $name = null,
29+
public ?int $priority = null,
30+
) {
31+
}
32+
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `ServicesConfigurator::remove()` in the PHP-DSL
88
* Add `%env(not:...)%` processor to negate boolean values
99
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
10+
* Add support for defining service ids, named autowiring aliases, index and priority via the `#[Service]` attribute on PHP 8
1011
* Add autoconfigurable attributes
1112

1213
5.2.0

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container
7676

7777
if (null !== $indexAttribute && isset($attribute[$indexAttribute])) {
7878
$index = $attribute[$indexAttribute];
79+
} elseif (isset($attribute['name'])) {
80+
$index = $attribute['name'];
7981
} elseif (null === $defaultIndex && $defaultPriorityMethod && $class) {
8082
$defaultIndex = PriorityTaggedServiceUtil::getDefaultIndex($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute);
8183
}

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

1414
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
15+
use Symfony\Component\DependencyInjection\Attribute\Service;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
1617
use Symfony\Component\DependencyInjection\Definition;
18+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1719
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
1820

1921
/**
@@ -73,7 +75,8 @@ public function process(ContainerBuilder $container)
7375

7476
foreach ($container->getDefinitions() as $id => $definition) {
7577
if ($this->accept($definition) && null !== $class = $container->getReflectionClass($definition->getClass())) {
76-
$this->processClass($container, $class);
78+
$this->registerAutoconfigureAttributes($container, $class);
79+
$this->registerServiceAttributes($container, $class, $id);
7780
}
7881
}
7982
}
@@ -83,10 +86,47 @@ public function accept(Definition $definition): bool
8386
return 80000 <= \PHP_VERSION_ID && $definition->isAutoconfigured() && !$definition->hasTag($this->ignoreAttributesTag);
8487
}
8588

86-
public function processClass(ContainerBuilder $container, \ReflectionClass $class)
89+
public function registerAutoconfigureAttributes(ContainerBuilder $container, \ReflectionClass $class): void
8790
{
8891
foreach ($class->getAttributes(Autoconfigure::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
8992
($this->registerForAutoconfiguration)($container, $class, $attribute);
9093
}
9194
}
95+
96+
public function registerServiceAttributes(ContainerBuilder $container, \ReflectionClass $class, string $id = null): string
97+
{
98+
$typedServices = [];
99+
100+
foreach ($class->getAttributes(Service::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
101+
/** @var Service $service */
102+
$service = $attribute->newInstance();
103+
104+
if ($service->type) {
105+
if ($service->type !== $class->name && !$class->isSubclassOf($service->type)) {
106+
throw new InvalidArgumentException(sprintf('Type "%s" in #[Service] attribute should be a supertype of class "%s".', $service->type, $class->name));
107+
}
108+
109+
if ($service->id || $service->name) {
110+
$typedServices[] = $service;
111+
}
112+
}
113+
114+
if (!$service->id || $service->id === $id) {
115+
continue;
116+
}
117+
118+
if (null === $id) {
119+
$definition = $container->getDefinition($class->name);
120+
$container->setDefinition($id = $service->id, $definition->setClass($class->name));
121+
} else {
122+
$container->setAlias($service->id, $id);
123+
}
124+
}
125+
126+
foreach ($typedServices as $service) {
127+
$container->registerAliasForArgument($service->id ?? $id ?? $class->name, $service->type, $service->name);
128+
}
129+
130+
return $id ?? $class->name;
131+
}
92132
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

14+
use Symfony\Component\DependencyInjection\Attribute\Service;
1415
use Symfony\Component\DependencyInjection\ChildDefinition;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
1617
use Symfony\Component\DependencyInjection\Definition;
@@ -72,6 +73,14 @@ private function processDefinition(ContainerBuilder $container, string $id, Defi
7273
$reflectionClass = null;
7374
$parent = $definition instanceof ChildDefinition ? $definition->getParent() : null;
7475

76+
$typedServices = [];
77+
if ($definition->isAutoconfigured() && \PHP_VERSION_ID >= 80000 && $reflectionClass = $container->getReflectionClass($class, false) ?: false) {
78+
foreach ($reflectionClass->getAttributes(Service::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
79+
$service = $attribute->newInstance();
80+
$typedServices += [$service->type ?? '' => $service];
81+
}
82+
}
83+
7584
foreach ($conditionals as $interface => $instanceofDefs) {
7685
if ($interface !== $class && !(null === $reflectionClass ? $reflectionClass = ($container->getReflectionClass($class, false) ?: false) : $reflectionClass)) {
7786
continue;
@@ -87,7 +96,22 @@ private function processDefinition(ContainerBuilder $container, string $id, Defi
8796
$instanceofDef->setAbstract(true)->setParent($parent ?: '.abstract.instanceof.'.$id);
8897
$parent = '.instanceof.'.$interface.'.'.$key.'.'.$id;
8998
$container->setDefinition($parent, $instanceofDef);
90-
$instanceofTags[] = $instanceofDef->getTags();
99+
100+
$tags = $instanceofDef->getTags();
101+
if ($tags && $service = $typedServices[$interface] ?? $typedServices[''] ?? null) {
102+
foreach ($tags as $tag => $attributes) {
103+
foreach ($attributes as $i => $attribute) {
104+
if (null !== $service->name) {
105+
$tags[$tag][$i] += ['name' => $service->name];
106+
}
107+
if (null !== $service->priority) {
108+
$tags[$tag][$i] += ['priority' => $service->priority];
109+
}
110+
}
111+
}
112+
}
113+
114+
$instanceofTags[] = $tags;
91115
$instanceofBindings = $instanceofDef->getBindings() + $instanceofBindings;
92116

93117
foreach ($instanceofDef->getMethodCalls() as $methodCall) {

src/Symfony/Component/DependencyInjection/Loader/FileLoader.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ public function registerClasses(Definition $prototype, string $namespace, string
9898
}
9999

100100
$autoconfigureAttributes = new RegisterAutoconfigureAttributesPass();
101-
$classes = $this->findClasses($namespace, $resource, (array) $exclude, $autoconfigureAttributes->accept($prototype) ? $autoconfigureAttributes : null);
101+
$autoconfigureAttributes = !$this->isLoadingInstanceof && $autoconfigureAttributes->accept($prototype) ? $autoconfigureAttributes : null;
102+
$classes = $this->findClasses($namespace, $resource, (array) $exclude, $autoconfigureAttributes);
102103
// prepare for deep cloning
103104
$serializedPrototype = serialize($prototype);
104105

@@ -112,8 +113,14 @@ public function registerClasses(Definition $prototype, string $namespace, string
112113

113114
continue;
114115
}
116+
117+
$id = $class;
118+
if ($autoconfigureAttributes) {
119+
$id = $autoconfigureAttributes->registerServiceAttributes($this->container, $this->container->getReflectionClass($class));
120+
}
121+
115122
foreach (class_implements($class, false) as $interface) {
116-
$this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class;
123+
$this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $id;
117124
}
118125
}
119126
}
@@ -211,7 +218,7 @@ private function findClasses(string $namespace, string $pattern, array $excludeP
211218
}
212219

213220
if ($autoconfigureAttributes && !$r->isInstantiable()) {
214-
$autoconfigureAttributes->processClass($this->container, $r);
221+
$autoconfigureAttributes->registerAutoconfigureAttributes($this->container, $r);
215222
}
216223
}
217224

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
16+
use Symfony\Component\DependencyInjection\Attribute\Service;
17+
use Symfony\Component\DependencyInjection\ChildDefinition;
1618
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
19+
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
1720
use Symfony\Component\DependencyInjection\ContainerBuilder;
1821
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1922
use Symfony\Component\DependencyInjection\Reference;
@@ -184,6 +187,33 @@ public function provideInvalidDefaultMethods(): iterable
184187
yield ['getMethodShouldBePublicInsteadPrivate', null, sprintf('Method "%s::getMethodShouldBePublicInsteadPrivate()" should be public.', FooTaggedForInvalidDefaultMethodClass::class)];
185188
yield ['getMethodShouldBePublicInsteadPrivate', 'foo', sprintf('Either method "%s::getMethodShouldBePublicInsteadPrivate()" should be public or tag "my_custom_tag" on service "service1" is missing attribute "foo".', FooTaggedForInvalidDefaultMethodClass::class)];
186189
}
190+
191+
/**
192+
* @requires PHP 8
193+
*/
194+
public function testServiceAttributes()
195+
{
196+
$container = new ContainerBuilder();
197+
$container->register('service1', FooTagClass::class)->addTag('my_custom_tag');
198+
$container->register('service2', HelloNamedService::class)
199+
->setAutoconfigured(true)
200+
->setInstanceofConditionals([
201+
HelloNamedService::class => (new ChildDefinition(''))->addTag('my_custom_tag'),
202+
]);
203+
204+
(new ResolveInstanceofConditionalsPass())->process($container);
205+
206+
$priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation();
207+
208+
$tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar');
209+
$expected = [
210+
'hello' => new TypedReference('service2', HelloNamedService::class),
211+
'service1' => new TypedReference('service1', FooTagClass::class),
212+
];
213+
$services = $priorityTaggedServiceTraitImplementation->test($tag, $container);
214+
$this->assertSame(array_keys($expected), array_keys($services));
215+
$this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container));
216+
}
187217
}
188218

189219
class PriorityTaggedServiceTraitImplementation
@@ -195,3 +225,8 @@ public function test($tagName, ContainerBuilder $container)
195225
return $this->findAndSortTaggedServices($tagName, $container);
196226
}
197227
}
228+
229+
#[Service(name: 'hello', priority: 1)]
230+
class HelloNamedService
231+
{
232+
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\DependencyInjection\Reference;
2020
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureAttributed;
2121
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredInterface;
22+
use Symfony\Component\DependencyInjection\Tests\Fixtures\ServiceAttributed;
2223

2324
/**
2425
* @requires PHP 8
@@ -78,4 +79,18 @@ public function testAutoconfiguredTag()
7879
;
7980
$this->assertEquals([AutoconfiguredInterface::class => $expected], $container->getAutoconfiguredInstanceof());
8081
}
82+
83+
public function testServiceAttribute()
84+
{
85+
$container = new ContainerBuilder();
86+
$container->register(ServiceAttributed::class, ServiceAttributed::class)
87+
->setAutoconfigured(true);
88+
89+
(new RegisterAutoconfigureAttributesPass())->process($container);
90+
91+
$this->assertTrue($container->hasAlias('foo'));
92+
$this->assertTrue($container->hasAlias(ServiceAttributed::class.' $bar'));
93+
$this->assertSame(ServiceAttributed::class, (string) $container->getAlias('foo'));
94+
$this->assertSame(ServiceAttributed::class, (string) $container->getAlias(ServiceAttributed::class.' $bar'));
95+
}
8196
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
44

5-
class Foo implements FooInterface, Sub\BarInterface
5+
use Symfony\Component\DependencyInjection\Attribute\Service;
6+
7+
#[Service(id: 'foo', type: FooInterface::class)]
8+
#[Service(id: 'foo', type: Sub\BarInterface::class, name: 'bar')]
9+
#[Service(type: Sub\Bar::class, name: 'baz')]
10+
class Foo extends Sub\Bar implements FooInterface, Sub\BarInterface
611
{
712
public function __construct($bar = null)
813
{
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\Service;
6+
7+
#[Service(id: 'foo')]
8+
#[Service(name: 'bar', type: ServiceAttributed::class)]
9+
class ServiceAttributed
10+
{
11+
}

src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,20 @@ public function testNestedRegisterClasses()
179179
$this->assertTrue($container->has(Baz::class));
180180
$this->assertTrue($container->has(Foo::class));
181181

182-
$this->assertEquals(
183-
[
184-
PsrContainerInterface::class,
185-
ContainerInterface::class,
186-
FooInterface::class,
187-
],
188-
array_keys($container->getAliases())
189-
);
182+
$expected = [
183+
PsrContainerInterface::class,
184+
ContainerInterface::class,
185+
];
186+
if (\PHP_VERSION_ID >= 80000) {
187+
$expected[] = FooInterface::class.' $foo';
188+
$expected[] = BarInterface::class.' $bar';
189+
$expected[] = Bar::class.' $baz';
190+
}
191+
$expected[] = FooInterface::class;
192+
$this->assertSame($expected, array_keys($container->getAliases()));
190193

191194
$alias = $container->getAlias(FooInterface::class);
192-
$this->assertSame(Foo::class, (string) $alias);
195+
$this->assertSame(\PHP_VERSION_ID >= 80000 ? 'foo' : Foo::class, (string) $alias);
193196
$this->assertFalse($alias->isPublic());
194197
$this->assertTrue($alias->isPrivate());
195198

0 commit comments

Comments
 (0)