Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eeb92bf

Browse filesBrowse files
[DI] Add support for "wither" methods - for greater immutable services
1 parent f54c89c commit eeb92bf
Copy full SHA for eeb92bf

16 files changed

+284
-20
lines changed

‎src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ private function getContainerDefinitionDocument(Definition $definition, string $
348348
foreach ($calls as $callData) {
349349
$callsXML->appendChild($callXML = $dom->createElement('call'));
350350
$callXML->setAttribute('method', $callData[0]);
351+
if ($callData[2] ?? false) {
352+
$callXML->setAttribute('use-result', 'true');
353+
}
351354
}
352355
}
353356

‎src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php
+32-2Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,41 @@ protected function processValue($value, $isRoot = false)
140140
$this->byConstructor = true;
141141
$this->processValue($value->getFactory());
142142
$this->processValue($value->getArguments());
143+
144+
$properties = $value->getProperties();
145+
$setters = $value->getMethodCalls();
146+
147+
// Any references before a "wither" are part of the constructor-instantiation graph
148+
$lastWitherIndex = null;
149+
foreach ($setters as $k => $call) {
150+
if ($call[2] ?? false) {
151+
$lastWitherIndex = $k;
152+
}
153+
}
154+
155+
if (null !== $lastWitherIndex) {
156+
$this->processValue($properties);
157+
$setters = $properties = [];
158+
159+
foreach ($value->getMethodCalls() as $k => $call) {
160+
if (null === $lastWitherIndex) {
161+
$setters[] = $call;
162+
continue;
163+
}
164+
165+
if ($lastWitherIndex === $k) {
166+
$lastWitherIndex = null;
167+
}
168+
169+
$this->processValue($call);
170+
}
171+
}
172+
143173
$this->byConstructor = $byConstructor;
144174

145175
if (!$this->onlyConstructorArguments) {
146-
$this->processValue($value->getProperties());
147-
$this->processValue($value->getMethodCalls());
176+
$this->processValue($properties);
177+
$this->processValue($setters);
148178
$this->processValue($value->getConfigurator());
149179
}
150180
$this->lazy = $lazy;

‎src/Symfony/Component/DependencyInjection/Compiler/AutowireRequiredMethodsPass.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/AutowireRequiredMethodsPass.php
+15-1Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ protected function processValue($value, $isRoot = false)
3535
}
3636

3737
$alreadyCalledMethods = [];
38+
$withers = [];
3839

3940
foreach ($value->getMethodCalls() as list($method)) {
4041
$alreadyCalledMethods[strtolower($method)] = true;
@@ -50,7 +51,11 @@ protected function processValue($value, $isRoot = false)
5051
while (true) {
5152
if (false !== $doc = $r->getDocComment()) {
5253
if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) {
53-
$value->addMethodCall($reflectionMethod->name);
54+
if (preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@return\s++static[\s\*]#i', $doc)) {
55+
$withers[] = [$reflectionMethod->name, [], true];
56+
} else {
57+
$value->addMethodCall($reflectionMethod->name, []);
58+
}
5459
break;
5560
}
5661
if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) {
@@ -65,6 +70,15 @@ protected function processValue($value, $isRoot = false)
6570
}
6671
}
6772

73+
if ($withers) {
74+
// Prepend withers to prevent creating circular loops
75+
$setters = $value->getMethodCalls();
76+
$value->setMethodCalls($withers);
77+
foreach ($setters as $call) {
78+
$value->addMethodCall($call[0], $call[1], $call[2] ?? false);
79+
}
80+
}
81+
6882
return $value;
6983
}
7084
}

‎src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/ContainerBuilder.php
+21-7Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,8 +1139,15 @@ private function createService(Definition $definition, array &$inlineServices, $
11391139
}
11401140
}
11411141

1142-
if ($tryProxy || !$definition->isLazy()) {
1143-
// share only if proxying failed, or if not a proxy
1142+
$lastWitherIndex = null;
1143+
foreach ($definition->getMethodCalls() as $k => $call) {
1144+
if ($call[2] ?? false) {
1145+
$lastWitherIndex = $k;
1146+
}
1147+
}
1148+
1149+
if (null === $lastWitherIndex && ($tryProxy || !$definition->isLazy())) {
1150+
// share only if proxying failed, or if not a proxy, and if no withers are found
11441151
$this->shareService($definition, $service, $id, $inlineServices);
11451152
}
11461153

@@ -1149,8 +1156,13 @@ private function createService(Definition $definition, array &$inlineServices, $
11491156
$service->$name = $value;
11501157
}
11511158

1152-
foreach ($definition->getMethodCalls() as $call) {
1153-
$this->callMethod($service, $call, $inlineServices);
1159+
foreach ($definition->getMethodCalls() as $k => $call) {
1160+
$service = $this->callMethod($service, $call, $inlineServices);
1161+
1162+
if ($lastWitherIndex === $k && ($tryProxy || !$definition->isLazy())) {
1163+
// share only if proxying failed, or if not a proxy, and this is the last wither
1164+
$this->shareService($definition, $service, $id, $inlineServices);
1165+
}
11541166
}
11551167

11561168
if ($callable = $definition->getConfigurator()) {
@@ -1568,16 +1580,18 @@ private function callMethod($service, $call, array &$inlineServices)
15681580
{
15691581
foreach (self::getServiceConditionals($call[1]) as $s) {
15701582
if (!$this->has($s)) {
1571-
return;
1583+
return $service;
15721584
}
15731585
}
15741586
foreach (self::getInitializedConditionals($call[1]) as $s) {
15751587
if (!$this->doGet($s, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE, $inlineServices)) {
1576-
return;
1588+
return $service;
15771589
}
15781590
}
15791591

1580-
$service->{$call[0]}(...$this->doResolveServices($this->getParameterBag()->unescapeValue($this->getParameterBag()->resolveValue($call[1])), $inlineServices));
1592+
$result = $service->{$call[0]}(...$this->doResolveServices($this->getParameterBag()->unescapeValue($this->getParameterBag()->resolveValue($call[1])), $inlineServices));
1593+
1594+
return empty($call[2]) ? $service : $result;
15811595
}
15821596

15831597
/**

‎src/Symfony/Component/DependencyInjection/Definition.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Definition.php
+4-3Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ public function setMethodCalls(array $calls = [])
330330
{
331331
$this->calls = [];
332332
foreach ($calls as $call) {
333-
$this->addMethodCall($call[0], $call[1]);
333+
$this->addMethodCall($call[0], $call[1], $call[2] ?? false);
334334
}
335335

336336
return $this;
@@ -341,17 +341,18 @@ public function setMethodCalls(array $calls = [])
341341
*
342342
* @param string $method The method name to call
343343
* @param array $arguments An array of arguments to pass to the method call
344+
* @param bool $useResult Whether the call returns the service instance or not
344345
*
345346
* @return $this
346347
*
347348
* @throws InvalidArgumentException on empty $method param
348349
*/
349-
public function addMethodCall($method, array $arguments = [])
350+
public function addMethodCall($method, array $arguments = []/*, bool $useResult = false*/)
350351
{
351352
if (empty($method)) {
352353
throw new InvalidArgumentException('Method name cannot be empty.');
353354
}
354-
$this->calls[] = [$method, $arguments];
355+
$this->calls[] = 2 < \func_num_args() && \func_get_arg(2) ? [$method, $arguments, true] : [$method, $arguments];
355356

356357
return $this;
357358
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
+28-5Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,14 @@ private function addServiceInstance(string $id, Definition $definition, bool $is
506506
$isProxyCandidate = $this->getProxyDumper()->isProxyCandidate($definition);
507507
$instantiation = '';
508508

509-
if (!$isProxyCandidate && $definition->isShared() && !isset($this->singleUsePrivateIds[$id])) {
509+
$lastWitherIndex = null;
510+
foreach ($definition->getMethodCalls() as $k => $call) {
511+
if ($call[2] ?? false) {
512+
$lastWitherIndex = $k;
513+
}
514+
}
515+
516+
if (!$isProxyCandidate && $definition->isShared() && !isset($this->singleUsePrivateIds[$id]) && null === $lastWitherIndex) {
510517
$instantiation = sprintf('$this->%s[\'%s\'] = %s', $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates', $id, $isSimpleInstance ? '' : '$instance');
511518
} elseif (!$isSimpleInstance) {
512519
$instantiation = '$instance';
@@ -563,16 +570,32 @@ private function isTrivialInstance(Definition $definition): bool
563570
return true;
564571
}
565572

566-
private function addServiceMethodCalls(Definition $definition, string $variableName = 'instance'): string
573+
private function addServiceMethodCalls(Definition $definition, string $variableName, ?string $sharedNonLazyId): string
567574
{
575+
$lastWitherIndex = null;
576+
foreach ($definition->getMethodCalls() as $k => $call) {
577+
if ($call[2] ?? false) {
578+
$lastWitherIndex = $k;
579+
}
580+
}
581+
568582
$calls = '';
569-
foreach ($definition->getMethodCalls() as $call) {
583+
foreach ($definition->getMethodCalls() as $k => $call) {
570584
$arguments = [];
571585
foreach ($call[1] as $value) {
572586
$arguments[] = $this->dumpValue($value);
573587
}
574588

575-
$calls .= $this->wrapServiceConditionals($call[1], sprintf(" \$%s->%s(%s);\n", $variableName, $call[0], implode(', ', $arguments)));
589+
$witherAssignation = '';
590+
591+
if ($call[2] ?? false) {
592+
if (null !== $sharedNonLazyId && $lastWitherIndex === $k) {
593+
$witherAssignation = sprintf('$this->%s[\'%s\'] = ', $definition->isPublic() ? 'services' : 'privates', $sharedNonLazyId);
594+
}
595+
$witherAssignation .= sprintf('$%s = ', $variableName);
596+
}
597+
598+
$calls .= $this->wrapServiceConditionals($call[1], sprintf(" %s\$%s->%s(%s);\n", $witherAssignation, $variableName, $call[0], implode(', ', $arguments)));
576599
}
577600

578601
return $calls;
@@ -814,7 +837,7 @@ private function addInlineService(string $id, Definition $definition, Definition
814837
}
815838

816839
$code .= $this->addServiceProperties($inlineDef, $name);
817-
$code .= $this->addServiceMethodCalls($inlineDef, $name);
840+
$code .= $this->addServiceMethodCalls($inlineDef, $name, !$this->getProxyDumper()->isProxyCandidate($inlineDef) && $inlineDef->isShared() && !isset($this->singleUsePrivateIds[$id]) ? $id : null);
818841
$code .= $this->addServiceConfigurator($inlineDef, $name);
819842
}
820843

‎src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ private function addMethodCalls(array $methodcalls, \DOMElement $parent)
8484
if (\count($methodcall[1])) {
8585
$this->convertParameters($methodcall[1], 'argument', $call);
8686
}
87+
if ($methodcall[2] ?? false) {
88+
$call->setAttribute('use-result', 'true');
89+
}
8790
$parent->appendChild($call);
8891
}
8992
}

‎src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults)
337337
}
338338

339339
foreach ($this->getChildren($service, 'call') as $call) {
340-
$definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument', $file));
340+
$definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument', $file), XmlUtils::phpize($call->getAttribute('use-result')));
341341
}
342342

343343
$tags = $this->getChildren($service, 'tag');

‎src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,15 +463,17 @@ private function parseDefinition($id, $service, $file, array $defaults)
463463
if (isset($call['method'])) {
464464
$method = $call['method'];
465465
$args = isset($call['arguments']) ? $this->resolveServices($call['arguments'], $file) : [];
466+
$useResult = $call['use_result'] ?? false;
466467
} else {
467468
$method = $call[0];
468469
$args = isset($call[1]) ? $this->resolveServices($call[1], $file) : [];
470+
$useResult = $call[2] ?? false;
469471
}
470472

471473
if (!\is_array($args)) {
472474
throw new InvalidArgumentException(sprintf('The second parameter for function call "%s" must be an array of its arguments for service "%s" in %s. Check your YAML syntax.', $method, $id, $file));
473475
}
474-
$definition->addMethodCall($method, $args);
476+
$definition->addMethodCall($method, $args, $useResult);
475477
}
476478
}
477479

‎src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
<xsd:element name="argument" type="argument" maxOccurs="unbounded" />
244244
</xsd:choice>
245245
<xsd:attribute name="method" type="xsd:string" />
246+
<xsd:attribute name="use-result" type="boolean" />
246247
</xsd:complexType>
247248

248249
<xsd:simpleType name="parameter_type">

‎src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireRequiredMethodsPassTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireRequiredMethodsPassTest.php
+22Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,26 @@ public function testExplicitMethodInjection()
7777
);
7878
$this->assertEquals([], $methodCalls[0][1]);
7979
}
80+
81+
public function testWitherInjection()
82+
{
83+
$container = new ContainerBuilder();
84+
$container->register(Foo::class);
85+
86+
$container
87+
->register('wither', Wither::class)
88+
->setAutowired(true);
89+
90+
(new ResolveClassPass())->process($container);
91+
(new AutowireRequiredMethodsPass())->process($container);
92+
93+
$methodCalls = $container->getDefinition('wither')->getMethodCalls();
94+
95+
$expected = [
96+
['withFoo1', [], true],
97+
['withFoo2', [], true],
98+
['setFoo', []],
99+
];
100+
$this->assertSame($expected, $methodCalls);
101+
}
80102
}

‎src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Tests;
1313

14+
require_once __DIR__.'/Fixtures/includes/autowiring_classes.php';
1415
require_once __DIR__.'/Fixtures/includes/classes.php';
1516
require_once __DIR__.'/Fixtures/includes/ProjectExtension.php';
1617

@@ -36,6 +37,8 @@
3637
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
3738
use Symfony\Component\DependencyInjection\Reference;
3839
use Symfony\Component\DependencyInjection\ServiceLocator;
40+
use Symfony\Component\DependencyInjection\Tests\Compiler\Foo;
41+
use Symfony\Component\DependencyInjection\Tests\Compiler\Wither;
3942
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
4043
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
4144
use Symfony\Component\DependencyInjection\Tests\Fixtures\SimilarArgumentsDummy;
@@ -1565,6 +1568,22 @@ public function testDecoratedSelfReferenceInvolvingPrivateServices()
15651568

15661569
$this->assertSame(['service_container'], array_keys($container->getDefinitions()));
15671570
}
1571+
1572+
public function testWither()
1573+
{
1574+
$container = new ContainerBuilder();
1575+
$container->register(Foo::class);
1576+
1577+
$container
1578+
->register('wither', Wither::class)
1579+
->setPublic(true)
1580+
->setAutowired(true);
1581+
1582+
$container->compile();
1583+
1584+
$wither = $container->get('wither');
1585+
$this->assertInstanceOf(Foo::class, $wither->foo);
1586+
}
15681587
}
15691588

15701589
class FooClass

‎src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,16 @@ public function testMethodCalls()
9595
$this->assertEquals([['foo', ['foo']]], $def->getMethodCalls(), '->getMethodCalls() returns the methods to call');
9696
$this->assertSame($def, $def->addMethodCall('bar', ['bar']), '->addMethodCall() implements a fluent interface');
9797
$this->assertEquals([['foo', ['foo']], ['bar', ['bar']]], $def->getMethodCalls(), '->addMethodCall() adds a method to call');
98+
$this->assertSame($def, $def->addMethodCall('foobar', ['foobar'], true), '->addMethodCall() implements a fluent interface with third parameter');
99+
$this->assertEquals([['foo', ['foo']], ['bar', ['bar']], ['foobar', ['foobar'], true]], $def->getMethodCalls(), '->addMethodCall() adds a method to call');
98100
$this->assertTrue($def->hasMethodCall('bar'), '->hasMethodCall() returns true if first argument is a method to call registered');
99101
$this->assertFalse($def->hasMethodCall('no_registered'), '->hasMethodCall() returns false if first argument is not a method to call registered');
100102
$this->assertSame($def, $def->removeMethodCall('bar'), '->removeMethodCall() implements a fluent interface');
103+
$this->assertTrue($def->hasMethodCall('foobar'), '->hasMethodCall() returns true if first argument is a method to call registered');
104+
$this->assertSame($def, $def->removeMethodCall('foobar'), '->removeMethodCall() implements a fluent interface');
101105
$this->assertEquals([['foo', ['foo']]], $def->getMethodCalls(), '->removeMethodCall() removes a method to call');
106+
$this->assertSame($def, $def->setMethodCalls([['foobar', ['foobar'], true]]), '->setMethodCalls() implements a fluent interface with third parameter');
107+
$this->assertEquals([['foobar', ['foobar'], true]], $def->getMethodCalls(), '->addMethodCall() adds a method to call');
102108
}
103109

104110
/**

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.