Commit 9f521c8d authored by catch's avatar catch

Issue #2408371 by dawehner, Fabianx, alexpott, bforchhammer: Proxies of module...

Issue #2408371 by dawehner, Fabianx, alexpott, bforchhammer: Proxies of module interfaces don't work
parent b1a6c3dc
......@@ -22,7 +22,30 @@ class ProxyBuilder {
* The class name of the proxy.
*/
public static function buildProxyClassName($class_name) {
return str_replace('\\', '_', $class_name) . '_Proxy';
$match = [];
preg_match('/([a-zA-Z0-9_]+\\\\[a-zA-Z0-9_]+)\\\\(.+)/', $class_name, $match);
$root_namespace = $match[1];
$rest_fqcn = $match[2];
$proxy_class_name = $root_namespace . '\\ProxyClass\\' . $rest_fqcn;
return $proxy_class_name;
}
/**
* Generates the used proxy namespace from a given class name.
*
* @param string $class_name
* The class name of the actual service.
*
* @return string
* The namespace name of the proxy.
*/
public static function buildProxyNamespace($class_name) {
$proxy_classname = static::buildProxyClassName($class_name);
preg_match('/(.+)\\\\[a-zA-Z0-9]+/', $proxy_classname, $match);
$proxy_namespace = $match[1];
return $proxy_namespace;
}
/**
......@@ -30,23 +53,38 @@ public static function buildProxyClassName($class_name) {
*
* @param string $class_name
* The class name of the actual service.
* @param string $proxy_class_name
* (optional) The class name of the proxy service.
*
* @return string
* The full string with namespace class and methods.
*/
public function build($class_name) {
public function build($class_name, $proxy_class_name = '') {
$reflection = new \ReflectionClass($class_name);
if ($proxy_class_name) {
$proxy_class_reflection = new \ReflectionClass($proxy_class_name);
$proxy_namespace = $proxy_class_reflection->getNamespaceName();
}
else {
$proxy_class_name = $this->buildProxyClassName($class_name);
$proxy_namespace = $this->buildProxyNamespace($class_name);
$proxy_class_shortname = str_replace($proxy_namespace . '\\', '', $proxy_class_name);
}
$output = '';
$class_documentation = <<<'EOS'
/**
* Provides a proxy class for \{{ class_name }}.
*
* @see \Drupal\Component\ProxyBuilder
*/
namespace {{ namespace }}{
/**
* Provides a proxy class for \{{ class_name }}.
*
* @see \Drupal\Component\ProxyBuilder
*/
EOS;
$class_start = 'class {{ proxy_class_name }}';
$class_start = ' class {{ proxy_class_shortname }}';
// For cases in which the implemented interface is a child of another
// interface, getInterfaceNames() also returns the parent. This causes a
......@@ -77,11 +115,15 @@ public function build($class_name) {
// The actual class;
$properties = <<<'EOS'
/**
* The id of the original proxied service.
*
* @var string
*/
protected $serviceId;
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \{{ class_name }}
*/
protected $service;
......@@ -123,13 +165,14 @@ public function build($class_name) {
if ($value === '') {
return $value;
}
return " $value";
return " $value";
}, explode("\n", $output)));
$final_output = $class_documentation . $class_start . "\n{\n\n" . $output . "\n}\n";
$final_output = $class_documentation . $class_start . "\n {\n\n" . $output . "\n }\n\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);
$final_output = str_replace('{{ namespace }}', $proxy_namespace ? $proxy_namespace . ' ' : '', $final_output);
$final_output = str_replace('{{ proxy_class_shortname }}', $proxy_class_shortname, $final_output);
return $final_output;
}
......@@ -141,11 +184,16 @@ public function build($class_name) {
*/
protected function buildLazyLoadItselfMethod() {
$output = <<<'EOS'
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$method_name = 'get' . Container::camelize($this->serviceId) . 'Service';
$this->service = $this->container->$method_name(false);
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
......@@ -177,11 +225,19 @@ protected function buildMethod(\ReflectionMethod $reflection_method) {
if ($reflection_method->returnsReference()) {
$reference = '&';
}
$signature_line = <<<'EOS'
/**
* {@inheritdoc}
*/
EOS;
if ($reflection_method->isStatic()) {
$signature_line = 'public static function ' . $reference . $function_name . '(';
$signature_line .= 'public static function ' . $reference . $function_name . '(';
}
else {
$signature_line = 'public function ' . $reference . $function_name . '(';
$signature_line .= 'public function ' . $reference . $function_name . '(';
}
$signature_line .= implode(', ', $parameters);
......@@ -269,10 +325,18 @@ protected function buildMethodBody(\ReflectionMethod $reflection_method) {
*/
protected function buildConstructorMethod() {
$output = <<<'EOS'
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $serviceId)
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->serviceId = $serviceId;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
EOS;
......
<?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 {
/**
* Keeps track of already existing proxy classes.
*
* @var array
*/
protected $buildClasses = [];
/**
* 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) {
// Maybe the same class is used in different services, which are both marked
// as lazy (just think about 2 database connections).
// In those cases we should not generate proxy code the second time.
if (!isset($this->buildClasses[$definition->getClass()])) {
$this->buildClasses[$definition->getClass()] = TRUE;
return $this->builder->build($definition->getClass());
}
else {
return '';
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Command\GenerateProxyClassApplication.
*/
namespace Drupal\Core\Command;
use Drupal\Component\ProxyBuilder\ProxyBuilder;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
/**
* Provides a console command to generate proxy classes.
*/
class GenerateProxyClassApplication extends Application {
/**
* The proxy builder.
*
* @var \Drupal\Component\ProxyBuilder\ProxyBuilder
*/
protected $proxyBuilder;
/**
* Constructs a new GenerateProxyClassApplication instance.
*
* @param \Drupal\Component\ProxyBuilder\ProxyBuilder $proxy_builder
* The proxy builder.
*/
public function __construct(ProxyBuilder $proxy_builder) {
$this->proxyBuilder = $proxy_builder;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function getCommandName(InputInterface $input) {
return 'generate-proxy-class';
}
/**
* {@inheritdoc}
*/
protected function getDefaultCommands() {
// Even though this is a single command, keep the HelpCommand (--help).
$default_commands = parent::getDefaultCommands();
$default_commands[] = new GenerateProxyClassCommand($this->proxyBuilder);
return $default_commands;
}
/**
* {@inheritdoc}
*
* Overridden so the application doesn't expect the command name as the first
* argument.
*/
public function getDefinition() {
$definition = parent::getDefinition();
// Clears the normal first argument (the command name).
$definition->setArguments();
return $definition;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Command\GenerateProxyClassCommand.
*/
namespace Drupal\Core\Command;
use Drupal\Component\ProxyBuilder\ProxyBuilder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Provides a console command to generate proxy classes.
*/
class GenerateProxyClassCommand extends Command {
/**
* The proxy builder.
*
* @var \Drupal\Component\ProxyBuilder\ProxyBuilder
*/
protected $proxyBuilder;
/**
* Constructs a new GenerateProxyClassCommand instance.
*
* @param \Drupal\Component\ProxyBuilder\ProxyBuilder $proxy_builder
* The proxy builder.
*/
public function __construct(ProxyBuilder $proxy_builder) {
parent::__construct();
$this->proxyBuilder = $proxy_builder;
}
/**
* {@inheritdoc}
*/
protected function configure() {
$this->setName('generate-proxy-class')
->setDefinition([
new InputArgument('class_name', InputArgument::REQUIRED, 'The class to be proxied'),
new InputArgument('namespace_root_path', InputArgument::REQUIRED, 'The filepath to the root of the namespace.'),
])
->setDescription('Dumps a generated proxy class into its appropriate namespace.')
->addUsage('\'Drupal\Core\Batch\BatchStorage\' "core/lib/Drupal/Core"')
->addUsage('\'Drupal\block\BlockRepository\' "core/modules/block/src"')
->addUsage('\'Drupal\mymodule\MyClass\' "modules/contrib/mymodule/src"');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$class_name = $input->getArgument('class_name');
$namespace_root = $input->getArgument('namespace_root_path');
$match = [];
preg_match('/([a-zA-Z0-9_]+\\\\[a-zA-Z0-9_]+)\\\\(.+)/', $class_name, $match);
if ($match) {
$root_namespace = $match[1];
$rest_fqcn = $match[2];
$proxy_filename = $namespace_root . '/ProxyClass/' . str_replace('\\', '/', $rest_fqcn) . '.php';
$proxy_class_name = $root_namespace . '\\ProxyClass\\' . $rest_fqcn;
$proxy_class_string = $this->proxyBuilder->build($class_name);
$file_string = <<<EOF
<?php
/**
* @file
* Contains {{ proxy_class_name }}.
*/
/**
* This file was generated via php core/scripts/generate-proxy-class.php '$class_name' "$namespace_root".
*/
{{ proxy_class_string }}
EOF;
$file_string = str_replace(['{{ proxy_class_name }}', '{{ proxy_class_string }}'], [$proxy_class_name, $proxy_class_string], $file_string);
mkdir(dirname($proxy_filename), 0775, TRUE);
file_put_contents($proxy_filename, $file_string);
$output->writeln(sprintf('Proxy of class %s written to %s', $class_name, $proxy_filename));
}
}
}
......@@ -10,6 +10,7 @@
use Drupal\Core\Cache\Context\CacheContextsPass;
use Drupal\Core\Cache\ListCacheBinsPass;
use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass;
use Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterLazyRouteEnhancers;
use Drupal\Core\DependencyInjection\Compiler\RegisterLazyRouteFilters;
use Drupal\Core\DependencyInjection\Compiler\DependencySerializationTraitPass;
......@@ -60,6 +61,8 @@ public function register(ContainerBuilder $container) {
// list-building passes are operating on the post-alter services list.
$container->addCompilerPass(new ModifyServiceDefinitionsPass());
$container->addCompilerPass(new ProxyServicesPass());
$container->addCompilerPass(new BackendCompilerPass());
$container->addCompilerPass(new StackedKernelPass());
......
<?php
/**
* @file
* Contains \Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass.
*/
namespace Drupal\Core\DependencyInjection\Compiler;
use Drupal\Component\ProxyBuilder\ProxyBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* Replaces all services with a lazy flag.
*/
class ProxyServicesPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container) {
foreach ($container->getDefinitions() as $service_id => $definition) {
if ($definition->isLazy()) {
$proxy_class = ProxyBuilder::buildProxyClassName($definition->getClass());
if (class_exists($proxy_class)) {
// Copy the existing definition to a new entry.
$definition->setLazy(FALSE);
// Ensure that the service is accessible.
$definition->setPublic(TRUE);
$new_service_id = 'drupal.proxy_original_service.' . $service_id;
$container->setDefinition($new_service_id, $definition);
$container->register($service_id, $proxy_class)
->setArguments([new Reference('service_container'), $new_service_id]);
}
else {
$class_name = $definition->getClass();
// Find the root namespace.
$match = [];
preg_match('/([a-zA-Z0-9_]+\\\\[a-zA-Z0-9_]+)\\\\(.+)/', $class_name, $match);
$root_namespace = $match[1];
// Find the root namespace path.
$root_namespace_dir = '[namespace_root_path]';
$namespaces = $container->getParameter('container.namespaces');
// Hardcode Drupal Core, because it is not registered.
$namespaces['Drupal\Core'] = 'core/lib/Drupal/Core';
if (isset($namespaces[$root_namespace])) {
$root_namespace_dir = $namespaces[$root_namespace];
}
$message =<<<EOF
Missing proxy class '$proxy_class' for lazy service '$service_id'.
Use the following command to generate the proxy class:
php core/scripts/generate-proxy-class.php '$class_name' "$root_namespace_dir"
EOF;
trigger_error($message, E_USER_WARNING);
}
}
}
}
}
......@@ -8,7 +8,6 @@
namespace Drupal\Core;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\ProxyBuilder\ProxyDumper;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
......@@ -24,7 +23,6 @@
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;
......@@ -1180,7 +1178,6 @@ protected function dumpDrupalContainer(ContainerBuilder $container, $baseClass)
}
// Cache the container.
$dumper = new PhpDumper($container);
$dumper->setProxyDumper(new ProxyDumper(new ProxyBuilder()));
$class = $this->getClassName();
$namespace = $this->getClassNamespace();
$content = $dumper->dump([
......
......@@ -101,6 +101,20 @@ protected function sortGuessers() {
*/
public static function registerWithSymfonyGuesser(ContainerInterface $container) {
$singleton = SymfonyMimeTypeGuesser::getInstance();
// @todo Remove once Symfony adds a reset() method.
$property = new \ReflectionProperty(get_class($singleton), 'guessers');
$property->setAccessible(TRUE);
if (isset($singleton->_beforeDrupalRegistration)) {
// Reset state, else we store more and more services during test runs.
$property->setValue($singleton, $singleton->_beforeDrupalRegistration);
} else {
// Store original state before we register our services.
$singleton->_beforeDrupalRegistration = $property->getValue($singleton);
}
//$singleton->reset();
$singleton->register($container->get('file.mime_type.guesser'));
}
......
......@@ -58,7 +58,10 @@ public function register(ContainerBuilder $container) {
// Replace the route builder with an empty implementation.
// @todo Convert installer steps into routes; add an installer.routing.yml.
$definition = $container->getDefinition('router.builder');
$definition->setClass('Drupal\Core\Installer\InstallerRouteBuilder');
$definition->setClass('Drupal\Core\Installer\InstallerRouteBuilder')
// The core router builder, but there is no reason here to be lazy, so
// we don't need to ship with a custom proxy class.
->setLazy(FALSE);
}
/**
......
......@@ -14,7 +14,7 @@
/**
* Defines a class which is capable of clearing the cache on plugin managers.
*/
class CachedDiscoveryClearer {
class CachedDiscoveryClearer implements CachedDiscoveryClearerInterface {
/**
* The stored discoveries.
......@@ -24,18 +24,14 @@ class CachedDiscoveryClearer {
protected $cachedDiscoveries = array();
/**
* Adds a plugin manager to the active list.
*
* @param \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface $cached_discovery
* An object that implements the cached discovery interface, typically a
* plugin manager.
* {@inheritdoc}
*/
public function addCachedDiscovery(CachedDiscoveryInterface $cached_discovery) {
$this->cachedDiscoveries[] = $cached_discovery;
}
/**
* Clears the cache on all cached discoveries.
* {@inheritdoc}
*/
public function clearCachedDefinitions() {
foreach ($this->cachedDiscoveries as $cached_discovery) {
......
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\CachedDiscoveryClearerInterface.
*/
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
/**
* Provides a way to clear static caches of all plugin managers.
*/
interface CachedDiscoveryClearerInterface {
/**
* Adds a plugin manager to the active list.
*
* @param \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface $cached_discovery
* An object that implements the cached discovery interface, typically a
* plugin manager.
*/
public function addCachedDiscovery(CachedDiscoveryInterface $cached_discovery);
/**
* Clears the cache on all cached discoveries.
*/
public function clearCachedDefinitions();
}
<?php
/**
* @file
* Contains Drupal\Core\ProxyClass\Batch\BatchStorage.
*/
/**
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\Core\Batch\BatchStorage' "core/lib/Drupal/Core".
*/
namespace Drupal\Core\ProxyClass\Batch {
/**
* Provides a proxy class for \Drupal\Core\Batch\BatchStorage.
*
* @see \Drupal\Component\ProxyBuilder
*/
class BatchStorage implements \Drupal\Core\Batch\BatchStorageInterface
{
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* The id of the original proxied service.
*
* @var string
*/
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \Drupal\Core\Batch\BatchStorage
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
}