Commit 2f628af6 authored by alexpott's avatar alexpott

Issue #1973618 by dawehner, donquixote, plach, Mile23: DIC: Lazy instantiation...

Issue #1973618 by dawehner, donquixote, plach, Mile23: DIC: Lazy instantiation of service dependencies (ProxyManager for "proxy services")
parent 3bbe5619
......@@ -179,6 +179,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']
......@@ -397,6 +398,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:
......
<?php
/**
* @file
* Contains \Drupal\Component\ProxyBuilder\ProxyBuilder.
*/
namespace Drupal\Component\ProxyBuilder;
/**
* Generates the string representation of the proxy service.
*/
class ProxyBuilder {
/**
* Generates the used proxy class name from a given class name.
*
* @param string $class_name
* The class name of the actual service.
*
* @return string
* The class name of the proxy.
*/
public static function buildProxyClassName($class_name) {
return str_replace('\\', '_', $class_name) . '_Proxy';
}
/**
* Builds a proxy class string.
*
* @param string $class_name
* The class name of the actual service.
*
* @return string
* The full string with namespace class and methods.
*/
public function build($class_name) {
$reflection = new \ReflectionClass($class_name);
$output = '';
$class_documentation = <<<'EOS'
/**
* Provides a proxy class for \{{ class_name }}.
*
* @see \Drupal\Component\ProxyBuilder
*/
EOS;
$class_start = 'class {{ proxy_class_name }}';
if ($interfaces = $reflection->getInterfaceNames()) {
foreach ($interfaces as &$interface) {
$interface = '\\' . $interface;
}
$class_start .= ' implements ' . implode(', ', $interfaces);
}
$output .= $this->buildUseStatements();
// The actual class;
$properties = <<<'EOS'
/**
* @var string
*/
protected $serviceId;
/**
* @var \{{ class_name }}
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
EOS;
$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)));
$final_output = $class_documentation . $class_start . "\n{\n\n" . $output . "\n}\n";
$final_output = str_replace('{{ class_name }}', $class_name, $final_output);
$final_output = str_replace('{{ proxy_class_name }}', $this->buildProxyClassName($class_name), $final_output);
return $final_output;
}
/**
* 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 = $this->container->$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();
$reference = '';
if ($reflection_method->returnsReference()) {
$reference = '&';
}
if ($reflection_method->isStatic()) {
$signature_line = 'public static function ' . $reference . $function_name . '(';
}
else {
$signature_line = 'public function ' . $reference . $function_name . '(';
}
$signature_line .= implode(', ', $parameters);
$signature_line .= ')';
$output = $signature_line . "\n{\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();
if (!$reflection_method->isStatic()) {
$output .= ' return $this->lazyLoadItself()->' . $function_name . '(';
}
else {
$class_name = $reflection_method->getDeclaringClass()->getName();
$output .= " \\$class_name::$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(\Symfony\Component\DependencyInjection\ContainerInterface $container, $serviceId)
{
$this->container = $container;
$this->serviceId = $serviceId;
}
EOS;
return $output;
}
/**
* Build the required use statements of the proxy class.
*
* @return string
*/
protected function buildUseStatements() {
$output = '';
return $output;
}
}
<?php
/**
* @file
* Contains \Drupal\Component\ProxyBuilder\ProxyDumper.
*/
namespace Drupal\Component\ProxyBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface;
/**
* Dumps the proxy service into the dumped PHP container file.
*/
class ProxyDumper implements DumperInterface {
/**
* The proxy builder.
*
* @var \Drupal\Component\ProxyBuilder\ProxyBuilder
*/
protected $builder;
public function __construct(ProxyBuilder $builder) {
$this->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 }}($this, '{{ 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());
}
}
{
"name": "drupal/proxy-builder",
"description": "Provides a lightweight mechanism to provide lazy loaded proxies.",
"keywords": ["drupal", "proxy"],
"homepage": "https://drupal.org/project/drupal",
"license": "GPL-2.0+",
"require": {
"php": ">=5.4.2",
"symfony/dependency-injection": "~2.6"
},
"autoload": {
"psr-4": {
"Drupal\\Component\\ProxyBuilder\\": ""
}
}
}
\ No newline at end of file
......@@ -7,6 +7,7 @@
namespace Drupal\Core;
use Drupal\Component\ProxyBuilder\ProxyDumper;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Timer;
use Drupal\Component\Utility\Unicode;
......@@ -22,6 +23,7 @@
use Drupal\Core\Language\Language;
use Drupal\Core\PageCache\RequestPolicyInterface;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\Core\ProxyBuilder\ProxyBuilder;
use Drupal\Core\Site\Settings;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -1089,6 +1091,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);
......
<?php
/**
* @file
* Contains \Drupal\Core\ProxyBuilder\ProxyBuilder.
*/
namespace Drupal\Core\ProxyBuilder;
use Drupal\Component\ProxyBuilder\ProxyBuilder as BaseProxyBuilder;
/**
* Extend the component proxy builder by using the DependencySerialziationTrait.
*/
class ProxyBuilder extends BaseProxyBuilder {
/**
* {@inheritdoc{
*/
protected function buildUseStatements() {
$output = parent::buildUseStatements();
$output .= 'use \Drupal\Core\DependencyInjection\DependencySerializationTrait;' . "\n\n";
return $output;
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Component\ProxyBuilder\ProxyBuilderTest.
*/
namespace Drupal\Tests\Component\ProxyBuilder;
use Drupal\Component\ProxyBuilder\ProxyBuilder;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\ProxyBuilder\ProxyBuilder
* @group proxy_builder
*/
class ProxyBuilderTest extends UnitTestCase {
/**
* The tested proxy builder.
*
* @var \Drupal\Component\ProxyBuilder\ProxyBuilder
*/
protected $proxyBuilder;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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 = <<<'EOS'
public function method()
{
return $this->lazyLoadItself()->method();
}
EOS;
$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 = <<<'EOS'
public function methodWithParameter($parameter)
{
return $this->lazyLoadItself()->methodWithParameter($parameter);
}
EOS;
$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 = <<<'EOS'
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);
}
EOS;
$this->assertEquals($this->buildExpectedClass($class, $method_body), $result);
}
/**
* @covers ::buildMethod()
* @covers ::buildMethodBody()
*/
public function testBuildReturnReference() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceReturnReference';
$result = $this->proxyBuilder->build($class);
// @todo Solve the silly linebreak for array()
$method_body = <<<'EOS'
public function &returnReference()
{
return $this->lazyLoadItself()->returnReference();
}
EOS;
$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);
$method_body = <<<'EOS'
public function testMethod($parameter)
{
return $this->lazyLoadItself()->testMethod($parameter);
}
EOS;
$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);
$method_body = <<<'EOS'
public function testMethod($parameter)
{
return $this->lazyLoadItself()->testMethod($parameter);
}
EOS;
$this->assertEquals($this->buildExpectedClass($class, $method_body), $result);
}
/**
* @covers ::buildMethod()
* @covers ::buildParameter()
* @covers ::buildMethodBody()
*/
public function testBuildWithPublicStaticMethod() {
$class = 'Drupal\Tests\Component\ProxyBuilder\TestServiceWithPublicStaticMethod';
$result = $this->proxyBuilder->build($class);
// Ensure that the static method is not wrapped.
$method_body = <<<'EOS'
public static function testMethod($parameter)
{
\Drupal\Tests\Component\ProxyBuilder\TestServiceWithPublicStaticMethod::testMethod($parameter);
}
EOS;
$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'
/**
* Provides a proxy class for \{{ class }}.
*
* @see \Drupal\Component\ProxyBuilder
*/
class {{ proxy_class }}{{ interface_string }}
{
/**
* @var string
*/
protected $serviceId;
/**
* @var \{{ class }}
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $serviceId)
{
$this->container = $container;
$this->serviceId = $serviceId;
}
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$method_name = 'get' . Container::camelize($this->serviceId) . 'Service';
$this->service = $this->container->$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) {
}
}