diff --git a/core/core.services.yml b/core/core.services.yml index 9ab7af7..564aeaa 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -177,6 +177,7 @@ services: cron: class: Drupal\Core\Cron arguments: ['@module_handler', '@lock', '@queue', '@state', '@account_switcher', '@logger.channel.cron', '@plugin.manager.queue_worker'] + lazy: true diff.formatter: class: Drupal\Core\Diff\DiffFormatter arguments: ['@config.factory'] @@ -395,6 +396,7 @@ services: parent: default_plugin_manager plugin.cache_clearer: class: Drupal\Core\Plugin\CachedDiscoveryClearer + lazy: true paramconverter.menu_link: class: Drupal\Core\ParamConverter\MenuLinkPluginConverter tags: diff --git a/core/lib/Drupal/Component/ProxyBuilder/ProxyBuilder.php b/core/lib/Drupal/Component/ProxyBuilder/ProxyBuilder.php new file mode 100644 index 0000000..e9fe802 --- /dev/null +++ b/core/lib/Drupal/Component/ProxyBuilder/ProxyBuilder.php @@ -0,0 +1,228 @@ +buildProxyClassName($class_name); + + if ($interfaces = $reflection->getInterfaceNames()) { + foreach ($interfaces as &$interface) { + $interface = '\\' . $interface; + } + $output .= ' implements ' . implode(', ', $interfaces); + } + + $properties = <<<'EOS' + { + +/** + * @var string + */ +protected $serviceId; + +/** + * @var \{{ class_name }} + */ +protected $service; + + +EOS; + + $properties = str_replace('{{ class_name }}', $class_name, $properties); + + $output .= $properties; + + // Add all the methods. + $methods = []; + $methods[] = $this->buildConstructorMethod(); + $methods[] = $this->buildLazyLoadItselfMethod(); + + // Add all the methods of the proxied service. + $reflection_methods = $reflection->getMethods(); + + foreach ($reflection_methods as $method) { + if ($method->getName() === '__construct') { + continue; + } + + if ($method->isPublic()) { + $methods[] = $this->buildMethod($method) . "\n"; + } + } + + $output .= implode("\n", $methods); + + // Indent the output. + $output = implode("\n", array_map(function($value) { + if ($value === '') { + return $value; + } + return " $value"; + }, explode("\n", $output))); + + return $class_start . $output . "\n}\n"; + } + + /** + * Generates the string for the method which loads the actual service. + * + * @return string + */ + protected function buildLazyLoadItselfMethod() { + $output = <<<'EOS' +protected function lazyLoadItself() { + if (!isset($this->service)) { + $method_name = 'get' . Container::camelize($this->serviceId) . 'Service'; + $this->service = \Drupal::getContainer()->$method_name(FALSE); + } + + return $this->service; +} + +EOS; + + return $output; + } + + /** + * Generates the string representation of a single method: signature, body. + * + * @param \ReflectionMethod $reflection_method + * A reflection method for the method. + * + * @return string + */ + protected function buildMethod(\ReflectionMethod $reflection_method) { + + $parameters = []; + foreach ($reflection_method->getParameters() as $parameter) { + $parameters[] = $this->buildParameter($parameter); + } + + $function_name = $reflection_method->getName(); + + $signature_line = 'public function ' . $function_name . '('; + $signature_line .= implode(', ', $parameters); + $signature_line .= ')'; + + $output = $signature_line . ' {' . "\n"; + + $output .= $this->buildMethodBody($reflection_method); + + $output .= "\n". '}'; + return $output; + } + + /** + * Builds a string for a single parameter of a method. + * + * @param \ReflectionParameter $parameter + * A reflection object of the parameter. + * + * @return string + */ + protected function buildParameter(\ReflectionParameter $parameter) { + $parameter_string = ''; + + if ($parameter->isArray()) { + $parameter_string .= 'array '; + } + elseif ($parameter->isCallable()) { + $parameter_string .= 'callable '; + } + elseif ($class = $parameter->getClass()) { + $parameter_string .= '\\' . $class->getName() . ' '; + } + + if ($parameter->isPassedByReference()) { + $parameter_string .= '&'; + } + + $parameter_string .= '$' . $parameter->getName(); + + if ($parameter->isDefaultValueAvailable()) { + $parameter_string .= ' = '; + $parameter_string .= var_export($parameter->getDefaultValue(), TRUE); + } + + return $parameter_string; + } + + /** + * Builds the body of a wrapped method. + * + * @param \ReflectionMethod $reflection_method + * A reflection method for the method. + * + * @return string + */ + protected function buildMethodBody(\ReflectionMethod $reflection_method) { + $output = ''; + + $function_name = $reflection_method->getName(); + + $output .= ' return $this->lazyLoadItself()->' . $function_name . '('; + + // Add parameters; + $parameters = []; + foreach ($reflection_method->getParameters() as $parameter) { + $parameters[] = '$' . $parameter->getName(); + } + + $output .= implode(', ', $parameters) . ');'; + + return $output; + } + + /** + * Builds the constructor used to inject the actual service ID. + * + * @return string + */ + protected function buildConstructorMethod() { + $output = <<<'EOS' +public function __construct($service_id) { + $this->serviceId = $service_id; +} + +EOS; + + return $output; + } +} diff --git a/core/lib/Drupal/Component/ProxyBuilder/ProxyDumper.php b/core/lib/Drupal/Component/ProxyBuilder/ProxyDumper.php new file mode 100644 index 0000000..db3278a --- /dev/null +++ b/core/lib/Drupal/Component/ProxyBuilder/ProxyDumper.php @@ -0,0 +1,62 @@ +builder = $builder; + } + + /** + * {@inheritdoc} + */ + public function isProxyCandidate(Definition $definition) { + return $definition->isLazy() && ($class = $definition->getClass()) && class_exists($class); + } + + /** + * {@inheritdoc} + */ + public function getProxyFactoryCode(Definition $definition, $id) { + // Note: the specific get method is called initially with $lazyLoad=TRUE; + // When you want to retrieve the actual service, the code generated in + // ProxyBuilder calls the method with lazy loading disabled. + $output = <<<'EOS' + if ($lazyLoad) { + return $this->services['{{ id }}'] = new {{ class_name }}('{{ id }}'); + } + +EOS; + $output = str_replace('{{ id }}', $id, $output); + $output = str_replace('{{ class_name }}', $this->builder->buildProxyClassName($definition->getClass()), $output); + + return $output; + } + + /** + * {@inheritdoc} + */ + public function getProxyCode(Definition $definition) { + return $this->builder->build($definition->getClass()); + } + +} diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index d5e6059..8a993d6 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -7,6 +7,8 @@ namespace Drupal\Core; +use Drupal\Component\ProxyBuilder\ProxyBuilder; +use Drupal\Component\ProxyBuilder\ProxyDumper; use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Timer; use Drupal\Component\Utility\Unicode; @@ -1151,6 +1153,7 @@ protected function dumpDrupalContainer(ContainerBuilder $container, $baseClass) } // Cache the container. $dumper = new PhpDumper($container); + $dumper->setProxyDumper(new ProxyDumper(new ProxyBuilder())); $class = $this->getClassName(); $content = $dumper->dump(array('class' => $class, 'base_class' => $baseClass)); return $this->storage()->save($class . '.php', $content); diff --git a/core/tests/Drupal/Tests/Component/ProxyBuilder/ProxyBuilderTest.php b/core/tests/Drupal/Tests/Component/ProxyBuilder/ProxyBuilderTest.php new file mode 100644 index 0000000..c8acb00 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/ProxyBuilder/ProxyBuilderTest.php @@ -0,0 +1,261 @@ +proxyBuilder = new ProxyBuilder(); + } + + /** + * @covers ::buildProxyClassName() + */ + public function testBuildProxyClassName() { + $class_name = $this->proxyBuilder->buildProxyClassName('Drupal\Tests\Component\ProxyBuilder\TestServiceNoMethod'); + $this->assertEquals('Drupal_Tests_Component_ProxyBuilder_TestServiceNoMethod_Proxy', $class_name); + } + + /** + * Tests the basic methods like the constructor and the lazyLoadItself method. + * + * @covers ::build() + * @covers ::buildConstructorMethod() + * @covers ::buildLazyLoadItselfMethod() + */ + public function testBuildNoMethod() { + $class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceNoMethod'; + + $result = $this->proxyBuilder->build($class); + $this->assertEquals($this->buildExpectedClass($class, ''), $result); + } + + /** + * @covers ::buildMethod() + * @covers ::buildMethodBody() + */ + public function testBuildSimpleMethod() { + $class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceSimpleMethod'; + + $result = $this->proxyBuilder->build($class); + + $method_body = ' + public function method() { + return $this->lazyLoadItself()->method(); + } +'; + $this->assertEquals($this->buildExpectedClass($class, $method_body), $result); + } + + /** + * @covers ::buildMethod() + * @covers ::buildParameter() + * @covers ::buildMethodBody() + */ + public function testBuildMethodWithParameter() { + $class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceMethodWithParameter'; + + $result = $this->proxyBuilder->build($class); + + $method_body = ' + public function methodWithParameter($parameter) { + return $this->lazyLoadItself()->methodWithParameter($parameter); + } +'; + $this->assertEquals($this->buildExpectedClass($class, $method_body), $result); + } + + /** + * @covers ::buildMethod() + * @covers ::buildParameter() + * @covers ::buildMethodBody() + */ + public function testBuildComplexMethod() { + $class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceComplexMethod'; + + $result = $this->proxyBuilder->build($class); + + // @todo Solve the silly linebreak for array() + $method_body = ' + public function complexMethod($parameter, callable $function, \Drupal\Tests\Component\ProxyBuilder\TestServiceNoMethod $test_service = NULL, array &$elements = array ( + )) { + return $this->lazyLoadItself()->complexMethod($parameter, $function, $test_service, $elements); + } +'; + $this->assertEquals($this->buildExpectedClass($class, $method_body), $result); + } + + /** + * @covers ::buildMethod() + * @covers ::buildParameter() + * @covers ::buildMethodBody() + */ + public function testBuildWithInterface() { + $class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceWithInterface'; + + $result = $this->proxyBuilder->build($class); + + // @todo Solve the silly linebreak for array() + $method_body = ' + public function testMethod($parameter) { + return $this->lazyLoadItself()->testMethod($parameter); + } +'; + + $interface_string = ' implements \Drupal\Tests\Component\ProxyBuilder\TestInterface'; + $this->assertEquals($this->buildExpectedClass($class, $method_body, $interface_string), $result); + } + + /** + * @covers ::buildMethod() + * @covers ::buildParameter() + * @covers ::buildMethodBody() + */ + public function testBuildWithProtectedAndPrivateMethod() { + $class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceWithProtectedMethods'; + + $result = $this->proxyBuilder->build($class); + + // @todo Solve the silly linebreak for array() + $method_body = ' + public function testMethod($parameter) { + return $this->lazyLoadItself()->testMethod($parameter); + } +'; + +$this->assertEquals($this->buildExpectedClass($class, $method_body), $result); + } + + /** + * Constructs the expected class output. + * + * @param string $expected_methods_body + * The expected body of decorated methods. + * + * @return string + * The code of the entire proxy. + */ + protected function buildExpectedClass($class, $expected_methods_body, $interface_string = ' ') { + $proxy_class = $this->proxyBuilder->buildProxyClassName($class); + $expected_string = <<<'EOS' +class {{ proxy_class }} {{ interface_string }} { + + /** + * @var string + */ + protected $serviceId; + + /** + * @var \{{ class }} + */ + protected $service; + + public function __construct($service_id) { + $this->serviceId = $service_id; + } + + protected function lazyLoadItself() { + if (!isset($this->service)) { + $method_name = 'get' . Container::camelize($this->serviceId) . 'Service'; + $this->service = \Drupal::getContainer()->$method_name(FALSE); + } + + return $this->service; + } + +{{ expected_methods_body }} + +} + +EOS; + $expected_string = str_replace('{{ proxy_class }}', $proxy_class, $expected_string); + $expected_string = str_replace('{{ class }}', $class, $expected_string); + $expected_string = str_replace('{{ expected_methods_body }}', $expected_methods_body, $expected_string); + $expected_string = str_replace('{{ interface_string }}', $interface_string, $expected_string); + + return $expected_string; + } + +} + +class TestServiceNoMethod { + +} + +class TestServiceSimpleMethod { + + public function method() { + + } + +} + +class TestServiceMethodWithParameter { + + public function methodWithParameter($parameter) { + + } + +} + +class TestServiceComplexMethod { + + public function complexMethod($parameter, callable $function, TestServiceNoMethod $test_service = NULL, array &$elements = array()) { + + } + +} + +interface TestInterface { + + public function testMethod($parameter); + +} + +class TestServiceWithInterface implements TestInterface { + + public function testMethod($parameter) { + + } + +} + +class TestServiceWithProtectedMethods { + + public function testMethod($parameter) { + + } + + protected function protectedMethod($parameter) { + + } + + protected function privateMethod($parameter) { + + } + +} diff --git a/core/tests/Drupal/Tests/Component/ProxyBuilder/ProxyDumperTest.php b/core/tests/Drupal/Tests/Component/ProxyBuilder/ProxyDumperTest.php new file mode 100644 index 0000000..70b6a7b --- /dev/null +++ b/core/tests/Drupal/Tests/Component/ProxyBuilder/ProxyDumperTest.php @@ -0,0 +1,104 @@ +proxyBuilder = $this->getMockBuilder('Drupal\Component\ProxyBuilder\ProxyBuilder') + ->disableOriginalConstructor() + ->setMethods(['build']) + ->getMock(); + $this->proxyDumper = new ProxyDumper($this->proxyBuilder); + } + + /** + * @dataProvider providerTestIsProxyCandidate + * @covers ::isProxyCandidate() + */ + public function testIsProxyCandidate(Definition $definition, $expected) { + $this->assertSame($expected, $this->proxyDumper->isProxyCandidate($definition)); + } + + public function providerTestIsProxyCandidate() { + // Not lazy service. + $data = []; + $definition = new Definition('Drupal\Tests\Component\ProxyBuilder\TestService'); + $data[] = [$definition, FALSE]; + // Not existing service. + $definition = new Definition('Drupal\Tests\Component\ProxyBuilder\TestNotExistingService'); + $definition->setLazy(TRUE); + $data[] = [$definition, FALSE]; + // Existing and lazy service. + $definition = new Definition('Drupal\Tests\Component\ProxyBuilder\TestService'); + $definition->setLazy(TRUE); + $data[] = [$definition, TRUE]; + // Existing and lazy service. + + return $data; + } + + public function testGetProxyFactoryCode() { + $definition = new Definition('Drupal\Tests\Component\ProxyBuilder\TestService'); + $definition->setLazy(TRUE); + + $result = $this->proxyDumper->getProxyFactoryCode($definition, 'test_service'); + + $this->assertEquals( +" if (\$lazyLoad) { + return \$this->services['test_service'] = new Drupal_Tests_Component_ProxyBuilder_TestService_Proxy('test_service'); + } +", $result); + } + + public function testGetProxyCode() { + $definition = new Definition('Drupal\Tests\Component\ProxyBuilder\TestService'); + $definition->setLazy(TRUE); + + $class = 'class Drupal_Tests_Component_ProxyBuilder_TestService_Proxy {}'; + $this->proxyBuilder->expects($this->once()) + ->method('build') + ->with('Drupal\Tests\Component\ProxyBuilder\TestService') + ->willReturn($class); + + $result = $this->proxyDumper->getProxyCode($definition); + $this->assertEquals($class, $result); + } + +} + +class TestService { + +}