Skip to content

[DependencyInjection] Add #[WhenNot] attribute #57379

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

Merged
merged 1 commit into from
Jun 25, 2024
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
5 changes: 5 additions & 0 deletions src/Symfony/Component/Config/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.2
---

* Add `#[WhenNot]` attribute to prevent service from being registered in a specific environment

7.1
---

Expand Down
26 changes: 26 additions & 0 deletions src/Symfony/Component/DependencyInjection/Attribute/WhenNot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <alex.daubois@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)]
class WhenNot
{
public function __construct(
public string $env,
) {
}
}
27 changes: 23 additions & 4 deletions src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
28 changes: 24 additions & 4 deletions src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

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

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

use Symfony\Component\DependencyInjection\Attribute\WhenNot;
use Symfony\Component\DependencyInjection\Attribute\When;

#[When(env: 'dev')]
#[WhenNot(env: 'test')]
class WhenNotWhenFoo
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

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

use Symfony\Component\DependencyInjection\Attribute\WhenNot;

#[NeverInProduction]
#[WhenNot(env: 'dev')]
class NotFoo
{
public function __construct($bar = null, ?iterable $foo = null, ?object $baz = null)
{
}

public function setFoo(self $foo)
{
}
}

#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)]
class NeverInProduction extends WhenNot
{
public function __construct()
{
parent::__construct('prod');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ services:

shared: false
configurator: c
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo:
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo
public: true
foo:
class: App\FooService
public: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use Symfony\Component\DependencyInjection\Attribute\WhenNot;

return #[WhenNot(env: 'prod')] function () {
throw new RuntimeException('This code should not be run.');
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
$di->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])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

use Symfony\Component\DependencyInjection\Attribute\WhenNot;
use Symfony\Component\DependencyInjection\Attribute\When;

return #[When(env: 'dev')] #[WhenNot(env: 'prod')] function () {
throw new RuntimeException('This code should not be run.');
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*" exclude="../Prototype/{OtherDir,BadClasses,SinglyImplementedInterface,StaticConstructor}" />
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*" exclude="../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}" />
</services>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*">
<exclude>../Prototype/OtherDir</exclude>
<exclude>../Prototype/BadClasses</exclude>
<exclude>../Prototype/BadAttributes</exclude>
<exclude>../Prototype/SinglyImplementedInterface</exclude>
<exclude>../Prototype/StaticConstructor</exclude>
</prototype>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*">
<exclude>../Prototype/OtherDir</exclude>
<exclude>../Prototype/BadClasses</exclude>
<exclude>../Prototype/BadAttributes</exclude>
<exclude>../Prototype/SinglyImplementedInterface</exclude>
<exclude>../Prototype/StaticConstructor</exclude>
<exclude> </exclude>
Expand Down
Original file line number Diff line number Diff line change
@@ -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}'
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));

Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down
Loading
Loading