diff --git a/composer.json b/composer.json index 504b48d92c76a..1e3fdae52142f 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,8 @@ "doctrine/orm": "~2.2,>=2.2.3", "monolog/monolog": "~1.3", "propel/propel1": "1.6.*", - "ircmaxell/password-compat": "1.0.*" + "ircmaxell/password-compat": "1.0.*", + "ocramius/proxy-manager": ">=0.3.1,<0.4-dev" }, "autoload": { "psr-0": { "Symfony\\": "src/" }, diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 5d8d0d1a10f15..654c42b302b3f 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -11,6 +11,10 @@ namespace Symfony\Component\DependencyInjection; +use ProxyManager\Configuration; +use ProxyManager\Factory\LazyLoadingValueHolderFactory; +use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy; +use ProxyManager\Proxy\LazyLoadingInterface; use Symfony\Component\DependencyInjection\Compiler\Compiler; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; @@ -222,15 +226,32 @@ public function setResources(array $resources) * @api */ public function addObjectResource($object) + { + if ($this->trackResources) { + $this->addClassResource(new \ReflectionClass($object)); + } + + return $this; + } + + /** + * Adds the given class hierarchy as resources. + * + * @param \ReflectionClass $class + * + * @return ContainerBuilder The current instance + * + * @api + */ + public function addClassResource(\ReflectionClass $class) { if (!$this->trackResources) { return $this; } - $parent = new \ReflectionObject($object); do { - $this->addResource(new FileResource($parent->getFileName())); - } while ($parent = $parent->getParentClass()); + $this->addResource(new FileResource($class->getFileName())); + } while ($class = $class->getParentClass()); return $this; } @@ -417,8 +438,10 @@ public function has($id) * * @return object The associated service * - * @throws InvalidArgumentException if the service is not defined - * @throws LogicException if the service has a circular reference to itself + * @throws InvalidArgumentException when no definitions are available + * @throws InactiveScopeException when the current scope is not active + * @throws LogicException when a circular dependency is detected + * @throws \Exception * * @see Reference * @@ -584,6 +607,12 @@ public function compile() foreach ($this->compiler->getPassConfig()->getPasses() as $pass) { $this->addObjectResource($pass); } + + foreach ($this->definitions as $definition) { + if ($definition->isLazy() && ($class = $definition->getClass()) && class_exists($class)) { + $this->addClassResource(new \ReflectionClass($class)); + } + } } $this->compiler->compile($this); @@ -865,6 +894,7 @@ public function findDefinition($id) * * @param Definition $definition A service definition instance * @param string $id The service identifier + * @param Boolean $tryProxy Whether to try proxying the service with a lazy proxy * * @return object The service described by the service definition * @@ -872,13 +902,43 @@ public function findDefinition($id) * @throws RuntimeException When the factory definition is incomplete * @throws RuntimeException When the service is a synthetic service * @throws InvalidArgumentException When configure callable is not callable + * + * @internal this method is public because of PHP 5.3 limitations, do not use it explicitly in your code */ - private function createService(Definition $definition, $id) + public function createService(Definition $definition, $id, $tryProxy = true) { if ($definition->isSynthetic()) { throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id)); } + if ( + $tryProxy + && ($className = $definition->getClass()) + && $definition->isLazy() + && class_exists('ProxyManager\\Factory\\LazyLoadingValueHolderFactory') + ) { + $config = new Configuration(); + + $config->setGeneratorStrategy(new EvaluatingGeneratorStrategy()); + + $factory = new LazyLoadingValueHolderFactory($config); + $container = $this; + $proxy = $factory->createProxy( + $className, + function (& $wrappedInstance, LazyLoadingInterface $proxy) use ($container, $definition, $id) { + $proxy->setProxyInitializer(null); + + $wrappedInstance = $container->createService($definition, $id, false); + + return true; + } + ); + + $this->shareService($definition, $proxy, $id); + + return $proxy; + } + $parameterBag = $this->getParameterBag(); if (null !== $definition->getFile()) { @@ -903,16 +963,9 @@ private function createService(Definition $definition, $id) $service = null === $r->getConstructor() ? $r->newInstance() : $r->newInstanceArgs($arguments); } - if (self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) { - if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) { - throw new InactiveScopeException($id, $scope); - } - - $this->services[$lowerId = strtolower($id)] = $service; - - if (self::SCOPE_CONTAINER !== $scope) { - $this->scopedServices[$scope][$lowerId] = $service; - } + if ($tryProxy || !$definition->isLazy()) { + // share only if proxying failed, or if not a proxy + $this->shareService($definition, $service, $id); } foreach ($definition->getMethodCalls() as $call) { @@ -1057,4 +1110,28 @@ private function callMethod($service, $call) call_user_func_array(array($service, $call[0]), $this->resolveServices($this->getParameterBag()->resolveValue($call[1]))); } + + /** + * Shares a given service in the container + * + * @param Definition $definition + * @param mixed $service + * @param string $id + * + * @throws InactiveScopeException + */ + private function shareService(Definition $definition, $service, $id) + { + if (self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) { + if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) { + throw new InactiveScopeException($id, $scope); + } + + $this->services[$lowerId = strtolower($id)] = $service; + + if (self::SCOPE_CONTAINER !== $scope) { + $this->scopedServices[$scope][$lowerId] = $service; + } + } + } } diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 9d52426121874..803dc1640d785 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -37,6 +37,7 @@ class Definition private $synthetic; private $abstract; private $synchronized; + private $lazy; protected $arguments; @@ -58,6 +59,7 @@ public function __construct($class = null, array $arguments = array()) $this->public = true; $this->synthetic = false; $this->synchronized = false; + $this->lazy = false; $this->abstract = false; $this->properties = array(); } @@ -599,6 +601,34 @@ public function isSynchronized() return $this->synchronized; } + /** + * Sets the lazy flag of this service. + * + * @param Boolean $lazy + * + * @return Definition The current instance + * + * @api + */ + public function setLazy($lazy) + { + $this->lazy = (Boolean) $lazy; + + return $this; + } + + /** + * Whether this service is lazy. + * + * @return Boolean + * + * @api + */ + public function isLazy() + { + return $this->lazy; + } + /** * 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 294de23a7773d..b502bdc6f99d8 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -11,6 +11,9 @@ namespace Symfony\Component\DependencyInjection\Dumper; +use ProxyManager\Generator\ClassGenerator; +use ProxyManager\GeneratorStrategy\BaseGeneratorStrategy; +use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator; use Symfony\Component\DependencyInjection\Variable; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -94,7 +97,8 @@ public function dump(array $options = array()) $code .= $this->addServices(). $this->addDefaultParametersMethod(). - $this->endClass() + $this->endClass(). + $this->addProxyClasses() ; return $code; @@ -149,6 +153,88 @@ private function addServiceLocalTempVariables($cId, $definition) return $code; } + /** + * Generates the logic required for proxy lazy loading + * + * @param string $id The service id + * @param Definition $definition + * + * @return string + */ + private function addProxyLoading($id, Definition $definition) + { + if (!$this->isProxyCandidate($definition)) { + return ''; + } + + $instantiation = 'return'; + + if (ContainerInterface::SCOPE_CONTAINER === $definition->getScope()) { + $instantiation .= " \$this->services['$id'] ="; + } elseif (ContainerInterface::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) { + $instantiation .= " \$this->services['$id'] = \$this->scopedServices['$scope']['$id'] ="; + } + + $methodName = 'get' . Container::camelize($id) . 'Service'; + $proxyClass = str_replace('\\', '', $definition->getClass()) . '_' . spl_object_hash($definition); + + return <<setProxyInitializer(null); + + \$wrappedInstance = \$container->$methodName(false); + + return true; + } + ); + } + + +EOF; + } + + /** + * Generates code for the proxy classes to be attached after the container class + * + * @return string + */ + private function addProxyClasses() + { + $proxyDefinitions = array(); + + foreach ($this->container->getDefinitions() as $definition) { + if ($this->isProxyCandidate($definition)) { + $proxyDefinitions[] = $definition; + } + } + + // avoids hard dependency to ProxyManager + if (empty($proxyDefinitions)) { + return ''; + } + + $proxyGenerator = new LazyLoadingValueHolderGenerator(); + $classGenerator = new BaseGeneratorStrategy(); + $code = ''; + + /* @var $proxyDefinitions Definition[] */ + foreach ($proxyDefinitions as $definition) { + $generatedClass = new ClassGenerator( + str_replace('\\', '', $definition->getClass()) . '_' . md5(spl_object_hash($definition)) + ); + + $proxyGenerator->generate(new \ReflectionClass($definition->getClass()), $generatedClass); + + $code .= "\n" . $classGenerator->generate($generatedClass); + } + + return $code; + } + /** * Generates the require_once statement for service includes. * @@ -280,12 +366,13 @@ private function addServiceInstance($id, $definition) throw new InvalidArgumentException(sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id)); } - $simple = $this->isSimpleInstance($id, $definition); + $simple = $this->isSimpleInstance($id, $definition); + $isProxyCandidate = $this->isProxyCandidate($definition); + $instantiation = ''; - $instantiation = ''; - if (ContainerInterface::SCOPE_CONTAINER === $definition->getScope()) { + if (!$isProxyCandidate && ContainerInterface::SCOPE_CONTAINER === $definition->getScope()) { $instantiation = "\$this->services['$id'] = ".($simple ? '' : '$instance'); - } elseif (ContainerInterface::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) { + } elseif (!$isProxyCandidate && ContainerInterface::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) { $instantiation = "\$this->services['$id'] = \$this->scopedServices['$scope']['$id'] = ".($simple ? '' : '$instance'); } elseif (!$simple) { $instantiation = '$instance'; @@ -483,18 +570,32 @@ private function addService($id, $definition) EOF; } - $code = <<isLazy()) { + $lazyInitialization = '$lazyLoad = true'; + $lazyInitializationDoc = "\n * @param boolean \$lazyLoad whether to try lazy-loading the" + . " service with a proxy\n *"; + } else { + $lazyInitialization = ''; + $lazyInitializationDoc = ''; + } + + // with proxies, for 5.3.3 compatibility, the getter must be public to be accessible to the initializer + $isProxyCandidate = $this->isProxyCandidate($definition); + $visibility = $isProxyCandidate ? 'public' : 'protected'; + $code = <<addProxyLoading($id, $definition) : ''; + if (!in_array($scope, array(ContainerInterface::SCOPE_CONTAINER, ContainerInterface::SCOPE_PROTOTYPE))) { $code .= <<scopedServices['$scope'])) { @@ -1178,6 +1279,29 @@ private function getServiceCall($id, Reference $reference = null) } } + /** + * Tells if the given definitions are to be used for proxying, and if proxying is possible, + * since ProxyManager may not be available + * + * @param Definition $definition + * + * @return bool + */ + private function isProxyCandidate(Definition $definition) + { + if (!( + $definition->isLazy() + && $definition->getClass() + && class_exists('ProxyManager\\Factory\\LazyLoadingValueHolderFactory') + )) { + return false; + } + + $class = $this->dumpValue($definition->getClass()); + + return (boolean) preg_match('/^\'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class); + } + /** * Returns the next name to use * diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index da2e2c4cd80be..a5ceb2c68d3c3 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -133,6 +133,9 @@ private function addService($definition, $id, \DOMElement $parent) if ($definition->isSynchronized()) { $service->setAttribute('synchronized', 'true'); } + if ($definition->isLazy()) { + $service->setAttribute('lazy', 'true'); + } foreach ($definition->getTags() as $name => $tags) { foreach ($tags as $attributes) { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index bc10c4a7b4e17..0059f0d0d7388 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -106,6 +106,10 @@ private function addService($id, $definition) $code .= sprintf(" factory_class: %s\n", $definition->getFactoryClass()); } + if ($definition->isLazy()) { + $code .= sprintf(" lazy: 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 85898d3d3fe86..9f25ab7683cd2 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', 'synchronized', 'abstract') as $key) { + foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'synchronized', 'lazy', '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 b95280dfcfe5f..cf68a33756747 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -157,6 +157,10 @@ private function parseDefinition($id, $service, $file) $definition->setSynchronized($service['synchronized']); } + if (isset($service['lazy'])) { + $definition->setLazy($service['lazy']); + } + 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 4d9addcd971f8..f1c2003c62258 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 @@ -87,6 +87,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 7d2cb278871cc..b1b0a040f5776 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -14,6 +14,7 @@ require_once __DIR__.'/Fixtures/includes/classes.php'; require_once __DIR__.'/Fixtures/includes/ProjectExtension.php'; +use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -263,6 +264,32 @@ public function testCreateService() $this->assertInstanceOf('\FooClass', $builder->get('foo2'), '->createService() replaces parameters in the file provided by the service definition'); } + /** + * @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService + */ + public function testCreateServiceWithDelegateFactory() + { + $builder = new ContainerBuilder(); + + $builder->register('foo1', 'FooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php'); + $builder->getDefinition('foo1')->setLazy(true); + + /* @var $foo1 \ProxyManager\Proxy\LazyLoadingInterface|\ProxyManager\Proxy\ValueHolderInterface */ + $foo1 = $builder->get('foo1'); + + $this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved on multiple subsequent calls'); + $this->assertInstanceOf('\FooClass', $foo1); + $this->assertInstanceOf('\ProxyManager\Proxy\LazyLoadingInterface', $foo1); + $this->assertFalse($foo1->isProxyInitialized()); + + $foo1->initializeProxy(); + + $this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved after initialization'); + $this->assertTrue($foo1->isProxyInitialized()); + $this->assertInstanceOf('\FooClass', $foo1->getWrappedValueHolderValue()); + $this->assertNotInstanceOf('\ProxyManager\Proxy\LazyLoadingInterface', $foo1->getWrappedValueHolderValue()); + } + /** * @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService */ @@ -465,6 +492,95 @@ public function testFindDefinition() $this->assertEquals($definition, $container->findDefinition('foobar'), '->findDefinition() returns a Definition'); } + /** + * @covers Symfony\Component\DependencyInjection\ContainerBuilder::addObjectResource + */ + public function testAddObjectResource() + { + if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { + $this->markTestSkipped('The "Config" component is not available'); + } + + $container = new ContainerBuilder(); + + $container->setResourceTracking(false); + $container->addObjectResource(new \BarClass()); + + $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking'); + + $container->setResourceTracking(true); + $container->addObjectResource(new \BarClass()); + + $resources = $container->getResources(); + + $this->assertCount(1, $resources, '1 resource was registered'); + + /* @var $resource \Symfony\Component\Config\Resource\FileResource */ + $resource = end($resources); + + $this->assertInstanceOf('Symfony\Component\Config\Resource\FileResource', $resource); + $this->assertSame(realpath(__DIR__.'/Fixtures/includes/classes.php'), realpath($resource->getResource())); + } + + /** + * @covers Symfony\Component\DependencyInjection\ContainerBuilder::addClassResource + */ + public function testAddClassResource() + { + if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { + $this->markTestSkipped('The "Config" component is not available'); + } + + $container = new ContainerBuilder(); + + $container->setResourceTracking(false); + $container->addClassResource(new \ReflectionClass('BarClass')); + + $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking'); + + $container->setResourceTracking(true); + $container->addClassResource(new \ReflectionClass('BarClass')); + + $resources = $container->getResources(); + + $this->assertCount(1, $resources, '1 resource was registered'); + + /* @var $resource \Symfony\Component\Config\Resource\FileResource */ + $resource = end($resources); + + $this->assertInstanceOf('Symfony\Component\Config\Resource\FileResource', $resource); + $this->assertSame(realpath(__DIR__.'/Fixtures/includes/classes.php'), realpath($resource->getResource())); + } + + /** + * @covers Symfony\Component\DependencyInjection\ContainerBuilder::compile + */ + public function testCompilesClassDefinitionsOfLazyServices() + { + if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { + $this->markTestSkipped('The "Config" component is not available'); + } + + $container = new ContainerBuilder(); + + $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking'); + + $container->register('foo', 'BarClass'); + $container->getDefinition('foo')->setLazy(true); + + $container->compile(); + + $classesPath = realpath(__DIR__.'/Fixtures/includes/classes.php'); + $matchingResources = array_filter( + $container->getResources(), + function (ResourceInterface $resource) use ($classesPath) { + return $resource instanceof FileResource && $classesPath === realpath($resource->getResource()); + } + ); + + $this->assertNotEmpty($matchingResources); + } + /** * @covers Symfony\Component\DependencyInjection\ContainerBuilder::getResources * @covers Symfony\Component\DependencyInjection\ContainerBuilder::addResource diff --git a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php index d9a4282efefbc..d41c6a8e2b2ae 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php @@ -164,6 +164,18 @@ public function testSetIsSynchronized() $this->assertTrue($def->isSynchronized(), '->isSynchronized() returns true if the service is synchronized.'); } + /** + * @covers Symfony\Component\DependencyInjection\Definition::setLazy + * @covers Symfony\Component\DependencyInjection\Definition::isLazy + */ + public function testSetIsLazy() + { + $def = new Definition('stdClass'); + $this->assertFalse($def->isLazy(), '->isLazy() returns false by default'); + $this->assertSame($def, $def->setLazy(true), '->setLazy() implements a fluent interface'); + $this->assertTrue($def->isLazy(), '->isLazy() returns true if the service is lazy.'); + } + /** * @covers Symfony\Component\DependencyInjection\Definition::setAbstract * @covers Symfony\Component\DependencyInjection\Definition::isAbstract diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 72d587ff070fc..12f88ca6b19a6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -52,6 +52,43 @@ public function testDumpFrozenContainerWithNoParameter() $this->assertNotRegexp("/function getDefaultParameters\(/", $dumpedString, '->dump() does not add getDefaultParameters() method definition.'); } + public function testDumpContainerWithProxyService() + { + $container = new ContainerBuilder(); + + $container->register('foo', 'stdClass'); + $container->getDefinition('foo')->setLazy(true); + $container->compile(); + + $dumper = new PhpDumper($container); + $dumpedString = $dumper->dump(); + + $this->assertStringMatchesFormatFile(self::$fixturesPath.'/php/lazy_service_structure.txt', $dumpedString, '->dump() does generate proxy lazy loading logic.'); + } + + /** + * Verifies that the generated container retrieves the same proxy instance on multiple subsequent requests + */ + public function testDumpContainerWithProxyServiceWillShareProxies() + { + require_once self::$fixturesPath.'/php/lazy_service.php'; + + $container = new \LazyServiceProjectServiceContainer(); + + /* @var $proxy \stdClass_c1d194250ee2e2b7d2eab8b8212368a8 */ + $proxy = $container->get('foo'); + + $this->assertInstanceOf('stdClass_c1d194250ee2e2b7d2eab8b8212368a8', $proxy); + $this->assertSame($proxy, $container->get('foo')); + + $this->assertFalse($proxy->isProxyInitialized()); + + $proxy->initializeProxy(); + + $this->assertTrue($proxy->isProxyInitialized()); + $this->assertSame($proxy, $container->get('foo')); + } + public function testDumpOptimizationString() { $definition = new Definition(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_service.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_service.php new file mode 100644 index 0000000000000..fa2c911f91c07 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_service.php @@ -0,0 +1,199 @@ + +services = + $this->scopedServices = + $this->scopeStacks = array(); + + $this->set('service_container', $this); + + $this->scopes = array(); + $this->scopeChildren = array(); + } + + /** + * Gets the 'foo' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @param boolean $lazyLoad whether to try lazy-loading the service with a proxy + * + * @return stdClass A stdClass instance. + */ + public function getFooService($lazyLoad = true) + { + if ($lazyLoad) { + $container = $this; + + return $this->services['foo'] = new stdClass_c1d194250ee2e2b7d2eab8b8212368a8( + function (& $wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) use ($container) { + $proxy->setProxyInitializer(null); + + $wrappedInstance = $container->getFooService(false); + + return true; + } + ); + } + + return new \stdClass(); + } +} + +class stdClass_c1d194250ee2e2b7d2eab8b8212368a8 extends \stdClass implements \ProxyManager\Proxy\LazyLoadingInterface, \ProxyManager\Proxy\ValueHolderInterface +{ + + /** + * @var \Closure|null initializer responsible for generating the wrapped object + */ + private $valueHolder5157dd96e88c0 = null; + + /** + * @var \Closure|null initializer responsible for generating the wrapped object + */ + private $initializer5157dd96e8924 = null; + + /** + * @override constructor for lazy initialization + * + * @param \Closure|null $initializer + */ + public function __construct($initializer) + { + $this->initializer5157dd96e8924 = $initializer; + } + + /** + * @param string $name + */ + public function __get($name) + { + $this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__get', array('name' => $name)); + + return $this->valueHolder5157dd96e88c0->$name; + } + + /** + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + $this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__set', array('name' => $name, 'value' => $value)); + + $this->valueHolder5157dd96e88c0->$name = $value; + } + + /** + * @param string $name + */ + public function __isset($name) + { + $this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__isset', array('name' => $name)); + + return isset($this->valueHolder5157dd96e88c0->$name); + } + + /** + * @param string $name + */ + public function __unset($name) + { + $this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__unset', array('name' => $name)); + + unset($this->valueHolder5157dd96e88c0->$name); + } + + /** + * + */ + public function __clone() + { + $this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__clone', array()); + + $this->valueHolder5157dd96e88c0 = clone $this->valueHolder5157dd96e88c0; + } + + /** + * + */ + public function __sleep() + { + $this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__sleep', array()); + + return array('valueHolder5157dd96e88c0'); + } + + /** + * + */ + public function __wakeup() + { + } + + /** + * {@inheritDoc} + */ + public function setProxyInitializer(\Closure $initializer = null) + { + $this->initializer5157dd96e8924 = $initializer; + } + + /** + * {@inheritDoc} + */ + public function getProxyInitializer() + { + return $this->initializer5157dd96e8924; + } + + /** + * {@inheritDoc} + */ + public function initializeProxy() + { + return $this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, 'initializeProxy', array()); + } + + /** + * {@inheritDoc} + */ + public function isProxyInitialized() + { + return null !== $this->valueHolder5157dd96e88c0; + } + + /** + * {@inheritDoc} + */ + public function getWrappedValueHolderValue() + { + return $this->valueHolder5157dd96e88c0; + } + + +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_service_structure.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_service_structure.txt new file mode 100644 index 0000000000000..1eaf8cad606c3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_service_structure.txt @@ -0,0 +1,67 @@ +services = + $this->scopedServices = + $this->scopeStacks = array(); + + $this->set('service_container', $this); + + $this->scopes = array(); + $this->scopeChildren = array(); + } + + /** + * Gets the 'foo' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @param boolean $lazyLoad whether to try lazy-loading the service with a proxy + * + * @return stdClass A stdClass instance. + */ + public function getFooService($lazyLoad = true) + { + if ($lazyLoad) { + $container = $this; + + return $this->services['foo'] = new stdClass_%s( + function (& $wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) use ($container) { + $proxy->setProxyInitializer(null); + + $wrappedInstance = $container->getFooService(false); + + return true; + } + ); + } + + return new \stdClass(); + } +} + +class stdClass_%s extends \stdClass implements \ProxyManager\Proxy\LazyLoadingInterface, \ProxyManager\Proxy\ValueHolderInterface +{%a} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml index 4d2aa3d79ae24..abd9fbc1529b1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml @@ -46,6 +46,6 @@ - + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml index 820c364a06556..7ba9453bdd6dd 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml @@ -28,3 +28,4 @@ services: class: Request synthetic: true synchronized: true + lazy: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index b355f0ac215c3..d8138f947541a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -187,6 +187,7 @@ public function testLoadServices() $this->assertTrue($services['request']->isSynthetic(), '->load() parses the synthetic flag'); $this->assertTrue($services['request']->isSynchronized(), '->load() parses the synchronized flag'); + $this->assertTrue($services['request']->isLazy(), '->load() parses the lazy flag'); $aliases = $container->getAliases(); $this->assertTrue(isset($aliases['alias_for_foo']), '->load() parses elements'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 290d6628acf5b..e452e5d221d19 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -130,6 +130,7 @@ public function testLoadServices() $this->assertTrue($services['request']->isSynthetic(), '->load() parses the synthetic flag'); $this->assertTrue($services['request']->isSynchronized(), '->load() parses the synchronized flag'); + $this->assertTrue($services['request']->isLazy(), '->load() parses the lazy flag'); $aliases = $container->getAliases(); $this->assertTrue(isset($aliases['alias_for_foo']), '->load() parses aliases'); diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 887686747eda3..469811a075d48 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -20,11 +20,13 @@ }, "require-dev": { "symfony/yaml": "~2.0", - "symfony/config": ">=2.2,<2.4-dev" + "symfony/config": ">=2.2,<2.4-dev", + "ocramius/proxy-manager": ">=0.3.1,<0.4.x-dev" }, "suggest": { "symfony/yaml": "2.2.*", - "symfony/config": "2.2.*" + "symfony/config": "2.2.*", + "ocramius/proxy-manager": "Generate service proxies to lazy load them" }, "autoload": { "psr-0": { "Symfony\\Component\\DependencyInjection\\": "" }