diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 780e1a2..2db11c3 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -5,10 +5,32 @@ * Contains Drupal. */ +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(NULL); + +/** * Static Service Container wrapper. * * Generally, code in Drupal should accept its dependencies via either @@ -108,8 +130,32 @@ class Drupal { * environment does not leak into a test. */ public static function setContainer(ContainerInterface $container = NULL) { + if (!isset($container)) { + // @todo Remove the NULL case, and use unsetContainer() instead. + $container = new PlaceholderContainer('\Drupal::$container was unset with setContainer(NULL).'); + } static::$container = $container; } + + /** + * Unsets the global container. + * + * @param string|null $message + */ + public static function unsetContainer($message = NULL) { + if (!isset($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. @@ -120,6 +166,13 @@ public static function setContainer(ContainerInterface $container = NULL) { * @return \Symfony\Component\DependencyInjection\ContainerInterface|null */ public static function getContainer() { + if (static::$container instanceof PlaceholderContainer) { + // Currently, some components depend on this method returning NULL if not + // initialized. + // @todo Throw an exception instead, and change all code that expects + // NULL. + return NULL; + } return static::$container; } @@ -149,7 +202,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 +221,8 @@ 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..6de8116 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/ContainerNotInitializedException.php @@ -0,0 +1,15 @@ +message = isset($exception_message) + ? $exception_message + : 'Container not initialized.'; + } + + /** + * {@inheritdoc} + */ + public function set($id, $service, $scope = self::SCOPE_CONTAINER) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function has($id) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function enterScope($name) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function leaveScope($name) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function addScope(ScopeInterface $scope) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function hasScope($name) { + throw new ContainerNotInitializedException($this->message); + } + + /** + * {@inheritdoc} + */ + public function isScopeActive($name) { + throw new ContainerNotInitializedException($this->message); + } +} 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'], + ); + } + +}