Skip to content

[DependencyInjection] Add support for excluding services with declared custom attribute #50447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 7.4
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.1
---

* Add support for excluding services with declared custom attribute (`ContainerBuilder::registerAttributeForExclusion`)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Add support for excluding services with declared custom attribute (`ContainerBuilder::registerAttributeForExclusion`)
* Add support for excluding services by attribute with `ContainerBuilder::registerAttributeForExclusion()`


7.0
---

Expand Down
28 changes: 28 additions & 0 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Attribute\Exclude;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
Expand Down Expand Up @@ -131,6 +132,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/
private array $autoconfiguredAttributes = [];

/**
* @var array<class-string>
Copy link
Member

Choose a reason for hiding this comment

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

?

Suggested change
* @var array<class-string>
* @var array<class-string, true>

*/
private array $exclusionAttributes = [Exclude::class];

/**
* @var array<string, bool>
*/
Expand Down Expand Up @@ -1341,6 +1347,20 @@ public function registerAttributeForAutoconfiguration(string $attributeClass, ca
$this->autoconfiguredAttributes[$attributeClass] = $configurator;
}

/**
* Registers an attribute that will be used for excluding all services from classes it is declared on.
*
* @param class-string $attributeClass
*/
public function registerAttributeForExclusion(string $attributeClass): void
{
if (\in_array($attributeClass, $this->exclusionAttributes)) {
return;
}

$this->exclusionAttributes[] = $attributeClass;
}

/**
* Registers an autowiring alias that only binds to a specific argument name.
*
Expand Down Expand Up @@ -1386,6 +1406,14 @@ public function getAutoconfiguredAttributes(): array
return $this->autoconfiguredAttributes;
}

/**
* @return array<class-string>
*/
public function getExclusionAttributes(): array
{
return $this->exclusionAttributes;
}

/**
* Resolves env parameter placeholders in a string or an array.
*
Expand Down
11 changes: 8 additions & 3 deletions src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,15 @@ public function registerClasses(Definition $prototype, string $namespace, string
foreach ($classes as $class => $errorMessage) {
if (null === $errorMessage && $autoconfigureAttributes) {
$r = $this->container->getReflectionClass($class);
if ($r->getAttributes(Exclude::class)[0] ?? null) {
$this->addContainerExcludedTag($class, $source);
continue;
$exclusionAttributes = $this->container->getExclusionAttributes();
Copy link
Member

Choose a reason for hiding this comment

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

IIRC, file loaders are run on separate container instances, right? This would mean that a bundle cannot expect registerAttributeForExclusion to have any effect on classes in src/. Am I correct? If yes, don't we want to make these app-wide instead?

Copy link
Member

Choose a reason for hiding this comment

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

what about this? did you give it a try? configuring an exclusion within a bundle and seeing it work on the app?


foreach ($exclusionAttributes as $attribute) {
if ($r->getAttributes($attribute)[0] ?? null) {
$this->addContainerExcludedTag($class, $source);
continue 2;
}
}

if ($this->env) {
$attribute = null;
foreach ($r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

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

#[ToBeExcluded]
class ServiceWithAttributeForExclusion
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

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

#[\Attribute(\Attribute::TARGET_CLASS)]
class ToBeExcluded
Copy link
Member

Choose a reason for hiding this comment

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

It'd be great to allow this feature to work even if the attribute class doesn't not exist.
Can you remove this one and make the feature still work?

{
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasMultiple;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Utils\NotAService;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Utils\ServiceWithAttributeForExclusion;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Utils\ToBeExcluded;

class FileLoaderTest extends TestCase
{
Expand Down Expand Up @@ -164,6 +166,27 @@ public function testRegisterClassesWithExcludeAttribute(bool $autoconfigure)
$this->assertSame($autoconfigure, $container->getDefinition(NotAService::class)->hasTag('container.excluded'));
}

/**
* @testWith [true]
* [false]
*/
public function testRegisterClassesWithAttributeMarkedForExclusion(bool $autoconfigure)
{
$container = new ContainerBuilder();

$container->registerAttributeForExclusion(ToBeExcluded::class);

$loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'));

$loader->registerClasses(
(new Definition())->setAutoconfigured($autoconfigure),
'Symfony\Component\DependencyInjection\Tests\Fixtures\Utils\\',
'Utils/*',
);

$this->assertSame($autoconfigure, $container->getDefinition(ServiceWithAttributeForExclusion::class)->hasTag('container.excluded'));
}

public function testRegisterClassesWithExcludeAsArray()
{
$container = new ContainerBuilder();
Expand Down