Skip to content

Commit 349c80c

Browse files
feature #62092 [Config][DependencyInjection] Deprecate the fluent PHP format for semantic configuration (nicolas-grekas)
This PR was merged into the 7.4 branch. Discussion ---------- [Config][DependencyInjection] Deprecate the fluent PHP format for semantic configuration | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT This PR deprecates the fluent PHP format for semantic configuration introduced in Symfony 5.4 by `@Nyholm` (see #40600). It aims to replace it with the new array-based PHP config format (see #61490). The fluent PHP config format was a great experiment: - It helped us improve the Config component and the code generation of fluent config builders. - It confirmed the community’s interest in PHP-based configuration. - And it showed us its limits. Those limits are structural. Writing fluent config is difficult and full of edge cases. Its rigidity comes from having to match one canonical interpretation of the semantic config tree. Automatic code generation can’t capture the custom logic that before-normalizers introduce, yet those normalizers are essential for flexibility and backward compatibility. This rigidity makes fluent config fragile. How do we deal with this fragility as config tree authors? At the moment, we don't care. Maybe this format is too niche for you to have experienced this issue, but we cannot guarantee that simple upgrades won't break your fluent PHP config. The new array-based PHP format builds directly on the same code used for loading YAML configs. That means: - trivial conversion between YAML and PHP arrays, - identical flexibility and behavior, - and support for auto-completion and static analysis through generated array shapes. The generated array shapes are rigid too, but that rigidity is non-breaking: even if your config no longer matches the canonical shape, your app keeps working. Static analyzers might warn you: that’s an invitation to update, not a failure. I'm submitting this PR a bit l late for 7.4 but I think it's important to merge now. Deprecating the fluent PHP config format will: - prevent new code from relying on a fragile approach, - make room in the documentation for the array-based format, - and consolidate Symfony’s configuration story around a robust PHP-based format. Fluent PHP for semantic config served us well but it's time to retire it. ```diff -return function (AcmeConfig $config) { - $config->color('red'); -} +return new AcmeConfig([ + 'color' => 'red', +]); ``` PS: there's another fluent config format for services and routes (see #23834 and #24180). This other format is handwritten. It doesn't have the issues listed above and it is *not* deprecated. It's actually the recommended way *for bundles* to declare their config (instead of XML, see #60568). Commits ------- 332b4ac [Config][DependencyInjection] Deprecate the fluent PHP format for semantic configuration
2 parents bcb1ba5 + 332b4ac commit 349c80c

38 files changed

+541
-145
lines changed

UPGRADE-7.4.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Config
2323

2424
* Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead
2525
* Deprecate setting a default value to a node that is required, and vice versa
26+
* Deprecate generating fluent methods in config builders
2627

2728
Console
2829
-------
@@ -40,6 +41,15 @@ DependencyInjection
4041
* Deprecate `ExtensionInterface::getXsdValidationBasePath()` and `getNamespace()`;
4142
bundles that need to support older versions of Symfony can keep the methods
4243
but need to add the `@deprecated` annotation on them
44+
* Deprecate the fluent PHP format for semantic configuration, instantiate builders inline with the config array as argument and return them instead:
45+
```diff
46+
-return function (AcmeConfig $config) {
47+
- $config->color('red');
48+
-}
49+
+return new AcmeConfig([
50+
+ 'color' => 'red',
51+
+]);
52+
```
4353

4454
DoctrineBridge
4555
--------------

src/Symfony/Component/Config/Builder/ClassBuilder.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public function __construct(
3838
private string $namespace,
3939
string $name,
4040
private NodeInterface $node,
41+
public readonly bool $isRoot = false,
4142
) {
4243
$this->name = ucfirst($this->camelCase($name)).'Config';
4344
}
@@ -73,7 +74,7 @@ public function build(): string
7374
$use .= \sprintf('use %s;', $statement)."\n";
7475
}
7576

76-
$implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements);
77+
$implements = $this->implements ? 'implements '.implode(', ', $this->implements) : '';
7778
$body = '';
7879
foreach ($this->properties as $property) {
7980
$body .= ' '.$property->getContent()."\n";

src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function build(ConfigurationInterface $configuration): \Closure
5151
$this->classes = [];
5252

5353
$rootNode = $configuration->getConfigTreeBuilder()->buildTree();
54-
$rootClass = new ClassBuilder('Symfony\\Config', $rootNode->getName(), $rootNode);
54+
$rootClass = new ClassBuilder('Symfony\\Config', $rootNode->getName(), $rootNode, true);
5555

5656
$path = $this->getFullPath($rootClass);
5757
if (!is_file($path)) {
@@ -68,7 +68,7 @@ public function NAME(): string
6868
$this->writeClasses();
6969
}
7070

71-
return function () use ($path, $rootClass) {
71+
return static function () use ($path, $rootClass) {
7272
require_once $path;
7373
$className = $rootClass->getFqcn();
7474

@@ -94,6 +94,9 @@ private function writeClasses(): void
9494
if ($class->getProperties()) {
9595
$class->addProperty('_usedProperties', null, '[]');
9696
}
97+
if ($class->isRoot) {
98+
$class->addProperty('_hasDeprecatedCalls', null, 'false');
99+
}
97100
$this->buildSetExtraKey($class);
98101

99102
file_put_contents($this->getFullPath($class), $class->build());
@@ -134,10 +137,13 @@ private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $n
134137
if ($acceptScalar) {
135138
$comment = \sprintf(" * @template TValue of %s\n * @param TValue \$value\n%s", $paramType, $comment);
136139
$comment .= \sprintf(' * @return %s|$this'."\n", $childClass->getFqcn());
137-
$comment .= \sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn());
140+
$comment .= \sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n", $childClass->getFqcn());
141+
}
142+
if ($class->isRoot) {
143+
$comment .= " * @deprecated since Symfony 7.4\n";
138144
}
139145
if ('' !== $comment) {
140-
$comment = "/**\n$comment*/\n";
146+
$comment = "/**\n$comment */\n";
141147
}
142148

143149
$property = $class->addProperty(
@@ -146,7 +152,7 @@ private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $n
146152
);
147153
$body = $acceptScalar ? '
148154
COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static
149-
{
155+
{DEPRECATED_BODY
150156
if (!\is_array($value)) {
151157
$this->_usedProperties[\'PROPERTY\'] = true;
152158
$this->PROPERTY = $value;
@@ -164,7 +170,7 @@ private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $n
164170
return $this->PROPERTY;
165171
}' : '
166172
COMMENTpublic function NAME(array $value = []): CLASS
167-
{
173+
{DEPRECATED_BODY
168174
if (null === $this->PROPERTY) {
169175
$this->_usedProperties[\'PROPERTY\'] = true;
170176
$this->PROPERTY = new CLASS($value);
@@ -176,6 +182,7 @@ private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $n
176182
}';
177183
$class->addUse(InvalidConfigurationException::class);
178184
$class->addMethod($node->getName(), $body, [
185+
'DEPRECATED_BODY' => $class->isRoot ? "\n \$this->_hasDeprecatedCalls = true;" : '',
179186
'COMMENT' => $comment,
180187
'PROPERTY' => $property->getName(),
181188
'CLASS' => $childClass->getFqcn(),
@@ -195,15 +202,17 @@ private function handleVariableNode(VariableNode $node, ClassBuilder $class): vo
195202
/**
196203
COMMENT *
197204
* @return $this
198-
*/
205+
*DEPRECATED_ANNOTATION/
199206
public function NAME(mixed $valueDEFAULT): static
200-
{
207+
{DEPRECATED_BODY
201208
$this->_usedProperties[\'PROPERTY\'] = true;
202209
$this->PROPERTY = $value;
203210
204211
return $this;
205212
}';
206213
$class->addMethod($node->getName(), $body, [
214+
'DEPRECATED_BODY' => $class->isRoot ? "\n \$this->_hasDeprecatedCalls = true;" : '',
215+
'DEPRECATED_ANNOTATION' => $class->isRoot ? " @deprecated since Symfony 7.4\n *" : '',
207216
'PROPERTY' => $property->getName(),
208217
'COMMENT' => $comment,
209218
'DEFAULT' => $node->hasDefaultValue() ? ' = '.var_export($node->getDefaultValue(), true) : '',
@@ -232,16 +241,18 @@ private function handlePrototypedArrayNode(PrototypedArrayNode $node, ClassBuild
232241
* @param ParamConfigurator|list<ParamConfigurator|PROTOTYPE_TYPE>EXTRA_TYPE $value
233242
*
234243
* @return $this
235-
*/
244+
*DEPRECATED_ANNOTATION/
236245
public function NAME(PARAM_TYPE $value): static
237-
{
246+
{DEPRECATED_BODY
238247
$this->_usedProperties[\'PROPERTY\'] = true;
239248
$this->PROPERTY = $value;
240249
241250
return $this;
242251
}';
243252

244253
$class->addMethod($node->getName(), $body, [
254+
'DEPRECATED_BODY' => $class->isRoot ? "\n \$this->_hasDeprecatedCalls = true;" : '',
255+
'DEPRECATED_ANNOTATION' => $class->isRoot ? " @deprecated since Symfony 7.4\n *" : '',
245256
'PROPERTY' => $property->getName(),
246257
'PROTOTYPE_TYPE' => implode('|', $prototypeParameterTypes),
247258
'EXTRA_TYPE' => $nodeTypesWithoutArray ? '|'.implode('|', $nodeTypesWithoutArray) : '',
@@ -251,16 +262,18 @@ public function NAME(PARAM_TYPE $value): static
251262
$body = '
252263
/**
253264
* @return $this
254-
*/
265+
*DEPRECATED_ANNOTATION/
255266
public function NAME(string $VAR, TYPE $VALUE): static
256-
{
267+
{DEPRECATED_BODY
257268
$this->_usedProperties[\'PROPERTY\'] = true;
258269
$this->PROPERTY[$VAR] = $VALUE;
259270
260271
return $this;
261272
}';
262273

263274
$class->addMethod($methodName, $body, [
275+
'DEPRECATED_BODY' => $class->isRoot ? "\n \$this->_hasDeprecatedCalls = true;" : '',
276+
'DEPRECATED_ANNOTATION' => $class->isRoot ? " @deprecated since Symfony 7.4\n *" : '',
264277
'PROPERTY' => $property->getName(),
265278
'TYPE' => ['mixed'] !== $prototypeParameterTypes ? 'ParamConfigurator|'.implode('|', $prototypeParameterTypes) : 'mixed',
266279
'VAR' => '' === $key ? 'key' : $key,
@@ -290,16 +303,19 @@ public function NAME(string $VAR, TYPE $VALUE): static
290303
if ($acceptScalar) {
291304
$comment = \sprintf(" * @template TValue of %s\n * @param TValue \$value\n%s", $paramType, $comment);
292305
$comment .= \sprintf(' * @return %s|$this'."\n", $childClass->getFqcn());
293-
$comment .= \sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn());
306+
$comment .= \sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n", $childClass->getFqcn());
307+
}
308+
if ($class->isRoot) {
309+
$comment .= " * @deprecated since Symfony 7.4\n";
294310
}
295311
if ('' !== $comment) {
296-
$comment = "/**\n$comment*/\n";
312+
$comment = "/**\n$comment */\n";
297313
}
298314

299315
if ($noKey) {
300316
$body = $acceptScalar ? '
301317
COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static
302-
{
318+
{DEPRECATED_BODY
303319
$this->_usedProperties[\'PROPERTY\'] = true;
304320
if (!\is_array($value)) {
305321
$this->PROPERTY[] = $value;
@@ -310,12 +326,13 @@ public function NAME(string $VAR, TYPE $VALUE): static
310326
return $this->PROPERTY[] = new CLASS($value);
311327
}' : '
312328
COMMENTpublic function NAME(array $value = []): CLASS
313-
{
329+
{DEPRECATED_BODY
314330
$this->_usedProperties[\'PROPERTY\'] = true;
315331
316332
return $this->PROPERTY[] = new CLASS($value);
317333
}';
318334
$class->addMethod($methodName, $body, [
335+
'DEPRECATED_BODY' => $class->isRoot ? "\n \$this->_hasDeprecatedCalls = true;" : '',
319336
'COMMENT' => $comment,
320337
'PROPERTY' => $property->getName(),
321338
'CLASS' => $childClass->getFqcn(),
@@ -324,7 +341,7 @@ public function NAME(string $VAR, TYPE $VALUE): static
324341
} else {
325342
$body = $acceptScalar ? '
326343
COMMENTpublic function NAME(string $VAR, PARAM_TYPE $VALUE = []): CLASS|static
327-
{
344+
{DEPRECATED_BODY
328345
if (!\is_array($VALUE)) {
329346
$this->_usedProperties[\'PROPERTY\'] = true;
330347
$this->PROPERTY[$VAR] = $VALUE;
@@ -342,7 +359,7 @@ public function NAME(string $VAR, TYPE $VALUE): static
342359
return $this->PROPERTY[$VAR];
343360
}' : '
344361
COMMENTpublic function NAME(string $VAR, array $VALUE = []): CLASS
345-
{
362+
{DEPRECATED_BODY
346363
if (!isset($this->PROPERTY[$VAR])) {
347364
$this->_usedProperties[\'PROPERTY\'] = true;
348365
$this->PROPERTY[$VAR] = new CLASS($VALUE);
@@ -354,7 +371,9 @@ public function NAME(string $VAR, TYPE $VALUE): static
354371
}';
355372
$class->addUse(InvalidConfigurationException::class);
356373
$class->addMethod($methodName, str_replace('$value', '$VAR', $body), [
357-
'COMMENT' => $comment, 'PROPERTY' => $property->getName(),
374+
'DEPRECATED_BODY' => $class->isRoot ? "\n \$this->_hasDeprecatedCalls = true;" : '',
375+
'COMMENT' => $comment,
376+
'PROPERTY' => $property->getName(),
358377
'CLASS' => $childClass->getFqcn(),
359378
'VAR' => '' === $key ? 'key' : $key,
360379
'VALUE' => 'value' === $key ? 'data' : 'value',
@@ -374,16 +393,21 @@ private function handleScalarNode(ScalarNode $node, ClassBuilder $class): void
374393
$body = '
375394
/**
376395
COMMENT * @return $this
377-
*/
396+
*DEPRECATED_ANNOTATION/
378397
public function NAME($value): static
379-
{
398+
{DEPRECATED_BODY
380399
$this->_usedProperties[\'PROPERTY\'] = true;
381400
$this->PROPERTY = $value;
382401
383402
return $this;
384403
}';
385404

386-
$class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment]);
405+
$class->addMethod($node->getName(), $body, [
406+
'DEPRECATED_BODY' => $class->isRoot ? "\n \$this->_hasDeprecatedCalls = true;" : '',
407+
'DEPRECATED_ANNOTATION' => $class->isRoot ? " @deprecated since Symfony 7.4\n *" : '',
408+
'PROPERTY' => $property->getName(),
409+
'COMMENT' => $comment,
410+
]);
387411
}
388412

389413
private function getParameterTypes(NodeInterface $node): array
@@ -509,6 +533,13 @@ private function buildToArray(ClassBuilder $class): void
509533

510534
$extraKeys = $class->shouldAllowExtraKeys() ? ' + $this->_extraKeys' : '';
511535

536+
if ($class->isRoot) {
537+
$body .= "
538+
if (\$this->_hasDeprecatedCalls) {
539+
trigger_deprecation('symfony/config', '7.4', 'Calling any fluent method on \"%s\" is deprecated; pass the configuration to the constructor instead.', \$this::class);
540+
}";
541+
}
542+
512543
$class->addMethod('toArray', '
513544
public function NAME(): array
514545
{
@@ -583,13 +614,16 @@ private function buildSetExtraKey(ClassBuilder $class): void
583614
* @param ParamConfigurator|mixed $value
584615
*
585616
* @return $this
586-
*/
617+
*DEPRECATED_ANNOTATION/
587618
public function NAME(string $key, mixed $value): static
588-
{
619+
{DEPRECATED_BODY
589620
$this->_extraKeys[$key] = $value;
590621
591622
return $this;
592-
}');
623+
}', [
624+
'DEPRECATED_BODY' => $class->isRoot ? "\n \$this->_hasDeprecatedCalls = true;" : '',
625+
'DEPRECATED_ANNOTATION' => $class->isRoot ? " @deprecated since Symfony 7.4\n *" : '',
626+
]);
593627
}
594628

595629
private function getSubNamespace(ClassBuilder $rootClass): string

src/Symfony/Component/Config/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Add array-shapes to generated config builders
1212
* Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead
1313
* Deprecate setting a default value to a node that is required, and vice versa
14+
* Deprecate generating fluent methods in config builders
1415

1516
7.3
1617
---

src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.config.php

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,35 @@
1111

1212
use Symfony\Config\AddToListConfig;
1313

14-
return static function (AddToListConfig $config) {
15-
$config->translator()->fallbacks(['sv', 'fr', 'es']);
16-
$config->translator()->source('\\Acme\\Foo', 'yellow');
17-
$config->translator()->source('\\Acme\\Bar', 'green');
18-
19-
$config->messenger([
14+
return new AddToListConfig([
15+
'translator' => [
16+
'fallbacks' => ['sv', 'fr', 'es'],
17+
'sources' => [
18+
'\\Acme\\Foo' => 'yellow',
19+
'\\Acme\\Bar' => 'green',
20+
],
21+
],
22+
'messenger' => [
2023
'routing' => [
2124
'Foo\\MyArrayMessage' => [
2225
'senders' => ['workqueue'],
2326
],
27+
'Foo\\Message' => [
28+
'senders' => ['workqueue'],
29+
],
30+
'Foo\\DoubleMessage' => [
31+
'senders' => ['sync', 'workqueue'],
32+
],
2433
],
25-
]);
26-
$config->messenger()
27-
->routing('Foo\\Message')->senders(['workqueue']);
28-
$config->messenger()
29-
->routing('Foo\\DoubleMessage')->senders(['sync', 'workqueue']);
30-
31-
$config->messenger()->receiving()
32-
->color('blue')
33-
->priority(10);
34-
$config->messenger()->receiving()
35-
->color('red')
36-
->priority(5);
37-
};
34+
'receiving' => [
35+
[
36+
'color' => 'blue',
37+
'priority' => 10,
38+
],
39+
[
40+
'color' => 'red',
41+
'priority' => 5,
42+
],
43+
],
44+
],
45+
]);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
use Symfony\Config\AddToListConfig;
13+
14+
return static function (AddToListConfig $config) {
15+
$config->translator()->fallbacks(['sv', 'fr', 'es']);
16+
$config->translator()->source('\\Acme\\Foo', 'yellow');
17+
$config->translator()->source('\\Acme\\Bar', 'green');
18+
19+
$config->messenger([
20+
'routing' => [
21+
'Foo\\MyArrayMessage' => [
22+
'senders' => ['workqueue'],
23+
],
24+
],
25+
]);
26+
$config->messenger()
27+
->routing('Foo\\Message')->senders(['workqueue']);
28+
$config->messenger()
29+
->routing('Foo\\DoubleMessage')->senders(['sync', 'workqueue']);
30+
31+
$config->messenger()->receiving()
32+
->color('blue')
33+
->priority(10);
34+
$config->messenger()->receiving()
35+
->color('red')
36+
->priority(5);
37+
};

0 commit comments

Comments
 (0)