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:
commit
858dca485d
@ -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)
|
||||
|
@ -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');
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 = [];
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
[parameters@some-env]
|
||||
foo = 234
|
||||
bar = 345
|
||||
|
||||
[parameters@some-other-env]
|
||||
foo = 456
|
||||
baz = 567
|
||||
|
||||
[parameters]
|
||||
foo = 123
|
@ -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>
|
@ -0,0 +1,12 @@
|
||||
when@some-env:
|
||||
parameters:
|
||||
foo: 234
|
||||
bar: 345
|
||||
|
||||
when@some-other-env:
|
||||
parameters:
|
||||
foo: 456
|
||||
baz: 567
|
||||
|
||||
parameters:
|
||||
foo: 123
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ class ClosureLoader extends Loader
|
||||
*/
|
||||
public function load($closure, string $type = null)
|
||||
{
|
||||
return $closure();
|
||||
return $closure($this->env);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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'])) {
|
||||
|
@ -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">
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
18
src/Symfony/Component/Routing/Tests/Fixtures/when-env.php
Normal file
18
src/Symfony/Component/Routing/Tests/Fixtures/when-env.php
Normal 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');
|
||||
};
|
17
src/Symfony/Component/Routing/Tests/Fixtures/when-env.xml
Normal file
17
src/Symfony/Component/Routing/Tests/Fixtures/when-env.xml
Normal 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>
|
@ -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}
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
Reference in New Issue
Block a user