Skip to content

Commit b76bdfd

Browse files
[FrameworkBundle] Auto-generate config/reference.php to assist in writing and discovering app's configuration
1 parent ba77be3 commit b76bdfd

File tree

8 files changed

+56
-124
lines changed

8 files changed

+56
-124
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ CHANGELOG
99
* Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute
1010
* Add argument `$parameters` to `RequestContext`'s constructor
1111
* Handle declaring routes using PHP arrays that follow the same shape as corresponding yaml files
12-
* Add `RoutesConfig` to help writing PHP configs using yaml-like array-shapes
12+
* Add `RoutesReference` to help writing PHP configs using yaml-like array-shapes
1313
* Deprecate class aliases in the `Annotation` namespace, use attributes instead
1414
* Deprecate getters and setters in attribute classes in favor of public properties
1515
* Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead

Loader/Config/RoutesConfig.php renamed to Loader/Configurator/RoutesReference.php

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,30 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\Config;
12+
namespace Symfony\Component\Routing\Loader\Configurator;
13+
14+
// For the phpdoc to remain compatible with the generation of per-app Routes class,
15+
// this file should have no "use" statements: all symbols referenced by
16+
// the phpdoc need to be in the current namespace or be root-scoped.
1317

1418
/**
15-
* @psalm-type Route = array{
19+
* This class provides array-shapes for configuring the routes of an application.
20+
*
21+
* Example:
22+
*
23+
* ```php
24+
* // config/routes.php
25+
* namespace Symfony\Component\Routing\Loader\Configurator;
26+
*
27+
* return Routes::config([
28+
* 'controllers' => [
29+
* 'resource' => 'attributes',
30+
* 'type' => 'tagged_services',
31+
* ],
32+
* ]);
33+
* ```
34+
*
35+
* @psalm-type RouteConfig = array{
1636
* path: string|array<string,string>,
1737
* controller?: string,
1838
* methods?: string|list<string>,
@@ -27,7 +47,7 @@
2747
* utf8?: bool,
2848
* stateless?: bool,
2949
* }
30-
* @psalm-type Import = array{
50+
* @psalm-type ImportConfig = array{
3151
* resource: string,
3252
* type?: string,
3353
* exclude?: string|list<string>,
@@ -47,19 +67,21 @@
4767
* utf8?: bool,
4868
* stateless?: bool,
4969
* }
50-
* @psalm-type Alias = array{
70+
* @psalm-type AliasConfig = array{
5171
* alias: string,
5272
* deprecated?: array{package:string, version:string, message?:string},
5373
* }
54-
* @psalm-type Routes = array<string, Route|Import|Alias|RoutesConfig|array<string, Route|Import|Alias>>
74+
* @psalm-type RoutesConfig = array<string, RouteConfig|ImportConfig|AliasConfig|array<string, RouteConfig|ImportConfig|AliasConfig>>
5575
*/
56-
class RoutesConfig
76+
class RoutesReference
5777
{
5878
/**
59-
* @param Routes $routes
79+
* @param RoutesConfig $config
80+
*
81+
* @psalm-return RoutesConfig
6082
*/
61-
public function __construct(
62-
public readonly array $routes,
63-
) {
83+
public static function config(array $config): array
84+
{
85+
return $config;
6486
}
6587
}

Loader/PhpFileLoader.php

Lines changed: 14 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,10 @@
1515
use Symfony\Component\Config\Loader\LoaderResolver;
1616
use Symfony\Component\Config\Resource\FileResource;
1717
use Symfony\Component\Routing\Exception\InvalidArgumentException;
18-
use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator;
19-
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
20-
use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;
21-
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
18+
use Symfony\Component\Routing\Loader\Configurator\Routes;
19+
use Symfony\Component\Routing\Loader\Configurator\RoutesReference;
2220
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
2321
use Symfony\Component\Routing\RouteCollection;
24-
use Symfony\Config\RoutesConfig;
2522

2623
/**
2724
* PhpFileLoader loads routes from a PHP file.
@@ -42,6 +39,11 @@ public function load(mixed $file, ?string $type = null): RouteCollection
4239
$path = $this->locator->locate($file);
4340
$this->setCurrentDir(\dirname($path));
4441

42+
// Expose RoutesReference::config() as Routes::config()
43+
if (!class_exists(Routes::class)) {
44+
class_alias(RoutesReference::class, Routes::class);
45+
}
46+
4547
// the closure forbids access to the private scope in the included file
4648
$loader = $this;
4749
$load = \Closure::bind(static function ($file) use ($loader) {
@@ -66,9 +68,13 @@ public function load(mixed $file, ?string $type = null): RouteCollection
6668

6769
if (\is_object($result) && \is_callable($result)) {
6870
$collection = $this->callConfigurator($result, $path, $file);
69-
} else {
71+
} elseif (\is_array($result)) {
7072
$collection = new RouteCollection();
71-
$this->loadRoutes($collection, $result, $path, $file);
73+
$loader = new YamlFileLoader($this->locator, $this->env);
74+
$loader->setResolver($this->resolver ?? new LoaderResolver([$this]));
75+
(new \ReflectionMethod(YamlFileLoader::class, 'loadContent'))->invoke($loader, $collection, $result, $path, $file);
76+
} elseif (!($collection = $result) instanceof RouteCollection) {
77+
throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is expected to be a RouteCollection, an array or a configurator callable, but got "%s".', $path, get_debug_type($result)));
7278
}
7379

7480
$collection->addResource(new FileResource($path));
@@ -85,61 +91,10 @@ protected function callConfigurator(callable $callback, string $path, string $fi
8591
{
8692
$collection = new RouteCollection();
8793

88-
$result = $callback(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
89-
$this->loadRoutes($collection, $result, $path, $file);
94+
$callback(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
9095

9196
return $collection;
9297
}
93-
94-
private function loadRoutes(RouteCollection $collection, mixed $routes, string $path, string $file): void
95-
{
96-
if (null === $routes
97-
|| $routes instanceof RouteCollection
98-
|| $routes instanceof AliasConfigurator
99-
|| $routes instanceof CollectionConfigurator
100-
|| $routes instanceof ImportConfigurator
101-
|| $routes instanceof RouteConfigurator
102-
|| $routes instanceof RoutingConfigurator
103-
) {
104-
if ($routes instanceof RouteCollection && $collection !== $routes) {
105-
$collection->addCollection($routes);
106-
}
107-
108-
return;
109-
}
110-
111-
if ($routes instanceof RoutesConfig) {
112-
$routes = $routes->routes;
113-
} elseif (!is_iterable($routes)) {
114-
throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes)));
115-
}
116-
117-
$loader = new YamlFileLoader($this->locator, $this->env);
118-
$loader->setResolver(new LoaderResolver([$this]));
119-
120-
\Closure::bind(function () use ($collection, $routes, $path, $file) {
121-
foreach ($routes as $name => $config) {
122-
if (str_starts_with($when = $name, 'when@')) {
123-
if (!$this->env || 'when@'.$this->env !== $name) {
124-
continue;
125-
}
126-
$when .= '" when "@'.$this->env;
127-
} elseif (!$config instanceof RoutesConfig) {
128-
$config = [$name => $config];
129-
} elseif (!\is_int($name)) {
130-
throw new InvalidArgumentException(\sprintf('Invalid key "%s" returned for the "%s" config builder; none or "when@%%env%%" expected in file "%s".', $name, get_debug_type($config), $path));
131-
}
132-
133-
if ($config instanceof RoutesConfig) {
134-
$config = $config->routes;
135-
} elseif (!\is_array($config)) {
136-
throw new InvalidArgumentException(\sprintf('The "%s" key should contain an array in "%s".', $name, $path));
137-
}
138-
139-
$this->loadContent($collection, $config, $path, $file);
140-
}
141-
}, $loader, YamlFileLoader::class)();
142-
}
14398
}
14499

145100
/**

Tests/Fixtures/array_routes.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<?php
22

3-
return [
3+
use Symfony\Component\Routing\Loader\Configurator\Routes;
4+
5+
return Routes::config([
46
'a' => ['path' => '/a'],
57
'b' => ['path' => '/b', 'methods' => ['GET']],
6-
];
8+
]);

Tests/Fixtures/array_when_env.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
<?php
22

3-
return [
3+
use Symfony\Component\Routing\Loader\Configurator\Routes;
4+
5+
return Routes::config([
46
'when@some-env' => [
57
'x' => ['path' => '/x'],
68
],
79
'a' => ['path' => '/a'],
8-
];
10+
]);

Tests/Fixtures/routes_object.php

Lines changed: 0 additions & 23 deletions
This file was deleted.

Tests/Loader/PhpFileLoaderTest.php

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -374,29 +374,6 @@ public function testLoadsArrayRoutes()
374374
$this->assertSame(['GET'], $routes->get('b')->getMethods());
375375
}
376376

377-
public function testLoadsObjectRoutes()
378-
{
379-
$loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']));
380-
$routes = $loader->load('routes_object.php');
381-
$this->assertSame('/a', $routes->get('a')->getPath());
382-
$this->assertSame('/b', $routes->get('b')->getPath());
383-
$this->assertSame(['GET'], $routes->get('b')->getMethods());
384-
$this->assertNull($routes->get('c'));
385-
$this->assertNull($routes->get('d'));
386-
387-
$loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'dev');
388-
$routes = $loader->load('routes_object.php');
389-
$this->assertSame('/a', $routes->get('a')->getPath());
390-
$this->assertSame('/b', $routes->get('b')->getPath());
391-
$this->assertSame('/c', $routes->get('c')->getPath());
392-
$this->assertNull($routes->get('d'));
393-
394-
$loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'test');
395-
$routes = $loader->load('routes_object.php');
396-
$this->assertNull($routes->get('c'));
397-
$this->assertSame('/d', $routes->get('d')->getPath());
398-
}
399-
400377
public function testWhenEnvWithArray()
401378
{
402379
$locator = new FileLocator([__DIR__.'/../Fixtures']);

composer.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@
3333
"symfony/yaml": "<6.4"
3434
},
3535
"autoload": {
36-
"psr-4": {
37-
"Symfony\\Component\\Routing\\": "",
38-
"Symfony\\Config\\": "Loader/Config/"
39-
},
36+
"psr-4": { "Symfony\\Component\\Routing\\": "" },
4037
"exclude-from-classmap": [
4138
"/Tests/"
4239
]

0 commit comments

Comments
 (0)