Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPGRADE-7.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ DependencyInjection
-------------------

* Add argument `$target` to `ContainerBuilder::registerAliasForArgument()`
* Add argument `$throwOnAbstract` to `ContainerBuilder::findTaggedResourceIds()`
* Deprecate registering a service without a class when its id is a non-existing FQCN

DoctrineBridge
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Allow adding resource tags using any config format
* Allow `#[AsAlias]` to be extended
* Parse attributes found on abstract classes for resource definitions
* Add argument `$target` to `ContainerBuilder::registerAliasForArgument()`
* Deprecate registering a service without a class when its id is a non-existing FQCN

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,18 @@ public function process(ContainerBuilder $container): void
}
}

parent::process($container);
$this->container = $container;
foreach ($container->getDefinitions() as $id => $definition) {
$this->currentId = $id;
$this->processValue($definition, true);
}
}

protected function processValue(mixed $value, bool $isRoot = false): mixed
{
if (!$value instanceof Definition
|| !$value->isAutoconfigured()
|| $value->isAbstract()
|| ($value->isAbstract() && !$value->hasTag('container.excluded'))
|| $value->hasTag('container.ignore_attributes')
|| !($classReflector = $this->container->getReflectionClass($value->getClass(), false))
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private function processValue(mixed $value, int $rootLevel = 0, int $level = 0):
} elseif ($value instanceof ArgumentInterface) {
$value->setValues($this->processValue($value->getValues(), $rootLevel, 1 + $level));
} elseif ($value instanceof Definition) {
if ($value->isSynthetic() || $value->isAbstract()) {
if ($value->isSynthetic() || $value->isAbstract() || $value->hasTag('container.excluded')) {
return $value;
}
$value->setArguments($this->processValue($value->getArguments(), 0));
Expand Down
23 changes: 17 additions & 6 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1366,19 +1366,30 @@ public function findTaggedServiceIds(string $name, bool $throwOnAbstract = false
* }
* }
*
* @param bool $throwOnAbstract
*
* @return array<string, array> An array of tags with the tagged service as key, holding a list of attribute arrays
*/
public function findTaggedResourceIds(string $tagName): array
public function findTaggedResourceIds(string $tagName/* , bool $throwOnAbstract = true */): array
{
$throwOnAbstract = \func_num_args() > 1 ? func_get_arg(1) : true;
$this->usedTags[] = $tagName;
$tags = [];
foreach ($this->getDefinitions() as $id => $definition) {
if ($definition->hasTag($tagName)) {
if (!$definition->hasTag('container.excluded')) {
throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName));
}
$tags[$id] = $definition->getTag($tagName);
if (!$definition->hasTag($tagName)) {
continue;
}
if (!$definition->hasTag('container.excluded')) {
throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName));
}
$class = $this->parameterBag->resolveValue($definition->getClass());
if (!$class || $throwOnAbstract && $definition->isAbstract()) {
throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "%s" must have a class and not be abstract.', $id, $tagName));
}
if ($definition->getClass() !== $class) {
$definition->setClass($class);
}
$tags[$id] = $definition->getTag($tagName);
}

return $tags;
Expand Down
3 changes: 1 addition & 2 deletions src/Symfony/Component/DependencyInjection/Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,7 @@ public function addTag(string $name, array $attributes = []): static
public function addResourceTag(string $name, array $attributes = []): static
{
return $this->addTag($name, $attributes)
->addTag('container.excluded', ['source' => \sprintf('by tag "%s"', $name)])
->setAbstract(true);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a resource tag is set using ContainerBuilder::registerAttributeForAutoconfiguration(), this "abstract" flag isn't set on corresponding services. The reason is that autoconfiguration cannot set this flag by design - it's ignored in ResolveInstanceofConditionalsPass (and used for something else - aka cannot be reclaimed).
This means resource definitions are most of the time NOT going to be defined as abstract.
Therefor, this call here is useless, and confusing.

->addTag('container.excluded', ['source' => \sprintf('by tag "%s"', $name)]);
}

/**
Expand Down
20 changes: 12 additions & 8 deletions src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,26 @@ public function registerClasses(Definition $prototype, string $namespace, string
}

$r = null === $errorMessage ? $this->container->getReflectionClass($class) : null;
if ($r?->isAbstract() || $r?->isInterface()) {
if ($r->isInterface()) {
$this->interfaces[] = $class;
}
$autoconfigureAttributes?->processClass($this->container, $r);
continue;
}

$this->setDefinition($class, $definition = $getPrototype());
$abstract = $r?->isAbstract() || $r?->isInterface() ? '.abstract.' : '';
$this->setDefinition($abstract.$class, $definition = $getPrototype());
if (null !== $errorMessage) {
$definition->addError($errorMessage);

continue;
}
$definition->setClass($class);

if ($abstract) {
if ($r->isInterface()) {
$this->interfaces[] = $class;
}
$autoconfigureAttributes?->processClass($this->container, $r);
$definition->setAbstract(true)
->addTag('container.excluded', ['source' => 'because the class is abstract']);
continue;
}

$interfaces = [];
foreach (class_implements($class, false) as $interface) {
$this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,31 @@ public function testAttributeConfiguratorCallableMissingType()
$this->expectExceptionMessage('Argument "$reflector" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in ');
(new AttributeAutoconfigurationPass())->process($container);
}

public function testProcessesAbstractServicesWithContainerExcludedTag()
{
$container = new ContainerBuilder();
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, static function (ChildDefinition $definition, AsTaggedItem $attribute, \ReflectionClass $reflector) {
$definition->addTag('processed.tag');
});

// Create an abstract service with container.excluded tag and attributes
$abstractService = $container->register('abstract_service', TestServiceWithAttributes::class)
->setAutoconfigured(true)
->setAbstract(true)
->addTag('container.excluded', ['source' => 'test']);

(new AttributeAutoconfigurationPass())->process($container);

// Abstract service with container.excluded tag should be processed
$expected = [
TestServiceWithAttributes::class => (new ChildDefinition(''))->addTag('processed.tag'),
];
$this->assertEquals($expected, $abstractService->getInstanceofConditionals());
}
}

#[AsTaggedItem]
class TestServiceWithAttributes
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public function testAddResourceTag()
$def->addResourceTag('foo', ['bar' => true]);

$this->assertSame([['bar' => true]], $def->getTag('foo'));
$this->assertTrue($def->isAbstract());
$this->assertFalse($def->isAbstract());
$this->assertSame([['source' => 'by tag "foo"']], $def->getTag('container.excluded'));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

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

abstract class AbstractClass
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\AbstractClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\BadClasses\MissingParent;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\FooInterface;
Expand Down Expand Up @@ -114,7 +115,7 @@ public function testRegisterClasses()
$loader->registerAliasesForSinglyImplementedInterfaces();

$this->assertEquals(
['service_container', Bar::class],
['service_container', Bar::class, '.abstract.'.BarInterface::class],
array_keys($container->getDefinitions())
);
$this->assertEquals([BarInterface::class], array_keys($container->getAliases()));
Expand Down Expand Up @@ -214,6 +215,24 @@ public function testNestedRegisterClasses()
$this->assertEquals([FooInterface::class => (new ChildDefinition(''))->addTag('foo')], $container->getAutoconfiguredInstanceof());
}

public function testRegisterClassesWithAbstractClassesAndAutoconfigure()
{
$container = new ContainerBuilder();
$loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'));

$loader->registerClasses(
(new Definition())->setAutoconfigured(true),
'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\',
'Prototype/*',
'Prototype/{StaticConstructor}'
);

$definition = $container->getDefinition('.abstract.'.AbstractClass::class);
$this->assertTrue($definition->isAbstract());
$this->assertTrue($definition->hasTag('container.excluded'));
$this->assertTrue($definition->isAutoconfigured());
}

public function testMissingParentClass()
{
$container = new ContainerBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public function testResourceTags()
$this->assertTrue($def->hasTag('another.tag'));
$this->assertSame([['foo' => 'bar']], $def->getTag('my.tag'));
$this->assertSame([[]], $def->getTag('another.tag'));
$this->assertTrue($def->isAbstract());
$this->assertFalse($def->isAbstract());
}

public function testAutoConfigureAndChildDefinition()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ public function testResourceTags()
$this->assertTrue($def->hasTag('another.tag'));
$this->assertSame([['foo' => 'bar']], $def->getTag('my.tag'));
$this->assertSame([[]], $def->getTag('another.tag'));
$this->assertTrue($def->isAbstract());
$this->assertFalse($def->isAbstract());
}

public function testParsesIteratorArgument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ public function testResourceTags()
$this->assertTrue($def->hasTag('another.tag'));
$this->assertSame([['foo' => 'bar']], $def->getTag('my.tag'));
$this->assertSame([[]], $def->getTag('another.tag'));
$this->assertTrue($def->isAbstract());
$this->assertFalse($def->isAbstract());
}

public function testLoadShortSyntax()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
* Sets the streamable metadata to the services that need them.
Expand All @@ -31,16 +32,20 @@ public function process(ContainerBuilder $container): void

// retrieve concrete services tagged with "json_streamer.streamable" tag
foreach ($container->getDefinitions() as $id => $definition) {
if (!$tag = ($definition->getTag('json_streamer.streamable')[0] ?? null)) {
if (!$tag = $definition->getTag('json_streamer.streamable')[0] ?? null) {
continue;
}

if (($className = $container->getDefinition($id)->getClass()) && !$container->getDefinition($id)->isAbstract()) {
$streamable[$className] = [
'object' => $tag['object'],
'list' => $tag['list'],
];
if (!$definition->hasTag('container.excluded')) {
throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "json_streamer.streamable" is missing the "container.excluded" tag.', $id));
}
$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (!$class || $definition->isAbstract()) {
throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "json_streamer.streamable" must have a class and not be abstract.', $id));
}
$streamable[$class] = [
'object' => $tag['object'],
'list' => $tag['list'],
];

$container->removeDefinition($id);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\JsonStreamer\DependencyInjection\StreamablePass;

class StreamablePassTest extends TestCase
Expand All @@ -25,8 +26,7 @@ public function testSetStreamable()
$container->register('.json_streamer.cache_warmer.streamer')->setArguments([null]);
$container->register('.json_streamer.cache_warmer.lazy_ghost')->setArguments([null]);

$container->register('streamable')->setClass('Foo')->addTag('json_streamer.streamable', ['object' => true, 'list' => true]);
$container->register('abstractStreamable')->setClass('Bar')->addTag('json_streamer.streamable', ['object' => true, 'list' => true])->setAbstract(true);
$container->register('streamable')->setClass('Foo')->addTag('json_streamer.streamable', ['object' => true, 'list' => true])->addTag('container.excluded');
$container->register('notStreamable')->setClass('Baz');

$pass = new StreamablePass();
Expand All @@ -37,5 +37,9 @@ public function testSetStreamable()

$this->assertSame(['Foo' => ['object' => true, 'list' => true]], $streamerCacheWarmer->getArgument(0));
$this->assertSame(['Foo'], $lazyGhostCacheWarmer->getArgument(0));

$container->register('abstractStreamable')->setClass('Bar')->addTag('json_streamer.streamable', ['object' => true, 'list' => true])->addTag('container.excluded')->setAbstract(true);
$this->expectException(InvalidArgumentException::class);
$pass->process($container);
}
}
Loading