Skip to content

[2.3] [WIP] Synchronized services... #7007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 23, 2013
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,8 @@ protected function getFragmentHandler($return)
$strategy->expects($this->once())->method('getName')->will($this->returnValue('inline'));
$strategy->expects($this->once())->method('render')->will($return);

// simulate a master request
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();
$event
->expects($this->once())
->method('getRequest')
->will($this->returnValue(Request::create('/')))
;

$renderer = new FragmentHandler(array($strategy));
$renderer->onKernelRequest($event);
$renderer->setRequest(Request::create('/'));

return $renderer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

<services>
<service id="fragment.handler" class="%fragment.handler.class%">
<tag name="kernel.event_subscriber" />
<argument type="collection" />
<argument>%kernel.debug%</argument>
<call method="setRequest"><argument type="service" id="request" on-invalid="null" strict="false" /></call>
Copy link
Member

Choose a reason for hiding this comment

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

wouldn't this break the retrieval of the service in the CLI (and so the retrieval of Twig) ? AFAIK, on-invalid="null" is about missing services, not about scope issues as $container->has('request') is always true even if request is only available in the request scope.

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right.

Copy link
Member Author

Choose a reason for hiding this comment

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

#6140 could be useful to fix this.

Copy link
Member

Choose a reason for hiding this comment

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

@fabpot IMO, has should be fixed to return false if the service is scoped and the scope is inactive. Currently, code wanting to check that it can get the service has to know that it is scoped to the request: https://github.com/symfony/symfony/blob/2.2/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php#L70

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO, 29e7b8a fixes the issue quite nicely (part of this commit is actually another fix that I've submitted as another PR -- #7450).

Copy link
Member

Choose a reason for hiding this comment

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

@fabpot what about oninvalid="ignore" ?

Copy link
Member Author

Choose a reason for hiding this comment

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

That would not work as it uses has to determine of the service is available or not.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

That's not a problem when using the dumper as it just means that when you use ignore, there is one more check. If not, I've slightly changed my patch to take care of that.

</service>

<service id="fragment.renderer.inline" class="%fragment.renderer.inline.class%">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
<argument type="service" id="router" />
<argument type="service" id="router.request_context" on-invalid="ignore" />
<argument type="service" id="logger" on-invalid="ignore" />
<call method="setRequest"><argument type="service" id="request" on-invalid="null" strict="false" /></call>
</service>
</services>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
This service definition only defines the scope of the request.
It is used to check references scope.
-->
<service id="request" scope="request" synthetic="true" />
<service id="request" scope="request" synthetic="true" synchronized="true" />

<service id="service_container" synthetic="true" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<tag name="kernel.event_subscriber" />
<argument>%kernel.default_locale%</argument>
<argument type="service" id="router" on-invalid="ignore" />
<call method="setRequest"><argument type="service" id="request" on-invalid="null" strict="false" /></call>
</service>
</services>
</container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?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\Tests\Functional\Bundle\TestBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpKernel\Controller\ControllerReference;

class SubRequestController extends ContainerAware
{
public function indexAction()
{
$handler = $this->container->get('fragment.handler');

$errorUrl = $this->generateUrl('subrequest_fragment_error', array('_locale' => 'fr', '_format' => 'json'));
$altUrl = $this->generateUrl('subrequest_fragment', array('_locale' => 'fr', '_format' => 'json'));

// simulates a failure during the rendering of a fragment...
// should render fr/json
$content = $handler->render($errorUrl, 'inline', array('alt' => $altUrl));

// ...to check that the FragmentListener still references the right Request
// when rendering another fragment after the error occured
// should render en/html instead of fr/json
$content .= $handler->render(new ControllerReference('TestBundle:SubRequest:fragment'));

// forces the LocaleListener to set fr for the locale...
// should render fr/json
$content .= $handler->render($altUrl);

// ...and check that after the rendering, the original Request is back
// and en is used as a locale
// should use en/html instead of fr/json
$content .= '--'.$this->generateUrl('subrequest_fragment');

// The RouterListener is also tested as if it does not keep the right
// Request in the context, a 301 would be generated

return new Response($content);
}

public function fragmentAction(Request $request)
{
return new Response('--'.$request->getLocale().'/'.$request->getRequestFormat());
}

public function fragmentErrorAction()
{
throw new \RuntimeException('error');
}

protected function generateUrl($name, $arguments = array())
{
return $this->container->get('router')->generate($name, $arguments);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,18 @@ session_showflash:
profiler:
path: /profiler
defaults: { _controller: TestBundle:Profiler:index }

subrequest_index:
path: /subrequest/{_locale}.{_format}
defaults: { _controller: TestBundle:SubRequest:index, _format: "html" }
schemes: [https]

subrequest_fragment_error:
path: /subrequest/fragment/error/{_locale}.{_format}
defaults: { _controller: TestBundle:SubRequest:fragmentError, _format: "html" }
schemes: [http]

subrequest_fragment:
path: /subrequest/fragment/{_locale}.{_format}
defaults: { _controller: TestBundle:SubRequest:fragment, _format: "html" }
schemes: [http]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Tests\Functional;

/**
* @group functional
*/
class SubRequestsTest extends WebTestCase
{
public function testStateAfterSubRequest()
{
$client = $this->createClient(array('test_case' => 'Session', 'root_config' => 'config.yml'));
$client->request('GET', 'https://localhost/subrequest/en');

$this->assertEquals('--fr/json--en/html--fr/json--http://localhost/subrequest/fragment/en', $client->getResponse()->getContent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,7 @@ private function processArguments(array $arguments, $inMethodCall = false)
$exists = $this->container->has($id);

// resolve invalid behavior
if ($exists && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) {
$arguments[$k] = new Reference($id, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $argument->isStrict());
} elseif (!$exists && ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
if (!$exists && ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
$arguments[$k] = null;
} elseif (!$exists && ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) {
if ($inMethodCall) {
Expand Down
24 changes: 18 additions & 6 deletions src/Symfony/Component/DependencyInjection/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Exception\InactiveScopeException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
Expand Down Expand Up @@ -206,6 +207,10 @@ public function set($id, $service, $scope = self::SCOPE_CONTAINER)
}

$this->services[$id] = $service;

if (method_exists($this, $method = 'synchronize'.strtr($id, array('_' => '', '.' => '_')).'Service')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Mixing the assignment into the method_exists() call like that makes the code quite hard to read. Can we break it out to a separate $method = line and then the if() statement? That would improve readability considerably.

$this->$method();
}
}

/**
Expand All @@ -221,7 +226,7 @@ public function has($id)
{
$id = strtolower($id);

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

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

if (isset($this->services[$id])) {
if (array_key_exists($id, $this->services)) {
Copy link
Member

Choose a reason for hiding this comment

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

why switching everything to array_key_exists ? Is it possible to have null as a service ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, when you leave the request scope, the Request is null.

Copy link
Member

Choose a reason for hiding this comment

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

Couldn't we use false or 0 or something that doesn't trip up isset()? As far as I can remember, services can only be objects, so it should be safe. But not sure if this code is sensitive enough that it is worth it.

Copy link
Member Author

Choose a reason for hiding this comment

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

No we can't as the value is used in the user code:

function setRequest($request = null)
{
}

Copy link
Member

Choose a reason for hiding this comment

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

Well we could pass null if the current value is 0/false, but the question is whether isset vs array_key_exists is worth the hackery in the code or not.

return $this->services[$id];
}

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

if (isset($this->services[$id])) {
if (array_key_exists($id, $this->services)) {
unset($this->services[$id]);
}

if ($e instanceof InactiveScopeException && self::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) {
return null;
}

throw $e;
}

Expand All @@ -289,7 +298,7 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE
*/
public function initialized($id)
{
return isset($this->services[strtolower($id)]);
return array_key_exists(strtolower($id), $this->services);
}

/**
Expand Down Expand Up @@ -393,8 +402,11 @@ public function leaveScope($name)
$services = $this->scopeStacks[$name]->pop();
$this->scopedServices += $services;

array_unshift($services, $this->services);
$this->services = call_user_func_array('array_merge', $services);
foreach ($services as $array) {
foreach ($array as $id => $service) {
$this->set($id, $service, $name);
}
}
}
}

Expand Down
74 changes: 60 additions & 14 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/
private $definitions = array();

/**
* @var Definition[]
*/
private $obsoleteDefinitions = array();

/**
* @var Alias[]
*/
Expand Down Expand Up @@ -351,14 +356,28 @@ public function set($id, $service, $scope = self::SCOPE_CONTAINER)

if ($this->isFrozen()) {
// setting a synthetic service on a frozen container is alright
if (!isset($this->definitions[$id]) || !$this->definitions[$id]->isSynthetic()) {
if (
(!isset($this->definitions[$id]) && !isset($this->obsoleteDefinitions[$id]))
||
(isset($this->definitions[$id]) && !$this->definitions[$id]->isSynthetic())
||
(isset($this->obsoleteDefinitions[$id]) && !$this->obsoleteDefinitions[$id]->isSynthetic())
) {
throw new BadMethodCallException(sprintf('Setting service "%s" on a frozen container is not allowed.', $id));
}
}

if (isset($this->definitions[$id])) {
$this->obsoleteDefinitions[$id] = $this->definitions[$id];
}

unset($this->definitions[$id], $this->aliases[$id]);

parent::set($id, $service, $scope);

if (isset($this->obsoleteDefinitions[$id]) && $this->obsoleteDefinitions[$id]->isSynchronized()) {
$this->synchronize($id);
}
}

/**
Expand Down Expand Up @@ -885,19 +904,7 @@ private function createService(Definition $definition, $id)
}

foreach ($definition->getMethodCalls() as $call) {
$services = self::getServiceConditionals($call[1]);

$ok = true;
foreach ($services as $s) {
if (!$this->has($s)) {
$ok = false;
break;
}
}

if ($ok) {
call_user_func_array(array($service, $call[0]), $this->resolveServices($parameterBag->resolveValue($call[1])));
}
$this->callMethod($service, $call);
}

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

return $services;
}

/**
* Synchronizes a service change.
*
* This method updates all services that depend on the given
* service by calling all methods referencing it.
*
* @param string $id A service id
*/
private function synchronize($id)
Copy link
Contributor

Choose a reason for hiding this comment

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

This method feels like it could get quite slow. Once you have a decent number of services initialized it's going to result in a lot of iterations, even if only a scant few of them result in a method call. Could we add a tracking index of some kind to keep track of what methods would need to get called on what services?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, or maybe the PhpDumper is handling that? I don't understand the dumper code well enough to understand if it is. If the dumped code is more optimized than this and would not iterate over all services every time a synchronized service changed, that's good enough, I think. If it would still use this method, then this method should be optimized.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's the case, this method is not called when using a dumped container (a dumper container extends Container and not ContainerBuilder). Here is an example of a dumped code (see the unit tests below):

    protected function synchronizeRequestService()
    {
        if ($this->initialized('depends_on_request')) {
            $this->get('depends_on_request')->setRequest($this->get('request', ContainerInterface::NULL_ON_INVALID_REFERENCE));
        }
    }

And for services without the syncrhonized flag, the method does not even exist.

{
foreach ($this->definitions as $definitionId => $definition) {
// only check initialized services
if (!$this->initialized($definitionId)) {
continue;
}

foreach ($definition->getMethodCalls() as $call) {
foreach ($call[1] as $argument) {
if ($argument instanceof Reference && $id == (string) $argument) {
$this->callMethod($this->get($definitionId), $call);
}
}
}
}
}

private function callMethod($service, $call)
Copy link
Contributor

Choose a reason for hiding this comment

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

The PHPDoc is missing here

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think private methods require a PHPDoc block in the symfony2 coding standards, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO all methods should have a PHPDoc block, regardless of their scope. Someone is going to read this code and wonder what those variables are supposed to be.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Crell I agree whole-heartedly, but I didn't think private methods needed them (think I'm wrong though according to the standards mentioned here -- http://symfony.com/doc/current/contributing/code/standards.html#documentation)

{
$services = self::getServiceConditionals($call[1]);

foreach ($services as $s) {
if (!$this->has($s)) {
return;
}
}

call_user_func_array(array($service, $call[0]), $this->resolveServices($this->getParameterBag()->resolveValue($call[1])));
}
}
Loading