Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bdbd94e

Browse files
committed
merged branch fabpot/contagious-services (PR #7007)
This PR was merged into the master branch. Discussion ---------- [2.3] [WIP] Synchronized services... | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #5300, #6756 | License | MIT | Doc PR | symfony/symfony-docs#2343 Todo: - [x] update documentation - [x] find a better name than contagious (synchronized)? refs #6932, refs #5012 This PR is a proof of concept that tries to find a solution for some problems we have with scopes and services depending on scoped services (mostly the request service in Symfony). Basically, whenever you want to inject the Request into a service, you have two possibilities: * put your own service into the request scope (a new service will be created whenever a sub-request is run, and the service is not available outside the request scope); * set the request service reference as non-strict (your service is always available but the request you have depends on when the service is created the first time). This PR addresses this issue by allowing to use the second option but you service still always has the right Request service (see below for a longer explanation on how it works). There is another issue that this PR fixes: edge cases and weird behaviors. There are several bug reports about some weird behaviors, and most of the time, this is related to the sub-requests. That's because the Request is injected into several Symfony objects without being updated correctly when leaving the request scope. Let me explain that: when a listener for instance needs the Request object, it can listen to the `kernel.request` event and store the request somewhere. So, whenever you enter a sub-request, the listener will get the new one. But when the sub-request ends, the listener has no way to know that it needs to reset the request to the master one. In practice, that's not really an issue, but let me show you an example of this issue in practice: * You have a controller that is called with the English locale; * The controller (probably via a template) renders a sub-request that uses the French locale; * After the rendering, and from the controller, you try to generate a URL. Which locale the router will use? Yes, the French locale, which is wrong. To fix these issues, this PR introduces a new notion in the DIC: synchronized services. When a service is marked as synchronized, all method calls involving this service will be called each time this service is set. When in a scope, methods are also called to restore the previous version of the service when the scope leaves. If you have a look at the router or the locale listener, you will see that there is now a `setRequest` method that will called whenever the request service changes (because the `Container::set()` method is called or because the service is changed by a scope change). Commits ------- 17269e1 [DependencyInjection] fixed management of scoped services with an invalid behavior set to null bb83b3e [HttpKernel] added a safeguard for when a fragment is rendered outside the context of a master request 5d7b835 [FrameworkBundle] added some functional tests ff9d688 fixed Request management for FragmentHandler 1b98ad3 fixed Request management for LocaleListener a7b2b7e fixed Request management for RequestListener 0892135 [HttpKernel] ensured that the Request is null when outside of the Request scope 2ffcfb9 [FrameworkBundle] made the Request service synchronized ec1e7ca [DependencyInjection] added a way to automatically update scoped services
2 parents d1a3115 + 6bb74bf commit bdbd94e

24 files changed

+388
-28
lines changed

Compiler/ResolveInvalidReferencesPass.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,7 @@ private function processArguments(array $arguments, $inMethodCall = false)
8888
$exists = $this->container->has($id);
8989

9090
// resolve invalid behavior
91-
if ($exists && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) {
92-
$arguments[$k] = new Reference($id, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $argument->isStrict());
93-
} elseif (!$exists && ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
91+
if (!$exists && ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
9492
$arguments[$k] = null;
9593
} elseif (!$exists && ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) {
9694
if ($inMethodCall) {

Container.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection;
1313

14+
use Symfony\Component\DependencyInjection\Exception\InactiveScopeException;
1415
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1516
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
1617
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
@@ -206,6 +207,10 @@ public function set($id, $service, $scope = self::SCOPE_CONTAINER)
206207
}
207208

208209
$this->services[$id] = $service;
210+
211+
if (method_exists($this, $method = 'synchronize'.strtr($id, array('_' => '', '.' => '_')).'Service')) {
212+
$this->$method();
213+
}
209214
}
210215

211216
/**
@@ -221,7 +226,7 @@ public function has($id)
221226
{
222227
$id = strtolower($id);
223228

224-
return isset($this->services[$id]) || method_exists($this, 'get'.strtr($id, array('_' => '', '.' => '_')).'Service');
229+
return array_key_exists($id, $this->services) || method_exists($this, 'get'.strtr($id, array('_' => '', '.' => '_')).'Service');
225230
}
226231

227232
/**
@@ -247,7 +252,7 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE
247252
{
248253
$id = strtolower($id);
249254

250-
if (isset($this->services[$id])) {
255+
if (array_key_exists($id, $this->services)) {
251256
return $this->services[$id];
252257
}
253258

@@ -263,10 +268,14 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE
263268
} catch (\Exception $e) {
264269
unset($this->loading[$id]);
265270

266-
if (isset($this->services[$id])) {
271+
if (array_key_exists($id, $this->services)) {
267272
unset($this->services[$id]);
268273
}
269274

275+
if ($e instanceof InactiveScopeException && self::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) {
276+
return null;
277+
}
278+
270279
throw $e;
271280
}
272281

@@ -289,7 +298,7 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE
289298
*/
290299
public function initialized($id)
291300
{
292-
return isset($this->services[strtolower($id)]);
301+
return array_key_exists(strtolower($id), $this->services);
293302
}
294303

295304
/**
@@ -393,8 +402,11 @@ public function leaveScope($name)
393402
$services = $this->scopeStacks[$name]->pop();
394403
$this->scopedServices += $services;
395404

396-
array_unshift($services, $this->services);
397-
$this->services = call_user_func_array('array_merge', $services);
405+
foreach ($services as $array) {
406+
foreach ($array as $id => $service) {
407+
$this->set($id, $service, $name);
408+
}
409+
}
398410
}
399411
}
400412

ContainerBuilder.php

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
4646
*/
4747
private $definitions = array();
4848

49+
/**
50+
* @var Definition[]
51+
*/
52+
private $obsoleteDefinitions = array();
53+
4954
/**
5055
* @var Alias[]
5156
*/
@@ -351,14 +356,28 @@ public function set($id, $service, $scope = self::SCOPE_CONTAINER)
351356

352357
if ($this->isFrozen()) {
353358
// setting a synthetic service on a frozen container is alright
354-
if (!isset($this->definitions[$id]) || !$this->definitions[$id]->isSynthetic()) {
359+
if (
360+
(!isset($this->definitions[$id]) && !isset($this->obsoleteDefinitions[$id]))
361+
||
362+
(isset($this->definitions[$id]) && !$this->definitions[$id]->isSynthetic())
363+
||
364+
(isset($this->obsoleteDefinitions[$id]) && !$this->obsoleteDefinitions[$id]->isSynthetic())
365+
) {
355366
throw new BadMethodCallException(sprintf('Setting service "%s" on a frozen container is not allowed.', $id));
356367
}
357368
}
358369

370+
if (isset($this->definitions[$id])) {
371+
$this->obsoleteDefinitions[$id] = $this->definitions[$id];
372+
}
373+
359374
unset($this->definitions[$id], $this->aliases[$id]);
360375

361376
parent::set($id, $service, $scope);
377+
378+
if (isset($this->obsoleteDefinitions[$id]) && $this->obsoleteDefinitions[$id]->isSynchronized()) {
379+
$this->synchronize($id);
380+
}
362381
}
363382

364383
/**
@@ -885,19 +904,7 @@ private function createService(Definition $definition, $id)
885904
}
886905

887906
foreach ($definition->getMethodCalls() as $call) {
888-
$services = self::getServiceConditionals($call[1]);
889-
890-
$ok = true;
891-
foreach ($services as $s) {
892-
if (!$this->has($s)) {
893-
$ok = false;
894-
break;
895-
}
896-
}
897-
898-
if ($ok) {
899-
call_user_func_array(array($service, $call[0]), $this->resolveServices($parameterBag->resolveValue($call[1])));
900-
}
907+
$this->callMethod($service, $call);
901908
}
902909

903910
$properties = $this->resolveServices($parameterBag->resolveValue($definition->getProperties()));
@@ -999,4 +1006,43 @@ public static function getServiceConditionals($value)
9991006

10001007
return $services;
10011008
}
1009+
1010+
/**
1011+
* Synchronizes a service change.
1012+
*
1013+
* This method updates all services that depend on the given
1014+
* service by calling all methods referencing it.
1015+
*
1016+
* @param string $id A service id
1017+
*/
1018+
private function synchronize($id)
1019+
{
1020+
foreach ($this->definitions as $definitionId => $definition) {
1021+
// only check initialized services
1022+
if (!$this->initialized($definitionId)) {
1023+
continue;
1024+
}
1025+
1026+
foreach ($definition->getMethodCalls() as $call) {
1027+
foreach ($call[1] as $argument) {
1028+
if ($argument instanceof Reference && $id == (string) $argument) {
1029+
$this->callMethod($this->get($definitionId), $call);
1030+
}
1031+
}
1032+
}
1033+
}
1034+
}
1035+
1036+
private function callMethod($service, $call)
1037+
{
1038+
$services = self::getServiceConditionals($call[1]);
1039+
1040+
foreach ($services as $s) {
1041+
if (!$this->has($s)) {
1042+
return;
1043+
}
1044+
}
1045+
1046+
call_user_func_array(array($service, $call[0]), $this->resolveServices($this->getParameterBag()->resolveValue($call[1])));
1047+
}
10021048
}

Definition.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Definition
3636
private $public;
3737
private $synthetic;
3838
private $abstract;
39+
private $synchronized;
3940

4041
protected $arguments;
4142

@@ -56,6 +57,7 @@ public function __construct($class = null, array $arguments = array())
5657
$this->tags = array();
5758
$this->public = true;
5859
$this->synthetic = false;
60+
$this->synchronized = false;
5961
$this->abstract = false;
6062
$this->properties = array();
6163
}
@@ -569,6 +571,34 @@ public function isPublic()
569571
return $this->public;
570572
}
571573

574+
/**
575+
* Sets the synchronized flag of this service.
576+
*
577+
* @param Boolean $boolean
578+
*
579+
* @return Definition The current instance
580+
*
581+
* @api
582+
*/
583+
public function setSynchronized($boolean)
584+
{
585+
$this->synchronized = (Boolean) $boolean;
586+
587+
return $this;
588+
}
589+
590+
/**
591+
* Whether this service is synchronized.
592+
*
593+
* @return Boolean
594+
*
595+
* @api
596+
*/
597+
public function isSynchronized()
598+
{
599+
return $this->synchronized;
600+
}
601+
572602
/**
573603
* Sets whether this definition is synthetic, that is not constructed by the
574604
* container, but dynamically injected.

Dumper/PhpDumper.php

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ protected function get{$name}Service()
567567
*/
568568
private function addServices()
569569
{
570-
$publicServices = $privateServices = $aliasServices = '';
570+
$publicServices = $privateServices = $aliasServices = $synchronizers = '';
571571
$definitions = $this->container->getDefinitions();
572572
ksort($definitions);
573573
foreach ($definitions as $id => $definition) {
@@ -576,6 +576,8 @@ private function addServices()
576576
} else {
577577
$privateServices .= $this->addService($id, $definition);
578578
}
579+
580+
$synchronizers .= $this->addServiceSynchronizer($id, $definition);
579581
}
580582

581583
$aliases = $this->container->getAliases();
@@ -584,7 +586,60 @@ private function addServices()
584586
$aliasServices .= $this->addServiceAlias($alias, $id);
585587
}
586588

587-
return $publicServices.$aliasServices.$privateServices;
589+
return $publicServices.$aliasServices.$synchronizers.$privateServices;
590+
}
591+
592+
/**
593+
* Adds synchronizer methods.
594+
*
595+
* @param string $id A service identifier
596+
* @param Definition $definition A Definition instance
597+
*/
598+
private function addServiceSynchronizer($id, Definition $definition)
599+
{
600+
if (!$definition->isSynchronized()) {
601+
return;
602+
}
603+
604+
$code = '';
605+
foreach ($this->container->getDefinitions() as $definitionId => $definition) {
606+
foreach ($definition->getMethodCalls() as $call) {
607+
foreach ($call[1] as $argument) {
608+
if ($argument instanceof Reference && $id == (string) $argument) {
609+
$arguments = array();
610+
foreach ($call[1] as $value) {
611+
$arguments[] = $this->dumpValue($value);
612+
}
613+
614+
$call = $this->wrapServiceConditionals($call[1], sprintf("\$this->get('%s')->%s(%s);", $definitionId, $call[0], implode(', ', $arguments)));
615+
616+
$code .= <<<EOF
617+
if (\$this->initialized('$definitionId')) {
618+
$call
619+
}
620+
621+
EOF;
622+
}
623+
}
624+
}
625+
}
626+
627+
if (!$code) {
628+
return;
629+
}
630+
631+
$name = Container::camelize($id);
632+
633+
return <<<EOF
634+
635+
/**
636+
* Updates the '$id' service.
637+
*/
638+
protected function synchronize{$name}Service()
639+
{
640+
$code }
641+
642+
EOF;
588643
}
589644

590645
private function addNewInstance($id, Definition $definition, $return, $instantiation)

Dumper/XmlDumper.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ private function addService($definition, $id, \DOMElement $parent)
127127
if (!$definition->isPublic()) {
128128
$service->setAttribute('public', 'false');
129129
}
130+
if ($definition->isSynthetic()) {
131+
$service->setAttribute('synthetic', 'true');
132+
}
133+
if ($definition->isSynchronized()) {
134+
$service->setAttribute('synchronized', 'true');
135+
}
130136

131137
foreach ($definition->getTags() as $name => $tags) {
132138
foreach ($tags as $attributes) {
@@ -239,6 +245,9 @@ private function convertParameters($parameters, $type, \DOMElement $parent, $key
239245
} elseif ($behaviour == ContainerInterface::IGNORE_ON_INVALID_REFERENCE) {
240246
$element->setAttribute('on-invalid', 'ignore');
241247
}
248+
if (!$value->isStrict()) {
249+
$element->setAttribute('strict', 'false');
250+
}
242251
} elseif ($value instanceof Definition) {
243252
$element->setAttribute('type', 'service');
244253
$this->addService($value, null, $element);

Dumper/YamlDumper.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ private function addService($id, $definition)
9494
$code .= sprintf(" file: %s\n", $definition->getFile());
9595
}
9696

97+
if ($definition->isSynthetic()) {
98+
$code .= sprintf(" synthetic: true\n");
99+
}
100+
101+
if ($definition->isSynchronized()) {
102+
$code .= sprintf(" synchronized: true\n");
103+
}
104+
97105
if ($definition->getFactoryMethod()) {
98106
$code .= sprintf(" factory_method: %s\n", $definition->getFactoryMethod());
99107
}

Loader/XmlFileLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ private function parseDefinition($id, $service, $file)
148148
$definition = new Definition();
149149
}
150150

151-
foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'abstract') as $key) {
151+
foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'synchronized', 'abstract') as $key) {
152152
if (isset($service[$key])) {
153153
$method = 'set'.str_replace('-', '', $key);
154154
$definition->$method((string) $service->getAttributeAsPhp($key));

Loader/YamlFileLoader.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ private function parseDefinition($id, $service, $file)
153153
$definition->setSynthetic($service['synthetic']);
154154
}
155155

156+
if (isset($service['synchronized'])) {
157+
$definition->setSynchronized($service['synchronized']);
158+
}
159+
156160
if (isset($service['public'])) {
157161
$definition->setPublic($service['public']);
158162
}

Loader/schema/dic/services/services-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
<xsd:attribute name="scope" type="xsd:string" />
8787
<xsd:attribute name="public" type="boolean" />
8888
<xsd:attribute name="synthetic" type="boolean" />
89+
<xsd:attribute name="synchronized" type="boolean" />
8990
<xsd:attribute name="abstract" type="boolean" />
9091
<xsd:attribute name="factory-class" type="xsd:string" />
9192
<xsd:attribute name="factory-method" type="xsd:string" />

0 commit comments

Comments
 (0)