Skip to content

Commit 523e45f

Browse files
[DependencyInjection] fix support for "new" in initializers on PHP 8.1
1 parent e6e641c commit 523e45f

File tree

8 files changed

+176
-17
lines changed

8 files changed

+176
-17
lines changed

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

+47-8
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class AutowirePass extends AbstractRecursivePass
3434
private $decoratedClass;
3535
private $decoratedId;
3636
private $methodCalls;
37+
private $defaultArgument;
3738
private $getPreviousValue;
3839
private $decoratedMethodIndex;
3940
private $decoratedMethodArgumentIndex;
@@ -42,6 +43,10 @@ class AutowirePass extends AbstractRecursivePass
4243
public function __construct(bool $throwOnAutowireException = true)
4344
{
4445
$this->throwOnAutowiringException = $throwOnAutowireException;
46+
$this->defaultArgument = new class() {
47+
public $value;
48+
public $names;
49+
};
4550
}
4651

4752
/**
@@ -56,6 +61,7 @@ public function process(ContainerBuilder $container)
5661
$this->decoratedClass = null;
5762
$this->decoratedId = null;
5863
$this->methodCalls = null;
64+
$this->defaultArgument->names = null;
5965
$this->getPreviousValue = null;
6066
$this->decoratedMethodIndex = null;
6167
$this->decoratedMethodArgumentIndex = null;
@@ -150,8 +156,9 @@ private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot):
150156
$this->decoratedClass = $this->container->findDefinition($this->decoratedId)->getClass();
151157
}
152158

159+
$patchedIndexes = [];
160+
153161
foreach ($this->methodCalls as $i => $call) {
154-
$this->decoratedMethodIndex = $i;
155162
[$method, $arguments] = $call;
156163

157164
if ($method instanceof \ReflectionFunctionAbstract) {
@@ -168,11 +175,37 @@ private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot):
168175
}
169176
}
170177

171-
$arguments = $this->autowireMethod($reflectionMethod, $arguments);
178+
$arguments = $this->autowireMethod($reflectionMethod, $arguments, $i);
172179

173180
if ($arguments !== $call[1]) {
174181
$this->methodCalls[$i][1] = $arguments;
182+
$patchedIndexes[] = $i;
183+
}
184+
}
185+
186+
// use named arguments to skip complex default values
187+
foreach ($patchedIndexes as $i) {
188+
$namedArguments = null;
189+
$arguments = $this->methodCalls[$i][1];
190+
191+
foreach ($arguments as $j => $value) {
192+
if ($namedArguments && !$value instanceof $this->defaultArgument) {
193+
unset($arguments[$j]);
194+
$arguments[$namedArguments[$j]] = $value;
195+
}
196+
if ($namedArguments || !$value instanceof $this->defaultArgument) {
197+
continue;
198+
}
199+
200+
if (\PHP_VERSION_ID >= 80100 && (\is_array($value->value) ? $value->value : \is_object($value->value))) {
201+
unset($arguments[$j]);
202+
$namedArguments = $value->names;
203+
} else {
204+
$arguments[$j] = $value->value;
205+
}
175206
}
207+
208+
$this->methodCalls[$i][1] = $arguments;
176209
}
177210

178211
return $this->methodCalls;
@@ -185,16 +218,19 @@ private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot):
185218
*
186219
* @throws AutowiringFailedException
187220
*/
188-
private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments): array
221+
private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments, int $methodIndex): array
189222
{
190223
$class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId;
191224
$method = $reflectionMethod->name;
192225
$parameters = $reflectionMethod->getParameters();
193226
if ($reflectionMethod->isVariadic()) {
194227
array_pop($parameters);
195228
}
229+
$this->defaultArgument->names = new \ArrayObject();
196230

197231
foreach ($parameters as $index => $parameter) {
232+
$this->defaultArgument->names[$index] = $parameter->name;
233+
198234
if (\array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
199235
continue;
200236
}
@@ -212,7 +248,8 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
212248
// be false when isOptional() returns true. If the
213249
// argument *is* optional, allow it to be missing
214250
if ($parameter->isOptional()) {
215-
continue;
251+
--$index;
252+
break;
216253
}
217254
$type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, false);
218255
$type = $type ? sprintf('is type-hinted "%s"', ltrim($type, '\\')) : 'has no type-hint';
@@ -221,7 +258,8 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
221258
}
222259

223260
// specifically pass the default value
224-
$arguments[$index] = $parameter->getDefaultValue();
261+
$arguments[$index] = clone $this->defaultArgument;
262+
$arguments[$index]->value = $parameter->getDefaultValue();
225263

226264
continue;
227265
}
@@ -231,7 +269,8 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
231269
$failureMessage = $this->createTypeNotFoundMessageCallback($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
232270

233271
if ($parameter->isDefaultValueAvailable()) {
234-
$value = $parameter->getDefaultValue();
272+
$value = clone $this->defaultArgument;
273+
$value->value = $parameter->getDefaultValue();
235274
} elseif (!$parameter->allowsNull()) {
236275
throw new AutowiringFailedException($this->currentId, $failureMessage);
237276
}
@@ -252,6 +291,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
252291
} else {
253292
$arguments[$index] = new TypedReference($this->decoratedId, $this->decoratedClass);
254293
$this->getPreviousValue = $getValue;
294+
$this->decoratedMethodIndex = $methodIndex;
255295
$this->decoratedMethodArgumentIndex = $index;
256296

257297
continue;
@@ -263,8 +303,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
263303

264304
if ($parameters && !isset($arguments[++$index])) {
265305
while (0 <= --$index) {
266-
$parameter = $parameters[$index];
267-
if (!$parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== $arguments[$index]) {
306+
if (!$arguments[$index] instanceof $this->defaultArgument) {
268307
break;
269308
}
270309
unset($arguments[$index]);

src/Symfony/Component/DependencyInjection/Compiler/CheckArgumentsValidityPass.php

+32
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ protected function processValue($value, $isRoot = false)
3939
}
4040

4141
$i = 0;
42+
$hasNamedArgs = false;
4243
foreach ($value->getArguments() as $k => $v) {
44+
if (\PHP_VERSION_ID >= 80000 && preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $k)) {
45+
$hasNamedArgs = true;
46+
continue;
47+
}
48+
4349
if ($k !== $i++) {
4450
if (!\is_int($k)) {
4551
$msg = sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k);
@@ -57,11 +63,27 @@ protected function processValue($value, $isRoot = false)
5763
throw new RuntimeException($msg);
5864
}
5965
}
66+
67+
if ($hasNamedArgs) {
68+
$msg = sprintf('Invalid constructor argument for service "%s": cannot use positional argument after named argument. Check your service definition.', $this->currentId);
69+
$value->addError($msg);
70+
if ($this->throwExceptions) {
71+
throw new RuntimeException($msg);
72+
}
73+
74+
break;
75+
}
6076
}
6177

6278
foreach ($value->getMethodCalls() as $methodCall) {
6379
$i = 0;
80+
$hasNamedArgs = false;
6481
foreach ($methodCall[1] as $k => $v) {
82+
if (\PHP_VERSION_ID >= 80000 && preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $k)) {
83+
$hasNamedArgs = true;
84+
continue;
85+
}
86+
6587
if ($k !== $i++) {
6688
if (!\is_int($k)) {
6789
$msg = sprintf('Invalid argument for method call "%s" of service "%s": integer expected but found string "%s". Check your service definition.', $methodCall[0], $this->currentId, $k);
@@ -79,6 +101,16 @@ protected function processValue($value, $isRoot = false)
79101
throw new RuntimeException($msg);
80102
}
81103
}
104+
105+
if ($hasNamedArgs) {
106+
$msg = sprintf('Invalid argument for method call "%s" of service "%s": cannot use positional argument after named argument. Check your service definition.', $methodCall[0], $this->currentId);
107+
$value->addError($msg);
108+
if ($this->throwExceptions) {
109+
throw new RuntimeException($msg);
110+
}
111+
112+
break;
113+
}
82114
}
83115
}
84116

src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public function process(ContainerBuilder $container)
124124
protected function processValue($value, $isRoot = false)
125125
{
126126
if ($value instanceof ArgumentInterface) {
127-
// Reference found in ArgumentInterface::getValues() are not inlineable
127+
// References found in ArgumentInterface::getValues() are not inlineable
128128
return $value;
129129
}
130130

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -714,8 +714,8 @@ private function addServiceMethodCalls(Definition $definition, string $variableN
714714
$calls = '';
715715
foreach ($definition->getMethodCalls() as $k => $call) {
716716
$arguments = [];
717-
foreach ($call[1] as $value) {
718-
$arguments[] = $this->dumpValue($value);
717+
foreach ($call[1] as $i => $value) {
718+
$arguments[] = (\is_string($i) ? $i.': ' : '').$this->dumpValue($value);
719719
}
720720

721721
$witherAssignation = '';
@@ -1080,8 +1080,8 @@ private function addNewInstance(Definition $definition, string $return = '', str
10801080
}
10811081

10821082
$arguments = [];
1083-
foreach ($definition->getArguments() as $value) {
1084-
$arguments[] = $this->dumpValue($value);
1083+
foreach ($definition->getArguments() as $i => $value) {
1084+
$arguments[] = (\is_string($i) ? $i.': ' : '').$this->dumpValue($value);
10851085
}
10861086

10871087
if (null !== $definition->getFactory()) {

src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckArgumentsValidityPassTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,22 @@ public function testProcess()
4646
*/
4747
public function testException(array $arguments, array $methodCalls)
4848
{
49-
$this->expectException(RuntimeException::class);
5049
$container = new ContainerBuilder();
5150
$definition = $container->register('foo');
5251
$definition->setArguments($arguments);
5352
$definition->setMethodCalls($methodCalls);
5453

5554
$pass = new CheckArgumentsValidityPass();
55+
$this->expectException(RuntimeException::class);
5656
$pass->process($container);
5757
}
5858

5959
public function definitionProvider()
6060
{
6161
return [
62-
[[null, 'a' => 'a'], []],
62+
[['a' => 'a', null], []],
6363
[[1 => 1], []],
64-
[[], [['baz', [null, 'a' => 'a']]]],
64+
[[], [['baz', ['a' => 'a', null]]]],
6565
[[], [['baz', [1 => 1]]]],
6666
];
6767
}
@@ -70,7 +70,7 @@ public function testNoException()
7070
{
7171
$container = new ContainerBuilder();
7272
$definition = $container->register('foo');
73-
$definition->setArguments([null, 'a' => 'a']);
73+
$definition->setArguments(['a' => 'a', null]);
7474

7575
$pass = new CheckArgumentsValidityPass(false);
7676
$pass->process($container);

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

+19
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum;
4545
use Symfony\Component\DependencyInjection\Tests\Fixtures\ScalarFactory;
4646
use Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator;
47+
use Symfony\Component\DependencyInjection\Tests\Fixtures\NewInInitializer;
4748
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition1;
4849
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber;
4950
use Symfony\Component\DependencyInjection\TypedReference;
@@ -1187,6 +1188,24 @@ public function testDumpHandlesObjectClassNames()
11871188
$this->assertInstanceOf(\stdClass::class, $container->get('bar'));
11881189
}
11891190

1191+
/**
1192+
* @requires PHP 8.1
1193+
*/
1194+
public function testNewInInitializer()
1195+
{
1196+
$container = new ContainerBuilder();
1197+
$container
1198+
->register('foo', NewInInitializer::class)
1199+
->setPublic(true)
1200+
->setAutowired(true)
1201+
->setArguments(['$bar' => 234]);
1202+
1203+
$container->compile();
1204+
1205+
$dumper = new PhpDumper($container);
1206+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_new_in_initializer.php', $dumper->dump());
1207+
}
1208+
11901209
/**
11911210
* @requires PHP 8.1
11921211
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
class NewInInitializer
6+
{
7+
public function __construct($foo = new \stdClass(), $bar = 123)
8+
{
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
4+
use Symfony\Component\DependencyInjection\ContainerInterface;
5+
use Symfony\Component\DependencyInjection\Container;
6+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
7+
use Symfony\Component\DependencyInjection\Exception\LogicException;
8+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
9+
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
10+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
11+
12+
/**
13+
* This class has been auto-generated
14+
* by the Symfony Dependency Injection Component.
15+
*
16+
* @final
17+
*/
18+
class ProjectServiceContainer extends Container
19+
{
20+
private $parameters = [];
21+
22+
public function __construct()
23+
{
24+
$this->services = $this->privates = [];
25+
$this->methodMap = [
26+
'foo' => 'getFooService',
27+
];
28+
29+
$this->aliases = [];
30+
}
31+
32+
public function compile(): void
33+
{
34+
throw new LogicException('You cannot compile a dumped container that was already compiled.');
35+
}
36+
37+
public function isCompiled(): bool
38+
{
39+
return true;
40+
}
41+
42+
public function getRemovedIds(): array
43+
{
44+
return [
45+
'Psr\\Container\\ContainerInterface' => true,
46+
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
47+
];
48+
}
49+
50+
/**
51+
* Gets the public 'foo' shared autowired service.
52+
*
53+
* @return \Symfony\Component\DependencyInjection\Tests\Fixtures\NewInInitializer
54+
*/
55+
protected function getFooService()
56+
{
57+
return $this->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\NewInInitializer(bar: 234);
58+
}
59+
}

0 commit comments

Comments
 (0)