Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function load(mixed $resource, ?string $type = null): mixed
}

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

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

private function executeCallback(callable $callback, DefinitionConfigurator $configurator, string $path): void
private function callConfigurator(callable $callback, DefinitionConfigurator $configurator, string $path): void
{
$callback = $callback(...);

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ CHANGELOG
* Deprecate registering a service without a class when its id is a non-existing FQCN
* Allow multiple `#[AsDecorator]` attributes
* Handle returning arrays and config-builders from config files
* Handle declaring services using PHP arrays that follow the same shape as corresponding yaml files
* Deprecate using `$this` or its internal scope from PHP config files; use the `$loader` variable instead

7.3
Expand Down
19 changes: 18 additions & 1 deletion src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,25 @@ public function registerAliasesForSinglyImplementedInterfaces(): void
$this->interfaces = $this->singlyImplemented = $this->aliases = [];
}

final protected function loadExtensionConfig(string $namespace, array $config): void
final protected function loadExtensionConfig(string $namespace, array $config, string $file = '?'): void
{
if (\in_array($namespace, ['imports', 'services', 'parameters'], true)) {
$yamlLoader = new YamlFileLoader($this->container, $this->locator, $this->env, $this->prepend);
$loadContent = new \ReflectionMethod(YamlFileLoader::class, 'loadContent');
$loadContent->invoke($yamlLoader, [$namespace => $config], $file);

if ($this->env && isset($config['when@'.$this->env])) {
if (!\is_array($config['when@'.$this->env])) {
throw new InvalidArgumentException(\sprintf('The "when@%s" key should contain an array in "%s".', $this->env, $file));
}

$yamlLoader->env = null;
$loadContent->invoke($yamlLoader, [$namespace => $config['when@'.$this->env]], $file);
}

return;
}

if (!$this->prepend) {
$this->container->loadFromExtension($namespace, $config);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,25 @@ class_exists(ContainerConfigurator::class);
}

if (\is_object($result) && \is_callable($result)) {
$result = $this->executeCallback($result, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path);
$result = $this->callConfigurator($result, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path);
}
if ($result instanceof ConfigBuilderInterface) {
$this->loadExtensionConfig($result->getExtensionAlias(), ContainerConfigurator::processValue($result->toArray()));
$this->loadExtensionConfig($result->getExtensionAlias(), ContainerConfigurator::processValue($result->toArray()), $path);
} elseif (is_iterable($result)) {
foreach ($result as $key => $config) {
if ($config instanceof ConfigBuilderInterface) {
if (\is_string($key) && $config->getExtensionAlias() !== $key) {
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));
}
$this->loadExtensionConfig($config->getExtensionAlias(), ContainerConfigurator::processValue($config->toArray()));
$this->loadExtensionConfig($config->getExtensionAlias(), ContainerConfigurator::processValue($config->toArray()), $path);
} elseif (!\is_string($key) || !\is_array($config)) {
throw new InvalidArgumentException(\sprintf('The configuration returned in file "%s" must yield only string-keyed arrays or ConfigBuilderInterface values.', $path));
} else {
$this->loadExtensionConfig($key, ContainerConfigurator::processValue($config));
$this->loadExtensionConfig($key, ContainerConfigurator::processValue($config), $path);
}
}
} elseif (null !== $result) {
throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid.', $path));
throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($result)));
}

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

++$this->importing;
try {
return $callback(...$arguments);
$result = $callback(...$arguments);

return \in_array($result, $configBuilders, true) ? null : $result;
} catch (\Throwable $e) {
$configBuilders = [];
throw $e;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
parameters:
foo: bar

services:
service_container:
class: Symfony\Component\DependencyInjection\ContainerInterface
public: true
synthetic: true
Symfony\Component\DependencyInjection\Tests\Fixtures\Bar:
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar
public: true
my_service:
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar
public: true
arguments: [bar]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar;

return [
'parameters' => [
'foo' => 'bar',
],
'services' => [
'_defaults' => [
'public' => true,
],
Bar::class => null,
'my_service' => [
'class' => Bar::class,
'arguments' => ['%foo%'],
],
],
];
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public static function provideConfig()
yield ['closure'];
yield ['from_callable'];
yield ['env_param'];
yield ['array_config'];
}

public function testResourceTags()
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Routing/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ CHANGELOG
* Allow query-specific parameters in `UrlGenerator` using `_query`
* Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute
* Add argument `$parameters` to `RequestContext`'s constructor
* Handle declaring routes using PHP arrays that follow the same shape as corresponding yaml files
* Deprecate class aliases in the `Annotation` namespace, use attributes instead
* Deprecate getters and setters in attribute classes in favor of public properties
* Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead
Expand Down
74 changes: 69 additions & 5 deletions src/Symfony/Component/Routing/Loader/PhpFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator;
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Component\Routing\RouteCollection;

Expand Down Expand Up @@ -42,21 +46,26 @@
}, null, null);

try {
$result = $load($path);
if (1 === $result = $load($path)) {
$result = null;
}
} catch (\Error $e) {
$load = \Closure::bind(static function ($file) use ($loader) {
return include $file;
}, null, ProtectedPhpFileLoader::class);

$result = $load($path);
if (1 === $result = $load($path)) {
$result = null;
}

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());
}

if (\is_object($result) && \is_callable($result)) {
$collection = $this->callConfigurator($result, $path, $file);
} else {
$collection = $result;
$collection = new RouteCollection();
$this->loadRoutes($collection, $result, $path, $file);
}

$collection->addResource(new FileResource($path));
Expand All @@ -69,14 +78,69 @@
return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type);
}

protected function callConfigurator(callable $result, string $path, string $file): RouteCollection
protected function callConfigurator(callable $callback, string $path, string $file): RouteCollection
{
$collection = new RouteCollection();

$result(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
$result = $callback(new RoutingConfigurator($collection, $this, $path, $file, $this->env));
$this->loadRoutes($collection, $result, $path, $file);

return $collection;
}

private function loadRoutes(RouteCollection $collection, mixed $routes, string $path, string $file): void
{
if (null === $routes
|| $routes instanceof RouteCollection
|| $routes instanceof AliasConfigurator
|| $routes instanceof CollectionConfigurator
|| $routes instanceof ImportConfigurator
|| $routes instanceof RouteConfigurator
|| $routes instanceof RoutingConfigurator
) {
if ($routes instanceof RouteCollection && $collection !== $routes) {
$collection->addCollection($routes);
}

return;
}

if (!is_iterable($routes)) {
throw new \InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes)));
}

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

\Closure::bind(function () use ($collection, $routes, $path, $file) {
foreach ($routes as $name => $config) {
if (str_starts_with($name, 'when@')) {
if (!$this->env || 'when@'.$this->env !== $name) {
continue;
}

foreach ($config as $name => $config) {
$this->validate($config, $name.'" when "@'.$this->env, $path);

Check failure on line 122 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:122:32: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::validate does not exist (see https://psalm.dev/022)

Check failure on line 122 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:122:32: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::validate does not exist (see https://psalm.dev/022)

if (isset($config['resource'])) {
$this->parseImport($collection, $config, $path, $file);

Check failure on line 125 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:125:36: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::parseImport does not exist (see https://psalm.dev/022)

Check failure on line 125 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:125:36: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::parseImport does not exist (see https://psalm.dev/022)
} else {
$this->parseRoute($collection, $name, $config, $path);

Check failure on line 127 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:127:36: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::parseRoute does not exist (see https://psalm.dev/022)

Check failure on line 127 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:127:36: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::parseRoute does not exist (see https://psalm.dev/022)
}
}

continue;
}

$this->validate($config, $name, $path);

Check failure on line 134 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:134:24: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::validate does not exist (see https://psalm.dev/022)

Check failure on line 134 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:134:24: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::validate does not exist (see https://psalm.dev/022)

if (isset($config['resource'])) {
$this->parseImport($collection, $config, $path, $file);

Check failure on line 137 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:137:28: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::parseImport does not exist (see https://psalm.dev/022)
} else {
$this->parseRoute($collection, $name, $config, $path);

Check failure on line 139 in src/Symfony/Component/Routing/Loader/PhpFileLoader.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/Routing/Loader/PhpFileLoader.php:139:28: UndefinedMethod: Method Symfony\Component\Routing\Loader\PhpFileLoader::parseRoute does not exist (see https://psalm.dev/022)
}
}
}, $loader, $loader::class)();
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Routing/Loader/YamlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ protected function parseImport(RouteCollection $collection, array $config, strin
protected function validate(mixed $config, string $name, string $path): void
{
if (!\is_array($config)) {
throw new \InvalidArgumentException(\sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path));
throw new \InvalidArgumentException(\sprintf('The definition of "%s" in "%s" must be an array.', $name, $path));
}
if (isset($config['alias'])) {
$this->validateAlias($config, $name, $path);
Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Component/Routing/Tests/Fixtures/array_routes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

return [
'a' => ['path' => '/a'],
'b' => ['path' => '/b', 'methods' => ['GET']],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

return [
'when@some-env' => [
'x' => ['path' => '/x'],
],
'a' => ['path' => '/a'],
];
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
use Symfony\Component\Routing\RouteCollection;

// access the loader's internal scope to trigger deprecation
$loader->callConfigurator(static fn () => 'dummy', 'dummy.php', 'dummy.php');
$loader->callConfigurator(static fn () => [], 'dummy.php', 'dummy.php');

return new RouteCollection();
15 changes: 15 additions & 0 deletions src/Symfony/Component/Routing/Tests/Fixtures/when-env.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return static function (RoutingConfigurator $routes): void {
if ('some-env' === $routes->env()) {
$routes->add('b', '/b');
$routes->add('a', '/a2');
} elseif ('some-other-env' === $routes->env()) {
$routes->add('a', '/a3');
$routes->add('c', '/c');
}

$routes->add('a', '/a1');
};
28 changes: 28 additions & 0 deletions src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,34 @@ public function testImportingAliases()
$this->assertEquals($expectedRoutes('php'), $routes);
}

public function testWhenEnv()
{
$locator = new FileLocator([__DIR__.'/../Fixtures']);
$loader = new PhpFileLoader($locator, 'some-env');
$routes = $loader->load('when-env.php');

$this->assertSame(['b', 'a'], array_keys($routes->all()));
$this->assertSame('/b', $routes->get('b')->getPath());
}

public function testLoadsArrayRoutes()
{
$loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']));
$routes = $loader->load('array_routes.php');
$this->assertSame('/a', $routes->get('a')->getPath());
$this->assertSame('/b', $routes->get('b')->getPath());
$this->assertSame(['GET'], $routes->get('b')->getMethods());
}

public function testWhenEnvWithArray()
{
$locator = new FileLocator([__DIR__.'/../Fixtures']);
$loader = new PhpFileLoader($locator, 'some-env');
$routes = $loader->load('array_when_env.php');
$this->assertSame('/a', $routes->get('a')->getPath());
$this->assertSame('/x', $routes->get('x')->getPath());
}

#[DataProvider('providePsr4ConfigFiles')]
public function testImportAttributesWithPsr4Prefix(string $configFile)
{
Expand Down
Loading