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
8 changes: 5 additions & 3 deletions UPGRADE-7.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ DependencyInjection
* Deprecate `ExtensionInterface::getXsdValidationBasePath()` and `getNamespace()`;
bundles that need to support older versions of Symfony can keep the methods
but need to add the `@deprecated` annotation on them
* Deprecate the fluent PHP format for semantic configuration, instantiate builders inline with the config array as argument and return them instead:
* Deprecate the fluent PHP format for semantic configuration, use `$container->extension()` or return an array instead
```diff
-return function (AcmeConfig $config) {
- $config->color('red');
-}
+return new AcmeConfig([
+ 'color' => 'red',
+return App::config([
+ 'acme' => [
+ 'color' => 'red',
+ ],
+]);
```

Expand Down
4 changes: 0 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,6 @@
"Symfony\\Bridge\\Twig\\": "src/Symfony/Bridge/Twig/",
"Symfony\\Bundle\\": "src/Symfony/Bundle/",
"Symfony\\Component\\": "src/Symfony/Component/",
"Symfony\\Config\\": [
"src/Symfony/Component/DependencyInjection/Loader/Config/",
"src/Symfony/Component/Routing/Loader/Config/"
],
"Symfony\\Runtime\\Symfony\\Component\\": "src/Symfony/Component/Runtime/Internal/"
},
"files": [
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
7.4
---

* Auto-generate `config/reference.php` to assist in writing and discovering app's configuration
* Auto-register routes from attributes found on controller services
* Add `ControllerHelper`; the helpers from AbstractController as a standalone service
* Allow using their name without added suffix when using `#[Target]` for custom services
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;

use Symfony\Component\Config\Definition\ArrayShapeGenerator;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\AppReference;
use Symfony\Component\Routing\Loader\Configurator\RoutesReference;

/**
* @internal
*/
class PhpConfigReferenceDumpPass implements CompilerPassInterface
{
private const REFERENCE_TEMPLATE = <<<'PHP'
<?php

// This file is auto-generated and is for apps only. Bundles SHOULD NOT rely on its content.

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

{APP_TYPES}
final class App extends AppReference
{
{APP_PARAM}
public static function config(array $config): array
{
return parent::config($config);
}
}

namespace Symfony\Component\Routing\Loader\Configurator;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this actually valid ? Don't we need to use the syntax with curly braces around the code when wanting to use several namespaces in the same PHP file ?

Copy link
Member

@Kocal Kocal Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's valid https://www.php.net/manual/en/language.namespaces.definitionmultiple.php, it works with or without curly braces


{ROUTES_TYPES}
final class Routes extends RoutesReference
{
{ROUTES_PARAM}
public static function config(array $config): array
{
return parent::config($config);
}
}

PHP;

private const WHEN_ENV_APP_TEMPLATE = <<<'PHPDOC'

* "when@{ENV}"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,{SHAPE}
* },
PHPDOC;

private const ROUTES_TYPES_TEMPLATE = <<<'PHPDOC'

* @psalm-type RoutesConfig = array{{SHAPE}
* ...<string, RouteConfig|ImportConfig|AliasConfig>
* }
*/
PHPDOC;

private const WHEN_ENV_ROUTES_TEMPLATE = <<<'PHPDOC'

* "when@{ENV}"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
PHPDOC;

public function __construct(
private string $referenceFile,
private array $bundlesDefinition,
) {
}

public function process(ContainerBuilder $container): void
{
$knownEnvs = $container->hasParameter('.container.known_envs') ? $container->getParameter('.container.known_envs') : [$container->getParameter('kernel.environment')];
$knownEnvs = array_unique($knownEnvs);
sort($knownEnvs);
$extensionsPerEnv = [];
$appTypes = '';

$anyEnvExtensions = [];
foreach ($this->bundlesDefinition as $bundle => $envs) {
if (!$extension = (new $bundle())->getContainerExtension()) {
continue;
}
if (!$configuration = $this->getConfiguration($extension, $container)) {
continue;
}
$anyEnvExtensions[$bundle] = $extension;
$type = $this->camelCase($extension->getAlias()).'Config';
$appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree()));

foreach ($knownEnvs as $env) {
if ($envs[$env] ?? $envs['all'] ?? false) {
$extensionsPerEnv[$env][] = $extension;
} else {
unset($anyEnvExtensions[$bundle]);
}
}
}
krsort($extensionsPerEnv);

$r = new \ReflectionClass(AppReference::class);

if (false === $i = strpos($phpdoc = $r->getDocComment(), "\n */")) {
throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', AppReference::class));
}
$appTypes = substr_replace($phpdoc, $appTypes, $i, 0);

if (false === $i = strpos($phpdoc = $r->getMethod('config')->getDocComment(), "\n * ...<string, ExtensionType|array{")) {
throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', AppReference::class));
}
$appParam = substr_replace($phpdoc, $this->getShapeForExtensions($anyEnvExtensions, $container), $i, 0);
$i += \strlen($appParam) - \strlen($phpdoc);

foreach ($extensionsPerEnv as $env => $extensions) {
$appParam = substr_replace($appParam, strtr(self::WHEN_ENV_APP_TEMPLATE, [
'{ENV}' => $env,
'{SHAPE}' => $this->getShapeForExtensions($extensions, $container, ' '),
]), $i, 0);
}

$r = new \ReflectionClass(RoutesReference::class);

if (false === $i = strpos($phpdoc = $r->getDocComment(), "\n * @psalm-type RoutesConfig = ")) {
throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', RoutesReference::class));
}
$routesTypes = '';
foreach ($knownEnvs as $env) {
$routesTypes .= strtr(self::WHEN_ENV_ROUTES_TEMPLATE, ['{ENV}' => $env]);
}
if ('' !== $routesTypes) {
$routesTypes = strtr(self::ROUTES_TYPES_TEMPLATE, ['{SHAPE}' => $routesTypes]);
$routesTypes = substr_replace($phpdoc, $routesTypes, $i);
}

$configReference = strtr(self::REFERENCE_TEMPLATE, [
'{APP_TYPES}' => $appTypes,
'{APP_PARAM}' => $appParam,
'{ROUTES_TYPES}' => $routesTypes,
'{ROUTES_PARAM}' => $r->getMethod('config')->getDocComment(),
]);

$dir = \dirname($this->referenceFile);
if (is_dir($dir) && is_writable($dir) && (!is_file($this->referenceFile) || file_get_contents($this->referenceFile) !== $configReference)) {
file_put_contents($this->referenceFile, $configReference);
}
}

private function camelCase(string $input): string
{
$output = ucfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input))));

return preg_replace('#\W#', '', $output);
}

private function getConfiguration(ExtensionInterface $extension, ContainerBuilder $container): ?ConfigurationInterface
{
return match (true) {
$extension instanceof ConfigurationInterface => $extension,
$extension instanceof ConfigurationExtensionInterface => $extension->getConfiguration([], $container),
default => null,
};
}

private function getShapeForExtensions(array $extensions, ContainerBuilder $container, string $indent = ''): string
{
$shape = '';
foreach ($extensions as $extension) {
if ($this->getConfiguration($extension, $container)) {
$type = $this->camelCase($extension->getAlias()).'Config';
$shape .= \sprintf("\n * %s%s?: %s,", $indent, $extension->getAlias(), $type);
}
}

return $shape;
}
}
6 changes: 5 additions & 1 deletion src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AssetsContextPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ErrorLoggerCompilerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PhpConfigReferenceDumpPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
Expand Down Expand Up @@ -148,7 +149,10 @@ public function build(ContainerBuilder $container): void
]);
}

$container->addCompilerPass(new AssetsContextPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION);
if ($container->hasParameter('.kernel.config_dir') && $container->hasParameter('.kernel.bundles_definition')) {
$container->addCompilerPass(new PhpConfigReferenceDumpPass($container->getParameter('.kernel.config_dir').'/reference.php', $container->getParameter('.kernel.bundles_definition')));
}
$container->addCompilerPass(new AssetsContextPass());
$container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);
Expand Down
23 changes: 23 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,27 @@ public function loadRoutes(LoaderInterface $loader): RouteCollection

return $collection;
}

/**
* Returns the kernel parameters.
*
* @return array<string, array|bool|string|int|float|\UnitEnum|null>
*/
protected function getKernelParameters(): array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Projects using this trait and overriding this method will have to call it explicitly.

Just to confirm that making the call to MicroKernelTrait::getKernelParameters() required would be a breaking change. Which is not the case now.

If people override getKernelParameters, they have to update their App\Kernel class like this to enable the new feature:

namespace App;

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait {
        getKernelParameters as private getMicroKernelParameters;
    }

    protected function getKernelParameters(): array
    {
        $parameters = $this->getMicroKernelParameters();
        $parameters['kernel.param'] = 'custom_kernel_value';

        return $parameters;
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. And the pass skips generating the reference.php file if the .kernel.config_dir parameter is missing, so that there's a nice incentive to do the change, but it's not a blocker either.

{
$parameters = parent::getKernelParameters();
$bundlesPath = $this->getBundlesPath();
$bundlesDefinition = !is_file($bundlesPath) ? [FrameworkBundle::class => ['all' => true]] : require $bundlesPath;
$knownEnvs = [$this->environment => true];

foreach ($bundlesDefinition as $envs) {
$knownEnvs += $envs;
}
unset($knownEnvs['all']);
$parameters['.container.known_envs'] = array_keys($knownEnvs);
$parameters['.kernel.config_dir'] = $this->getConfigDir();
$parameters['.kernel.bundles_definition'] = $bundlesDefinition;

return $parameters;
}
}
Loading
Loading