core/lib/Drupal/Core/Cache/Cache.php | 43 ++++++++++++++++++- core/modules/views/src/Entity/View.php | 4 +- core/tests/Drupal/Tests/Core/Cache/CacheTest.php | 54 ++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php index 91cc4a8..b5508a1 100644 --- a/core/lib/Drupal/Core/Cache/Cache.php +++ b/core/lib/Drupal/Core/Cache/Cache.php @@ -37,11 +37,52 @@ public static function mergeContexts() { $cache_contexts = array_merge($cache_contexts, $contexts); } $cache_contexts = array_unique($cache_contexts); + static::validateContexts($cache_contexts); sort($cache_contexts); return $cache_contexts; } /** + * Validates an array of cache contexts. + * + * Can be called before using cache tags in operations, to ensure validity. + * + * @param string[] $context_tokens + * An array of cache context tokens. + * + * @throws \LogicException + * + * @see \Drupal\Core\Cache\CacheContexts::parseTokens() + */ + public static function validateContexts(array $context_tokens) { + if (empty($context_tokens)) { + return; + } + + // The set of existing (thus valid) cache contexts is stored on the + // container; it's safe to statically cache this because it cannot change + // during the request. + static $valid_contexts; + if (!isset($valid_contexts)) { + $valid_contexts = \Drupal::getContainer()->getParameter('cache_contexts'); + } + + foreach ($context_tokens as $value) { + if (!is_string($value)) { + throw new \LogicException('Cache contexts must be strings, ' . gettype($value) . ' given.'); + } + // If a cache context token has a parameter, drop it to get the cache + // context ID. + if (strpos($value, ':') !== FALSE) { + $value = substr($value, 0, strpos($value, ':')); + } + if (!in_array($value, $valid_contexts)) { + throw new \LogicException('"' . $value. '" is not a valid cache context ID.'); + } + } + } + + /** * Merges arrays of cache tags and removes duplicates. * * The cache tags array is returned in a format that is valid for @@ -62,10 +103,10 @@ public static function mergeTags() { $cache_tag_arrays = func_get_args(); $cache_tags = []; foreach ($cache_tag_arrays as $tags) { - static::validateTags($tags); $cache_tags = array_merge($cache_tags, $tags); } $cache_tags = array_unique($cache_tags); + static::validateTags($cache_tags); sort($cache_tags); return $cache_tags; } diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index daca68f..bf1423d 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -8,6 +8,7 @@ namespace Drupal\views\Entity; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\Cache; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\views\Views; @@ -317,8 +318,7 @@ protected function addCacheMetadata() { list($display['cache_metadata']['cacheable'], $display['cache_metadata']['contexts']) = $executable->getDisplay()->calculateCacheMetadata(); // Always include at least the language context as there will be most // probable translatable strings in the view output. - $display['cache_metadata']['contexts'][] = 'language'; - $display['cache_metadata']['contexts'] = array_unique($display['cache_metadata']['contexts']); + $display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['language']); } // Restore the previous active display. $executable->setDisplay($current_display); diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheTest.php index 75362a7..a13a4cb 100644 --- a/core/tests/Drupal/Tests/Core/Cache/CacheTest.php +++ b/core/tests/Drupal/Tests/Core/Cache/CacheTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Tests\UnitTestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; /** * @coversDefaultClass \Drupal\Core\Cache\Cache @@ -27,6 +28,7 @@ public function validateTagsProvider() { [['foo'], FALSE], [['foo', 'bar'], FALSE], [['foo', 'bar', 'llama:2001988', 'baz', 'llama:14031991'], FALSE], + // Invalid. [[FALSE], 'Cache tags must be strings, boolean given.'], [[TRUE], 'Cache tags must be strings, boolean given.'], [['foo', FALSE], 'Cache tags must be strings, boolean given.'], @@ -119,4 +121,56 @@ public function testBuildTags($prefix, array $suffixes, array $expected, $glue = $this->assertEquals($expected, Cache::buildTags($prefix, $suffixes, $glue)); } + /** + * Provides a list of cache context token arrays. + * + * @return array + */ + public function validateContextsProvider() { + return [ + [[], FALSE], + [['foo'], FALSE], + [['foo', 'foo.bar'], FALSE], + [['foo', 'baz:llama'], FALSE], + // Invalid. + [[FALSE], 'Cache contexts must be strings, boolean given.'], + [[TRUE], 'Cache contexts must be strings, boolean given.'], + [['foo', FALSE], 'Cache contexts must be strings, boolean given.'], + [[NULL], 'Cache contexts must be strings, NULL given.'], + [['foo', NULL], 'Cache contexts must be strings, NULL given.'], + [[1337], 'Cache contexts must be strings, integer given.'], + [['foo', 1337], 'Cache contexts must be strings, integer given.'], + [[3.14], 'Cache contexts must be strings, double given.'], + [['foo', 3.14], 'Cache contexts must be strings, double given.'], + [[[]], 'Cache contexts must be strings, array given.'], + [['foo', []], 'Cache contexts must be strings, array given.'], + [['foo', ['bar']], 'Cache contexts must be strings, array given.'], + [[new \stdClass()], 'Cache contexts must be strings, object given.'], + [['foo', new \stdClass()], 'Cache contexts must be strings, object given.'], + // Non-existing. + [['foo.bar', 'qux'], '"qux" is not a valid cache context ID.'], + [['qux', 'baz'], '"qux" is not a valid cache context ID.'], + ]; + } + + /** + * @covers ::validateContexts + * + * @dataProvider validateContextsProvider + */ + public function testValidateContexts(array $contexts, $expected_exception_message) { + $container = new ContainerBuilder(); + $container->setParameter('cache_contexts', [ + 'foo', + 'foo.bar', + 'baz', + ]); + \Drupal::setContainer($container); + if ($expected_exception_message !== FALSE) { + $this->setExpectedException('LogicException', $expected_exception_message); + } + // If it doesn't throw an exception, validateContexts() returns NULL. + $this->assertNull(Cache::validateContexts($contexts)); + } + }