Skip to content

Commit 308652c

Browse files
feature #46564 [DependencyInjection] Add Enum Env Var Processor (jack-worman)
This PR was merged into the 6.2 branch. Discussion ---------- [DependencyInjection] Add Enum Env Var Processor | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | N/A | License | MIT | Doc PR | waiting on approval of feature Add the ability to transform env variables into \BackedEnums. For example, you could now autowire an enum from an environment variable: ```php <?php use Symfony\Component\DependencyInjection\Attribute\Autowire; enum AppEnv: string { case Test = 'test'; case Dev = 'dev'; case Stage = 'stage'; case Prod = 'prod'; } class Foo { public function __construct( #[Autowire('%env(enum:'.AppEnv::class.':APP_ENV)%')] private AppEnv $appEnv, ) {} } ``` Commits ------- 41e2272 Add Enum Env Var Processor
2 parents 870eeb9 + 41e2272 commit 308652c

File tree

13 files changed

+165
-9
lines changed

13 files changed

+165
-9
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1722,7 +1722,7 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c
17221722
}
17231723

17241724
if ($config['decryption_env_var']) {
1725-
if (!preg_match('/^(?:[-.\w]*+:)*+\w++$/', $config['decryption_env_var'])) {
1725+
if (!preg_match('/^(?:[-.\w\\\\]*+:)*+\w++$/', $config['decryption_env_var'])) {
17261726
throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var']));
17271727
}
17281728

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add argument `&$asGhostObject` to LazyProxy's `DumperInterface` to allow using ghost objects for lazy loading services
8+
* Add `enum` env var processor
89

910
6.1
1011
---

src/Symfony/Component/DependencyInjection/Compiler/RegisterEnvVarProcessorsPass.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*/
2626
class RegisterEnvVarProcessorsPass implements CompilerPassInterface
2727
{
28-
private const ALLOWED_TYPES = ['array', 'bool', 'float', 'int', 'string'];
28+
private const ALLOWED_TYPES = ['array', 'bool', 'float', 'int', 'string', \BackedEnum::class];
2929

3030
public function process(ContainerBuilder $container)
3131
{

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,7 +1539,7 @@ private function addDefaultParametersMethod(): string
15391539
$export = $this->exportParameters([$value], '', 12, $hasEnum);
15401540
$export = explode('0 => ', substr(rtrim($export, " ]\n"), 2, -1), 2);
15411541

1542-
if ($hasEnum || preg_match("/\\\$this->(?:getEnv\('(?:[-.\w]*+:)*+\w++'\)|targetDir\.'')/", $export[1])) {
1542+
if ($hasEnum || preg_match("/\\\$this->(?:getEnv\('(?:[-.\w\\\\]*+:)*+\w++'\)|targetDir\.'')/", $export[1])) {
15431543
$dynamicPhp[$key] = sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
15441544
} else {
15451545
$php[] = sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
@@ -1952,7 +1952,7 @@ private function dumpParameter(string $name): string
19521952
return $dumpedValue;
19531953
}
19541954

1955-
if (!preg_match("/\\\$this->(?:getEnv\('(?:[-.\w]*+:)*+\w++'\)|targetDir\.'')/", $dumpedValue)) {
1955+
if (!preg_match("/\\\$this->(?:getEnv\('(?:[-.\w\\\\]*+:)*+\w++'\)|targetDir\.'')/", $dumpedValue)) {
19561956
return sprintf('$this->parameters[%s]', $this->doExport($name));
19571957
}
19581958
}

src/Symfony/Component/DependencyInjection/EnvVarProcessor.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
class EnvVarProcessor implements EnvVarProcessorInterface
2222
{
2323
private ContainerInterface $container;
24+
/** @var \Traversable<EnvVarLoaderInterface> */
2425
private \Traversable $loaders;
2526
private array $loadedVars = [];
2627

2728
/**
28-
* @param EnvVarLoaderInterface[] $loaders
29+
* @param \Traversable<EnvVarLoaderInterface>|null $loaders
2930
*/
3031
public function __construct(ContainerInterface $container, \Traversable $loaders = null)
3132
{
@@ -56,6 +57,7 @@ public static function getProvidedTypes(): array
5657
'string' => 'string',
5758
'trim' => 'string',
5859
'require' => 'bool|int|float|string|array',
60+
'enum' => \BackedEnum::class,
5961
];
6062
}
6163

@@ -86,6 +88,26 @@ public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed
8688
return $array[$key];
8789
}
8890

91+
if ('enum' === $prefix) {
92+
if (false === $i) {
93+
throw new RuntimeException(sprintf('Invalid env "enum:%s": a "%s" class-string should be provided.', $name, \BackedEnum::class));
94+
}
95+
96+
$next = substr($name, $i + 1);
97+
$backedEnumClassName = substr($name, 0, $i);
98+
$backedEnumValue = $getEnv($next);
99+
100+
if (!\is_string($backedEnumValue) && !\is_int($backedEnumValue)) {
101+
throw new RuntimeException(sprintf('Resolved value of "%s" did not result in a string or int value.', $next));
102+
}
103+
104+
if (!is_subclass_of($backedEnumClassName, \BackedEnum::class)) {
105+
throw new RuntimeException(sprintf('"%s" is not a "%s".', $backedEnumClassName, \BackedEnum::class));
106+
}
107+
108+
return $backedEnumClassName::tryFrom($backedEnumValue) ?? throw new RuntimeException(sprintf('Enum value "%s" is not backed by "%s".', $backedEnumValue, $backedEnumClassName));
109+
}
110+
89111
if ('default' === $prefix) {
90112
if (false === $i) {
91113
throw new RuntimeException(sprintf('Invalid env "default:%s": a fallback parameter should be provided.', $name));

src/Symfony/Component/DependencyInjection/Loader/Configurator/EnvConfigurator.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,16 @@ public function require(): static
221221

222222
return $this;
223223
}
224+
225+
/**
226+
* @param class-string<\BackedEnum> $backedEnumClassName
227+
*
228+
* @return $this
229+
*/
230+
public function enum(string $backedEnumClassName): static
231+
{
232+
array_unshift($this->stack, 'enum', $backedEnumClassName);
233+
234+
return $this;
235+
}
224236
}

src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function get(string $name): array|bool|string|int|float|null
4444
return $placeholder; // return first result
4545
}
4646
}
47-
if (!preg_match('/^(?:[-.\w]*+:)*+\w++$/', $env)) {
47+
if (!preg_match('/^(?:[-.\w\\\\]*+:)*+\w++$/', $env)) {
4848
throw new InvalidArgumentException(sprintf('Invalid %s name: only "word" characters are allowed.', $name));
4949
}
5050
if ($this->has($name) && null !== ($defaultValue = parent::get($name)) && !\is_string($defaultValue)) {

src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function testSimpleProcessor()
4848
'string' => ['string'],
4949
'trim' => ['string'],
5050
'require' => ['bool', 'int', 'float', 'string', 'array'],
51+
'enum' => [\BackedEnum::class],
5152
];
5253

5354
$this->assertSame($expected, $container->getParameterBag()->getProvidedTypes());
@@ -65,7 +66,7 @@ public function testNoProcessor()
6566
public function testBadProcessor()
6667
{
6768
$this->expectException(InvalidArgumentException::class);
68-
$this->expectExceptionMessage('Invalid type "foo" returned by "Symfony\Component\DependencyInjection\Tests\Compiler\BadProcessor::getProvidedTypes()", expected one of "array", "bool", "float", "int", "string".');
69+
$this->expectExceptionMessage('Invalid type "foo" returned by "Symfony\Component\DependencyInjection\Tests\Compiler\BadProcessor::getProvidedTypes()", expected one of "array", "bool", "float", "int", "string", "BackedEnum".');
6970
$container = new ContainerBuilder();
7071
$container->register('foo', BadProcessor::class)->addTag('container.env_var_processor');
7172

src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
2121
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
2222
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
23+
use Symfony\Component\DependencyInjection\Tests\Fixtures\IntBackedEnum;
24+
use Symfony\Component\DependencyInjection\Tests\Fixtures\StringBackedEnum;
2325

2426
class EnvVarProcessorTest extends TestCase
2527
{
@@ -464,6 +466,78 @@ public function testGetEnvKeyChained()
464466
}));
465467
}
466468

469+
/**
470+
* @dataProvider provideGetEnvEnum
471+
*/
472+
public function testGetEnvEnum(\BackedEnum $backedEnum)
473+
{
474+
$processor = new EnvVarProcessor(new Container());
475+
476+
$result = $processor->getEnv('enum', $backedEnum::class.':foo', function (string $name) use ($backedEnum) {
477+
$this->assertSame('foo', $name);
478+
479+
return $backedEnum->value;
480+
});
481+
482+
$this->assertSame($backedEnum, $result);
483+
}
484+
485+
public function provideGetEnvEnum(): iterable
486+
{
487+
return [
488+
[StringBackedEnum::Bar],
489+
[IntBackedEnum::Nine],
490+
];
491+
}
492+
493+
public function testGetEnvEnumInvalidEnum()
494+
{
495+
$processor = new EnvVarProcessor(new Container());
496+
497+
$this->expectException(RuntimeException::class);
498+
$this->expectExceptionMessage('Invalid env "enum:foo": a "BackedEnum" class-string should be provided.');
499+
500+
$processor->getEnv('enum', 'foo', function () {
501+
$this->fail('Should not get here');
502+
});
503+
}
504+
505+
public function testGetEnvEnumInvalidResolvedValue()
506+
{
507+
$processor = new EnvVarProcessor(new Container());
508+
509+
$this->expectException(RuntimeException::class);
510+
$this->expectExceptionMessage('Resolved value of "foo" did not result in a string or int value.');
511+
512+
$processor->getEnv('enum', StringBackedEnum::class.':foo', function () {
513+
return null;
514+
});
515+
}
516+
517+
public function testGetEnvEnumInvalidArg()
518+
{
519+
$processor = new EnvVarProcessor(new Container());
520+
521+
$this->expectException(RuntimeException::class);
522+
$this->expectExceptionMessage('"bogus" is not a "BackedEnum".');
523+
524+
$processor->getEnv('enum', 'bogus:foo', function () {
525+
return '';
526+
});
527+
}
528+
529+
public function testGetEnvEnumInvalidBackedValue()
530+
{
531+
$processor = new EnvVarProcessor(new Container());
532+
533+
$this->expectException(RuntimeException::class);
534+
$this->expectExceptionMessage('Enum value "bogus" is not backed by "'.StringBackedEnum::class.'".');
535+
536+
$processor->getEnv('enum', StringBackedEnum::class.':foo', function () {
537+
return 'bogus';
538+
});
539+
}
540+
467541
/**
468542
* @dataProvider validNullables
469543
*/
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\Tests\Fixtures;
13+
14+
enum IntBackedEnum: int
15+
{
16+
case Nine = 9;
17+
}

0 commit comments

Comments
 (0)