From ec1e7ca6ac7364efecf5cc1ac891089a6d38da49 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Feb 2013 21:42:01 +0100 Subject: [PATCH 1/9] [DependencyInjection] added a way to automatically update scoped services A service can now be marked as synchronized; when set, 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. --- .../DependencyInjection/Container.php | 19 +++-- .../DependencyInjection/ContainerBuilder.php | 74 +++++++++++++++---- .../DependencyInjection/Definition.php | 30 ++++++++ .../DependencyInjection/Dumper/PhpDumper.php | 59 ++++++++++++++- .../DependencyInjection/Dumper/XmlDumper.php | 9 +++ .../DependencyInjection/Dumper/YamlDumper.php | 8 ++ .../Loader/XmlFileLoader.php | 2 +- .../Loader/YamlFileLoader.php | 4 + .../schema/dic/services/services-1.0.xsd | 1 + .../Tests/ContainerBuilderTest.php | 47 ++++++++++++ .../Tests/DefinitionTest.php | 14 +++- .../Tests/Fixtures/containers/container9.php | 9 +++ .../Tests/Fixtures/graphviz/services9.dot | 3 + .../Tests/Fixtures/includes/classes.php | 5 ++ .../Tests/Fixtures/php/services9.php | 40 ++++++++++ .../Tests/Fixtures/php/services9_compiled.php | 40 ++++++++++ .../Tests/Fixtures/xml/services6.xml | 1 + .../Tests/Fixtures/xml/services9.xml | 6 ++ .../Tests/Fixtures/yaml/services6.yml | 4 + .../Tests/Fixtures/yaml/services9.yml | 9 +++ .../Tests/Loader/XmlFileLoaderTest.php | 3 + .../Tests/Loader/YamlFileLoaderTest.php | 3 + 22 files changed, 366 insertions(+), 24 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php index 8a2c1b3a1ed91..34e9a328767d0 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php @@ -206,6 +206,10 @@ public function set($id, $service, $scope = self::SCOPE_CONTAINER) } $this->services[$id] = $service; + + if (method_exists($this, $method = 'synchronize'.strtr($id, array('_' => '', '.' => '_')).'Service')) { + $this->$method(); + } } /** @@ -221,7 +225,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'); } /** @@ -247,7 +251,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)) { return $this->services[$id]; } @@ -263,7 +267,7 @@ 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]); } @@ -289,7 +293,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); } /** @@ -393,8 +397,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); + } + } } } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 5b178714bffee..39c6b0362e5b0 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -46,6 +46,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface */ private $definitions = array(); + /** + * @var Definition[] + */ + private $obsoleteDefinitions = array(); + /** * @var Alias[] */ @@ -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); + } } /** @@ -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())); @@ -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) + { + 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) + { + $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]))); + } } diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 359553a0bf469..9d52426121874 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -36,6 +36,7 @@ class Definition private $public; private $synthetic; private $abstract; + private $synchronized; protected $arguments; @@ -56,6 +57,7 @@ public function __construct($class = null, array $arguments = array()) $this->tags = array(); $this->public = true; $this->synthetic = false; + $this->synchronized = false; $this->abstract = false; $this->properties = array(); } @@ -569,6 +571,34 @@ public function isPublic() return $this->public; } + /** + * Sets the synchronized flag of this service. + * + * @param Boolean $boolean + * + * @return Definition The current instance + * + * @api + */ + public function setSynchronized($boolean) + { + $this->synchronized = (Boolean) $boolean; + + return $this; + } + + /** + * Whether this service is synchronized. + * + * @return Boolean + * + * @api + */ + public function isSynchronized() + { + return $this->synchronized; + } + /** * Sets whether this definition is synthetic, that is not constructed by the * container, but dynamically injected. diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 4d66d05c154be..52f80b656d8c8 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -567,7 +567,7 @@ protected function get{$name}Service() */ private function addServices() { - $publicServices = $privateServices = $aliasServices = ''; + $publicServices = $privateServices = $aliasServices = $synchronizers = ''; $definitions = $this->container->getDefinitions(); ksort($definitions); foreach ($definitions as $id => $definition) { @@ -576,6 +576,8 @@ private function addServices() } else { $privateServices .= $this->addService($id, $definition); } + + $synchronizers .= $this->addServiceSynchronizer($id, $definition); } $aliases = $this->container->getAliases(); @@ -584,7 +586,60 @@ private function addServices() $aliasServices .= $this->addServiceAlias($alias, $id); } - return $publicServices.$aliasServices.$privateServices; + return $publicServices.$aliasServices.$synchronizers.$privateServices; + } + + /** + * Adds synchronizer methods. + * + * @param string $id A service identifier + * @param Definition $definition A Definition instance + */ + private function addServiceSynchronizer($id, Definition $definition) + { + if (!$definition->isSynchronized()) { + return; + } + + $code = ''; + foreach ($this->container->getDefinitions() as $definitionId => $definition) { + foreach ($definition->getMethodCalls() as $call) { + foreach ($call[1] as $argument) { + if ($argument instanceof Reference && $id == (string) $argument) { + $arguments = array(); + foreach ($call[1] as $value) { + $arguments[] = $this->dumpValue($value); + } + + $call = $this->wrapServiceConditionals($call[1], sprintf("\$this->get('%s')->%s(%s);", $definitionId, $call[0], implode(', ', $arguments))); + + $code .= <<initialized('$definitionId')) { + $call + } + +EOF; + } + } + } + } + + if (!$code) { + return; + } + + $name = Container::camelize($id); + + return <<isPublic()) { $service->setAttribute('public', 'false'); } + if ($definition->isSynthetic()) { + $service->setAttribute('synthetic', 'true'); + } + if ($definition->isSynchronized()) { + $service->setAttribute('synchronized', 'true'); + } foreach ($definition->getTags() as $name => $tags) { foreach ($tags as $attributes) { @@ -239,6 +245,9 @@ private function convertParameters($parameters, $type, \DOMElement $parent, $key } elseif ($behaviour == ContainerInterface::IGNORE_ON_INVALID_REFERENCE) { $element->setAttribute('on-invalid', 'ignore'); } + if (!$value->isStrict()) { + $element->setAttribute('strict', 'false'); + } } elseif ($value instanceof Definition) { $element->setAttribute('type', 'service'); $this->addService($value, null, $element); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index e4e168257e4e6..98903bf77b5db 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -94,6 +94,14 @@ private function addService($id, $definition) $code .= sprintf(" file: %s\n", $definition->getFile()); } + if ($definition->isSynthetic()) { + $code .= sprintf(" synthetic: true\n"); + } + + if ($definition->isSynchronized()) { + $code .= sprintf(" synchronized: true\n"); + } + if ($definition->getFactoryMethod()) { $code .= sprintf(" factory_method: %s\n", $definition->getFactoryMethod()); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index f925f78ab27e1..85898d3d3fe86 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -148,7 +148,7 @@ private function parseDefinition($id, $service, $file) $definition = new Definition(); } - foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'abstract') as $key) { + foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'synchronized', 'abstract') as $key) { if (isset($service[$key])) { $method = 'set'.str_replace('-', '', $key); $definition->$method((string) $service->getAttributeAsPhp($key)); diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 71de4ac6592f6..423ca15d45184 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -153,6 +153,10 @@ private function parseDefinition($id, $service, $file) $definition->setSynthetic($service['synthetic']); } + if (isset($service['synchronized'])) { + $definition->setSynchronized($service['synchronized']); + } + if (isset($service['public'])) { $definition->setPublic($service['public']); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index 316f2d7596870..4d9addcd971f8 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -86,6 +86,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index bfd92798dc4c1..af0181ecc1f1b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\Scope; use Symfony\Component\Config\Resource\FileResource; class ContainerBuilderTest extends \PHPUnit_Framework_TestCase @@ -577,6 +578,52 @@ public function testNoExceptionWhenSetSyntheticServiceOnAFrozenContainer() $this->assertEquals($a, $container->get('a')); } + public function testSetOnSynchronizedService() + { + $container = new ContainerBuilder(); + $container->register('baz', 'BazClass') + ->setSynchronized(true) + ; + $container->register('bar', 'BarClass') + ->addMethodCall('setBaz', array(new Reference('baz'))) + ; + + $container->set('baz', $baz = new \BazClass()); + $this->assertSame($baz, $container->get('bar')->getBaz()); + + $container->set('baz', $baz = new \BazClass()); + $this->assertSame($baz, $container->get('bar')->getBaz()); + } + + public function testSynchronizedServiceWithScopes() + { + $container = new ContainerBuilder(); + $container->addScope(new Scope('foo')); + $container->register('baz', 'BazClass') + ->setSynthetic(true) + ->setSynchronized(true) + ->setScope('foo') + ; + $container->register('bar', 'BarClass') + ->addMethodCall('setBaz', array(new Reference('baz', ContainerInterface::NULL_ON_INVALID_REFERENCE, false))) + ; + $container->compile(); + + $container->enterScope('foo'); + $container->set('baz', $outerBaz = new \BazClass(), 'foo'); + $this->assertSame($outerBaz, $container->get('bar')->getBaz()); + + $container->enterScope('foo'); + $container->set('baz', $innerBaz = new \BazClass(), 'foo'); + $this->assertSame($innerBaz, $container->get('bar')->getBaz()); + $container->leaveScope('foo'); + + $this->assertNotSame($innerBaz, $container->get('bar')->getBaz()); + $this->assertSame($outerBaz, $container->get('bar')->getBaz()); + + $container->leaveScope('foo'); + } + /** * @expectedException BadMethodCallException */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php index 89f7ae1ea0ad0..d9a4282efefbc 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php @@ -149,7 +149,19 @@ public function testSetIsSynthetic() $def = new Definition('stdClass'); $this->assertFalse($def->isSynthetic(), '->isSynthetic() returns false by default'); $this->assertSame($def, $def->setSynthetic(true), '->setSynthetic() implements a fluent interface'); - $this->assertTrue($def->isSynthetic(), '->isSynthetic() returns true if the instance must not be public.'); + $this->assertTrue($def->isSynthetic(), '->isSynthetic() returns true if the service is synthetic.'); + } + + /** + * @covers Symfony\Component\DependencyInjection\Definition::setSynchronized + * @covers Symfony\Component\DependencyInjection\Definition::isSynchronized + */ + public function testSetIsSynchronized() + { + $def = new Definition('stdClass'); + $this->assertFalse($def->isSynchronized(), '->isSynchronized() returns false by default'); + $this->assertSame($def, $def->setSynchronized(true), '->setSynchronized() implements a fluent interface'); + $this->assertTrue($def->isSynchronized(), '->isSynchronized() returns true if the service is synchronized.'); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php index 71d29f209bfcb..6ba3ad349d5d8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php @@ -71,5 +71,14 @@ ->register('baz', 'Baz') ->addMethodCall('setFoo', array(new Reference('foo_with_inline'))) ; +$container + ->register('request', 'Request') + ->setSynthetic(true) + ->setSynchronized(true) +; +$container + ->register('depends_on_request', 'stdClass') + ->addMethodCall('setRequest', array(new Reference('request', ContainerInterface::NULL_ON_INVALID_REFERENCE, false))) +; return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot index 73608e27fc912..cb006641d0b13 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot @@ -12,6 +12,8 @@ digraph sc { node_foo_with_inline [label="foo_with_inline\nFoo\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_inlined [label="inlined\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_baz [label="baz\nBaz\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_request [label="request\nRequest\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_depends_on_request [label="depends_on_request\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; node_foo2 [label="foo2\n\n", shape=record, fillcolor="#ff9999", style="filled"]; node_foo3 [label="foo3\n\n", shape=record, fillcolor="#ff9999", style="filled"]; @@ -28,4 +30,5 @@ digraph sc { node_foo_with_inline -> node_inlined [label="setBar()" style="dashed"]; node_inlined -> node_baz [label="setBaz()" style="dashed"]; node_baz -> node_foo_with_inline [label="setFoo()" style="dashed"]; + node_depends_on_request -> node_request [label="setRequest()" style="dashed"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php index fb6d4cf19d5c5..48e1c2c84e12d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php @@ -13,6 +13,11 @@ public function setBaz(BazClass $baz) { $this->baz = $baz; } + + public function getBaz() + { + return $this->baz; + } } class BazClass diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php index 324b77f66c7fb..4aad5e6e9e506 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php @@ -60,6 +60,23 @@ protected function getBazService() return $instance; } + /** + * Gets the 'depends_on_request' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getDependsOnRequestService() + { + $this->services['depends_on_request'] = $instance = new \stdClass(); + + $instance->setRequest($this->get('request', ContainerInterface::NULL_ON_INVALID_REFERENCE)); + + return $instance; + } + /** * Gets the 'factory_service' service. * @@ -168,6 +185,19 @@ protected function getMethodCall1Service() return $instance; } + /** + * Gets the 'request' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @throws RuntimeException always since this service is expected to be injected dynamically + */ + protected function getRequestService() + { + throw new RuntimeException('You have requested a synthetic service ("request"). The DIC does not know how to construct this service.'); + } + /** * Gets the alias_for_foo service alias. * @@ -178,6 +208,16 @@ protected function getAliasForFooService() return $this->get('foo'); } + /** + * Updates the 'request' service. + */ + protected function synchronizeRequestService() + { + if ($this->initialized('depends_on_request')) { + $this->get('depends_on_request')->setRequest($this->get('request', ContainerInterface::NULL_ON_INVALID_REFERENCE)); + } + } + /** * Gets the 'inlined' service. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php index 5c71db9e66aba..0002845a46eb6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php @@ -69,6 +69,23 @@ protected function getBazService() return $instance; } + /** + * Gets the 'depends_on_request' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getDependsOnRequestService() + { + $this->services['depends_on_request'] = $instance = new \stdClass(); + + $instance->setRequest($this->get('request')); + + return $instance; + } + /** * Gets the 'factory_service' service. * @@ -174,6 +191,19 @@ protected function getMethodCall1Service() return $instance; } + /** + * Gets the 'request' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @throws RuntimeException always since this service is expected to be injected dynamically + */ + protected function getRequestService() + { + throw new RuntimeException('You have requested a synthetic service ("request"). The DIC does not know how to construct this service.'); + } + /** * Gets the alias_for_foo service alias. * @@ -184,6 +214,16 @@ protected function getAliasForFooService() return $this->get('foo'); } + /** + * Updates the 'request' service. + */ + protected function synchronizeRequestService() + { + if ($this->initialized('depends_on_request')) { + $this->get('depends_on_request')->setRequest($this->get('request')); + } + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml index 45bc042f61906..4d2aa3d79ae24 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml @@ -46,5 +46,6 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml index 58eb6d79a3c1e..d04d492e8d824 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml @@ -67,6 +67,12 @@ + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml index eaa52bda62065..820c364a06556 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml @@ -24,3 +24,7 @@ services: alias: foo public: false factory_service: { class: BazClass, factory_method: getInstance, factory_service: baz_factory } + request: + class: Request + synthetic: true + synchronized: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml index fcbb83f4ae7e1..3d9c6417812f1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml @@ -57,4 +57,13 @@ services: calls: - [setFoo, ['@foo_with_inline']] + request: + class: Request + synthetic: true + synchronized: true + depends_on_request: + class: stdClass + calls: + - [setRequest, ['@?request']] + alias_for_foo: @foo diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 0287a1b0aa334..b355f0ac215c3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -185,6 +185,9 @@ public function testLoadServices() $this->assertEquals('getInstance', $services['factory_service']->getFactoryMethod()); $this->assertEquals('baz_factory', $services['factory_service']->getFactoryService()); + $this->assertTrue($services['request']->isSynthetic(), '->load() parses the synthetic flag'); + $this->assertTrue($services['request']->isSynchronized(), '->load() parses the synchronized flag'); + $aliases = $container->getAliases(); $this->assertTrue(isset($aliases['alias_for_foo']), '->load() parses elements'); $this->assertEquals('foo', (string) $aliases['alias_for_foo'], '->load() parses aliases'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index e655d3bd8981a..1c700df7b5b9d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -128,6 +128,9 @@ public function testLoadServices() $this->assertEquals(array(array('setBar', array('foo', new Reference('foo'), array(true, false)))), $services['method_call2']->getMethodCalls(), '->load() parses the method_call tag'); $this->assertEquals('baz_factory', $services['factory_service']->getFactoryService()); + $this->assertTrue($services['request']->isSynthetic(), '->load() parses the synthetic flag'); + $this->assertTrue($services['request']->isSynchronized(), '->load() parses the synchronized flag'); + $aliases = $container->getAliases(); $this->assertTrue(isset($aliases['alias_for_foo']), '->load() parses aliases'); $this->assertEquals('foo', (string) $aliases['alias_for_foo'], '->load() parses aliases'); From 2ffcfb98c870f1d88fcfc1e403e56613400394a9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Feb 2013 22:18:13 +0100 Subject: [PATCH 2/9] [FrameworkBundle] made the Request service synchronized This change allows any service to depend on the Request (via a method call) and always have the right Request instance without the need for the service to be in the request scope (you still need to set the Request reference as non-strict). --- .../Bundle/FrameworkBundle/Resources/config/services.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index fbddc0e07fbfd..c1c28c0fa0939 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -40,7 +40,7 @@ This service definition only defines the scope of the request. It is used to check references scope. --> - + From 0892135f4597d3c52959d5e8b98e94e1e2649d67 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Feb 2013 22:23:08 +0100 Subject: [PATCH 3/9] [HttpKernel] ensured that the Request is null when outside of the Request scope --- .../ContainerAwareHttpKernel.php | 2 ++ .../ContainerAwareHttpKernelTest.php | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php index 20b4a5e75e9ad..0aae26b483504 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php @@ -56,11 +56,13 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ try { $response = parent::handle($request, $type, $catch); } catch (\Exception $e) { + $this->container->set('request', null, 'request'); $this->container->leaveScope('request'); throw $e; } + $this->container->set('request', null, 'request'); $this->container->leaveScope('request'); return $response; diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php index 80d5ffa61a664..6da06c01d1544 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Tests; +namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel; @@ -54,10 +54,15 @@ public function testHandle($type) ->with($this->equalTo('request')) ; $container - ->expects($this->once()) + ->expects($this->at(1)) ->method('set') ->with($this->equalTo('request'), $this->equalTo($request), $this->equalTo('request')) ; + $container + ->expects($this->at(2)) + ->method('set') + ->with($this->equalTo('request'), $this->equalTo(null), $this->equalTo('request')) + ; $dispatcher = new EventDispatcher(); $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); @@ -101,10 +106,15 @@ public function testHandleRestoresThePreviousRequestOnException($type) ->with($this->equalTo('request')) ; $container - ->expects($this->once()) + ->expects($this->at(1)) ->method('set') ->with($this->equalTo('request'), $this->equalTo($request), $this->equalTo('request')) ; + $container + ->expects($this->at(2)) + ->method('set') + ->with($this->equalTo('request'), $this->equalTo(null), $this->equalTo('request')) + ; $dispatcher = new EventDispatcher(); $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); From a7b2b7e92b5ce3e234464d47ee3b5cbfa41a55ed Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Feb 2013 22:20:41 +0100 Subject: [PATCH 4/9] fixed Request management for RequestListener --- .../Resources/config/routing.xml | 1 + .../EventListener/RouterListener.php | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 6d6671b7ae33f..9e21db4519151 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -94,6 +94,7 @@ + diff --git a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php index 606b358e72f04..58ea6f0a5436d 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php @@ -23,6 +23,7 @@ use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RequestContextAwareInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; /** * Initializes the context from the request and sets request attributes based on a matching route. @@ -59,12 +60,31 @@ public function __construct($matcher, RequestContext $context = null, LoggerInte $this->logger = $logger; } + /** + * Sets the current Request. + * + * The application should call this method whenever the Request + * object changes (entering a Request scope for instance, but + * also when leaving a Request scope -- especially when they are + * nested). + * + * @param Request|null $request A Request instance + */ + public function setRequest(Request $request = null) + { + if (null !== $request) { + $this->context->fromRequest($request); + } + } + public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); // initialize the context that is also used by the generator (assuming matcher and generator share the same context instance) - $this->context->fromRequest($request); + // we call setRequest even if most of the time, it has already been done to keep compatibility + // with frameworks which do not use the Symfony service container + $this->setRequest($request); if ($request->attributes->has('_controller')) { // routing is already done From 1b98ad34ff28e1318395c070099ddbb22af1163f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Feb 2013 22:21:56 +0100 Subject: [PATCH 5/9] fixed Request management for LocaleListener --- .../FrameworkBundle/Resources/config/web.xml | 1 + .../EventListener/LocaleListener.php | 33 +++++++------------ 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index d34ecdf3c3727..177821a5afb24 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -38,6 +38,7 @@ %kernel.default_locale% + diff --git a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php index f3cb804832660..0b864c02f2bc4 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RequestContextAwareInterface; @@ -27,7 +26,6 @@ class LocaleListener implements EventSubscriberInterface { private $router; private $defaultLocale; - private $locales = array(); public function __construct($defaultLocale = 'en', RequestContextAwareInterface $router = null) { @@ -35,24 +33,27 @@ public function __construct($defaultLocale = 'en', RequestContextAwareInterface $this->router = $router; } - public function onKernelResponse(FilterResponseEvent $event) + public function setRequest(Request $request = null) { - array_shift($this->locales); + if (null === $request) { + return; + } - // setting back the locale to the previous value - $locale = isset($this->locales[0]) ? $this->locales[0] : $this->defaultLocale; - $request = $event->getRequest(); - $this->setLocale($request, $locale); + if ($locale = $request->attributes->get('_locale')) { + $request->setLocale($locale); + } + + if (null !== $this->router) { + $this->router->getContext()->setParameter('_locale', $request->getLocale()); + } } public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); - $request->setDefaultLocale($this->defaultLocale); - $this->setLocale($request, $request->attributes->get('_locale', $this->defaultLocale)); - array_unshift($this->locales, $request->getLocale()); + $this->setRequest($request); } public static function getSubscribedEvents() @@ -60,16 +61,6 @@ public static function getSubscribedEvents() return array( // must be registered after the Router to have access to the _locale KernelEvents::REQUEST => array(array('onKernelRequest', 16)), - KernelEvents::RESPONSE => 'onKernelResponse', ); } - - private function setLocale(Request $request, $locale) - { - $request->setLocale($locale); - - if (null !== $this->router) { - $this->router->getContext()->setParameter('_locale', $request->getLocale()); - } - } } From ff9d6883bb0ec83af73d9b85b52ef9f6a9806091 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Feb 2013 22:23:26 +0100 Subject: [PATCH 6/9] fixed Request management for FragmentHandler --- .../Extension/HttpKernelExtensionTest.php | 10 +---- .../Resources/config/fragment_renderer.xml | 2 +- .../HttpKernel/Fragment/FragmentHandler.php | 39 ++++--------------- .../Tests/Fragment/FragmentHandlerTest.php | 16 +------- 4 files changed, 11 insertions(+), 56 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index c0dc42d2f35f0..8e5e4d1d4756b 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -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; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml index cbe3db3257b6e..662f042adeeac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml @@ -14,9 +14,9 @@ - %kernel.debug% + diff --git a/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php b/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php index b4f3f9c1eef47..70de8d0e2bdd2 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php +++ b/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php @@ -15,10 +15,6 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Controller\ControllerReference; -use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Renders a URI that represents a resource fragment. @@ -30,11 +26,11 @@ * * @see FragmentRendererInterface */ -class FragmentHandler implements EventSubscriberInterface +class FragmentHandler { private $debug; private $renderers; - private $requests; + private $request; /** * Constructor. @@ -49,7 +45,6 @@ public function __construct(array $renderers = array(), $debug = false) $this->addRenderer($renderer); } $this->debug = $debug; - $this->requests = array(); } /** @@ -63,23 +58,13 @@ public function addRenderer(FragmentRendererInterface $renderer) } /** - * Stores the Request object. + * Sets the current Request. * - * @param GetResponseEvent $event A GetResponseEvent instance + * @param Request $request The current Request */ - public function onKernelRequest(GetResponseEvent $event) + public function setRequest(Request $request = null) { - array_unshift($this->requests, $event->getRequest()); - } - - /** - * Removes the most recent Request object. - * - * @param FilterResponseEvent $event A FilterResponseEvent instance - */ - public function onKernelResponse(FilterResponseEvent $event) - { - array_shift($this->requests); + $this->request = $request; } /** @@ -108,7 +93,7 @@ public function render($uri, $renderer = 'inline', array $options = array()) throw new \InvalidArgumentException(sprintf('The "%s" renderer does not exist.', $renderer)); } - return $this->deliver($this->renderers[$renderer]->render($uri, $this->requests[0], $options)); + return $this->deliver($this->renderers[$renderer]->render($uri, $this->request, $options)); } /** @@ -126,7 +111,7 @@ public function render($uri, $renderer = 'inline', array $options = array()) protected function deliver(Response $response) { if (!$response->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $this->requests[0]->getUri(), $response->getStatusCode())); + throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $this->request->getUri(), $response->getStatusCode())); } if (!$response instanceof StreamedResponse) { @@ -136,14 +121,6 @@ protected function deliver(Response $response) $response->sendContent(); } - public static function getSubscribedEvents() - { - return array( - KernelEvents::REQUEST => 'onKernelRequest', - KernelEvents::RESPONSE => 'onKernelResponse', - ); - } - // to be removed in 2.3 public function fixOptions(array $options) { diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php index e0a5b0ad59342..f2a0838a46424 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php @@ -17,13 +17,6 @@ class FragmentHandlerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - } - /** * @expectedException \InvalidArgumentException */ @@ -102,14 +95,7 @@ protected function getHandler($returnValue, $arguments = array()) $handler = new FragmentHandler(); $handler->addRenderer($renderer); - - $event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock(); - $event - ->expects($this->once()) - ->method('getRequest') - ->will($this->returnValue(Request::create('/'))) - ; - $handler->onKernelRequest($event); + $handler->setRequest(Request::create('/')); return $handler; } From 5d7b8356ab7799cc47a6874a5fe2e14ec6ac160e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Feb 2013 22:24:20 +0100 Subject: [PATCH 7/9] [FrameworkBundle] added some functional tests --- .../Controller/SubRequestController.php | 66 +++++++++++++++++++ .../TestBundle/Resources/config/routing.yml | 15 +++++ .../Tests/Functional/SubRequestsTest.php | 26 ++++++++ 3 files changed, 107 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SubRequestController.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SubRequestsTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SubRequestController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SubRequestController.php new file mode 100644 index 0000000000000..14d002642ba81 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SubRequestController.php @@ -0,0 +1,66 @@ + + * + * 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); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml index d9b380ef8dbd1..6a9f5a8f8be47 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml @@ -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] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SubRequestsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SubRequestsTest.php new file mode 100644 index 0000000000000..2676653b1ef6d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SubRequestsTest.php @@ -0,0 +1,26 @@ + + * + * 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()); + } +} From bb83b3ea43f04f3d2040f19cf4df811b502b295a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 20 Mar 2013 16:45:42 +0100 Subject: [PATCH 8/9] [HttpKernel] added a safeguard for when a fragment is rendered outside the context of a master request --- src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php b/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php index 70de8d0e2bdd2..54d0a70b7dbec 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php +++ b/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php @@ -93,6 +93,10 @@ public function render($uri, $renderer = 'inline', array $options = array()) throw new \InvalidArgumentException(sprintf('The "%s" renderer does not exist.', $renderer)); } + if (null === $this->request) { + throw new \LogicException('Rendering a fragment can only be done when handling a master Request.'); + } + return $this->deliver($this->renderers[$renderer]->render($uri, $this->request, $options)); } From 17269e137d9f878d43946c99b8d8328fb20710d0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 21 Mar 2013 17:34:16 +0100 Subject: [PATCH 9/9] [DependencyInjection] fixed management of scoped services with an invalid behavior set to null The optimization for references has been removed as it does not take scopes into account. --- .../Compiler/ResolveInvalidReferencesPass.php | 4 +--- .../Component/DependencyInjection/Container.php | 5 +++++ .../DependencyInjection/Tests/ContainerTest.php | 17 ++++++++++++++++- .../Tests/Fixtures/php/services9_compiled.php | 4 ++-- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php index 6fad9a2841651..93d5806036bde 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php @@ -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) { diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php index 34e9a328767d0..be72e8fc686a5 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php @@ -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; @@ -271,6 +272,10 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE unset($this->services[$id]); } + if ($e instanceof InactiveScopeException && self::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) { + return null; + } + throw $e; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php index 23c13c5094cb5..726af13904cc7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\Exception\InactiveScopeException; class ContainerTest extends \PHPUnit_Framework_TestCase { @@ -98,7 +99,7 @@ public function testGetServiceIds() $this->assertEquals(array('service_container', 'foo', 'bar'), $sc->getServiceIds(), '->getServiceIds() returns all defined service ids'); $sc = new ProjectServiceContainer(); - $this->assertEquals(array('scoped', 'scoped_foo', 'bar', 'foo_bar', 'foo.baz', 'circular', 'throw_exception', 'throws_exception_on_service_configuration', 'service_container'), $sc->getServiceIds(), '->getServiceIds() returns defined service ids by getXXXService() methods'); + $this->assertEquals(array('scoped', 'scoped_foo', 'inactive', 'bar', 'foo_bar', 'foo.baz', 'circular', 'throw_exception', 'throws_exception_on_service_configuration', 'service_container'), $sc->getServiceIds(), '->getServiceIds() returns defined service ids by getXXXService() methods'); } /** @@ -179,6 +180,15 @@ public function testGetCircularReference() } } + /** + * @covers Symfony\Component\DependencyInjection\Container::get + */ + public function testGetReturnsNullOnInactiveScope() + { + $sc = new ProjectServiceContainer(); + $this->assertNull($sc->get('inactive', ContainerInterface::NULL_ON_INVALID_REFERENCE)); + } + /** * @covers Symfony\Component\DependencyInjection\Container::has */ @@ -476,6 +486,11 @@ protected function getScopedFooService() return $this->services['scoped_foo'] = $this->scopedServices['foo']['scoped_foo'] = new \stdClass(); } + protected function getInactiveService() + { + throw new InactiveScopeException('request', 'request'); + } + protected function getBarService() { return $this->__bar; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php index 0002845a46eb6..2deea119f47fc 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php @@ -81,7 +81,7 @@ protected function getDependsOnRequestService() { $this->services['depends_on_request'] = $instance = new \stdClass(); - $instance->setRequest($this->get('request')); + $instance->setRequest($this->get('request', ContainerInterface::NULL_ON_INVALID_REFERENCE)); return $instance; } @@ -220,7 +220,7 @@ protected function getAliasForFooService() protected function synchronizeRequestService() { if ($this->initialized('depends_on_request')) { - $this->get('depends_on_request')->setRequest($this->get('request')); + $this->get('depends_on_request')->setRequest($this->get('request', ContainerInterface::NULL_ON_INVALID_REFERENCE)); } }