diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 363cf12..1dadf42 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 @@ -93,7 +115,7 @@ class Drupal { const CORE_MINIMUM_SCHEMA_VERSION = 8000; /** - * The currently active container object. + * The currently active container object, or a placeholder container. * * @var \Symfony\Component\DependencyInjection\ContainerInterface */ @@ -108,18 +130,46 @@ 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; } /** + * @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. * * @deprecated This method is only useful for the testing environment. It * should not be used otherwise. * - * @return \Symfony\Component\DependencyInjection\ContainerInterface + * @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 +199,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); } /** @@ -159,7 +209,8 @@ public static function hasService($id) { * 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; } /** @@ -469,6 +520,17 @@ public static function urlGenerator() { * the base path (like robots.txt) use Url::fromUri()->toString() with the * base:// scheme. * + * @param string $route_name + * The name of the route + * @param array $route_parameters + * An associative array of parameter names and values. + * @param array $options + * (optional) An associative array of additional options, + * + * @return string + * The generated URL for the given route. + * + * @see \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() * @see \Drupal\Core\Url * @see \Drupal\Core\Url::fromRoute() * @see \Drupal\Core\Url::fromUri() @@ -493,6 +555,12 @@ public static function linkGenerator() { * generate() method. For detailed documentation, see * \Drupal\Core\Routing\LinkGeneratorInterface::generate(). * + * @param string $text + * @param \Drupal\Core\Url $url + * + * @return string + * An HTML string containing a link to the given route and parameters. + * * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate() * @see \Drupal\Core\Url */ 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..14d7158 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DrupalContainerNotInitializedTest.php @@ -0,0 +1,175 @@ +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(..) + */ + public function testContainerNotInitializedException($method, $args = array()) { + try { + call_user_func_array(['Drupal', $method], $args); + } + catch (ContainerNotInitializedException $e) { + $this->assertEquals( + '\Drupal::$container is not initialized yet. \Drupal::setContainer() must be called with a real container.', + $e->getMessage()); + return; + } + $this->fail('\Drupal::service() should trigger an exception, if no container is available.'); + } + + /** + * 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(..) + */ + public function testUnsetContainerException($method, $args = array()) { + try { + \Drupal::unsetContainer(__METHOD__); + call_user_func_array(['Drupal', $method], $args); + } + catch (ContainerNotInitializedException $e) { + $this->assertEquals(__METHOD__, $e->getMessage()); + return; + } + $this->fail('\Drupal::service() should trigger an exception, if no container is available.'); + } + + /** + * 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'], + ); + } + +}