Skip to content

Commit 0aaeef6

Browse files
[DependencyInjection][Routing] Handle declaring services and routes using PHP arrays that follow the same shape as corresponding yaml files
1 parent 172e24c commit 0aaeef6

File tree

15 files changed

+194
-17
lines changed

15 files changed

+194
-17
lines changed

src/Symfony/Component/Config/Definition/Loader/DefinitionFileLoader.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function load(mixed $resource, ?string $type = null): mixed
6161
}
6262

6363
if (\is_object($callback) && \is_callable($callback)) {
64-
$this->executeCallback($callback, new DefinitionConfigurator($this->treeBuilder, $this, $path, $resource), $path);
64+
$this->callConfigurator($callback, new DefinitionConfigurator($this->treeBuilder, $this, $path, $resource), $path);
6565
}
6666

6767
return null;
@@ -80,7 +80,7 @@ public function supports(mixed $resource, ?string $type = null): bool
8080
return 'php' === $type;
8181
}
8282

83-
private function executeCallback(callable $callback, DefinitionConfigurator $configurator, string $path): void
83+
private function callConfigurator(callable $callback, DefinitionConfigurator $configurator, string $path): void
8484
{
8585
$callback = $callback(...);
8686

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Deprecate registering a service without a class when its id is a non-existing FQCN
1212
* Allow multiple `#[AsDecorator]` attributes
1313
* Handle returning arrays and config-builders from config files
14+
* Handle declaring services using PHP arrays that follow the same shape as corresponding yaml files
1415
* Deprecate using `$this` or its internal scope from PHP config files; use the `$loader` variable instead
1516

1617
7.3

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,25 @@ public function registerAliasesForSinglyImplementedInterfaces(): void
259259
$this->interfaces = $this->singlyImplemented = $this->aliases = [];
260260
}
261261

262-
final protected function loadExtensionConfig(string $namespace, array $config): void
262+
final protected function loadExtensionConfig(string $namespace, array $config, string $file = '?'): void
263263
{
264+
if (\in_array($namespace, ['imports', 'services', 'parameters'], true)) {
265+
$yamlLoader = new YamlFileLoader($this->container, $this->locator, $this->env, $this->prepend);
266+
$loadContent = new \ReflectionMethod(YamlFileLoader::class, 'loadContent');
267+
$loadContent->invoke($yamlLoader, [$namespace => $config], $file);
268+
269+
if ($this->env && isset($config['when@'.$this->env])) {
270+
if (!\is_array($config['when@'.$this->env])) {
271+
throw new InvalidArgumentException(\sprintf('The "when@%s" key should contain an array in "%s".', $this->env, $file));
272+
}
273+
274+
$yamlLoader->env = null;
275+
$loadContent->invoke($yamlLoader, [$namespace => $config['when@'.$this->env]], $file);
276+
}
277+
278+
return;
279+
}
280+
264281
if (!$this->prepend) {
265282
$this->container->loadFromExtension($namespace, $config);
266283

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,25 +83,25 @@ class_exists(ContainerConfigurator::class);
8383
}
8484

8585
if (\is_object($result) && \is_callable($result)) {
86-
$result = $this->executeCallback($result, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path);
86+
$result = $this->callConfigurator($result, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path);
8787
}
8888
if ($result instanceof ConfigBuilderInterface) {
89-
$this->loadExtensionConfig($result->getExtensionAlias(), ContainerConfigurator::processValue($result->toArray()));
89+
$this->loadExtensionConfig($result->getExtensionAlias(), ContainerConfigurator::processValue($result->toArray()), $path);
9090
} elseif (is_iterable($result)) {
9191
foreach ($result as $key => $config) {
9292
if ($config instanceof ConfigBuilderInterface) {
9393
if (\is_string($key) && $config->getExtensionAlias() !== $key) {
9494
throw new InvalidArgumentException(\sprintf('The extension alias "%s" of the "%s" config builder does not match the key "%s" in file "%s".', $config->getExtensionAlias(), get_debug_type($config), $key, $path));
9595
}
96-
$this->loadExtensionConfig($config->getExtensionAlias(), ContainerConfigurator::processValue($config->toArray()));
96+
$this->loadExtensionConfig($config->getExtensionAlias(), ContainerConfigurator::processValue($config->toArray()), $path);
9797
} elseif (!\is_string($key) || !\is_array($config)) {
9898
throw new InvalidArgumentException(\sprintf('The configuration returned in file "%s" must yield only string-keyed arrays or ConfigBuilderInterface values.', $path));
9999
} else {
100-
$this->loadExtensionConfig($key, ContainerConfigurator::processValue($config));
100+
$this->loadExtensionConfig($key, ContainerConfigurator::processValue($config), $path);
101101
}
102102
}
103103
} elseif (null !== $result) {
104-
throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid.', $path));
104+
throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($result)));
105105
}
106106

107107
$this->loadExtensionConfigs();
@@ -129,7 +129,7 @@ public function supports(mixed $resource, ?string $type = null): bool
129129
/**
130130
* Resolve the parameters to the $callback and execute it.
131131
*/
132-
private function executeCallback(callable $callback, ContainerConfigurator $containerConfigurator, string $path): mixed
132+
private function callConfigurator(callable $callback, ContainerConfigurator $containerConfigurator, string $path): mixed
133133
{
134134
$callback = $callback(...);
135135
$arguments = [];
@@ -202,7 +202,9 @@ private function executeCallback(callable $callback, ContainerConfigurator $cont
202202

203203
++$this->importing;
204204
try {
205-
return $callback(...$arguments);
205+
$result = $callback(...$arguments);
206+
207+
return \in_array($result, $configBuilders, true) ? null : $result;
206208
} catch (\Throwable $e) {
207209
$configBuilders = [];
208210
throw $e;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
parameters:
2+
foo: bar
3+
4+
services:
5+
service_container:
6+
class: Symfony\Component\DependencyInjection\ContainerInterface
7+
public: true
8+
synthetic: true
9+
Symfony\Component\DependencyInjection\Tests\Fixtures\Bar:
10+
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar
11+
public: true
12+
my_service:
13+
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar
14+
public: true
15+
arguments: [bar]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar;
4+
5+
return [
6+
'parameters' => [
7+
'foo' => 'bar',
8+
],
9+
'services' => [
10+
'_defaults' => [
11+
'public' => true,
12+
],
13+
Bar::class => null,
14+
'my_service' => [
15+
'class' => Bar::class,
16+
'arguments' => ['%foo%'],
17+
],
18+
],
19+
];

src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public static function provideConfig()
144144
yield ['closure'];
145145
yield ['from_callable'];
146146
yield ['env_param'];
147+
yield ['array_config'];
147148
}
148149

149150
public function testResourceTags()

src/Symfony/Component/Routing/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Allow query-specific parameters in `UrlGenerator` using `_query`
99
* Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute
1010
* Add argument `$parameters` to `RequestContext`'s constructor
11+
* Handle declaring routes using PHP arrays that follow the same shape as corresponding yaml files
1112
* Deprecate class aliases in the `Annotation` namespace, use attributes instead
1213
* Deprecate getters and setters in attribute classes in favor of public properties
1314
* Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead

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

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
use Symfony\Component\Config\Loader\FileLoader;
1515
use Symfony\Component\Config\Resource\FileResource;
16+
use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator;
17+
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
18+
use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;
19+
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
1620
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
1721
use Symfony\Component\Routing\RouteCollection;
1822

@@ -42,21 +46,26 @@ public function load(mixed $file, ?string $type = null): RouteCollection
4246
}, null, null);
4347

4448
try {
45-
$result = $load($path);
49+
if (1 === $result = $load($path)) {
50+
$result = null;
51+
}
4652
} catch (\Error $e) {
4753
$load = \Closure::bind(static function ($file) use ($loader) {
4854
return include $file;
4955
}, null, ProtectedPhpFileLoader::class);
5056

51-
$result = $load($path);
57+
if (1 === $result = $load($path)) {
58+
$result = null;
59+
}
5260

5361
trigger_deprecation('symfony/routing', '7.4', 'Accessing the internal scope of the loader in config files is deprecated, use only its public API instead in "%s" on line %d.', $e->getFile(), $e->getLine());
5462
}
5563

5664
if (\is_object($result) && \is_callable($result)) {
5765
$collection = $this->callConfigurator($result, $path, $file);
5866
} else {
59-
$collection = $result;
67+
$collection = new RouteCollection();
68+
$this->loadRoutes($collection, $result, $path, $file);
6069
}
6170

6271
$collection->addResource(new FileResource($path));
@@ -69,14 +78,69 @@ public function supports(mixed $resource, ?string $type = null): bool
6978
return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type);
7079
}
7180

72-
protected function callConfigurator(callable $result, string $path, string $file): RouteCollection
81+
protected function callConfigurator(callable $callback, string $path, string $file): RouteCollection
7382
{
7483
$collection = new RouteCollection();
7584

76-
$result(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
85+
$result = $callback(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
86+
$this->loadRoutes($collection, $result, $path, $file);
7787

7888
return $collection;
7989
}
90+
91+
private function loadRoutes(RouteCollection $collection, mixed $routes, string $path, string $file): void
92+
{
93+
if (null === $routes
94+
|| $routes instanceof RouteCollection
95+
|| $routes instanceof AliasConfigurator
96+
|| $routes instanceof CollectionConfigurator
97+
|| $routes instanceof ImportConfigurator
98+
|| $routes instanceof RouteConfigurator
99+
|| $routes instanceof RoutingConfigurator
100+
) {
101+
if ($routes instanceof RouteCollection && $collection !== $routes) {
102+
$collection->addCollection($routes);
103+
}
104+
105+
return;
106+
}
107+
108+
if (!is_iterable($routes)) {
109+
throw new \InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes)));
110+
}
111+
112+
$loader = new YamlFileLoader($this->locator, $this->env);
113+
114+
\Closure::bind(function () use ($collection, $routes, $path, $file) {
115+
foreach ($routes as $name => $config) {
116+
if (str_starts_with($name, 'when@')) {
117+
if (!$this->env || 'when@'.$this->env !== $name) {
118+
continue;
119+
}
120+
121+
foreach ($config as $name => $config) {
122+
$this->validate($config, $name.'" when "@'.$this->env, $path);
123+
124+
if (isset($config['resource'])) {
125+
$this->parseImport($collection, $config, $path, $file);
126+
} else {
127+
$this->parseRoute($collection, $name, $config, $path);
128+
}
129+
}
130+
131+
continue;
132+
}
133+
134+
$this->validate($config, $name, $path);
135+
136+
if (isset($config['resource'])) {
137+
$this->parseImport($collection, $config, $path, $file);
138+
} else {
139+
$this->parseRoute($collection, $name, $config, $path);
140+
}
141+
}
142+
}, $loader, $loader::class)();
143+
}
80144
}
81145

82146
/**

src/Symfony/Component/Routing/Loader/YamlFileLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ protected function parseImport(RouteCollection $collection, array $config, strin
246246
protected function validate(mixed $config, string $name, string $path): void
247247
{
248248
if (!\is_array($config)) {
249-
throw new \InvalidArgumentException(\sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path));
249+
throw new \InvalidArgumentException(\sprintf('The definition of "%s" in "%s" must be an array.', $name, $path));
250250
}
251251
if (isset($config['alias'])) {
252252
$this->validateAlias($config, $name, $path);

0 commit comments

Comments
 (0)