diff --git a/composer.json b/composer.json index 504b48d92c76..1e3fdae52142 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/Bridge/ProxyManager/.gitignore b/src/Symfony/Bridge/ProxyManager/.gitignore new file mode 100644 index 000000000000..44de97a36a6d --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/.gitignore @@ -0,0 +1,4 @@ +vendor/ +composer.lock +phpunit.xml + diff --git a/src/Symfony/Bridge/ProxyManager/CHANGELOG.md b/src/Symfony/Bridge/ProxyManager/CHANGELOG.md new file mode 100644 index 000000000000..1f8f60c48bfe --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +2.3.0 +----- + + * First introduction of `Symfony\Bridge\ProxyManager` diff --git a/src/Symfony/Bridge/ProxyManager/LICENSE b/src/Symfony/Bridge/ProxyManager/LICENSE new file mode 100644 index 000000000000..88a57f8d8da4 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2013 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php new file mode 100644 index 000000000000..7550b9a55ec5 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\ProxyManager\LazyProxy\Instantiator; + +use ProxyManager\Configuration; +use ProxyManager\Factory\LazyLoadingValueHolderFactory; +use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy; +use ProxyManager\Proxy\LazyLoadingInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface; + +/** + * Runtime lazy loading proxy generator + * + * @author Marco Pivetta + */ +class RuntimeInstantiator implements InstantiatorInterface +{ + /** + * @var \ProxyManager\Factory\LazyLoadingValueHolderFactory + */ + private $factory; + + /** + * Constructor + */ + public function __construct() + { + $config = new Configuration(); + + $config->setGeneratorStrategy(new EvaluatingGeneratorStrategy()); + + $this->factory = new LazyLoadingValueHolderFactory($config); + } + + /** + * {@inheritDoc} + */ + public function instantiateProxy(ContainerInterface $container, Definition $definition, $id, $realInstantiator) + { + return $this->factory->createProxy( + $definition->getClass(), + function (& $wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) { + $proxy->setProxyInitializer(null); + + $wrappedInstance = call_user_func($realInstantiator); + + return true; + } + ); + } +} diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php new file mode 100644 index 000000000000..23361e04a4af --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper; + +use ProxyManager\Generator\ClassGenerator; +use ProxyManager\GeneratorStrategy\BaseGeneratorStrategy; +use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface; + +/** + * Generates dumped php code of proxies via reflection + * + * @author Marco Pivetta + */ +class ProxyDumper implements DumperInterface +{ + /** + * @var \ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator + */ + private $proxyGenerator; + + /** + * @var \ProxyManager\GeneratorStrategy\BaseGeneratorStrategy + */ + private $classGenerator; + + /** + * Constructor + */ + public function __construct() + { + $this->proxyGenerator = new LazyLoadingValueHolderGenerator(); + $this->classGenerator = new BaseGeneratorStrategy(); + } + + /** + * {@inheritDoc} + */ + public function isProxyCandidate(Definition $definition) + { + return $definition->isLazy() + && ($class = $definition->getClass()) + && class_exists($class); + } + + /** + * {@inheritDoc} + */ + public function getProxyFactoryCode(Definition $definition, $id) + { + $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 = $this->getProxyClassName($definition); + + return <<setProxyInitializer(null); + + \$wrappedInstance = \$container->$methodName(false); + + return true; + } + ); + } + + +EOF; + } + + /** + * {@inheritDoc} + */ + public function getProxyCode(Definition $definition) + { + $generatedClass = new ClassGenerator($this->getProxyClassName($definition)); + + $this->proxyGenerator->generate(new \ReflectionClass($definition->getClass()), $generatedClass); + + return $this->classGenerator->generate($generatedClass); + } + + /** + * Produces the proxy class name for the given definition + * + * @param Definition $definition + * + * @return string + */ + private function getProxyClassName(Definition $definition) + { + return str_replace('\\', '', $definition->getClass()) . '_' . spl_object_hash($definition); + } +} diff --git a/src/Symfony/Bridge/ProxyManager/README.md b/src/Symfony/Bridge/ProxyManager/README.md new file mode 100644 index 000000000000..7319ddbbf455 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/README.md @@ -0,0 +1,13 @@ +ProxyManager Bridge +=================== + +Provides integration for [ProxyManager](https://github.com/Ocramius/ProxyManager) with various Symfony2 components. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Bridge/ProxyManager/ + $ composer.phar install --dev + $ phpunit diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php new file mode 100644 index 000000000000..ebe8f2c66642 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\ProxyManager\LazyProxy\Tests; + +require_once __DIR__ . '/Fixtures/includes/foo.php'; + +use ProxyManager\Configuration; +use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Integration tests for {@see \Symfony\Component\DependencyInjection\ContainerBuilder} combined + * with the ProxyManager bridge + * + * @author Marco Pivetta + */ +class ContainerBuilderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService + */ + public function testCreateProxyServiceWithRuntimeInstantiator() + { + $builder = new ContainerBuilder(); + + $builder->setProxyInstantiator(new RuntimeInstantiator()); + + $builder->register('foo1', 'ProxyManagerBridgeFooClass')->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('\ProxyManagerBridgeFooClass', $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('\ProxyManagerBridgeFooClass', $foo1->getWrappedValueHolderValue()); + $this->assertNotInstanceOf('\ProxyManager\Proxy\LazyLoadingInterface', $foo1->getWrappedValueHolderValue()); + } +} diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php new file mode 100644 index 000000000000..0b51235bf019 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\ProxyManager\LazyProxy\Tests\Dumper; + +use ProxyManager\Configuration; +use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Dumper\PhpDumper; + +/** + * Integration tests for {@see \Symfony\Component\DependencyInjection\Dumper\PhpDumper} combined + * with the ProxyManager bridge + * + * @author Marco Pivetta + */ +class PhpDumperTest extends \PHPUnit_Framework_TestCase +{ + public function testDumpContainerWithProxyService() + { + $container = new ContainerBuilder(); + + $container->register('foo', 'stdClass'); + $container->getDefinition('foo')->setLazy(true); + $container->compile(); + + $dumper = new PhpDumper($container); + + $dumper->setProxyDumper(new ProxyDumper()); + + $dumpedString = $dumper->dump(); + + $this->assertStringMatchesFormatFile( + __DIR__ . '/../Fixtures/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 __DIR__ . '/../Fixtures/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')); + } +} diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/includes/foo.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/includes/foo.php new file mode 100644 index 000000000000..1013a8c57232 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/includes/foo.php @@ -0,0 +1,36 @@ +arguments = $arguments; + } + + public static function getInstance($arguments = array()) + { + $obj = new self($arguments); + $obj->called = true; + + return $obj; + } + + public function initialize() + { + $this->initialized = true; + } + + public function configure() + { + $this->configured = true; + } + + public function setBar($value = null) + { + $this->bar = $value; + } +} diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php new file mode 100644 index 000000000000..fa2c911f91c0 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/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/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt new file mode 100644 index 000000000000..1f855950528e --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt @@ -0,0 +1,27 @@ +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}%A \ No newline at end of file diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Instantiator/RuntimeInstantiatorTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Instantiator/RuntimeInstantiatorTest.php new file mode 100644 index 000000000000..1fb4c01c7660 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Instantiator/RuntimeInstantiatorTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\ProxyManager\LazyProxy\Tests\Instantiator; + +use ProxyManager\Configuration; +use ProxyManager\Proxy\LazyLoadingInterface; +use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Tests for {@see \Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator} + * + * @author Marco Pivetta + * + * @covers \Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator + */ +class RuntimeInstantiatorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var RuntimeInstantiator + */ + protected $instantiator; + + /** + * {@inheritDoc} + */ + public function setUp() + { + $this->instantiator = new RuntimeInstantiator(); + } + + public function testInstantiateProxy() + { + $instance = new \stdClass(); + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $definition = new Definition('stdClass'); + $instantiator = function () use ($instance) { + return $instance; + }; + + /* @var $proxy \ProxyManager\Proxy\LazyLoadingInterface|\ProxyManager\Proxy\ValueHolderInterface */ + $proxy = $this->instantiator->instantiateProxy($container, $definition, 'foo', $instantiator); + + $this->assertInstanceOf('ProxyManager\Proxy\LazyLoadingInterface', $proxy); + $this->assertInstanceOf('ProxyManager\Proxy\ValueHolderInterface', $proxy); + $this->assertFalse($proxy->isProxyInitialized()); + + $proxy->initializeProxy(); + + $this->assertSame($instance, $proxy->getWrappedValueHolderValue()); + } +} diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php new file mode 100644 index 000000000000..1a61a0565c4f --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\ProxyManager\LazyProxy\Tests\Instantiator; + +use ProxyManager\Configuration; +use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Tests for {@see \Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper} + * + * @author Marco Pivetta + * + * @covers \Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper + */ +class ProxyDumperTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ProxyDumper + */ + protected $dumper; + + /** + * {@inheritDoc} + */ + public function setUp() + { + $this->dumper = new ProxyDumper(); + } + + /** + * @dataProvider getProxyCandidates + * + * @param Definition $definition + * @param bool $expected + */ + public function testIsProxyCandidate(Definition $definition, $expected) + { + $this->assertSame($expected, $this->dumper->isProxyCandidate($definition)); + } + + public function testGetProxyCode() + { + $definition = new Definition(__CLASS__); + + $definition->setLazy(true); + + $code = $this->dumper->getProxyCode($definition); + + $this->assertStringMatchesFormat( + '%Aclass SymfonyBridgeProxyManagerLazyProxyTestsInstantiatorProxyDumperTest%aextends%w' + . '\Symfony\Bridge\ProxyManager\LazyProxy\Tests\Instantiator%a', + $code + ); + } + + public function testGetProxyFactoryCode() + { + $definition = new Definition(__CLASS__); + + $definition->setLazy(true); + + $code = $this->dumper->getProxyFactoryCode($definition, 'foo'); + + $this->assertStringMatchesFormat( + '%wif ($lazyLoad) {%w$container = $this;%wreturn $this->services[\'foo\'] = new ' + . 'SymfonyBridgeProxyManagerLazyProxyTestsInstantiatorProxyDumperTest_%s(%wfunction ' + . '(& $wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) use ($container) {' + . '%w$proxy->setProxyInitializer(null);%w$wrappedInstance = $container->getFooService(false);' + . '%wreturn true;%w}%w);%w}%w', + $code + ); + } + + /** + * @return array + */ + public function getProxyCandidates() + { + $definitions = array( + array(new Definition(__CLASS__), true), + array(new Definition('stdClass'), true), + array(new Definition('foo' . uniqid()), false), + array(new Definition(), false), + ); + + array_map( + function ($definition) { + $definition[0]->setLazy(true); + }, + $definitions + ); + + return $definitions; + } +} diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json new file mode 100644 index 000000000000..1dd4afaf8420 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/proxy-manager-bridge", + "type": "symfony-bridge", + "description": "Symfony ProxyManager Bridge", + "keywords": [], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3", + "symfony/dependency-injection": ">=2.3-dev,<2.4-dev", + "ocramius/proxy-manager": ">=0.3.1,<0.4-dev" + }, + "autoload": { + "psr-0": { + "Symfony\\Bridge\\ProxyManager\\": "" + } + }, + "target-dir": "Symfony/Bridge/ProxyManager", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + } +} diff --git a/src/Symfony/Bridge/ProxyManager/phpunit.xml.dist b/src/Symfony/Bridge/ProxyManager/phpunit.xml.dist new file mode 100644 index 000000000000..5e7c4337f91b --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 5d8d0d1a10f1..6f842270fc9e 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -22,6 +22,8 @@ use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface; +use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator; /** * ContainerBuilder is a DI container that provides an API to easily describe services. @@ -71,6 +73,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface private $trackResources = true; + /** + * @var InstantiatorInterface|null + */ + private $proxyInstantiator; + /** * Sets the track resources flag. * @@ -94,6 +101,16 @@ public function isTrackingResources() return $this->trackResources; } + /** + * Sets the instantiator to be used when fetching proxies. + * + * @param InstantiatorInterface $proxyInstantiator + */ + public function setProxyInstantiator(InstantiatorInterface $proxyInstantiator) + { + $this->proxyInstantiator = $proxyInstantiator; + } + /** * Registers an extension. * @@ -222,15 +239,30 @@ 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 + */ + 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 +449,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 +618,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 +905,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 +913,32 @@ 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 && $definition->isLazy()) { + $container = $this; + + $proxy = $this + ->getProxyInstantiator() + ->instantiateProxy( + $container, + $definition, + $id, function () use ($definition, $id, $container) { + return $container->createService($definition, $id, false); + } + ); + $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) { @@ -1019,6 +1072,20 @@ public static function getServiceConditionals($value) return $services; } + /** + * Retrieves the currently set proxy instantiator or instantiates one. + * + * @return InstantiatorInterface + */ + private function getProxyInstantiator() + { + if (!$this->proxyInstantiator) { + $this->proxyInstantiator = new RealServiceInstantiator(); + } + + return $this->proxyInstantiator; + } + /** * Synchronizes a service change. * @@ -1057,4 +1124,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 9d5242612187..1168444389ef 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,30 @@ public function isSynchronized() return $this->synchronized; } + /** + * Sets the lazy flag of this service. + * + * @param Boolean $lazy + * + * @return Definition The current instance + */ + public function setLazy($lazy) + { + $this->lazy = (Boolean) $lazy; + + return $this; + } + + /** + * Whether this service is lazy. + * + * @return Boolean + */ + 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 294de23a7773..9d6878050fe9 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -21,6 +21,8 @@ use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; +use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface as ProxyDumper; +use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper; /** * PhpDumper dumps a service container as a PHP class. @@ -50,6 +52,11 @@ class PhpDumper extends Dumper private $variableCount; private $reservedVariables = array('instance', 'class'); + /** + * @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface + */ + private $proxyDumper; + /** * {@inheritDoc} * @@ -62,6 +69,16 @@ public function __construct(ContainerBuilder $container) $this->inlinedDefinitions = new \SplObjectStorage; } + /** + * Sets the dumper to be used when dumping proxies in the generated container. + * + * @param ProxyDumper $proxyDumper + */ + public function setProxyDumper(ProxyDumper $proxyDumper) + { + $this->proxyDumper = $proxyDumper; + } + /** * Dumps the service container as a PHP class. * @@ -94,12 +111,27 @@ public function dump(array $options = array()) $code .= $this->addServices(). $this->addDefaultParametersMethod(). - $this->endClass() + $this->endClass(). + $this->addProxyClasses() ; return $code; } + /** + * Retrieves the currently set proxy dumper or instantiates one. + * + * @return ProxyDumper + */ + private function getProxyDumper() + { + if (!$this->proxyDumper) { + $this->proxyDumper = new NullDumper(); + } + + return $this->proxyDumper; + } + /** * Generates Service local temp variables. * @@ -149,6 +181,27 @@ private function addServiceLocalTempVariables($cId, $definition) return $code; } + /** + * Generates code for the proxies to be attached after the container class + * + * @return string + */ + private function addProxyClasses() + { + /* @var $proxyDefinitions Definition[] */ + $definitions = array_filter( + $this->container->getDefinitions(), + array($this->getProxyDumper(), 'isProxyCandidate') + ); + $code = ''; + + foreach ($definitions as $definition) { + $code .= "\n" . $this->getProxyDumper()->getProxyCode($definition); + } + + return $code; + } + /** * Generates the require_once statement for service includes. * @@ -280,12 +333,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->getProxyDumper()->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 +537,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->getProxyDumper()->isProxyCandidate($definition); + $visibility = $isProxyCandidate ? 'public' : 'protected'; + $code = <<getProxyDumper()->getProxyFactoryCode($definition, $id) : ''; + if (!in_array($scope, array(ContainerInterface::SCOPE_CONTAINER, ContainerInterface::SCOPE_PROTOTYPE))) { $code .= <<scopedServices['$scope'])) { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index da2e2c4cd80b..a5ceb2c68d3c 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 bc10c4a7b4e1..0059f0d0d738 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/LazyProxy/Instantiator/InstantiatorInterface.php b/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/InstantiatorInterface.php new file mode 100644 index 000000000000..4e4a00a547e2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/InstantiatorInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\LazyProxy\Instantiator; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Lazy proxy instantiator, capable of instantiating a proxy given a container, the + * service definitions and a callback that produces the real service instance. + * + * @author Marco Pivetta + */ +interface InstantiatorInterface +{ + /** + * @param ContainerInterface $container the container from which the service is being requested + * @param Definition $definition the definitions of the requested service + * @param string $id identifier of the requested service + * @param callable $realInstantiator zero-argument callback that is capable of producing the real + * service instance + * + * @return object + */ + public function instantiateProxy(ContainerInterface $container, Definition $definition, $id, $realInstantiator); +} diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/RealServiceInstantiator.php b/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/RealServiceInstantiator.php new file mode 100644 index 000000000000..6495df2813db --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/RealServiceInstantiator.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\LazyProxy\Instantiator; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; + +/** + * {@inheritDoc} + * + * Noop proxy instantiator - simply produces the real service instead of a proxy instance. + * + * @author Marco Pivetta + */ +class RealServiceInstantiator implements InstantiatorInterface +{ + /** + * {@inheritDoc} + */ + public function instantiateProxy(ContainerInterface $container, Definition $definition, $id, $realInstantiator) + { + return call_user_func($realInstantiator); + } +} diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php new file mode 100644 index 000000000000..d8d5dac47095 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\LazyProxy\PhpDumper; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Lazy proxy dumper capable of generating the instantiation logic php code for proxied services. + * + * @author Marco Pivetta + */ +interface DumperInterface +{ + /** + * Inspects whether the given definitions should produce proxy instantiation logic in the dumped container. + * + * @param Definition $definition + * + * @return bool + */ + public function isProxyCandidate(Definition $definition); + + /** + * Generates the code to be used to instantiate a proxy in the dumped factory code. + * + * @param Definition $definition + * @param string $id service identifier + * + * @return string + */ + public function getProxyFactoryCode(Definition $definition, $id); + + /** + * Generates the code for the lazy proxy. + * + * @param Definition $definition + * + * @return string + */ + public function getProxyCode(Definition $definition); +} diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/NullDumper.php b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/NullDumper.php new file mode 100644 index 000000000000..e1d4ff4dea90 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/NullDumper.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\LazyProxy\PhpDumper; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Null dumper, negates any proxy code generation for any given service definition. + * + * @author Marco Pivetta + */ +class NullDumper implements DumperInterface +{ + /** + * {@inheritDoc} + */ + public function isProxyCandidate(Definition $definition) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getProxyFactoryCode(Definition $definition, $id) + { + return ''; + } + + /** + * {@inheritDoc} + */ + public function getProxyCode(Definition $definition) + { + return ''; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 85898d3d3fe8..9f25ab7683cd 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 b95280dfcfe5..cf68a3375674 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 4d9addcd971f..f1c2003c6225 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 7d2cb278871c..a5e7531b68af 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,22 @@ 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 testCreateProxyWithRealServiceInstantiator() + { + $builder = new ContainerBuilder(); + + $builder->register('foo1', 'FooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php'); + $builder->getDefinition('foo1')->setLazy(true); + + $foo1 = $builder->get('foo1'); + + $this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved on multiple subsequent calls'); + $this->assertSame('FooClass', get_class($foo1)); + } + /** * @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService */ @@ -465,6 +482,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 d9a4282efefb..d41c6a8e2b2a 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/Fixtures/xml/services6.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml index 4d2aa3d79ae2..abd9fbc1529b 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 820c364a0655..7ba9453bdd6d 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/LazyProxy/Instantiator/RealServiceInstantiatorTest.php b/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/Instantiator/RealServiceInstantiatorTest.php new file mode 100644 index 000000000000..5fb202696492 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/Instantiator/RealServiceInstantiatorTest.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\LazyProxy\Instantiator; + +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator; + +/** + * Tests for {@see \Symfony\Component\DependencyInjection\Instantiator\RealServiceInstantiator} + * + * @author Marco Pivetta + * + * @covers \Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator + */ +class RealServiceInstantiatorTest extends \PHPUnit_Framework_TestCase +{ + public function testInstantiateProxy() + { + $instantiator = new RealServiceInstantiator(); + $instance = new \stdClass(); + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $callback = function () use ($instance) { + return $instance; + }; + + $this->assertSame($instance, $instantiator->instantiateProxy($container, new Definition(), 'foo', $callback)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/PhpDumper/NullDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/PhpDumper/NullDumperTest.php new file mode 100644 index 000000000000..646673662a61 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/PhpDumper/NullDumperTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\LazyProxy\PhpDumper; + +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper; + +/** + * Tests for {@see \Symfony\Component\DependencyInjection\PhpDumper\NullDumper} + * + * @author Marco Pivetta + * + * @covers \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper + */ +class NullDumperTest extends \PHPUnit_Framework_TestCase +{ + public function testNullDumper() + { + $dumper = new NullDumper(); + $definition = new Definition('stdClass'); + + $this->assertFalse($dumper->isProxyCandidate($definition)); + $this->assertSame('', $dumper->getProxyFactoryCode($definition, 'foo')); + $this->assertSame('', $dumper->getProxyCode($definition)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index b355f0ac215c..d8138f947541 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 290d6628acf5..e452e5d221d1 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 887686747eda..34a7006782bd 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -24,7 +24,8 @@ }, "suggest": { "symfony/yaml": "2.2.*", - "symfony/config": "2.2.*" + "symfony/config": "2.2.*", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them" }, "autoload": { "psr-0": { "Symfony\\Component\\DependencyInjection\\": "" } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 48d2a8ce893b..90779b73d207 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel; +use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; +use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; @@ -684,7 +686,13 @@ protected function prepareContainer(ContainerBuilder $container) */ protected function getContainerBuilder() { - return new ContainerBuilder(new ParameterBag($this->getKernelParameters())); + $container = new ContainerBuilder(new ParameterBag($this->getKernelParameters())); + + if (class_exists('Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator')) { + $container->setProxyInstantiator(new RuntimeInstantiator()); + } + + return $container; } /** @@ -699,6 +707,11 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container { // cache the container $dumper = new PhpDumper($container); + + if (class_exists('Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator')) { + $dumper->setProxyDumper(new ProxyDumper()); + } + $content = $dumper->dump(array('class' => $class, 'base_class' => $baseClass)); if (!$this->debug) { $content = self::stripComments($content);