Skip to content

Commit fac639d

Browse files
[DependencyInjection][Routing] Define array-shapes to help writing PHP configs using yaml-like arrays
1 parent 6994e26 commit fac639d

File tree

6 files changed

+119
-19
lines changed

6 files changed

+119
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +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
1213
* Deprecate class aliases in the `Annotation` namespace, use attributes instead
1314
* Deprecate getters and setters in attribute classes in favor of public properties
1415
* Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead

Loader/Config/RoutesConfig.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Config;
13+
14+
/**
15+
* @psalm-type Route = array{
16+
* path: string|array<string,string>,
17+
* controller?: string,
18+
* methods?: string|list<string>,
19+
* requirements?: array<string,string>,
20+
* defaults?: array<string,mixed>,
21+
* options?: array<string,mixed>,
22+
* host?: string|array<string,string>,
23+
* schemes?: string|list<string>,
24+
* condition?: string,
25+
* locale?: string,
26+
* format?: string,
27+
* utf8?: bool,
28+
* stateless?: bool,
29+
* }
30+
* @psalm-type Import = array{
31+
* resource: string,
32+
* type?: string,
33+
* exclude?: string|list<string>,
34+
* prefix?: string|array<string,string>,
35+
* name_prefix?: string,
36+
* trailing_slash_on_root?: bool,
37+
* controller?: string,
38+
* methods?: string|list<string>,
39+
* requirements?: array<string,string>,
40+
* defaults?: array<string,mixed>,
41+
* options?: array<string,mixed>,
42+
* host?: string|array<string,string>,
43+
* schemes?: string|list<string>,
44+
* condition?: string,
45+
* locale?: string,
46+
* format?: string,
47+
* utf8?: bool,
48+
* stateless?: bool,
49+
* }
50+
* @psalm-type Alias = array{
51+
* alias: string,
52+
* deprecated?: array{package:string, version:string, message?:string},
53+
* }
54+
* @psalm-type Routes = array<string, Route|Import|Alias>
55+
*/
56+
class RoutesConfig
57+
{
58+
/**
59+
* @param Routes $routes
60+
*/
61+
public function __construct(
62+
public readonly array $routes,
63+
) {
64+
}
65+
}

Loader/PhpFileLoader.php

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313

1414
use Symfony\Component\Config\Loader\FileLoader;
1515
use Symfony\Component\Config\Resource\FileResource;
16+
use Symfony\Component\Routing\Exception\InvalidArgumentException;
1617
use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator;
1718
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
1819
use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;
1920
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
2021
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
2122
use Symfony\Component\Routing\RouteCollection;
23+
use Symfony\Config\RoutesConfig;
2224

2325
/**
2426
* PhpFileLoader loads routes from a PHP file.
@@ -105,38 +107,45 @@ private function loadRoutes(RouteCollection $collection, mixed $routes, string $
105107
return;
106108
}
107109

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+
if ($routes instanceof RoutesConfig) {
111+
$routes = [$routes];
112+
} elseif (!is_iterable($routes)) {
113+
throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes)));
110114
}
111115

112116
$loader = new YamlFileLoader($this->locator, $this->env);
113117

114118
\Closure::bind(function () use ($collection, $routes, $path, $file) {
115119
foreach ($routes as $name => $config) {
120+
$when = $name;
116121
if (str_starts_with($name, 'when@')) {
117122
if (!$this->env || 'when@'.$this->env !== $name) {
118123
continue;
119124
}
125+
$when .= '" when "@'.$this->env;
126+
} elseif (!$config instanceof RoutesConfig) {
127+
$config = [$name => $config];
128+
} elseif (!\is_int($name)) {
129+
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));
130+
}
120131

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+
if ($config instanceof RoutesConfig) {
133+
$config = $config->routes;
134+
} elseif (!is_iterable($config)) {
135+
throw new InvalidArgumentException(\sprintf('The "%s" key should contain an array in "%s".', $name, $path));
132136
}
133137

134-
$this->validate($config, $name, $path);
138+
foreach ($config as $name => $config) {
139+
if (str_starts_with($name, 'when@')) {
140+
throw new InvalidArgumentException(\sprintf('A route name cannot start with "when@" in "%s".', $path));
141+
}
142+
$this->validate($config, $when, $path);
135143

136-
if (isset($config['resource'])) {
137-
$this->parseImport($collection, $config, $path, $file);
138-
} else {
139-
$this->parseRoute($collection, $name, $config, $path);
144+
if (isset($config['resource'])) {
145+
$this->parseImport($collection, $config, $path, $file);
146+
} else {
147+
$this->parseRoute($collection, $name, $config, $path);
148+
}
140149
}
141150
}
142151
}, $loader, $loader::class)();

Tests/Fixtures/routes_object.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
use Symfony\Config\RoutesConfig;
4+
5+
return new RoutesConfig([
6+
'a' => [
7+
'path' => '/a',
8+
],
9+
'b' => [
10+
'path' => '/b',
11+
'methods' => ['GET'],
12+
],
13+
]);

Tests/Loader/PhpFileLoaderTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,15 @@ 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+
}
385+
377386
public function testWhenEnvWithArray()
378387
{
379388
$locator = new FileLocator([__DIR__.'/../Fixtures']);

composer.json

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

0 commit comments

Comments
 (0)