feature #40214 [FrameworkBundle] allow container/routing configurators to vary by env (nicolas-grekas)

This PR was merged into the 5.3-dev branch.

Discussion
----------

[FrameworkBundle] allow container/routing configurators to vary by env

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | #40215
| License       | MIT
| Doc PR        | -

Inspired by https://github.com/symfony/webpack-encore/pull/900 and by a chat on Slack with @weaverryan

This aims at allowing conditional configuration, which would allow merging config files in one.

Using the PHP-DSL:
```php
$container
    ->when(env: 'prod')
    ->services()
        ->set(Foo::class)
    //...
```

In Yaml:
```yaml
framework:
    secret: '%env(APP_SECRET)%'

when@dev:
    services:
        App\FooForDev: ~

when@test:
    framework:
        test: true
        session:
            storage_factory_id: session.storage.mock_file
```

In XML (omitting namespaces):
```xml
<when env="test">
	<framework test="true">
		<!-- ... -->
	</framework>
</when>

```

A similar syntax is also provided for routes, with support for annotations:
`@Route(env="prod")` defines a route that is enabled only on the "prod" env.

Commits
-------

108375b068 [FrameworkBundle] allow container/routing configurators to vary by env
This commit is contained in:
Nicolas Grekas 2021-02-22 09:45:48 +01:00
commit 858dca485d
51 changed files with 474 additions and 66 deletions

View File

@ -1011,7 +1011,10 @@ class FrameworkExtension extends Extension
$container->register('routing.loader.annotation', AnnotatedRouteControllerLoader::class)
->setPublic(false)
->addTag('routing.loader', ['priority' => -10])
->addArgument(new Reference('annotation_reader', ContainerInterface::NULL_ON_INVALID_REFERENCE));
->setArguments([
new Reference('annotation_reader', ContainerInterface::NULL_ON_INVALID_REFERENCE),
'%kernel.environment%',
]);
$container->register('routing.loader.annotation.directory', AnnotationDirectoryLoader::class)
->setPublic(false)

View File

@ -152,7 +152,7 @@ trait MicroKernelTrait
};
try {
$this->configureContainer(new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file), $loader);
$this->configureContainer(new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file, $this->getEnvironment()), $loader);
} finally {
$instanceof = [];
$kernelLoader->registerAliasesForSinglyImplementedInterfaces();
@ -193,7 +193,7 @@ trait MicroKernelTrait
return $routes->build();
}
$this->configureRoutes(new RoutingConfigurator($collection, $kernelLoader, $file, $file));
$this->configureRoutes(new RoutingConfigurator($collection, $kernelLoader, $file, $file, $this->getEnvironment()));
foreach ($collection as $route) {
$controller = $route->getDefault('_controller');

View File

@ -49,36 +49,42 @@ return static function (ContainerConfigurator $container) {
->set('routing.loader.xml', XmlFileLoader::class)
->args([
service('file_locator'),
'%kernel.environment%',
])
->tag('routing.loader')
->set('routing.loader.yml', YamlFileLoader::class)
->args([
service('file_locator'),
'%kernel.environment%',
])
->tag('routing.loader')
->set('routing.loader.php', PhpFileLoader::class)
->args([
service('file_locator'),
'%kernel.environment%',
])
->tag('routing.loader')
->set('routing.loader.glob', GlobFileLoader::class)
->args([
service('file_locator'),
'%kernel.environment%',
])
->tag('routing.loader')
->set('routing.loader.directory', DirectoryLoader::class)
->args([
service('file_locator'),
'%kernel.environment%',
])
->tag('routing.loader')
->set('routing.loader.container', ContainerLoader::class)
->args([
tagged_locator('routing.route_loader'),
'%kernel.environment%',
])
->tag('routing.loader')

View File

@ -19,7 +19,7 @@
"php": ">=7.2.5",
"ext-xml": "*",
"symfony/cache": "^5.2",
"symfony/config": "^5.0",
"symfony/config": "^5.3",
"symfony/dependency-injection": "^5.3",
"symfony/deprecation-contracts": "^2.1",
"symfony/event-dispatcher": "^5.1",
@ -30,7 +30,7 @@
"symfony/polyfill-php80": "^1.15",
"symfony/filesystem": "^4.4|^5.0",
"symfony/finder": "^4.4|^5.0",
"symfony/routing": "^5.2"
"symfony/routing": "^5.3"
},
"require-dev": {
"doctrine/annotations": "^1.10.4",

View File

@ -31,9 +31,10 @@ abstract class FileLoader extends Loader
private $currentDir;
public function __construct(FileLocatorInterface $locator)
public function __construct(FileLocatorInterface $locator, string $env = null)
{
$this->locator = $locator;
parent::__construct($env);
}
/**

View File

@ -21,6 +21,12 @@ use Symfony\Component\Config\Exception\LoaderLoadException;
abstract class Loader implements LoaderInterface
{
protected $resolver;
protected $env;
public function __construct(string $env = null)
{
$this->env = $env;
}
/**
* {@inheritdoc}

View File

@ -8,6 +8,7 @@ CHANGELOG
* Add `%env(not:...)%` processor to negate boolean values
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
* Add autoconfigurable attributes
* Add support for per-env configuration in loaders
5.2.0
-----

View File

@ -25,9 +25,10 @@ class ClosureLoader extends Loader
{
private $container;
public function __construct(ContainerBuilder $container)
public function __construct(ContainerBuilder $container, string $env = null)
{
$this->container = $container;
parent::__construct($env);
}
/**
@ -35,7 +36,7 @@ class ClosureLoader extends Loader
*/
public function load($resource, string $type = null)
{
$resource($this->container);
$resource($this->container, $this->env);
}
/**

View File

@ -35,14 +35,16 @@ class ContainerConfigurator extends AbstractConfigurator
private $path;
private $file;
private $anonymousCount = 0;
private $env;
public function __construct(ContainerBuilder $container, PhpFileLoader $loader, array &$instanceof, string $path, string $file)
public function __construct(ContainerBuilder $container, PhpFileLoader $loader, array &$instanceof, string $path, string $file, string $env = null)
{
$this->container = $container;
$this->loader = $loader;
$this->instanceof = &$instanceof;
$this->path = $path;
$this->file = $file;
$this->env = $env;
}
final public function extension(string $namespace, array $config)
@ -71,6 +73,23 @@ class ContainerConfigurator extends AbstractConfigurator
return new ServicesConfigurator($this->container, $this->loader, $this->instanceof, $this->path, $this->anonymousCount);
}
/**
* @return static
*/
final public function when(string $env): self
{
if ($env === $this->env) {
return clone $this;
}
$instanceof = $this->instanceof;
$clone = clone $this;
$clone->container = new ContainerBuilder(clone $this->container->getParameterBag());
$clone->instanceof = &$instanceof;
return $clone;
}
/**
* @return static
*/

View File

@ -39,11 +39,11 @@ abstract class FileLoader extends BaseFileLoader
protected $singlyImplemented = [];
protected $autoRegisterAliasesForSinglyImplementedInterfaces = true;
public function __construct(ContainerBuilder $container, FileLocatorInterface $locator)
public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, string $env = null)
{
$this->container = $container;
parent::__construct($locator);
parent::__construct($locator, $env);
}
/**

View File

@ -44,6 +44,12 @@ class IniFileLoader extends FileLoader
$this->container->setParameter($key, $this->phpize($value));
}
}
if ($this->env && \is_array($result['parameters@'.$this->env] ?? null)) {
foreach ($result['parameters@'.$this->env] as $key => $value) {
$this->container->setParameter($key, $this->phpize($value));
}
}
}
/**

View File

@ -47,7 +47,7 @@ class PhpFileLoader extends FileLoader
$callback = $load($path);
if (\is_object($callback) && \is_callable($callback)) {
$callback(new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource), $this->container, $this);
$callback(new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $this->container, $this);
}
} finally {
$this->instanceof = [];

View File

@ -50,23 +50,36 @@ class XmlFileLoader extends FileLoader
$this->container->fileExists($path);
$defaults = $this->getServiceDefaults($xml, $path);
$this->loadXml($xml, $path);
if ($this->env) {
$xpath = new \DOMXPath($xml);
$xpath->registerNamespace('container', self::NS);
foreach ($xpath->query(sprintf('//container:when[@env="%s"]', $this->env)) ?: [] as $root) {
$this->loadXml($xml, $path, $root);
}
}
}
private function loadXml(\DOMDocument $xml, string $path, \DOMNode $root = null): void
{
$defaults = $this->getServiceDefaults($xml, $path, $root);
// anonymous services
$this->processAnonymousServices($xml, $path);
$this->processAnonymousServices($xml, $path, $root);
// imports
$this->parseImports($xml, $path);
$this->parseImports($xml, $path, $root);
// parameters
$this->parseParameters($xml, $path);
$this->parseParameters($xml, $path, $root);
// extensions
$this->loadFromExtensions($xml);
$this->loadFromExtensions($xml, $root);
// services
try {
$this->parseDefinitions($xml, $path, $defaults);
$this->parseDefinitions($xml, $path, $defaults, $root);
} finally {
$this->instanceof = [];
$this->registerAliasesForSinglyImplementedInterfaces();
@ -89,19 +102,19 @@ class XmlFileLoader extends FileLoader
return 'xml' === $type;
}
private function parseParameters(\DOMDocument $xml, string $file)
private function parseParameters(\DOMDocument $xml, string $file, \DOMNode $root = null)
{
if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) {
if ($parameters = $this->getChildren($root ?? $xml->documentElement, 'parameters')) {
$this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter', $file));
}
}
private function parseImports(\DOMDocument $xml, string $file)
private function parseImports(\DOMDocument $xml, string $file, \DOMNode $root = null)
{
$xpath = new \DOMXPath($xml);
$xpath->registerNamespace('container', self::NS);
if (false === $imports = $xpath->query('//container:imports/container:import')) {
if (false === $imports = $xpath->query('.//container:imports/container:import', $root)) {
return;
}
@ -112,19 +125,19 @@ class XmlFileLoader extends FileLoader
}
}
private function parseDefinitions(\DOMDocument $xml, string $file, Definition $defaults)
private function parseDefinitions(\DOMDocument $xml, string $file, Definition $defaults, \DOMNode $root = null)
{
$xpath = new \DOMXPath($xml);
$xpath->registerNamespace('container', self::NS);
if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype|//container:services/container:stack')) {
if (false === $services = $xpath->query('.//container:services/container:service|.//container:services/container:prototype|.//container:services/container:stack', $root)) {
return;
}
$this->setCurrentDir(\dirname($file));
$this->instanceof = [];
$this->isLoadingInstanceof = true;
$instanceof = $xpath->query('//container:services/container:instanceof');
$instanceof = $xpath->query('.//container:services/container:instanceof', $root);
foreach ($instanceof as $service) {
$this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, new Definition()));
}
@ -170,12 +183,12 @@ class XmlFileLoader extends FileLoader
}
}
private function getServiceDefaults(\DOMDocument $xml, string $file): Definition
private function getServiceDefaults(\DOMDocument $xml, string $file, \DOMNode $root = null): Definition
{
$xpath = new \DOMXPath($xml);
$xpath->registerNamespace('container', self::NS);
if (null === $defaultsNode = $xpath->query('//container:services/container:defaults')->item(0)) {
if (null === $defaultsNode = $xpath->query('.//container:services/container:defaults', $root)->item(0)) {
return new Definition();
}
@ -393,7 +406,7 @@ class XmlFileLoader extends FileLoader
/**
* Processes anonymous services.
*/
private function processAnonymousServices(\DOMDocument $xml, string $file)
private function processAnonymousServices(\DOMDocument $xml, string $file, \DOMNode $root = null)
{
$definitions = [];
$count = 0;
@ -403,7 +416,7 @@ class XmlFileLoader extends FileLoader
$xpath->registerNamespace('container', self::NS);
// anonymous services as arguments/properties
if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:bind[not(@id)]|//container:factory[not(@service)]|//container:configurator[not(@service)]')) {
if (false !== $nodes = $xpath->query('.//container:argument[@type="service"][not(@id)]|.//container:property[@type="service"][not(@id)]|.//container:bind[not(@id)]|.//container:factory[not(@service)]|.//container:configurator[not(@service)]', $root)) {
foreach ($nodes as $node) {
if ($services = $this->getChildren($node, 'service')) {
// give it a unique name
@ -422,7 +435,7 @@ class XmlFileLoader extends FileLoader
}
// anonymous services "in the wild"
if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) {
if (false !== $nodes = $xpath->query('.//container:services/container:service[not(@id)]', $root)) {
foreach ($nodes as $node) {
throw new InvalidArgumentException(sprintf('Top-level services must have "id" attribute, none found in "%s" at line %d.', $file, $node->getLineNo()));
}

View File

@ -129,6 +129,20 @@ class YamlFileLoader extends FileLoader
return;
}
$this->loadContent($content, $path);
// per-env configuration
if ($this->env && isset($content['when@'.$this->env])) {
if (!\is_array($content['when@'.$this->env])) {
throw new InvalidArgumentException(sprintf('The "when@%s" key should contain an array in "%s". Check your YAML syntax.', $this->env, $path));
}
$this->loadContent($content['when@'.$this->env], $path);
}
}
private function loadContent($content, $path)
{
// imports
$this->parseImports($content, $path);
@ -770,7 +784,7 @@ class YamlFileLoader extends FileLoader
}
foreach ($content as $namespace => $data) {
if (\in_array($namespace, ['imports', 'parameters', 'services'])) {
if (\in_array($namespace, ['imports', 'parameters', 'services']) || 0 === strpos($namespace, 'when@')) {
continue;
}
@ -907,7 +921,7 @@ class YamlFileLoader extends FileLoader
private function loadFromExtensions(array $content)
{
foreach ($content as $namespace => $values) {
if (\in_array($namespace, ['imports', 'parameters', 'services'])) {
if (\in_array($namespace, ['imports', 'parameters', 'services']) || 0 === strpos($namespace, 'when@')) {
continue;
}

View File

@ -37,7 +37,29 @@
<xsd:element name="services" type="services" />
<xsd:group ref="foreign" />
</xsd:sequence>
<xsd:sequence minOccurs="0" maxOccurs="unbounded">
<xsd:element name="when" type="when" />
</xsd:sequence>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="when">
<xsd:sequence>
<xsd:group ref="foreign" />
<xsd:sequence minOccurs="0">
<xsd:element name="imports" type="imports" />
<xsd:group ref="foreign" />
</xsd:sequence>
<xsd:sequence minOccurs="0">
<xsd:element name="parameters" type="parameters" />
<xsd:group ref="foreign" />
</xsd:sequence>
<xsd:sequence minOccurs="0">
<xsd:element name="services" type="services" />
<xsd:group ref="foreign" />
</xsd:sequence>
</xsd:sequence>
<xsd:attribute name="env" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:group name="foreign">

View File

@ -0,0 +1,20 @@
<?php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\BarService;
return function (ContainerConfigurator $c) {
$c->parameters()
->set('foo', 123);
$c->when('some-env')
->parameters()
->set('foo', 234)
->set('bar', 345);
$c->when('some-other-env')
->parameters()
->set('foo', 456)
->set('baz', 567);
};

View File

@ -0,0 +1,10 @@
[parameters@some-env]
foo = 234
bar = 345
[parameters@some-other-env]
foo = 456
baz = 567
[parameters]
foo = 123

View File

@ -0,0 +1,18 @@
<?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">
<parameters>
<parameter key="foo">123</parameter>
</parameters>
<when env="some-env">
<parameters>
<parameter key="foo">234</parameter>
<parameter key="bar">345</parameter>
</parameters>
</when>
<when env="some-other-env">
<parameters>
<parameter key="foo">456</parameter>
<parameter key="baz">567</parameter>
</parameters>
</when>
</container>

View File

@ -0,0 +1,12 @@
when@some-env:
parameters:
foo: 234
bar: 345
when@some-other-env:
parameters:
foo: 456
baz: 567
parameters:
foo: 123

View File

@ -27,12 +27,13 @@ class ClosureLoaderTest extends TestCase
public function testLoad()
{
$loader = new ClosureLoader($container = new ContainerBuilder());
$loader = new ClosureLoader($container = new ContainerBuilder(), 'some-env');
$loader->load(function ($container) {
$loader->load(function ($container, $env) {
$container->setParameter('foo', 'foo');
$container->setParameter('env', $env);
});
$this->assertEquals('foo', $container->getParameter('foo'), '->load() loads a \Closure resource');
$this->assertSame(['foo' => 'foo', 'env' => 'some-env'], $container->getParameterBag()->all());
}
}

View File

@ -120,4 +120,13 @@ class IniFileLoaderTest extends TestCase
$this->assertFalse($loader->supports('foo.foo'), '->supports() returns false if the resource is not loadable');
$this->assertTrue($loader->supports('with_wrong_ext.yml', 'ini'), '->supports() returns true if the resource with forced type is loadable');
}
public function testWhenEnv()
{
$container = new ContainerBuilder();
$loader = new IniFileLoader($container, new FileLocator(realpath(__DIR__.'/../Fixtures/').'/ini'), 'some-env');
$loader->load('when-env.ini');
$this->assertSame(['foo' => 234, 'bar' => 345], $container->getParameterBag()->all());
}
}

View File

@ -138,6 +138,15 @@ class PhpFileLoaderTest extends TestCase
$this->assertEquals($expected, $container->get('stack_d'));
}
public function testWhenEnv()
{
$container = new ContainerBuilder();
$loader = new PhpFileLoader($container, new FileLocator(realpath(__DIR__.'/../Fixtures').'/config'), 'some-env');
$loader->load('when-env.php');
$this->assertSame(['foo' => 234, 'bar' => 345], $container->getParameterBag()->all());
}
/**
* @group legacy
*/

View File

@ -1083,4 +1083,13 @@ class XmlFileLoaderTest extends TestCase
$expected->label = 'Z';
$this->assertEquals($expected, $container->get('stack_d'));
}
public function testWhenEnv()
{
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'), 'some-env');
$loader->load('when-env.xml');
$this->assertSame(['foo' => 234, 'bar' => 345], $container->getParameterBag()->all());
}
}

View File

@ -1002,4 +1002,13 @@ class YamlFileLoaderTest extends TestCase
];
$this->assertEquals($expected, $container->get('stack_e'));
}
public function testWhenEnv()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'), 'some-env');
$loader->load('when-env.yaml');
$this->assertSame(['foo' => 234, 'bar' => 345], $container->getParameterBag()->all());
}
}

View File

@ -24,7 +24,7 @@
},
"require-dev": {
"symfony/yaml": "^4.4|^5.0",
"symfony/config": "^5.1",
"symfony/config": "^5.3",
"symfony/expression-language": "^4.4|^5.0"
},
"suggest": {
@ -35,7 +35,7 @@
"symfony/proxy-manager-bridge": "Generate service proxies to lazy load them"
},
"conflict": {
"symfony/config": "<5.1",
"symfony/config": "<5.3",
"symfony/finder": "<4.4",
"symfony/proxy-manager-bridge": "<4.4",
"symfony/yaml": "<4.4"

View File

@ -751,15 +751,16 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
protected function getContainerLoader(ContainerInterface $container)
{
$env = $this->getEnvironment();
$locator = new FileLocator($this);
$resolver = new LoaderResolver([
new XmlFileLoader($container, $locator),
new YamlFileLoader($container, $locator),
new IniFileLoader($container, $locator),
new PhpFileLoader($container, $locator),
new GlobFileLoader($container, $locator),
new DirectoryLoader($container, $locator),
new ClosureLoader($container),
new XmlFileLoader($container, $locator, $env),
new YamlFileLoader($container, $locator, $env),
new IniFileLoader($container, $locator, $env),
new PhpFileLoader($container, $locator, $env),
new GlobFileLoader($container, $locator, $env),
new DirectoryLoader($container, $locator, $env),
new ClosureLoader($container, $env),
]);
return new DelegatingLoader($resolver);

View File

@ -34,6 +34,7 @@ class Route
private $schemes = [];
private $condition;
private $priority;
private $env;
/**
* @param array|string $data data array managed by the Doctrine Annotations library or the path
@ -59,7 +60,8 @@ class Route
string $locale = null,
string $format = null,
bool $utf8 = null,
bool $stateless = null
bool $stateless = null,
string $env = null
) {
if (\is_string($data)) {
$data = ['path' => $data];
@ -84,6 +86,7 @@ class Route
$data['format'] = $data['format'] ?? $format;
$data['utf8'] = $data['utf8'] ?? $utf8;
$data['stateless'] = $data['stateless'] ?? $stateless;
$data['env'] = $data['env'] ?? $env;
$data = array_filter($data, static function ($value): bool {
return null !== $value;
@ -241,4 +244,14 @@ class Route
{
return $this->priority;
}
public function setEnv(?string $env): void
{
$this->env = $env;
}
public function getEnv(): ?string
{
return $this->env;
}
}

View File

@ -1,10 +1,11 @@
CHANGELOG
=========
5.3.0
-----
5.3
---
* already encoded slashes are not decoded nor double-encoded anymore when generating URLs
* Already encoded slashes are not decoded nor double-encoded anymore when generating URLs
* Add support for per-env configuration in loaders
5.2.0
-----

View File

@ -73,6 +73,7 @@ use Symfony\Component\Routing\RouteCollection;
abstract class AnnotationClassLoader implements LoaderInterface
{
protected $reader;
protected $env;
/**
* @var string
@ -84,9 +85,10 @@ abstract class AnnotationClassLoader implements LoaderInterface
*/
protected $defaultRouteIndex = 0;
public function __construct(Reader $reader = null)
public function __construct(Reader $reader = null, string $env = null)
{
$this->reader = $reader;
$this->env = $env;
}
/**
@ -122,6 +124,10 @@ abstract class AnnotationClassLoader implements LoaderInterface
$collection = new RouteCollection();
$collection->addResource(new FileResource($class->getFileName()));
if ($globals['env'] && $this->env !== $globals['env']) {
return $collection;
}
foreach ($class->getMethods() as $method) {
$this->defaultRouteIndex = 0;
foreach ($this->getAnnotations($method) as $annot) {
@ -144,6 +150,10 @@ abstract class AnnotationClassLoader implements LoaderInterface
*/
protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method)
{
if ($annot->getEnv() && $annot->getEnv() !== $this->env) {
return;
}
$name = $annot->getName();
if (null === $name) {
$name = $this->getDefaultRouteName($class, $method);
@ -317,6 +327,7 @@ abstract class AnnotationClassLoader implements LoaderInterface
}
$globals['priority'] = $annot->getPriority() ?? 0;
$globals['env'] = $annot->getEnv();
foreach ($globals['requirements'] as $placeholder => $requirement) {
if (\is_int($placeholder)) {
@ -342,6 +353,7 @@ abstract class AnnotationClassLoader implements LoaderInterface
'condition' => '',
'name' => '',
'priority' => 0,
'env' => null,
];
}

View File

@ -33,7 +33,7 @@ class ClosureLoader extends Loader
*/
public function load($closure, string $type = null)
{
return $closure();
return $closure($this->env);
}
/**

View File

@ -24,13 +24,15 @@ class RoutingConfigurator
private $loader;
private $path;
private $file;
private $env;
public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file)
public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file, string $env = null)
{
$this->collection = $collection;
$this->loader = $loader;
$this->path = $path;
$this->file = $file;
$this->env = $env;
}
/**
@ -58,6 +60,21 @@ class RoutingConfigurator
return new CollectionConfigurator($this->collection, $name);
}
/**
* @return static
*/
final public function when(string $env): self
{
if ($env === $this->env) {
return clone $this;
}
$clone = clone $this;
$clone->collection = new RouteCollection();
return $clone;
}
/**
* @return static
*/

View File

@ -22,9 +22,10 @@ class ContainerLoader extends ObjectLoader
{
private $container;
public function __construct(ContainerInterface $container)
public function __construct(ContainerInterface $container, string $env = null)
{
$this->container = $container;
parent::__construct($env);
}
/**

View File

@ -59,7 +59,7 @@ abstract class ObjectLoader extends Loader
throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s".', $method, get_debug_type($loaderObject), $resource));
}
$routeCollection = $loaderObject->$method($this);
$routeCollection = $loaderObject->$method($this, $this->env);
if (!$routeCollection instanceof RouteCollection) {
$type = get_debug_type($routeCollection);

View File

@ -71,7 +71,7 @@ class PhpFileLoader extends FileLoader
{
$collection = new RouteCollection();
$result(new RoutingConfigurator($collection, $this, $path, $file));
$result(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
return $collection;
}

View File

@ -88,6 +88,16 @@ class XmlFileLoader extends FileLoader
case 'import':
$this->parseImport($collection, $node, $path, $file);
break;
case 'when':
if (!$this->env || $node->getAttribute('env') !== $this->env) {
break;
}
foreach ($node->childNodes as $node) {
if ($node instanceof \DOMElement) {
$this->parseNode($collection, $node, $path, $file);
}
}
break;
default:
throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path));
}

View File

@ -84,6 +84,24 @@ class YamlFileLoader extends FileLoader
}
foreach ($parsedConfig as $name => $config) {
if (0 === strpos($name, 'when@')) {
if (!$this->env || 'when@'.$this->env !== $name) {
continue;
}
foreach ($config as $name => $config) {
$this->validate($config, $name.'" when "@'.$this->env, $path);
if (isset($config['resource'])) {
$this->parseImport($collection, $config, $path, $file);
} else {
$this->parseRoute($collection, $name, $config, $path);
}
}
continue;
}
$this->validate($config, $name, $path);
if (isset($config['resource'])) {

View File

@ -21,9 +21,18 @@
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="import" type="import" />
<xsd:element name="route" type="route" />
<xsd:element name="when" type="when" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="when">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="import" type="import" />
<xsd:element name="route" type="route" />
</xsd:choice>
<xsd:attribute name="env" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="localized-path">
<xsd:simpleContent>
<xsd:extension base="xsd:string">

View File

@ -0,0 +1,25 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(env="some-env")
*/
class RouteWithEnv
{
/**
* @Route("/path", name="action")
*/
public function action()
{
}
/**
* @Route("/path2", name="action2", env="some-other-env")
*/
public function action2()
{
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures;
use Symfony\Component\Routing\Annotation\Route;
#[Route(env: 'some-env')]
class RouteWithEnv
{
#[Route(path: '/path', name: 'action')]
public function action()
{
}
#[Route(path: '/path2', name: 'action2', env: 'some-other-env')]
public function action2()
{
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Symfony\Component\Routing\Loader\Configurator;
return function (RoutingConfigurator $routes) {
$routes
->when('some-env')
->add('a', '/a2')
->add('b', '/b');
$routes
->when('some-other-env')
->add('a', '/a3')
->add('c', '/c');
$routes
->add('a', '/a1');
};

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
https://symfony.com/schema/routing/routing-1.0.xsd">
<when env="some-env">
<route id="a" path="/a2" />
<route id="b" path="/b" />
</when>
<when env="some-other-env">
<route id="a" path="/a3" />
<route id="c" path="/c" />
</when>
<route id="a" path="/a1" />
</routes>

View File

@ -0,0 +1,9 @@
when@some-env:
a: {path: /a2}
b: {path: /b}
when@some-other-env:
a: {path: /a3}
c: {path: /c}
a: {path: /a1}

View File

@ -247,5 +247,16 @@ abstract class AnnotationClassLoaderTest extends TestCase
$this->assertEquals('/prefix/path', $routes->get('action')->getPath());
}
public function testWhenEnv()
{
$routes = $this->loader->load($this->getNamespace().'\RouteWithEnv');
$this->assertCount(0, $routes);
$this->setUp('some-env');
$routes = $this->loader->load($this->getNamespace().'\RouteWithEnv');
$this->assertCount(1, $routes);
$this->assertSame('/path', $routes->get('action')->getPath());
}
abstract protected function getNamespace(): string;
}

View File

@ -9,10 +9,10 @@ use Symfony\Component\Routing\Route;
class AnnotationClassLoaderWithAnnotationsTest extends AnnotationClassLoaderTest
{
protected function setUp(): void
protected function setUp(string $env = null): void
{
$reader = new AnnotationReader();
$this->loader = new class($reader) extends AnnotationClassLoader {
$this->loader = new class($reader, $env) extends AnnotationClassLoader {
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot): void
{
}

View File

@ -10,9 +10,9 @@ use Symfony\Component\Routing\Route;
*/
class AnnotationClassLoaderWithAttributesTest extends AnnotationClassLoaderTest
{
protected function setUp(): void
protected function setUp(string $env = null): void
{
$this->loader = new class() extends AnnotationClassLoader {
$this->loader = new class(null, $env) extends AnnotationClassLoader {
protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot): void
{
}

View File

@ -33,10 +33,12 @@ class ClosureLoaderTest extends TestCase
public function testLoad()
{
$loader = new ClosureLoader();
$loader = new ClosureLoader('some-env');
$route = new Route('/');
$routes = $loader->load(function () use ($route) {
$routes = $loader->load(function (string $env = null) use ($route) {
$this->assertSame('some-env', $env);
$routes = new RouteCollection();
$routes->add('foo', $route);

View File

@ -20,14 +20,14 @@ class ObjectLoaderTest extends TestCase
{
public function testLoadCallsServiceAndReturnsCollection()
{
$loader = new TestObjectLoader();
$loader = new TestObjectLoader('some-env');
// create a basic collection that will be returned
$collection = new RouteCollection();
$collection->add('foo', new Route('/foo'));
$loader->loaderMap = [
'my_route_provider_service' => new TestObjectLoaderRouteService($collection),
'my_route_provider_service' => new TestObjectLoaderRouteService($collection, 'some-env'),
];
$actualRoutes = $loader->load(
@ -112,14 +112,20 @@ class TestObjectLoader extends ObjectLoader
class TestObjectLoaderRouteService
{
private $collection;
private $env;
public function __construct($collection)
public function __construct($collection, string $env = null)
{
$this->collection = $collection;
$this->env = $env;
}
public function loadRoutes()
public function loadRoutes(TestObjectLoader $loader, string $env = null)
{
if ($this->env !== $env) {
throw new \InvalidArgumentException(sprintf('Expected env "%s", "%s" given.', $this->env, $env));
}
return $this->collection;
}
}

View File

@ -284,4 +284,14 @@ class PhpFileLoaderTest extends TestCase
$this->assertEquals($expectedRoutes('php'), $routes);
}
public function testWhenEnv()
{
$loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'some-env');
$routes = $loader->load('when-env.php');
$this->assertSame(['b', 'a'], array_keys($routes->all()));
$this->assertSame('/b', $routes->get('b')->getPath());
$this->assertSame('/a1', $routes->get('a')->getPath());
}
}

View File

@ -563,4 +563,14 @@ class XmlFileLoaderTest extends TestCase
$this->assertEquals($expectedRoutes('xml'), $routes);
}
public function testWhenEnv()
{
$loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'some-env');
$routes = $loader->load('when-env.xml');
$this->assertSame(['b', 'a'], array_keys($routes->all()));
$this->assertSame('/b', $routes->get('b')->getPath());
$this->assertSame('/a1', $routes->get('a')->getPath());
}
}

View File

@ -435,4 +435,14 @@ class YamlFileLoaderTest extends TestCase
$this->assertEquals($expectedRoutes('yml'), $routes);
}
public function testWhenEnv()
{
$loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'some-env');
$routes = $loader->load('when-env.yml');
$this->assertSame(['b', 'a'], array_keys($routes->all()));
$this->assertSame('/b', $routes->get('b')->getPath());
$this->assertSame('/a1', $routes->get('a')->getPath());
}
}

View File

@ -21,7 +21,7 @@
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"symfony/config": "^5.0",
"symfony/config": "^5.3",
"symfony/http-foundation": "^4.4|^5.0",
"symfony/yaml": "^4.4|^5.0",
"symfony/expression-language": "^4.4|^5.0",
@ -30,7 +30,7 @@
"psr/log": "~1.0"
},
"conflict": {
"symfony/config": "<5.0",
"symfony/config": "<5.3",
"symfony/dependency-injection": "<4.4",
"symfony/yaml": "<4.4"
},