From 7349e3f517f76a4ce2a53e1993a0a01800e0c0e2 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 12 Jun 2024 13:46:33 +0200 Subject: [PATCH] [DependencyInjection] Add `#[WhenNot]` attribute --- src/Symfony/Component/Config/CHANGELOG.md | 5 +++ .../DependencyInjection/Attribute/WhenNot.php | 26 +++++++++++ .../DependencyInjection/Loader/FileLoader.php | 27 ++++++++++-- .../Loader/PhpFileLoader.php | 28 ++++++++++-- .../BadAttributes/WhenNotWhenFoo.php | 12 ++++++ .../Tests/Fixtures/Prototype/NotFoo.php | 27 ++++++++++++ .../Fixtures/config/instanceof.expected.yml | 3 ++ .../Tests/Fixtures/config/not_when_env.php | 7 +++ .../Fixtures/config/prototype.expected.yml | 13 ++++++ .../Tests/Fixtures/config/prototype.php | 2 +- .../config/prototype_array.expected.yml | 13 ++++++ .../Tests/Fixtures/config/prototype_array.php | 2 +- .../Fixtures/config/when_not_when_env.php | 8 ++++ .../Tests/Fixtures/xml/services_prototype.xml | 2 +- .../Fixtures/xml/services_prototype_array.xml | 1 + ...rvices_prototype_array_with_space_node.xml | 1 + .../Fixtures/yaml/services_prototype.yml | 2 +- .../Tests/Loader/FileLoaderTest.php | 43 +++++++++++++++++++ .../Tests/Loader/PhpFileLoaderTest.php | 24 +++++++++++ .../Tests/Loader/XmlFileLoaderTest.php | 6 ++- .../Tests/Loader/YamlFileLoaderTest.php | 3 +- 21 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Attribute/WhenNot.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadAttributes/WhenNotWhenFoo.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/NotFoo.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/not_when_env.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/when_not_when_env.php diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md index 1697989556fd0..5d33f7d733426 100644 --- a/src/Symfony/Component/Config/CHANGELOG.md +++ b/src/Symfony/Component/Config/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add `#[WhenNot]` attribute to prevent service from being registered in a specific environment + 7.1 --- diff --git a/src/Symfony/Component/DependencyInjection/Attribute/WhenNot.php b/src/Symfony/Component/DependencyInjection/Attribute/WhenNot.php new file mode 100644 index 0000000000000..878985dc97834 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/WhenNot.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Attribute; + +/** + * An attribute to tell under which environment this class should NOT be registered as a service. + * + * @author Alexandre Daubois + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)] +class WhenNot +{ + public function __construct( + public string $env, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 9e3a37456a453..22bd4c2243762 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\DependencyInjection\Attribute\Exclude; use Symfony\Component\DependencyInjection\Attribute\When; +use Symfony\Component\DependencyInjection\Attribute\WhenNot; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -154,14 +155,32 @@ public function registerClasses(Definition $prototype, string $namespace, string continue; } if ($this->env) { - $attribute = null; - foreach ($r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $excluded = true; + $whenAttributes = $r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF); + $notWhenAttributes = $r->getAttributes(WhenNot::class, \ReflectionAttribute::IS_INSTANCEOF); + + if ($whenAttributes && $notWhenAttributes) { + throw new LogicException(sprintf('The "%s" class cannot have both #[When] and #[WhenNot] attributes.', $class)); + } + + if (!$whenAttributes && !$notWhenAttributes) { + $excluded = false; + } + + foreach ($whenAttributes as $attribute) { if ($this->env === $attribute->newInstance()->env) { - $attribute = null; + $excluded = false; break; } } - if (null !== $attribute) { + + foreach ($notWhenAttributes as $attribute) { + if ($excluded = $this->env === $attribute->newInstance()->env) { + break; + } + } + + if ($excluded) { $this->addContainerExcludedTag($class, $source); continue; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index 7df014de7a1bd..893e5331b43f3 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -16,9 +16,11 @@ use Symfony\Component\Config\Builder\ConfigBuilderInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\DependencyInjection\Attribute\When; +use Symfony\Component\DependencyInjection\Attribute\WhenNot; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -97,14 +99,32 @@ private function executeCallback(callable $callback, ContainerConfigurator $cont $configBuilders = []; $r = new \ReflectionFunction($callback); - $attribute = null; - foreach ($r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $excluded = true; + $whenAttributes = $r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF); + $notWhenAttributes = $r->getAttributes(WhenNot::class, \ReflectionAttribute::IS_INSTANCEOF); + + if ($whenAttributes && $notWhenAttributes) { + throw new LogicException('Using both #[When] and #[WhenNot] attributes on the same target is not allowed.'); + } + + if (!$whenAttributes && !$notWhenAttributes) { + $excluded = false; + } + + foreach ($whenAttributes as $attribute) { if ($this->env === $attribute->newInstance()->env) { - $attribute = null; + $excluded = false; break; } } - if (null !== $attribute) { + + foreach ($notWhenAttributes as $attribute) { + if ($excluded = $this->env === $attribute->newInstance()->env) { + break; + } + } + + if ($excluded) { return; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadAttributes/WhenNotWhenFoo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadAttributes/WhenNotWhenFoo.php new file mode 100644 index 0000000000000..2cf1a492084e3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadAttributes/WhenNotWhenFoo.php @@ -0,0 +1,12 @@ +load(Prototype::class.'\\', '../Prototype') ->public() ->autoconfigure() - ->exclude('../Prototype/{OtherDir,BadClasses,SinglyImplementedInterface,StaticConstructor}') + ->exclude('../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}') ->factory('f') ->deprecate('vendor/package', '1.1', '%service_id%') ->args([0]) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.expected.yml index 8796091ea8474..8f5ee524d26ce 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.expected.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.expected.yml @@ -16,6 +16,19 @@ services: message: '%service_id%' arguments: [1] factory: f + Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo + public: true + tags: + - foo + - baz + deprecated: + package: vendor/package + version: '1.1' + message: '%service_id%' + lazy: true + arguments: [1] + factory: f Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar: class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar public: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php index cc9d98c4e5d0b..bb40e08ec4328 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php @@ -10,7 +10,7 @@ $di->load(Prototype::class.'\\', '../Prototype') ->public() ->autoconfigure() - ->exclude(['../Prototype/OtherDir', '../Prototype/BadClasses', '../Prototype/SinglyImplementedInterface', '../Prototype/StaticConstructor']) + ->exclude(['../Prototype/OtherDir', '../Prototype/BadClasses', '../Prototype/BadAttributes', '../Prototype/SinglyImplementedInterface', '../Prototype/StaticConstructor']) ->factory('f') ->deprecate('vendor/package', '1.1', '%service_id%') ->args([0]) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/when_not_when_env.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/when_not_when_env.php new file mode 100644 index 0000000000000..16bed621f85ad --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/when_not_when_env.php @@ -0,0 +1,8 @@ + - + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml index 463ffdffc34c3..2780d582bc788 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml @@ -4,6 +4,7 @@ ../Prototype/OtherDir ../Prototype/BadClasses + ../Prototype/BadAttributes ../Prototype/SinglyImplementedInterface ../Prototype/StaticConstructor diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array_with_space_node.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array_with_space_node.xml index 6f6727b8a4a00..7a8c9c544bfb9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array_with_space_node.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array_with_space_node.xml @@ -4,6 +4,7 @@ ../Prototype/OtherDir ../Prototype/BadClasses + ../Prototype/BadAttributes ../Prototype/SinglyImplementedInterface ../Prototype/StaticConstructor diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml index 8b890c11f4311..7eff75294b179 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml @@ -1,4 +1,4 @@ services: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\: resource: ../Prototype - exclude: '../Prototype/{OtherDir,BadClasses,SinglyImplementedInterface,StaticConstructor}' + exclude: '../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php index 406e51eba789a..a195f2a93410c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php @@ -29,6 +29,7 @@ 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; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub\DeeperBaz; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Baz; @@ -196,6 +197,7 @@ public function testNestedRegisterClasses() $this->assertTrue($container->has(Bar::class)); $this->assertTrue($container->has(Baz::class)); $this->assertTrue($container->has(Foo::class)); + $this->assertTrue($container->has(NotFoo::class)); $this->assertEquals([FooInterface::class], array_keys($container->getAliases())); @@ -302,6 +304,47 @@ public function testRegisterClassesWithWhenEnv(?string $env, bool $expected) $this->assertSame($expected, $container->getDefinition(Foo::class)->hasTag('container.excluded')); } + /** + * @dataProvider provideEnvAndExpectedExclusions + */ + public function testRegisterWithNotWhenAttributes(string $env, bool $expectedNotFooExclusion) + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'), $env); + + $loader->registerClasses( + (new Definition())->setAutoconfigured(true), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', + 'Prototype/*', + 'Prototype/BadAttributes/*' + ); + + $this->assertTrue($container->has(NotFoo::class)); + $this->assertSame($expectedNotFooExclusion, $container->getDefinition(NotFoo::class)->hasTag('container.excluded')); + } + + public static function provideEnvAndExpectedExclusions(): iterable + { + yield ['dev', true]; + yield ['prod', true]; + yield ['test', false]; + } + + public function testRegisterThrowsWithBothWhenAndNotWhenAttribute() + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'), 'dev'); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\BadAttributes\WhenNotWhenFoo" class cannot have both #[When] and #[WhenNot] attributes.'); + + $loader->registerClasses( + (new Definition())->setAutoconfigured(true), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\BadAttributes\\', + 'Prototype/BadAttributes/*', + ); + } + /** * @dataProvider provideResourcesWithAsAliasAttributes */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index d7df9b6f11875..46936a72405a7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\Dumper\YamlDumper; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; @@ -226,6 +227,29 @@ public function testWhenEnv() $loader->load($fixtures.'/config/when_env.php'); } + public function testNotWhenEnv() + { + $this->expectNotToPerformAssertions(); + + $fixtures = realpath(__DIR__.'/../Fixtures'); + $container = new ContainerBuilder(); + $loader = new PhpFileLoader($container, new FileLocator(), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir())); + + $loader->load($fixtures.'/config/not_when_env.php'); + } + + public function testUsingBothWhenAndNotWhenEnv() + { + $fixtures = realpath(__DIR__.'/../Fixtures'); + $container = new ContainerBuilder(); + $loader = new PhpFileLoader($container, new FileLocator(), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir())); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Using both #[When] and #[WhenNot] attributes on the same target is not allowed.'); + + $loader->load($fixtures.'/config/when_not_when_env.php'); + } + public function testServiceWithServiceLocatorArgument() { $fixtures = realpath(__DIR__.'/../Fixtures'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index afd822d584b6e..64df6cc7f79b5 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -779,7 +779,7 @@ public function testPrototype() $ids = array_keys(array_filter($container->getDefinitions(), fn ($def) => !$def->hasTag('container.excluded'))); sort($ids); - $this->assertSame([Prototype\Foo::class, Prototype\Sub\Bar::class, 'service_container'], $ids); + $this->assertSame([Prototype\Foo::class, Prototype\NotFoo::class, Prototype\Sub\Bar::class, 'service_container'], $ids); $resources = array_map('strval', $container->getResources()); @@ -795,6 +795,7 @@ public function testPrototype() [ str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'OtherDir') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadClasses') => true, + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadAttributes') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'SinglyImplementedInterface') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'StaticConstructor') => true, ] @@ -815,7 +816,7 @@ public function testPrototypeExcludeWithArray(string $fileName) $ids = array_keys(array_filter($container->getDefinitions(), fn ($def) => !$def->hasTag('container.excluded'))); sort($ids); - $this->assertSame([Prototype\Foo::class, Prototype\Sub\Bar::class, 'service_container'], $ids); + $this->assertSame([Prototype\Foo::class, Prototype\NotFoo::class, Prototype\Sub\Bar::class, 'service_container'], $ids); $resources = array_map('strval', $container->getResources()); @@ -830,6 +831,7 @@ public function testPrototypeExcludeWithArray(string $fileName) false, [ str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadClasses') => true, + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadAttributes') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'OtherDir') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'SinglyImplementedInterface') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'StaticConstructor') => true, diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 77f3b3f2430a9..6aa4376525893 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -551,7 +551,7 @@ public function testPrototype() $ids = array_keys(array_filter($container->getDefinitions(), fn ($def) => !$def->hasTag('container.excluded'))); sort($ids); - $this->assertSame([Prototype\Foo::class, Prototype\Sub\Bar::class, 'service_container'], $ids); + $this->assertSame([Prototype\Foo::class, Prototype\NotFoo::class, Prototype\Sub\Bar::class, 'service_container'], $ids); $resources = array_map('strval', $container->getResources()); @@ -565,6 +565,7 @@ public function testPrototype() true, false, [ str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadClasses') => true, + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadAttributes') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'OtherDir') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'SinglyImplementedInterface') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'StaticConstructor') => true,