Commit 22fbcd41 authored by alexpott's avatar alexpott

Issue #2497243 by Fabianx, znerol, fgm, Wim Leers, darol100, jhedstrom,...

Issue #2497243 by Fabianx, znerol, fgm, Wim Leers, darol100, jhedstrom, hussainweb, pfrenssen, neclimdul, jibran, Nitesh Sethia, dawehner, chx, catch, benjy, Aki Tendo: Replace Symfony container with a Drupal one, stored in cache
parent 8cf5b80c
This diff is collapsed.
<?php
/**
* @file
* Contains \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper.
*/
namespace Drupal\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* PhpArrayDumper dumps a service container as a PHP array.
*
* The format of this dumper is a human-readable serialized PHP array, which is
* very similar to the YAML based format, but based on PHP arrays instead of
* YAML strings.
*
* It is human-readable, for a machine-optimized version based on this one see
* \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper.
*
* @see \Drupal\Component\DependencyInjection\PhpArrayContainer
*/
class PhpArrayDumper extends OptimizedPhpArrayDumper {
/**
* {@inheritdoc}
*/
public function getArray() {
$this->serialize = FALSE;
return parent::getArray();
}
/**
* {@inheritdoc}
*/
protected function dumpCollection($collection, &$resolve = FALSE) {
$code = array();
foreach ($collection as $key => $value) {
if (is_array($value)) {
$code[$key] = $this->dumpCollection($value);
}
else {
$code[$key] = $this->dumpValue($value);
}
}
return $code;
}
/**
* {@inheritdoc}
*/
protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return '@?' . $id;
}
return '@' . $id;
}
/**
* {@inheritdoc}
*/
protected function getParameterCall($name) {
return '%' . $name . '%';
}
/**
* {@inheritdoc}
*/
protected function supportsMachineFormat() {
return FALSE;
}
}
{
"name": "drupal/core-dependency-injection",
"description": "Dependency Injection container optimized for Drupal's needs.",
"keywords": ["drupal", "dependency injection"],
"type": "library",
"homepage": "https://www.drupal.org/project/drupal",
"license": "GPL-2.0+",
"support": {
"issues": "https://www.drupal.org/project/issues/drupal",
"irc": "irc://irc.freenode.net/drupal-contribute",
"source": "https://www.drupal.org/project/drupal/git-instructions"
},
"autoload": {
"psr-4": {
"Drupal\\Component\\DependencyInjection\\": ""
}
}
}
......@@ -7,17 +7,18 @@
namespace Drupal\Core\DependencyInjection;
use Symfony\Component\DependencyInjection\Container as SymfonyContainer;
use Drupal\Component\DependencyInjection\Container as DrupalContainer;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Extends the symfony container to set the service ID on the created object.
* Extends the Drupal container to set the service ID on the created object.
*/
class Container extends SymfonyContainer {
class Container extends DrupalContainer {
/**
* {@inheritdoc}
*/
public function set($id, $service, $scope = SymfonyContainer::SCOPE_CONTAINER) {
public function set($id, $service, $scope = ContainerInterface::SCOPE_CONTAINER) {
parent::set($id, $service, $scope);
// Ensure that the _serviceId property is set on synthetic services as well.
......
......@@ -23,12 +23,10 @@
use Drupal\Core\Http\TrustedHostsRequestFactory;
use Drupal\Core\Language\Language;
use Drupal\Core\PageCache\RequestPolicyInterface;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\Core\Site\Settings;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
......@@ -53,6 +51,55 @@
*/
class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
/**
* Holds the class used for dumping the container to a PHP array.
*
* In combination with swapping the container class this is useful to e.g.
* dump to the human-readable PHP array format to debug the container
* definition in an easier way.
*
* @var string
*/
protected $phpArrayDumperClass = '\Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper';
/**
* Holds the default bootstrap container definition.
*
* @var array
*/
protected $defaultBootstrapContainerDefinition = [
'parameters' => [],
'services' => [
'database' => [
'class' => 'Drupal\Core\Database\Connection',
'factory' => 'Drupal\Core\Database\Database::getConnection',
'arguments' => ['default'],
],
'cache.container' => [
'class' => 'Drupal\Core\Cache\DatabaseBackend',
'arguments' => ['@database', '@cache_tags_provider.container', 'container'],
],
'cache_tags_provider.container' => [
'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum',
'arguments' => ['@database'],
],
],
];
/**
* Holds the class used for instantiating the bootstrap container.
*
* @var string
*/
protected $bootstrapContainerClass = '\Drupal\Component\DependencyInjection\PhpArrayContainer';
/**
* Holds the bootstrap container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $bootstrapContainer;
/**
* Holds the container instance.
*
......@@ -97,13 +144,6 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
*/
protected $moduleData = array();
/**
* PHP code storage object to use for the compiled container.
*
* @var \Drupal\Component\PhpStorage\PhpStorageInterface
*/
protected $storage;
/**
* The class loader object.
*
......@@ -397,6 +437,8 @@ public function boot() {
FileCacheFactory::setConfiguration($configuration);
FileCacheFactory::setPrefix(Settings::getApcuPrefix('file_cache', $this->root));
$this->bootstrapContainer = new $this->bootstrapContainerClass(Settings::get('bootstrap_container_definition', $this->defaultBootstrapContainerDefinition));
// Initialize the container.
$this->initializeContainer();
......@@ -446,6 +488,19 @@ public function setContainer(ContainerInterface $container = NULL) {
return $this;
}
/**
* {@inheritdoc}
*/
public function getCachedContainerDefinition() {
$cache = $this->bootstrapContainer->get('cache.container')->get($this->getContainerCacheKey());
if ($cache) {
return $cache->data;
}
return NULL;
}
/**
* {@inheritdoc}
*/
......@@ -706,24 +761,14 @@ public function updateModules(array $module_list, array $module_filenames = arra
}
/**
* Returns the classname based on environment.
* Returns the container cache key based on the environment.
*
* @return string
* The class name.
* The cache key used for the service container.
*/
protected function getClassName() {
$parts = array('service_container', $this->environment, hash('crc32b', \Drupal::VERSION . Settings::get('deployment_identifier')));
return implode('_', $parts);
}
/**
* Returns the container class namespace based on the environment.
*
* @return string
* The class name.
*/
protected function getClassNamespace() {
return 'Drupal\\Core\\DependencyInjection\\Container\\' . $this->environment;
protected function getContainerCacheKey() {
$parts = array('service_container', $this->environment, \Drupal::VERSION, Settings::get('deployment_identifier'));
return implode(':', $parts);
}
/**
......@@ -770,28 +815,34 @@ protected function initializeContainer() {
}
// If the module list hasn't already been set in updateModules and we are
// not forcing a rebuild, then try and load the container from the disk.
// not forcing a rebuild, then try and load the container from the cache.
if (empty($this->moduleList) && !$this->containerNeedsRebuild) {
$fully_qualified_class_name = '\\' . $this->getClassNamespace() . '\\' . $this->getClassName();
// First, try to load from storage.
if (!class_exists($fully_qualified_class_name, FALSE)) {
$this->storage()->load($this->getClassName() . '.php');
}
// If the load succeeded or the class already existed, use it.
if (class_exists($fully_qualified_class_name, FALSE)) {
$container = new $fully_qualified_class_name;
}
$container_definition = $this->getCachedContainerDefinition();
}
// If there is still no container, build a new one from scratch.
if (!isset($container)) {
// If there is no container and no cached container definition, build a new
// one from scratch.
if (!isset($container) && !isset($container_definition)) {
$container = $this->compileContainer();
// Only dump the container if dumping is allowed. This is useful for
// KernelTestBase, which never wants to use the real container, but always
// the container builder.
if ($this->allowDumping) {
$dumper = new $this->phpArrayDumperClass($container);
$container_definition = $dumper->getArray();
}
}
// The container was rebuilt successfully.
$this->containerNeedsRebuild = FALSE;
// Only create a new class if we have a container definition.
if (isset($container_definition)) {
$class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container');
$container = new $class($container_definition);
}
$this->attachSynthetic($container);
$this->container = $container;
......@@ -816,9 +867,8 @@ protected function initializeContainer() {
\Drupal::setContainer($this->container);
// If needs dumping flag was set, dump the container.
$base_class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container');
if ($this->containerNeedsDumping && !$this->dumpDrupalContainer($this->container, $base_class)) {
$this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be written to disk');
if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) {
$this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be saved to cache.');
}
return $this->container;
......@@ -1034,9 +1084,8 @@ public function invalidateContainer() {
return;
}
// Also wipe the PHP Storage caches, so that the container is rebuilt
// for the next request.
$this->storage()->deleteAll();
// Also remove the container definition from the cache backend.
$this->bootstrapContainer->get('cache.container')->deleteAll();
}
/**
......@@ -1194,35 +1243,28 @@ protected function getContainerBuilder() {
}
/**
* Dumps the service container to PHP code in the config directory.
* Stores the container definition in a cache.
*
* This method is based on the dumpContainer method in the parent class, but
* that method is reliant on the Config component which we do not use here.
*
* @param ContainerBuilder $container
* The service container.
* @param string $baseClass
* The name of the container's base class
* @param array $container_definition
* The container definition to cache.
*
* @return bool
* TRUE if the container was successfully dumped to disk.
* TRUE if the container was successfully cached.
*/
protected function dumpDrupalContainer(ContainerBuilder $container, $baseClass) {
if (!$this->storage()->writeable()) {
return FALSE;
protected function cacheDrupalContainer(array $container_definition) {
$saved = TRUE;
try {
$this->bootstrapContainer->get('cache.container')->set($this->getContainerCacheKey(), $container_definition);
}
// Cache the container.
$dumper = new PhpDumper($container);
$class = $this->getClassName();
$namespace = $this->getClassNamespace();
$content = $dumper->dump([
'class' => $class,
'base_class' => $baseClass,
'namespace' => $namespace,
]);
return $this->storage()->save($class . '.php', $content);
catch (\Exception $e) {
// There is no way to get from the Cache API if the cache set was
// successful or not, hence an Exception is caught and the caller informed
// about the error condition.
$saved = FALSE;
}
return $saved;
}
/**
* Gets a http kernel from the container
......@@ -1233,18 +1275,6 @@ protected function getHttpKernel() {
return $this->container->get('http_kernel');
}
/**
* Gets the PHP code storage object to use for the compiled container.
*
* @return \Drupal\Component\PhpStorage\PhpStorageInterface
*/
protected function storage() {
if (!isset($this->storage)) {
$this->storage = PhpStorageFactory::get('service_container');
}
return $this->storage;
}
/**
* Returns the active configuration storage to use during building the container.
*
......
......@@ -58,6 +58,16 @@ public function getServiceProviders($origin);
*/
public function getContainer();
/**
* Returns the cached container definition - if any.
*
* This also allows inspecting a built container for debugging purposes.
*
* @return array|NULL
* The cached container definition or NULL if not found in cache.
*/
public function getCachedContainerDefinition();
/**
* Set the current site path.
*
......
......@@ -55,13 +55,11 @@ protected function prepareConfigDirectories() {
* A request object to use in booting the kernel.
* @param array $modules_enabled
* A list of modules to enable on the kernel.
* @param bool $read_only
* Build the kernel in a read only state.
*
* @return \Drupal\Core\DrupalKernel
* New kernel for testing.
*/
protected function getTestKernel(Request $request, array $modules_enabled = NULL, $read_only = FALSE) {
protected function getTestKernel(Request $request, array $modules_enabled = NULL) {
// Manually create kernel to avoid replacing settings.
$class_loader = require DRUPAL_ROOT . '/autoload.php';
$kernel = DrupalKernel::createFromRequest($request, $class_loader, 'testing');
......@@ -72,11 +70,6 @@ protected function getTestKernel(Request $request, array $modules_enabled = NULL
}
$kernel->boot();
if ($read_only) {
$php_storage = Settings::get('php_storage');
$php_storage['service_container']['class'] = 'Drupal\Component\PhpStorage\FileReadOnlyStorage';
$this->settingsSet('php_storage', $php_storage);
}
return $kernel;
}
......@@ -98,24 +91,19 @@ public function testCompileDIC() {
$kernel = $this->getTestKernel($request);
$container = $kernel->getContainer();
$refClass = new \ReflectionClass($container);
$is_compiled_container =
$refClass->getParentClass()->getName() == 'Drupal\Core\DependencyInjection\Container' &&
!$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder');
$is_compiled_container = !$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder');
$this->assertTrue($is_compiled_container);
// Verify that the list of modules is the same for the initial and the
// compiled container.
$module_list = array_keys($container->get('module_handler')->getModuleList());
$this->assertEqual(array_values($modules_enabled), $module_list);
// Now use the read-only storage implementation, simulating a "production"
// environment.
$container = $this->getTestKernel($request, NULL, TRUE)
// Get the container another time, simulating a "production" environment.
$container = $this->getTestKernel($request, NULL)
->getContainer();
$refClass = new \ReflectionClass($container);
$is_compiled_container =
$refClass->getParentClass()->getName() == 'Drupal\Core\DependencyInjection\Container' &&
!$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder');
$is_compiled_container = !$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder');
$this->assertTrue($is_compiled_container);
// Verify that the list of modules is the same for the initial and the
......@@ -137,16 +125,16 @@ public function testCompileDIC() {
// Add another module so that we can test that the new module's bundle is
// registered to the new container.
$modules_enabled['service_provider_test'] = 'service_provider_test';
$this->getTestKernel($request, $modules_enabled, TRUE);
$this->getTestKernel($request, $modules_enabled);
// Instantiate it a second time and we should still get a ContainerBuilder
// class because we are using the read-only PHP storage.
$kernel = $this->getTestKernel($request, $modules_enabled, TRUE);
// Instantiate it a second time and we should not get a ContainerBuilder
// class because we are loading the container definition from cache.
$kernel = $this->getTestKernel($request, $modules_enabled);
$container = $kernel->getContainer();
$refClass = new \ReflectionClass($container);
$is_container_builder = $refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder');
$this->assertTrue($is_container_builder, 'Container is a builder');
$this->assertFalse($is_container_builder, 'Container is not a builder');
// Assert that the new module's bundle was registered to the new container.
$this->assertTrue($container->has('service_provider_test_class'), 'Container has test service');
......
......@@ -7,14 +7,14 @@
namespace Drupal\system\Tests\ServiceProvider;
use Drupal\simpletest\WebTestBase;
use Drupal\simpletest\KernelTestBase;
/**
* Tests service provider registration to the DIC.
*
* @group ServiceProvider
*/
class ServiceProviderTest extends WebTestBase {
class ServiceProviderTest extends KernelTestBase {
/**
* Modules to enable.
......@@ -27,13 +27,9 @@ class ServiceProviderTest extends WebTestBase {
* Tests that services provided by module service providers get registered to the DIC.
*/
function testServiceProviderRegistration() {
$this->assertTrue(\Drupal::getContainer()->getDefinition('file.usage')->getClass() == 'Drupal\\service_provider_test\\TestFileUsage', 'Class has been changed');
$definition = $this->container->getDefinition('file.usage');
$this->assertTrue($definition->getClass() == 'Drupal\\service_provider_test\\TestFileUsage', 'Class has been changed');
$this->assertTrue(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service has been registered to the DIC');
// The event subscriber method in the test class calls drupal_set_message with
// a message saying it has fired. This will fire on every page request so it
// should show up on the front page.
$this->drupalGet('');
$this->assertText(t('The service_provider_test event subscriber fired!'), 'The service_provider_test event subscriber fired');
}
/**
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\ServiceProvider\ServiceProviderWebTest.
*/
namespace Drupal\system\Tests\ServiceProvider;
use Drupal\simpletest\WebTestBase;
/**
* Tests service provider registration to the DIC.
*
* @group ServiceProvider
*/
class ServiceProviderWebTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('file', 'service_provider_test');
/**
* Tests that module service providers get registered to the DIC.
*
* Also tests that services provided by module service providers get
* registered to the DIC.
*/
public function testServiceProviderRegistrationIntegration() {
$this->assertTrue(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service has been registered to the DIC');
// The event subscriber method in the test class calls drupal_set_message()
// with a message saying it has fired. This will fire on every page request
// so it should show up on the front page.
$this->drupalGet('');
$this->assertText(t('The service_provider_test event subscriber fired!'), 'The service_provider_test event subscriber fired');
}
}
......@@ -170,14 +170,7 @@ public function testErrorContainer() {
'required' => TRUE,
];
$this->writeSettings($settings);
// Need to rebuild the container, so the dumped container can be tested
// and not the container builder.
\Drupal::service('kernel')->rebuildContainer();
// Ensure that we don't use the now broken generated container on the test
// process.
\Drupal::setContainer($this->container);
\Drupal::service('kernel')->invalidateContainer();
$this->expectedExceptionMessage = 'Argument 1 passed to Drupal\system\Tests\Bootstrap\ErrorContainer::Drupal\system\Tests\Bootstrap\{closur';
$this->drupalGet('');
......@@ -196,14 +189,7 @@ public function testExceptionContainer() {
'required' => TRUE,
];
$this->writeSettings($settings);
// Need to rebuild the container, so the dumped container can be tested
// and not the container builder.
\Drupal::service('kernel')->rebuildContainer();
// Ensure that we don't use the now broken generated container on the test
// process.
\Drupal::setContainer($this->container);
\Drupal::service('kernel')->invalidateContainer();
$this->expectedExceptionMessage = 'Thrown exception during Container::get';
$this->drupalGet('');
......
<?php
/**
* @file
* Contains \Drupal\Tests\Component\DependencyInjection\Dumper\PhpArrayDumperTest.
*/
namespace Drupal\Tests\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @coversDefaultClass \Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper
* @group DependencyInjection
*/
class PhpArrayDumperTest extends OptimizedPhpArrayDumperTest {
/**
* {@inheritdoc}
*/
public function setUp() {
$this->machineFormat = FALSE;
$this->dumperClass = '\Drupal\Component\DependencyInjection\Dumper\PhpArrayDumper';
parent::setUp();
}
/**
* {@inheritdoc}
*/
protected function serializeDefinition(array $service_definition) {
return $service_definition;
}
/**
* {@inheritdoc}
*/
protected function getServiceCall($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
if ($invalid_behavior !== ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
return sprintf('@?%s', $id);
}
return sprintf('@%s', $id);
}
/**
* {@inheritdoc}
*/
protected function getParameterCall($name) {
return '%' . $name . '%';
}
/**
* {@inheritdoc}
*/
protected function getCollection($collection, $resolve = TRUE) {
return $collection;
}
}
<?php
/**
* @file
* Contains a test function for container 'file' include testing.
*/
/**
* Test function for container testing.
*
* @return string
* A string just for testing.
*/
function container_test_file_service_test_service_function() {
return 'Hello Container';
}
<?php
/**
* @file
* Contains \Drupal\Tests\Component\DependencyInjection\PhpArrayContainerTest.
*/
namespace Drupal\Tests\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\LogicException;
/**
* @coversDefaultClass \Drupal\Component\DependencyInjection\PhpArrayContainer
* @group DependencyInjection
*/
class PhpArrayContainerTest extends ContainerTest {
/**
* {@inheritdoc}
*/
public function setUp() {