diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 7b52d13..cd11ce5 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -5,10 +5,33 @@ * Contains Drupal. */ +use Drupal\Core\DependencyInjection\ContainerNotInitializedException; +use Drupal\Core\DependencyInjection\PlaceholderContainer; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Url; /** + * Initialize \Drupal::$container with a placeholder object. + * See https://www.drupal.org/node/2363341 + * + * This technique is the most reliable way to initialize static properties with + * non-trivial expressions. It should NOT be used for anything else. Also, the + * code being called MUST NOT have any side effects other than initializing the + * static properties. + * + * In general (e.g. in PSR-1), a PHP file should either declare symbols OR have + * side-effects, but not both. This specific case is ok only because the side + * effect applies to nothing else but the class declared in the same file, and + * it happens immediately after the class is being declared. A version of the + * class without this initialization applied is never available to the outside + * world. + * + * Note: PHP does not care whether this is called before or after the class + * declaration. It is called before only for better visibility. + */ +\Drupal::initStaticProperties(); + +/** * Static Service Container wrapper. * * Generally, code in Drupal should accept its dependencies via either @@ -103,15 +126,32 @@ class Drupal { * Sets a new global container. * * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * A new container instance to replace the current. NULL may be passed by - * testing frameworks to ensure that the global state of a previous - * environment does not leak into a test. + * A new container instance to replace the current. */ - public static function setContainer(ContainerInterface $container = NULL) { + public static function setContainer(ContainerInterface $container) { static::$container = $container; } /** + * Unsets the global container. + * + * @param string|null $message + * The message to pass to the placeholder container. + */ + public static function unsetContainer($message = NULL) { + $message = $message ?: '\Drupal::$container was unset with \Drupal::unsetContainer().'; + static::setContainer(new PlaceholderContainer($message)); + } + + /** + * Initializes the static properties. Called from within the class file. + */ + public static function initStaticProperties() { + $message = '\Drupal::$container is not initialized yet. \Drupal::setContainer() must be called with a real container.'; + static::setContainer(new PlaceholderContainer($message)); + } + + /** * Returns the currently active global container. * * @deprecated This method is only useful for the testing environment. It @@ -120,6 +160,15 @@ public static function setContainer(ContainerInterface $container = NULL) { * @return \Symfony\Component\DependencyInjection\ContainerInterface|null */ public static function getContainer() { + if (static::$container instanceof PlaceholderContainer) { + // @todo Currently drush depends on this method returning NULL if not + // initialized. Once drush is fixed, remove this workaround. + if (PHP_SAPI === 'cli') { + return NULL; + } + // Trigger the exception from the placeholder container. + static::$container->throwException(); + } return static::$container; } @@ -149,7 +198,7 @@ public static function service($id) { * TRUE if the specified service exists, FALSE otherwise. */ public static function hasService($id) { - return static::$container && static::$container->has($id); + return static::$container->has($id); } /** @@ -168,7 +217,7 @@ public static function root() { * TRUE if there is a currently active request object, FALSE otherwise. */ public static function hasRequest() { - return static::$container && static::$container->has('request_stack') && static::$container->get('request_stack')->getCurrentRequest() !== NULL; + return static::$container->has('request_stack') && static::$container->get('request_stack')->getCurrentRequest() !== NULL; } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/ContainerNotInitializedException.php b/core/lib/Drupal/Core/DependencyInjection/ContainerNotInitializedException.php new file mode 100644 index 0000000..e593243 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/ContainerNotInitializedException.php @@ -0,0 +1,19 @@ +message = $exception_message ?: 'Container not initialized.'; + } + + /** + * {@inheritdoc} + */ + public function set($id, $service, $scope = self::SCOPE_CONTAINER) { + $this->throwException(); + } + + /** + * Throws an exception. + * + * @throws \Drupal\Core\DependencyInjection\ContainerNotInitializedException + */ + public function throwException() { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) { + $this->throwException(); + } + + /** + * {@inheritdoc} + */ + public function has($id) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) { + $this->throwException(); + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) { + $this->throwException(); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) { + $this->throwException(); + } + + /** + * {@inheritdoc} + */ + public function enterScope($name) { + $this->throwException(); + } + + /** + * {@inheritdoc} + */ + public function leaveScope($name) { + $this->throwException(); + } + + /** + * {@inheritdoc} + */ + public function addScope(ScopeInterface $scope) { + $this->throwException(); + } + + /** + * {@inheritdoc} + */ + public function hasScope($name) { + $this->throwException(); + } + + /** + * {@inheritdoc} + */ + public function isScopeActive($name) { + $this->throwException(); + } + +} diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index b4901af..a6d1bf2 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -1187,7 +1187,7 @@ private function prepareEnvironment() { // Ensure there is no service container. $this->container = NULL; - \Drupal::setContainer(NULL); + \Drupal::unsetContainer(); // Unset globals. unset($GLOBALS['config_directories']); diff --git a/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php index a7bcca7..43dee4f 100644 --- a/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php +++ b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php @@ -19,7 +19,7 @@ class GetFilenameUnitTest extends KernelTestBase { protected function setUp() { parent::setUp(); $this->container = NULL; - \Drupal::setContainer(NULL); + \Drupal::unsetContainer(); } /** diff --git a/core/tests/Drupal/Tests/Core/DrupalContainerNotInitializedTest.php b/core/tests/Drupal/Tests/Core/DrupalContainerNotInitializedTest.php new file mode 100644 index 0000000..5161e71 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DrupalContainerNotInitializedTest.php @@ -0,0 +1,156 @@ +assertEquals($expected, $result); + } + + /** + * Tests the case where \Drupal::$container is not initialized. + * + * @dataProvider methodsWithExceptionProvider + * + * @param string $method + * The static method to call on \Drupal:: + * @param array $args + * Arguments to pass into \Drupal::$method(..) + * + * @expectedException \Drupal\Core\DependencyInjection\ContainerNotInitializedException + * @expectedExceptionMessage \Drupal::$container is not initialized yet. \Drupal::setContainer() must be called with a real container. + */ + public function testContainerNotInitializedException($method, $args = array()) { + call_user_func_array(['Drupal', $method], $args); + } + + /** + * Tests the case where \Drupal::$container was unset. + * + * @dataProvider methodsWithReturnProvider + * + * @param string $method + * The static method to call on \Drupal:: + * @param mixed $expected + * Expected return value. + * @param array $args + * Arguments to pass into \Drupal::$method(..) + */ + public function testUnsetContainerReturn($method, $expected, $args = array()) { + \Drupal::unsetContainer(__METHOD__); + $result = call_user_func_array(['Drupal', $method], $args); + $this->assertEquals($expected, $result); + } + + /** + * Tests the case where \Drupal::$container was unset. + * + * @dataProvider methodsWithExceptionProvider + * + * @param string $method + * The static method to call on \Drupal:: + * @param array $args + * Arguments to pass into \Drupal::$method(..) + * + * @expectedException \Drupal\Core\DependencyInjection\ContainerNotInitializedException + * @expectedExceptionMessage Custom exception message. + */ + public function testUnsetContainerException($method, $args = array()) { + \Drupal::unsetContainer('Custom exception message.'); + call_user_func_array(['Drupal', $method], $args); + } + + /** + * Data provider for two methods, see "@see" below. + * + * @return array[] + * + * @see testContainerNotInitializedReturn() + * @see testUnsetContainerReturn() + */ + public function methodsWithReturnProvider() { + return array( + ['getContainer', NULL], + ['hasService', FALSE, ['test_service']], + ['hasRequest', FALSE], + ); + } + + /** + * Data provider for two methods, see "@see" below. + * + * @return array[] + * + * @see testContainerNotInitializedException() + * @see testUnsetContainerException() + */ + public function methodsWithExceptionProvider() { + return array( + ['service', ['test_service']], + ['request'], + ['requestStack'], + ['routeMatch'], + ['currentUser'], + ['entityManager'], + ['database'], + ['cache', ['test']], + ['keyValueExpirable', ['test_collection']], + ['lock'], + ['config', ['test_config']], + ['configFactory'], + ['queue', ['test_queue', TRUE]], + ['keyValue', ['test_collection']], + ['state'], + ['httpClient'], + ['entityQuery', ['OR']], + ['entityQueryAggregate', ['test_entity', 'OR']], + ['flood', ['test_service']], + ['moduleHandler', ['test_service']], + ['typedDataManager', ['test_service']], + ['token'], + ['urlGenerator'], + ['url', ['test_route']], + ['linkGenerator'], + ['l', ['Test title', new Url('test_route')]], + ['translation'], + ['languageManager'], + ['csrfToken'], + ['transliteration'], + ['formBuilder'], + ['theme'], + ['isConfigSyncing'], + ['logger', ['test_channel']], + ['menuTree'], + ['pathValidator'], + ['accessManager'], + ); + } + +} diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php index d6deb9e..e82c3a9 100644 --- a/core/tests/Drupal/Tests/UnitTestCase.php +++ b/core/tests/Drupal/Tests/UnitTestCase.php @@ -39,7 +39,7 @@ protected function setUp() { parent::setUp(); // Ensure that an instantiated container in the global state of \Drupal from // a previous test does not leak into this test. - \Drupal::setContainer(NULL); + \Drupal::unsetContainer(); $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)))); }