Skip to content

Commit 0455a84

Browse files
committed
feature #57379 [DependencyInjection] Add #[WhenNot] attribute (alexandre-daubois)
This PR was merged into the 7.2 branch. Discussion ---------- [DependencyInjection] Add `#[WhenNot]` attribute | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT When dealing with many environment and concrete+test implementations of many services, it can be tricky/verbose to repeat `#[When]` for each and every existing env. By using `#[NotWhen]`, it becomes handy to deal with different implementations of a service across different env: ```php // ConcreteService.php #[WhenNot(env: 'test')] class ConcreteService implements MyServiceInterface { public function call() { dump("I'm a concrete service!"); } } // TestService.php #[When(env: 'test')] class TestService implements MyServiceInterface { public function call() { dump("I'm a test service!"); } } // MyCommand.php class MyCommand extends Command { public function __construct(private MyServiceInterface $service) { parent::__construct(); } // ... } ``` It also eases the creation of a new env: no more need to go across all `#[When]` occurrences to update service definitions. You cannot use When and NotWhen at the same time: ```php #[WhenNot(env: 'dev')] #[When(env: 'test')] class TestService implements MyServiceInterface { public function call() { dump("I'm a test service!"); } } // Throws a LogicException: The "App\Service\TestService" class cannot have both #[When] and #[NotWhen] attributes. ``` Commits ------- 7349e3f [DependencyInjection] Add `#[WhenNot]` attribute
2 parents 645f868 + 7349e3f commit 0455a84

21 files changed

+240
-15
lines changed

src/Symfony/Component/Config/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.2
5+
---
6+
7+
* Add `#[WhenNot]` attribute to prevent service from being registered in a specific environment
8+
49
7.1
510
---
611

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 environment this class should NOT be registered as a service.
16+
*
17+
* @author Alexandre Daubois <alex.daubois@gmail.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)]
20+
class WhenNot
21+
{
22+
public function __construct(
23+
public string $env,
24+
) {
25+
}
26+
}

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
2222
use Symfony\Component\DependencyInjection\Attribute\Exclude;
2323
use Symfony\Component\DependencyInjection\Attribute\When;
24+
use Symfony\Component\DependencyInjection\Attribute\WhenNot;
2425
use Symfony\Component\DependencyInjection\ChildDefinition;
2526
use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass;
2627
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -154,14 +155,32 @@ public function registerClasses(Definition $prototype, string $namespace, string
154155
continue;
155156
}
156157
if ($this->env) {
157-
$attribute = null;
158-
foreach ($r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
158+
$excluded = true;
159+
$whenAttributes = $r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF);
160+
$notWhenAttributes = $r->getAttributes(WhenNot::class, \ReflectionAttribute::IS_INSTANCEOF);
161+
162+
if ($whenAttributes && $notWhenAttributes) {
163+
throw new LogicException(sprintf('The "%s" class cannot have both #[When] and #[WhenNot] attributes.', $class));
164+
}
165+
166+
if (!$whenAttributes && !$notWhenAttributes) {
167+
$excluded = false;
168+
}
169+
170+
foreach ($whenAttributes as $attribute) {
159171
if ($this->env === $attribute->newInstance()->env) {
160-
$attribute = null;
172+
$excluded = false;
161173
break;
162174
}
163175
}
164-
if (null !== $attribute) {
176+
177+
foreach ($notWhenAttributes as $attribute) {
178+
if ($excluded = $this->env === $attribute->newInstance()->env) {
179+
break;
180+
}
181+
}
182+
183+
if ($excluded) {
165184
$this->addContainerExcludedTag($class, $source);
166185
continue;
167186
}

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
use Symfony\Component\Config\Builder\ConfigBuilderInterface;
1717
use Symfony\Component\Config\FileLocatorInterface;
1818
use Symfony\Component\DependencyInjection\Attribute\When;
19+
use Symfony\Component\DependencyInjection\Attribute\WhenNot;
1920
use Symfony\Component\DependencyInjection\Container;
2021
use Symfony\Component\DependencyInjection\ContainerBuilder;
2122
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
23+
use Symfony\Component\DependencyInjection\Exception\LogicException;
2224
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
2325
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
2426
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
@@ -97,14 +99,32 @@ private function executeCallback(callable $callback, ContainerConfigurator $cont
9799
$configBuilders = [];
98100
$r = new \ReflectionFunction($callback);
99101

100-
$attribute = null;
101-
foreach ($r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
102+
$excluded = true;
103+
$whenAttributes = $r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF);
104+
$notWhenAttributes = $r->getAttributes(WhenNot::class, \ReflectionAttribute::IS_INSTANCEOF);
105+
106+
if ($whenAttributes && $notWhenAttributes) {
107+
throw new LogicException('Using both #[When] and #[WhenNot] attributes on the same target is not allowed.');
108+
}
109+
110+
if (!$whenAttributes && !$notWhenAttributes) {
111+
$excluded = false;
112+
}
113+
114+
foreach ($whenAttributes as $attribute) {
102115
if ($this->env === $attribute->newInstance()->env) {
103-
$attribute = null;
116+
$excluded = false;
104117
break;
105118
}
106119
}
107-
if (null !== $attribute) {
120+
121+
foreach ($notWhenAttributes as $attribute) {
122+
if ($excluded = $this->env === $attribute->newInstance()->env) {
123+
break;
124+
}
125+
}
126+
127+
if ($excluded) {
108128
return;
109129
}
110130

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\BadAttributes;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\WhenNot;
6+
use Symfony\Component\DependencyInjection\Attribute\When;
7+
8+
#[When(env: 'dev')]
9+
#[WhenNot(env: 'test')]
10+
class WhenNotWhenFoo
11+
{
12+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\WhenNot;
6+
7+
#[NeverInProduction]
8+
#[WhenNot(env: 'dev')]
9+
class NotFoo
10+
{
11+
public function __construct($bar = null, ?iterable $foo = null, ?object $baz = null)
12+
{
13+
}
14+
15+
public function setFoo(self $foo)
16+
{
17+
}
18+
}
19+
20+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)]
21+
class NeverInProduction extends WhenNot
22+
{
23+
public function __construct()
24+
{
25+
parent::__construct('prod');
26+
}
27+
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof.expected.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ services:
1616

1717
shared: false
1818
configurator: c
19+
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo:
20+
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo
21+
public: true
1922
foo:
2023
class: App\FooService
2124
public: true
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Attribute\WhenNot;
4+
5+
return #[WhenNot(env: 'prod')] function () {
6+
throw new RuntimeException('This code should not be run.');
7+
};

src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype.expected.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ services:
1616
message: '%service_id%'
1717
arguments: [1]
1818
factory: f
19+
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo:
20+
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\NotFoo
21+
public: true
22+
tags:
23+
- foo
24+
- baz
25+
deprecated:
26+
package: vendor/package
27+
version: '1.1'
28+
message: '%service_id%'
29+
lazy: true
30+
arguments: [1]
31+
factory: f
1932
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar:
2033
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar
2134
public: true

src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
$di->load(Prototype::class.'\\', '../Prototype')
1111
->public()
1212
->autoconfigure()
13-
->exclude('../Prototype/{OtherDir,BadClasses,SinglyImplementedInterface,StaticConstructor}')
13+
->exclude('../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}')
1414
->factory('f')
1515
->deprecate('vendor/package', '1.1', '%service_id%')
1616
->args([0])

0 commit comments

Comments
 (0)