/** * {@inheritdoc} */ public function getCacheContexts() { // The block by itself doesn't really vary by user, but some of its // implementations are (collection module, I'm looking at you). For the sake // of semplicity, we add the user context here already. $contexts = parent::getCacheContexts(); return Cache::mergeContexts($contexts, ['user']); }
/** * {@inheritdoc} */ public function getCacheContexts() { return Cache::mergeContexts(parent::getCacheContexts(), ['route.book_navigation']); }
/** * Tests that the block is cached with the correct contexts and tags. */ public function testBlock() { $block = $this->drupalPlaceBlock('block_content:' . $this->entity->uuid()); $build = $this->container->get('entity.manager')->getViewBuilder('block')->view($block, 'block'); // Render the block. // @todo The request stack manipulation won't be necessary once // https://www.drupal.org/node/2367555 is fixed and the // corresponding $request->isMethodSafe() checks are removed from // Drupal\Core\Render\Renderer. $request_stack = $this->container->get('request_stack'); $request_stack->push(new Request()); $this->container->get('renderer')->renderRoot($build); $request_stack->pop(); // Expected keys, contexts, and tags for the block. // @see \Drupal\block\BlockViewBuilder::viewMultiple() $expected_block_cache_keys = ['entity_view', 'block', $block->id()]; $expected_block_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme']; $expected_block_cache_tags = Cache::mergeTags(['block_view', 'rendered'], $block->getCacheTags(), $block->getPlugin()->getCacheTags()); // Expected contexts and tags for the BlockContent entity. // @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults(). $expected_entity_cache_contexts = ['theme']; $expected_entity_cache_tags = Cache::mergeTags(['block_content_view'], $this->entity->getCacheTags(), $this->getAdditionalCacheTagsForEntity($this->entity)); // Verify that what was render cached matches the above expectations. $cid = $this->createCacheId($expected_block_cache_keys, $expected_block_cache_contexts); $redirected_cid = $this->createCacheId($expected_block_cache_keys, Cache::mergeContexts($expected_block_cache_contexts, $expected_entity_cache_contexts)); $this->verifyRenderCache($cid, Cache::mergeTags($expected_block_cache_tags, $expected_entity_cache_tags), $cid !== $redirected_cid ? $redirected_cid : NULL); }
/** * {@inheritdoc} */ public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) { // @todo Remove when https://www.drupal.org/node/2453059 lands. $default_cache_contexts = ['languages', 'theme']; /** @var \Drupal\block\BlockInterface[] $entities */ $build = array(); foreach ($entities as $entity) { $entity_id = $entity->id(); $plugin = $entity->getPlugin(); $plugin_id = $plugin->getPluginId(); $base_id = $plugin->getBaseId(); $derivative_id = $plugin->getDerivativeId(); $configuration = $plugin->getConfiguration(); // Create the render array for the block as a whole. // @see template_preprocess_block(). $build[$entity_id] = array('#theme' => 'block', '#attributes' => array(), '#contextual_links' => array('block' => array('route_parameters' => array('block' => $entity->id()))), '#weight' => $entity->getWeight(), '#configuration' => $configuration, '#plugin_id' => $plugin_id, '#base_plugin_id' => $base_id, '#derivative_plugin_id' => $derivative_id, '#id' => $entity->id(), '#cache' => ['contexts' => Cache::mergeContexts($default_cache_contexts, $plugin->getCacheContexts()), 'tags' => Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags(), $plugin->getCacheTags()), 'max-age' => $plugin->getCacheMaxAge()], '#block' => $entity); $build[$entity_id]['#configuration']['label'] = String::checkPlain($configuration['label']); if ($plugin->isCacheable()) { $build[$entity_id]['#pre_render'][] = array($this, 'buildBlock'); // Generic cache keys, with the block plugin's custom keys appended. $default_cache_keys = array('entity_view', 'block', $entity->id()); $build[$entity_id]['#cache']['keys'] = array_merge($default_cache_keys, $plugin->getCacheKeys()); } else { $build[$entity_id] = $this->buildBlock($build[$entity_id]); } // Don't run in ::buildBlock() to ensure cache keys can be altered. If an // alter hook wants to modify the block contents, it can append another // #pre_render hook. $this->moduleHandler()->alter(array('block_view', "block_view_{$base_id}"), $build[$entity_id], $plugin); } return $build; }
/** * {@inheritdoc} */ public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) { /** @var \Drupal\block\BlockInterface[] $entities */ $build = array(); foreach ($entities as $entity) { $entity_id = $entity->id(); $plugin = $entity->getPlugin(); $cache_tags = Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags()); $cache_tags = Cache::mergeTags($cache_tags, $plugin->getCacheTags()); // Create the render array for the block as a whole. // @see template_preprocess_block(). $build[$entity_id] = array('#cache' => ['keys' => ['entity_view', 'block', $entity->id()], 'contexts' => Cache::mergeContexts($entity->getCacheContexts(), $plugin->getCacheContexts()), 'tags' => $cache_tags, 'max-age' => $plugin->getCacheMaxAge()], '#weight' => $entity->getWeight()); // Allow altering of cacheability metadata or setting #create_placeholder. $this->moduleHandler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin); if ($plugin instanceof MainContentBlockPluginInterface || $plugin instanceof TitleBlockPluginInterface) { // Immediately build a #pre_render-able block, since this block cannot // be built lazily. $build[$entity_id] += static::buildPreRenderableBlock($entity, $this->moduleHandler()); } else { // Assign a #lazy_builder callback, which will generate a #pre_render- // able block lazily (when necessary). $build[$entity_id] += ['#lazy_builder' => [static::class . '::lazyBuilder', [$entity_id, $view_mode, $langcode]]]; } } return $build; }
/** * Confirms that our FinishResponseSubscriber logic works properly. */ public function testFinishResponseSubscriber() { $renderer_required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; $expected_cache_contexts = Cache::mergeContexts($renderer_required_cache_contexts, ['url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT]); // Confirm that the router can get to a controller. $this->drupalGet('router_test/test1'); $this->assertRaw('test1', 'The correct string was returned because the route was successful.'); // Check expected headers from FinishResponseSubscriber. $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-ua-compatible'], 'IE=edge'); $this->assertEqual($headers['content-language'], 'en'); $this->assertEqual($headers['x-content-type-options'], 'nosniff'); $this->assertEqual($headers['x-frame-options'], 'SAMEORIGIN'); $this->drupalGet('router_test/test2'); $this->assertRaw('test2', 'The correct string was returned because the route was successful.'); // Check expected headers from FinishResponseSubscriber. $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', $expected_cache_contexts)); $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous rendered'); // Confirm that the page wrapping is being added, so we're not getting a // raw body returned. $this->assertRaw('</html>', 'Page markup was found.'); // In some instances, the subrequest handling may get confused and render // a page inception style. This test verifies that is not happening. $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.'); // Confirm that route-level access check's cacheability is applied to the // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags headers. // 1. controller result: render array, globally cacheable route access. $this->drupalGet('router_test/test18'); $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url']))); $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo rendered'); // 2. controller result: render array, per-role cacheable route access. $this->drupalGet('router_test/test19'); $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url', 'user.roles']))); $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo rendered'); // 3. controller result: Response object, globally cacheable route access. $this->drupalGet('router_test/test1'); $headers = $this->drupalGetHeaders(); $this->assertFalse(isset($headers['x-drupal-cache-contexts'])); $this->assertFalse(isset($headers['x-drupal-cache-tags'])); // 4. controller result: Response object, per-role cacheable route access. $this->drupalGet('router_test/test20'); $headers = $this->drupalGetHeaders(); $this->assertFalse(isset($headers['x-drupal-cache-contexts'])); $this->assertFalse(isset($headers['x-drupal-cache-tags'])); // 5. controller result: CacheableResponse object, globally cacheable route access. $this->drupalGet('router_test/test21'); $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-drupal-cache-contexts'], ''); $this->assertEqual($headers['x-drupal-cache-tags'], ''); // 6. controller result: CacheableResponse object, per-role cacheable route access. $this->drupalGet('router_test/test22'); $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles'); $this->assertEqual($headers['x-drupal-cache-tags'], ''); }
/** * Tests that #type=link bubbles outbound route/path processors' cacheability. */ function testLinkCacheability() { $cases = [['Regular link', 'internal:/user', [], ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]], ['Regular link, absolute', 'internal:/user', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => [], 'max-age' => Cache::PERMANENT]], ['Route processor link', 'route:system.run_cron', [], ['contexts' => [], 'tags' => [], 'max-age' => 0]], ['Route processor link, absolute', 'route:system.run_cron', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => [], 'max-age' => 0]], ['Path processor link', 'internal:/user/1', [], ['contexts' => [], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT]], ['Path processor link, absolute', 'internal:/user/1', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT]]]; foreach ($cases as $case) { list($title, $uri, $options, $expected_cacheability) = $case; $expected_cacheability['contexts'] = Cache::mergeContexts($expected_cacheability['contexts'], ['languages:language_interface', 'theme']); $link = ['#type' => 'link', '#title' => $title, '#options' => $options, '#url' => Url::fromUri($uri)]; \Drupal::service('renderer')->renderRoot($link); $this->pass($title); $this->assertEqual($expected_cacheability, $link['#cache']); } }
/** * {@inheritdoc} */ public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) { /** @var \Drupal\block\BlockInterface[] $entities */ $build = array(); foreach ($entities as $entity) { $entity_id = $entity->id(); $plugin = $entity->getPlugin(); $plugin_id = $plugin->getPluginId(); $base_id = $plugin->getBaseId(); $derivative_id = $plugin->getDerivativeId(); $configuration = $plugin->getConfiguration(); $cache_tags = Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags()); $cache_tags = Cache::mergeTags($cache_tags, $plugin->getCacheTags()); // Create the render array for the block as a whole. // @see template_preprocess_block(). $build[$entity_id] = array('#theme' => 'block', '#attributes' => array(), '#contextual_links' => array('block' => array('route_parameters' => array('block' => $entity->id()))), '#weight' => $entity->getWeight(), '#configuration' => $configuration, '#plugin_id' => $plugin_id, '#base_plugin_id' => $base_id, '#derivative_plugin_id' => $derivative_id, '#id' => $entity->id(), '#cache' => ['keys' => ['entity_view', 'block', $entity->id()], 'contexts' => Cache::mergeContexts($entity->getCacheContexts(), $plugin->getCacheContexts()), 'tags' => $cache_tags, 'max-age' => $plugin->getCacheMaxAge()], '#pre_render' => [[$this, 'buildBlock']], '#block' => $entity); $build[$entity_id]['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']); // Don't run in ::buildBlock() to ensure cache keys can be altered. If an // alter hook wants to modify the block contents, it can append another // #pre_render hook. $this->moduleHandler()->alter(array('block_view', "block_view_{$base_id}"), $build[$entity_id], $plugin); } return $build; }
/** * {@inheritdoc} */ public function getCacheContexts() { return Cache::mergeContexts(parent::getCacheContexts(), ['user.node_grants:view']); }
/** * Asserts that a block is built/rendered/cached with expected cacheability. * * @param string[] $expected_keys * The expected cache keys. * @param string[] $expected_contexts * The expected cache contexts. * @param string[] $expected_tags * The expected cache tags. * @param int $expected_max_age * The expected max-age. */ protected function assertBlockRenderedWithExpectedCacheability(array $expected_keys, array $expected_contexts, array $expected_tags, $expected_max_age) { $required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; // Check that the expected cacheability metadata is present in: // - the built render array; $this->pass('Built render array'); $build = $this->getBlockRenderArray(); $this->assertIdentical($expected_keys, $build['#cache']['keys']); $this->assertIdentical($expected_contexts, $build['#cache']['contexts']); $this->assertIdentical($expected_tags, $build['#cache']['tags']); $this->assertIdentical($expected_max_age, $build['#cache']['max-age']); $this->assertFalse(isset($build['#create_placeholder'])); // - the rendered render array; $this->pass('Rendered render array'); $this->renderer->renderRoot($build); // - the render cache item. $this->pass('Render cache item'); $final_cache_contexts = Cache::mergeContexts($expected_contexts, $required_cache_contexts); $cid = implode(':', $expected_keys) . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys($final_cache_contexts)->getKeys()); $cache_item = $this->container->get('cache.render')->get($cid); $this->assertTrue($cache_item, 'The block render element has been cached with the expected cache ID.'); $this->assertIdentical(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags); $this->assertIdentical($final_cache_contexts, $cache_item->data['#cache']['contexts']); $this->assertIdentical($expected_tags, $cache_item->data['#cache']['tags']); $this->assertIdentical($expected_max_age, $cache_item->data['#cache']['max-age']); $this->container->get('cache.render')->delete($cid); }
/** * Tests that cache contexts are applied for both users. * * @param string[] $cache_contexts * Expected cache contexts for both users. * @param string $message * (optional) A verbose message to output. * * @return * TRUE if the assertion succeeded, FALSE otherwise. */ protected function assertToolbarCacheContexts(array $cache_contexts, $message = NULL) { // Default cache contexts that should exist on all test cases. $default_cache_contexts = ['languages:language_interface', 'theme']; $cache_contexts = Cache::mergeContexts($default_cache_contexts, $cache_contexts); // Assert contexts for user1 which has only default permissions. $this->drupalLogin($this->adminUser); $this->drupalGet('test-page'); $return = $this->assertCacheContexts($cache_contexts); $this->drupalLogout(); // Assert contexts for user2 which has some additional permissions. $this->drupalLogin($this->adminUser2); $this->drupalGet('test-page'); $return = $return && $this->assertCacheContexts($cache_contexts); if ($return) { $this->pass($message); } else { $this->fail($message); } return $return; }
/** * {@inheritdoc} */ public function set(array &$elements, array $pre_bubbling_elements) { // Form submissions rely on the form being built during the POST request, // and render caching of forms prevents this from happening. // @todo remove the isMethodSafe() check when // https://www.drupal.org/node/2367555 lands. if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !($cid = $this->createCacheID($elements))) { return FALSE; } $data = $this->getCacheableRenderArray($elements); $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; $expire = $elements['#cache']['max-age'] === Cache::PERMANENT ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $elements['#cache']['max-age']; $cache = $this->cacheFactory->get($bin); // Calculate the pre-bubbling CID. $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements); // Two-tier caching: detect different CID post-bubbling, create redirect, // update redirect if different set of cache contexts. // @see \Drupal\Core\Render\RendererInterface::render() // @see ::get() if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) { // The cache redirection strategy we're implementing here is pretty // simple in concept. Suppose we have the following render structure: // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) // -- B (specifies #cache['contexts'] = ['b']) // // At the time that we're evaluating whether A's rendering can be // retrieved from cache, we won't know the contexts required by its // children (the children might not even be built yet), so cacheGet() // will only be able to get what is cached for a $cid of 'foo'. But at // the time we're writing to that cache, we do know all the contexts that // were specified by all children, so what we need is a way to // persist that information between the cache write and the next cache // read. So, what we can do is store the following into 'foo': // [ // '#cache_redirect' => TRUE, // '#cache' => [ // ... // 'contexts' => ['b'], // ], // ] // // This efficiently lets cacheGet() redirect to a $cid that includes all // of the required contexts. The strategy is on-demand: in the case where // there aren't any additional contexts required by children that aren't // already included in the parent's pre-bubbled #cache information, no // cache redirection is needed. // // When implementing this redirection strategy, special care is needed to // resolve potential cache ping-pong problems. For example, consider the // following render structure: // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) // -- B (pre-bubbling, specifies #cache['contexts'] = ['b']) // --- C (pre-bubbling, specifies #cache['contexts'] = ['c']) // --- D (pre-bubbling, specifies #cache['contexts'] = ['d']) // // Additionally, suppose that: // - C only exists for a 'b' context value of 'b1' // - D only exists for a 'b' context value of 'b2' // This is an acceptable variation, since B specifies that its contents // vary on context 'b'. // // A naive implementation of cache redirection would result in the // following: // - When a request is processed where context 'b' = 'b1', what would be // cached for a $pre_bubbling_cid of 'foo' is: // [ // '#cache_redirect' => TRUE, // '#cache' => [ // ... // 'contexts' => ['b', 'c'], // ], // ] // - When a request is processed where context 'b' = 'b2', we would // retrieve the above from cache, but when following that redirection, // get a cache miss, since we're processing a 'b' context value that // has not yet been cached. Given the cache miss, we would continue // with rendering the structure, perform the required context bubbling // and then overwrite the above item with: // [ // '#cache_redirect' => TRUE, // '#cache' => [ // ... // 'contexts' => ['b', 'd'], // ], // ] // - Now, if a request comes in where context 'b' = 'b1' again, the above // would redirect to a cache key that doesn't exist, since we have not // yet cached an item that includes 'b'='b1' and something for 'd'. So // we would process this request as a cache miss, at the end of which, // we would overwrite the above item back to: // [ // '#cache_redirect' => TRUE, // '#cache' => [ // ... // 'contexts' => ['b', 'c'], // ], // ] // - The above would always result in accurate renderings, but would // result in poor performance as we keep processing requests as cache // misses even though the target of the redirection is cached, and // it's only the redirection element itself that is creating the // ping-pong problem. // // A way to resolve the ping-pong problem is to eventually reach a cache // state where the redirection element includes all of the contexts used // throughout all requests: // [ // '#cache_redirect' => TRUE, // '#cache' => [ // ... // 'contexts' => ['b', 'c', 'd'], // ], // ] // // We can't reach that state right away, since we don't know what the // result of future requests will be, but we can incrementally move // towards that state by progressively merging the 'contexts' value // across requests. That's the strategy employed below and tested in // \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing(). // The set of cache contexts for this element, including the bubbled ones, // for which we are handling a cache miss. $cache_contexts = $data['#cache']['contexts']; // Get the contexts by which this element should be varied according to // the current redirecting cache item, if any. $stored_cache_contexts = []; $stored_cache_tags = []; if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { $stored_cache_contexts = $stored_cache_redirect->data['#cache']['contexts']; $stored_cache_tags = $stored_cache_redirect->data['#cache']['tags']; } // Calculate the union of the cache contexts for this request and the // stored cache contexts. $merged_cache_contexts = Cache::mergeContexts($stored_cache_contexts, $cache_contexts); // Stored cache contexts incomplete: this request causes cache contexts to // be added to the redirecting cache item. if (array_diff($merged_cache_contexts, $stored_cache_contexts)) { $redirect_data = ['#cache_redirect' => TRUE, '#cache' => ['keys' => $elements['#cache']['keys'], 'contexts' => $merged_cache_contexts, 'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags'])]]; $cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); } // Current cache contexts incomplete: this request only uses a subset of // the cache contexts stored in the redirecting cache item. Vary by these // additional (conditional) cache contexts as well, otherwise the // redirecting cache item would be pointing to a cache item that can never // exist. if (array_diff($merged_cache_contexts, $cache_contexts)) { // Recalculate the cache ID. $recalculated_cid_pseudo_element = ['#cache' => ['keys' => $elements['#cache']['keys'], 'contexts' => $merged_cache_contexts]]; $cid = $this->createCacheID($recalculated_cid_pseudo_element); // Ensure the about-to-be-cached data uses the merged cache contexts. $data['#cache']['contexts'] = $merged_cache_contexts; } } $cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered'])); }
/** * Fills in the cache metadata of this view. * * Cache metadata is set per view and per display, and ends up being stored in * the view's configuration. This allows Views to determine very efficiently: * - the max-age * - the cache contexts * - the cache tags * * In other words: this allows us to do the (expensive) work of initializing * Views plugins and handlers to determine their effect on the cacheability of * a view at save time rather than at runtime. */ protected function addCacheMetadata() { $executable = $this->getExecutable(); $current_display = $executable->current_display; $displays = $this->get('display'); foreach (array_keys($displays) as $display_id) { $display =& $this->getDisplay($display_id); $executable->setDisplay($display_id); $cache_metadata = $executable->getDisplay()->calculateCacheMetadata(); $display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge(); $display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts(); $display['cache_metadata']['tags'] = $cache_metadata->getCacheTags(); // Always include at least the 'languages:' context as there will most // probably be translatable strings in the view output. $display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['languages:' . LanguageInterface::TYPE_INTERFACE]); } // Restore the previous active display. $executable->setDisplay($current_display); }
/** * {@inheritdoc} * * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'. */ public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { list($page, $title) = $this->prepare($main_content, $request, $route_match); if (!isset($page['#type']) || $page['#type'] !== 'page') { throw new \LogicException('Must be #type page'); } $page['#title'] = $title; // Now render the rendered page.html.twig template inside the html.html.twig // template, and use the bubbled #attached metadata from $page to ensure we // load all attached assets. $html = ['#type' => 'html', 'page' => $page]; // The special page regions will appear directly in html.html.twig, not in // page.html.twig, hence add them here, just before rendering html.html.twig. $this->buildPageTopAndBottom($html); // Render, but don't replace placeholders yet, because that happens later in // the render pipeline. To not replace placeholders yet, we use // RendererInterface::render() instead of RendererInterface::renderRoot(). // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor. $render_context = new RenderContext(); $this->renderer->executeInRenderContext($render_context, function () use(&$html) { // RendererInterface::render() renders the $html render array and updates // it in place. We don't care about the return value (which is just // $html['#markup']), but about the resulting render array. // @todo Simplify this when https://www.drupal.org/node/2495001 lands. $this->renderer->render($html); }); // RendererInterface::render() always causes bubbleable metadata to be // stored in the render context, no need to check it conditionally. $bubbleable_metadata = $render_context->pop(); $bubbleable_metadata->applyTo($html); $content = $this->renderCache->getCacheableRenderArray($html); // Also associate the required cache contexts. // (Because we use ::render() above and not ::renderRoot(), we manually must // ensure the HTML response varies by the required cache contexts.) $content['#cache']['contexts'] = Cache::mergeContexts($content['#cache']['contexts'], $this->rendererConfig['required_cache_contexts']); // Also associate the "rendered" cache tag. This allows us to invalidate the // entire render cache, regardless of the cache bin. $content['#cache']['tags'][] = 'rendered'; $response = new HtmlResponse($content, 200, ['Content-Type' => 'text/html; charset=UTF-8']); return $response; }
/** * Tests random ordering with tags based caching. * * The random sorting should opt out of caching by defining a max age of 0. * At the same time, the row render caching still works. */ public function testRandomOrderingWithRenderCaching() { $view_random = $this->getBasicRandomView(); $display =& $view_random->storage->getDisplay('default'); $display['display_options']['cache'] = ['type' => 'tag']; $view_random->storage->save(); /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */ $render_cache = \Drupal::service('render_cache'); $original = $build = DisplayPluginBase::buildBasicRenderable($view_random->id(), 'default'); $result = $renderer->renderPlain($build); $original['#cache'] += ['contexts' => []]; $original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']); $this->assertFalse($render_cache->get($original), 'Ensure there is no render cache entry.'); $build = DisplayPluginBase::buildBasicRenderable($view_random->id(), 'default'); $result2 = $renderer->renderPlain($build); // Ensure that the random ordering works and don't produce the same result. $this->assertNotEqual($result, $result2); }
/** * Asserts a view's result & render cache items' cache tags. * * This method starts with a pre bubbling basic render array. * * @param \Drupal\views\ViewExecutable $view * The view. * @param string[] $expected_render_array_cache_tags * The expected render cache tags. * @param bool $views_caching_is_enabled * Defines whether views output / render caching is enabled. * * @return array * The render array. */ protected function assertViewsCacheTagsFromStaticRenderArray(ViewExecutable $view, array $expected_render_array_cache_tags, $views_caching_is_enabled) { $original = $build = DisplayPluginBase::buildBasicRenderable($view->id(), $view->current_display ?: 'default', $view->args); /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */ $render_cache = \Drupal::service('render_cache'); // Ensure the current request is a GET request so that render caching is // active for direct rendering of views, just like for actual requests. /** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */ $request_stack = \Drupal::service('request_stack'); $request = new Request(); $request->server->set('REQUEST_TIME', REQUEST_TIME); $request_stack->push($request); $renderer->renderRoot($build); // Render array cache tags. $this->pass('Checking render array cache tags.'); sort($expected_render_array_cache_tags); $this->assertEqual($build['#cache']['tags'], $expected_render_array_cache_tags); $this->debugCacheTags($build['#cache']['tags'], $expected_render_array_cache_tags); $this->pass('Checking Views render cache item cache tags.'); $original['#cache'] += ['contexts' => []]; $original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']); $render_cache_item = $render_cache->get($original); if ($views_caching_is_enabled) { $this->assertTrue(!empty($render_cache_item), 'Render cache item found.'); if ($render_cache_item) { $this->assertEqual($render_cache_item['#cache']['tags'], $expected_render_array_cache_tags); $this->debugCacheTags($render_cache_item['#cache']['tags'], $expected_render_array_cache_tags); } } else { $this->assertFalse($render_cache_item, 'Render cache item not found.'); } return $build; }
/** * Tests cache tags presence and invalidation of the entity at its URI. * * Tests the following cache tags: * - "<entity type>_view" * - "<entity_type>:<entity ID>" */ public function testEntityUri() { $entity_url = $this->entity->urlInfo(); $entity_type = $this->entity->getEntityTypeId(); // Selects the view mode that will be used. $view_mode = $this->selectViewMode($entity_type); // The default cache contexts for rendered entities. $entity_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; // Generate the standardized entity cache tags. $cache_tag = $this->entity->getCacheTags(); $view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)->getCacheTags(); $render_cache_tag = 'rendered'; $this->pass("Test entity.", 'Debug'); $this->verifyPageCache($entity_url, 'MISS'); // Verify a cache hit, but also the presence of the correct cache tags. $this->verifyPageCache($entity_url, 'HIT'); // Also verify the existence of an entity render cache entry, if this entity // type supports render caching. if (\Drupal::entityManager()->getDefinition($entity_type)->isRenderCacheable()) { $cache_keys = ['entity_view', $entity_type, $this->entity->id(), $view_mode]; $cid = $this->createCacheId($cache_keys, $entity_cache_contexts); $redirected_cid = NULL; $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->entity); if (count($additional_cache_contexts)) { $redirected_cid = $this->createCacheId($cache_keys, Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts)); } $expected_cache_tags = Cache::mergeTags($cache_tag, $view_cache_tag); $expected_cache_tags = Cache::mergeTags($expected_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity)); $expected_cache_tags = Cache::mergeTags($expected_cache_tags, array($render_cache_tag)); $this->verifyRenderCache($cid, $expected_cache_tags, $redirected_cid); } // Verify that after modifying the entity, there is a cache miss. $this->pass("Test modification of entity.", 'Debug'); $this->entity->save(); $this->verifyPageCache($entity_url, 'MISS'); // Verify a cache hit. $this->verifyPageCache($entity_url, 'HIT'); // Verify that after modifying the entity's display, there is a cache miss. $this->pass("Test modification of entity's '{$view_mode}' display.", 'Debug'); $entity_display = entity_get_display($entity_type, $this->entity->bundle(), $view_mode); $entity_display->save(); $this->verifyPageCache($entity_url, 'MISS'); // Verify a cache hit. $this->verifyPageCache($entity_url, 'HIT'); if ($bundle_entity_type_id = $this->entity->getEntityType()->getBundleEntityType()) { // Verify that after modifying the corresponding bundle entity, there is a // cache miss. $this->pass("Test modification of entity's bundle entity.", 'Debug'); $bundle_entity = entity_load($bundle_entity_type_id, $this->entity->bundle()); $bundle_entity->save(); $this->verifyPageCache($entity_url, 'MISS'); // Verify a cache hit. $this->verifyPageCache($entity_url, 'HIT'); } if ($this->entity->getEntityType()->get('field_ui_base_route')) { // Verify that after modifying a configurable field on the entity, there // is a cache miss. $this->pass("Test modification of entity's configurable field.", 'Debug'); $field_storage_name = $this->entity->getEntityTypeId() . '.configurable_field'; $field_storage = FieldStorageConfig::load($field_storage_name); $field_storage->save(); $this->verifyPageCache($entity_url, 'MISS'); // Verify a cache hit. $this->verifyPageCache($entity_url, 'HIT'); // Verify that after modifying a configurable field on the entity, there // is a cache miss. $this->pass("Test modification of entity's configurable field.", 'Debug'); $field_name = $this->entity->getEntityTypeId() . '.' . $this->entity->bundle() . '.configurable_field'; $field = FieldConfig::load($field_name); $field->save(); $this->verifyPageCache($entity_url, 'MISS'); // Verify a cache hit. $this->verifyPageCache($entity_url, 'HIT'); } // Verify that after invalidating the entity's cache tag directly, there is // a cache miss. $this->pass("Test invalidation of entity's cache tag.", 'Debug'); Cache::invalidateTags($this->entity->getCacheTagsToInvalidate()); $this->verifyPageCache($entity_url, 'MISS'); // Verify a cache hit. $this->verifyPageCache($entity_url, 'HIT'); // Verify that after invalidating the generic entity type's view cache tag // directly, there is a cache miss. $this->pass("Test invalidation of entity's 'view' cache tag.", 'Debug'); Cache::invalidateTags($view_cache_tag); $this->verifyPageCache($entity_url, 'MISS'); // Verify a cache hit. $this->verifyPageCache($entity_url, 'HIT'); // Verify that after deleting the entity, there is a cache miss. $this->pass('Test deletion of entity.', 'Debug'); $this->entity->delete(); $this->verifyPageCache($entity_url, 'MISS'); $this->assertResponse(404); }
/** * Tests cache tags presence and invalidation of the entity when referenced. * * Tests the following cache tags: * - entity type view cache tag: "<entity type>_view" * - entity cache tag: "<entity type>:<entity ID>" * - entity type list cache tag: "<entity type>_list" * - referencing entity type view cache tag: "<referencing entity type>_view" * - referencing entity type cache tag: "<referencing entity type>:<referencing entity ID>" */ public function testReferencedEntity() { $entity_type = $this->entity->getEntityTypeId(); $referencing_entity_url = $this->referencingEntity->urlInfo('canonical'); $non_referencing_entity_url = $this->nonReferencingEntity->urlInfo('canonical'); $listing_url = Url::fromRoute('entity.entity_test.collection_referencing_entities', ['entity_reference_field_name' => $entity_type . '_reference', 'referenced_entity_type' => $entity_type, 'referenced_entity_id' => $this->entity->id()]); $empty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_empty', ['entity_type_id' => $entity_type]); $nonempty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_labels_alphabetically', ['entity_type_id' => $entity_type]); // The default cache contexts for rendered entities. $default_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; $entity_cache_contexts = $default_cache_contexts; $page_cache_contexts = Cache::mergeContexts($default_cache_contexts, ['url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT]); // Cache tags present on every rendered page. // 'user.permissions' is a required cache context, and responses that vary // by this cache context when requested by anonymous users automatically // also get this cache tag, to ensure correct invalidation. $page_cache_tags = Cache::mergeTags(['rendered'], ['config:user.role.anonymous']); // If the block module is used, the Block page display variant is used, // which adds the block config entity type's list cache tags. $page_cache_tags = Cache::mergeTags($page_cache_tags, \Drupal::moduleHandler()->moduleExists('block') ? ['config:block_list'] : []); $page_cache_tags_referencing_entity = in_array('user.permissions', $this->getAccessCacheContextsForEntity($this->referencingEntity)) ? ['config:user.role.anonymous'] : []; $view_cache_tag = array(); if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) { $view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)->getCacheTags(); } $context_metadata = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($entity_cache_contexts); $cache_context_tags = $context_metadata->getCacheTags(); // Generate the cache tags for the (non) referencing entities. $referencing_entity_cache_tags = Cache::mergeTags($this->referencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags()); // Includes the main entity's cache tags, since this entity references it. $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $this->entity->getCacheTags()); $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity)); $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $view_cache_tag); $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $cache_context_tags); $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['rendered']); $non_referencing_entity_cache_tags = Cache::mergeTags($this->nonReferencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags()); $non_referencing_entity_cache_tags = Cache::mergeTags($non_referencing_entity_cache_tags, ['rendered']); // Generate the cache tags for all two possible entity listing paths. // 1. list cache tag only (listing query has no match) // 2. list cache tag plus entity cache tag (listing query has a match) $empty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $page_cache_tags); $nonempty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->entity->getCacheTags()); $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $this->getAdditionalCacheTagsForEntityListing($this->entity)); $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $page_cache_tags); $this->pass("Test referencing entity.", 'Debug'); $this->verifyPageCache($referencing_entity_url, 'MISS'); // Verify a cache hit, but also the presence of the correct cache tags. $expected_tags = Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags); $expected_tags = Cache::mergeTags($expected_tags, $page_cache_tags_referencing_entity); $this->verifyPageCache($referencing_entity_url, 'HIT', $expected_tags); // Also verify the existence of an entity render cache entry. $cache_keys = ['entity_view', 'entity_test', $this->referencingEntity->id(), 'full']; $cid = $this->createCacheId($cache_keys, $entity_cache_contexts); $access_cache_contexts = $this->getAccessCacheContextsForEntity($this->entity); $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->referencingEntity); $redirected_cid = NULL; if (count($access_cache_contexts) || count($additional_cache_contexts)) { $cache_contexts = Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts); $cache_contexts = Cache::mergeContexts($cache_contexts, $access_cache_contexts); $redirected_cid = $this->createCacheId($cache_keys, $cache_contexts); $context_metadata = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts); $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $context_metadata->getCacheTags()); } $this->verifyRenderCache($cid, $referencing_entity_cache_tags, $redirected_cid); $this->pass("Test non-referencing entity.", 'Debug'); $this->verifyPageCache($non_referencing_entity_url, 'MISS'); // Verify a cache hit, but also the presence of the correct cache tags. $this->verifyPageCache($non_referencing_entity_url, 'HIT', Cache::mergeTags($non_referencing_entity_cache_tags, $page_cache_tags)); // Also verify the existence of an entity render cache entry. $cache_keys = ['entity_view', 'entity_test', $this->nonReferencingEntity->id(), 'full']; $cid = $this->createCacheId($cache_keys, $entity_cache_contexts); $this->verifyRenderCache($cid, $non_referencing_entity_cache_tags); $this->pass("Test listing of referencing entities.", 'Debug'); // Prime the page cache for the listing of referencing entities. $this->verifyPageCache($listing_url, 'MISS'); // Verify a cache hit, but also the presence of the correct cache tags. $expected_tags = Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags); $expected_tags = Cache::mergeTags($expected_tags, $page_cache_tags_referencing_entity); $this->verifyPageCache($listing_url, 'HIT', $expected_tags); $this->pass("Test empty listing.", 'Debug'); // Prime the page cache for the empty listing. $this->verifyPageCache($empty_entity_listing_url, 'MISS'); // Verify a cache hit, but also the presence of the correct cache tags. $this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags); // Verify the entity type's list cache contexts are present. $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts'); $this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header)); $this->pass("Test listing containing referenced entity.", 'Debug'); // Prime the page cache for the listing containing the referenced entity. $this->verifyPageCache($nonempty_entity_listing_url, 'MISS', $nonempty_entity_listing_cache_tags); // Verify a cache hit, but also the presence of the correct cache tags. $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags); // Verify the entity type's list cache contexts are present. $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts'); $this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header)); // Verify that after modifying the referenced entity, there is a cache miss // for every route except the one for the non-referencing entity. $this->pass("Test modification of referenced entity.", 'Debug'); $this->entity->save(); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($empty_entity_listing_url, 'MISS'); $this->verifyPageCache($nonempty_entity_listing_url, 'MISS'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); // Verify cache hits. $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); // Verify that after modifying the referencing entity, there is a cache miss // for every route except the ones for the non-referencing entity and the // empty entity listing. $this->pass("Test modification of referencing entity.", 'Debug'); $this->referencingEntity->save(); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); $this->verifyPageCache($empty_entity_listing_url, 'HIT'); // Verify cache hits. $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); // Verify that after modifying the non-referencing entity, there is a cache // miss only for the non-referencing entity route. $this->pass("Test modification of non-referencing entity.", 'Debug'); $this->nonReferencingEntity->save(); $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); $this->verifyPageCache($non_referencing_entity_url, 'MISS'); // Verify cache hits. $this->verifyPageCache($non_referencing_entity_url, 'HIT'); if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) { // Verify that after modifying the entity's display, there is a cache miss // for both the referencing entity, and the listing of referencing // entities, but not for any other routes. $referenced_entity_view_mode = $this->selectViewMode($this->entity->getEntityTypeId()); $this->pass("Test modification of referenced entity's '{$referenced_entity_view_mode}' display.", 'Debug'); $entity_display = entity_get_display($entity_type, $this->entity->bundle(), $referenced_entity_view_mode); $entity_display->save(); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); // Verify cache hits. $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); } if ($bundle_entity_type_id = $this->entity->getEntityType()->getBundleEntityType()) { // Verify that after modifying the corresponding bundle entity, there is a // cache miss for both the referencing entity, and the listing of // referencing entities, but not for any other routes. $this->pass("Test modification of referenced entity's bundle entity.", 'Debug'); $bundle_entity = entity_load($bundle_entity_type_id, $this->entity->bundle()); $bundle_entity->save(); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); // Special case: entity types may choose to use their bundle entity type // cache tags, to avoid having excessively granular invalidation. $is_special_case = $bundle_entity->getCacheTags() == $this->entity->getCacheTags() && $bundle_entity->getEntityType()->getListCacheTags() == $this->entity->getEntityType()->getListCacheTags(); if ($is_special_case) { $this->verifyPageCache($empty_entity_listing_url, 'MISS'); $this->verifyPageCache($nonempty_entity_listing_url, 'MISS'); } else { $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); } // Verify cache hits. $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); if ($is_special_case) { $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); } } if ($this->entity->getEntityType()->get('field_ui_base_route')) { // Verify that after modifying a configurable field on the entity, there // is a cache miss. $this->pass("Test modification of referenced entity's configurable field.", 'Debug'); $field_storage_name = $this->entity->getEntityTypeId() . '.configurable_field'; $field_storage = FieldStorageConfig::load($field_storage_name); $field_storage->save(); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); // Verify cache hits. $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); // Verify that after modifying a configurable field on the entity, there // is a cache miss. $this->pass("Test modification of referenced entity's configurable field.", 'Debug'); $field_name = $this->entity->getEntityTypeId() . '.' . $this->entity->bundle() . '.configurable_field'; $field = FieldConfig::load($field_name); $field->save(); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); // Verify cache hits. $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); } // Verify that after invalidating the entity's cache tag directly, there is // a cache miss for every route except the ones for the non-referencing // entity and the empty entity listing. $this->pass("Test invalidation of referenced entity's cache tag.", 'Debug'); Cache::invalidateTags($this->entity->getCacheTagsToInvalidate()); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($nonempty_entity_listing_url, 'MISS'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); $this->verifyPageCache($empty_entity_listing_url, 'HIT'); // Verify cache hits. $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); // Verify that after invalidating the entity's list cache tag directly, // there is a cache miss for both the empty entity listing and the non-empty // entity listing routes, but not for other routes. $this->pass("Test invalidation of referenced entity's list cache tag.", 'Debug'); Cache::invalidateTags($this->entity->getEntityType()->getListCacheTags()); $this->verifyPageCache($empty_entity_listing_url, 'MISS'); $this->verifyPageCache($nonempty_entity_listing_url, 'MISS'); $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); // Verify cache hits. $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); if (!empty($view_cache_tag)) { // Verify that after invalidating the generic entity type's view cache tag // directly, there is a cache miss for both the referencing entity, and the // listing of referencing entities, but not for other routes. $this->pass("Test invalidation of referenced entity's 'view' cache tag.", 'Debug'); Cache::invalidateTags($view_cache_tag); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); $this->verifyPageCache($empty_entity_listing_url, 'HIT'); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT'); // Verify cache hits. $this->verifyPageCache($referencing_entity_url, 'HIT'); $this->verifyPageCache($listing_url, 'HIT'); } // Verify that after deleting the entity, there is a cache miss for every // route except for the non-referencing entity one. $this->pass('Test deletion of referenced entity.', 'Debug'); $this->entity->delete(); $this->verifyPageCache($referencing_entity_url, 'MISS'); $this->verifyPageCache($listing_url, 'MISS'); $this->verifyPageCache($empty_entity_listing_url, 'MISS'); $this->verifyPageCache($nonempty_entity_listing_url, 'MISS'); $this->verifyPageCache($non_referencing_entity_url, 'HIT'); // Verify cache hits. $referencing_entity_cache_tags = Cache::mergeTags($this->referencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags()); $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['rendered']); $nonempty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->getAdditionalCacheTagsForEntityListing()); $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $page_cache_tags); $this->verifyPageCache($referencing_entity_url, 'HIT', Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags)); $this->verifyPageCache($listing_url, 'HIT', $page_cache_tags); $this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags); $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags); }
/** * @covers ::process * @covers ::processCallback */ function testProcess() { $cache_contexts = Cache::mergeContexts(['baz', 'qux']); $cache_tags = Cache::mergeTags(['foo', 'bar']); $map = [['100', TRUE, LanguageInterface::TYPE_CONTENT, '€100.00'], ['100.7654', TRUE, LanguageInterface::TYPE_CONTENT, '€100.77'], ['1.99', TRUE, LanguageInterface::TYPE_CONTENT, '€1.99'], ['2.99', TRUE, LanguageInterface::TYPE_CONTENT, '€2.99']]; $currency = $this->getMock(CurrencyInterface::class); $currency->expects($this->any())->method('formatAmount')->willReturnMap($map); $currency->expects($this->atLeastOnce())->method('getCacheContexts')->willReturn($cache_contexts); $currency->expects($this->atLeastOnce())->method('getCacheTags')->willReturn($cache_tags); $this->currencyStorage->expects($this->any())->method('load')->with('EUR')->willReturn($currency); $this->input->expects($this->any())->method('parseAmount')->will($this->returnArgument(0)); $langcode = $this->randomMachineName(2); $tokens_valid = ['[currency-localize:EUR:100]' => '€100.00', '[currency-localize:EUR:100.7654]' => '€100.77', '[currency-localize:EUR:1.99]' => '€1.99', '[currency-localize:EUR:2.99]' => '€2.99']; $tokens_invalid = ['[currency-localize]', '[currency-localize:]', '[currency-localize::]', '[currency-localize:EUR]', '[currency-localize:123:456]', '[currency-localize:123]']; foreach ($tokens_valid as $token => $replacement) { $result = $this->sut->process($token, $langcode); $this->assertInstanceOf(FilterProcessResult::class, $result); $this->assertSame($replacement, $result->getProcessedText()); $this->assertSame($cache_contexts, $result->getCacheContexts()); $this->assertSame($cache_tags, $result->getCacheTags()); } foreach ($tokens_invalid as $token) { $result = $this->sut->process($token, $langcode); $this->assertInstanceOf(FilterProcessResult::class, $result); $this->assertSame($token, $result->getProcessedText()); $this->assertEmpty($result->getCacheContexts()); $this->assertEmpty($result->getCacheTags()); } }
/** * {@inheritdoc} */ public function build() { $build = []; // Default the max page age to permanent. $max_page_age = Cache::PERMANENT; $page = $this->executable->getPage(); // Set default page cache keys that include the page and display. $page_cache_keys = ['page_manager_page', $page->id(), $this->configuration['uuid']]; $page_cache_contexts = []; $contexts = $this->getContexts(); foreach ($this->getRegionAssignments() as $region => $blocks) { if (!$blocks) { continue; } $region_name = Html::getClass("block-region-{$region}"); $build['regions'][$region]['#prefix'] = '<div class="' . $region_name . '">'; $build['regions'][$region]['#suffix'] = '</div>'; /** @var $blocks \Drupal\Core\Block\BlockPluginInterface[] */ $weight = 0; foreach ($blocks as $block_id => $block) { if ($block instanceof ContextAwarePluginInterface) { $this->contextHandler()->applyContextMapping($block, $contexts); } if (!$block->access($this->account)) { continue; } $max_age = $block->getCacheMaxAge(); $block_build = ['#theme' => 'block', '#attributes' => [], '#weight' => $weight++, '#configuration' => $block->getConfiguration(), '#plugin_id' => $block->getPluginId(), '#base_plugin_id' => $block->getBaseId(), '#derivative_plugin_id' => $block->getDerivativeId(), '#block_plugin' => $block, '#pre_render' => [[$this, 'buildBlock']], '#cache' => ['keys' => ['page_manager_page', $page->id(), 'block', $block_id], 'tags' => Cache::mergeTags($page->getCacheTags(), $block->getCacheTags()), 'contexts' => $block->getCacheContexts(), 'max-age' => $max_age]]; // Build the cache key and a list of all contexts for the whole page. $page_cache_keys[] = $block_id; $page_cache_contexts = Cache::mergeContexts($page_cache_contexts, $block_build['#cache']['contexts']); if (!empty($block_build['#configuration']['label'])) { $block_build['#configuration']['label'] = SafeMarkup::checkPlain($block_build['#configuration']['label']); } // Update the page max age, set it to the lowest max age of all blocks. $max_page_age = Cache::mergeMaxAges($max_age, $max_page_age); $build['regions'][$region][$block_id] = $block_build; } } $build['#title'] = $this->renderPageTitle($this->configuration['page_title']); if ($max_page_age !== 0) { // If all blocks of this page can be cached, then the max page age is not // 0. In this case, we additionally cache the whole page, so we need // to fetch fewer caches. Also explicitly provide the cache contexts, // additional contexts might still bubble up from the block content, but // if not, then we save a cache redirection. // We don't have to set those values in case we can't cache all blocks, // as they will bubble up from the blocks. $build['regions']['#cache'] = ['keys' => $page_cache_keys, 'contexts' => $page_cache_contexts, 'max-age' => $max_page_age]; } return $build; }
/** * {@inheritdoc} */ public function getCacheContexts() { $contexts = []; if ($this->options['expose_sort_order']) { // The sort order query arg is just important in case there is a exposed // sort order. $has_exposed_sort_handler = FALSE; /** @var \Drupal\views\Plugin\views\sort\SortPluginBase $sort_handler */ foreach ($this->displayHandler->getHandlers('sort') as $sort_handler) { if ($sort_handler->isExposed()) { $has_exposed_sort_handler = TRUE; } } if ($has_exposed_sort_handler) { $contexts[] = 'url.query_args:sort_order'; } } // Merge in cache contexts for all exposed filters to prevent display of // cached forms. foreach ($this->displayHandler->getHandlers('filter') as $filter_hander) { if ($filter_hander->isExposed()) { $contexts = Cache::mergeContexts($contexts, $filter_hander->getCacheContexts()); } } return $contexts; }
/** * See the docs for ::render(). */ protected function doRender(&$elements, $is_root_call = FALSE) { if (empty($elements)) { return ''; } if (!isset($elements['#access']) && isset($elements['#access_callback'])) { if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); } $elements['#access'] = call_user_func($elements['#access_callback'], $elements); } // Early-return nothing if user does not have access. if (isset($elements['#access'])) { // If #access is an AccessResultInterface object, we must apply it's // cacheability metadata to the render array. if ($elements['#access'] instanceof AccessResultInterface) { $this->addCacheableDependency($elements, $elements['#access']); if (!$elements['#access']->isAllowed()) { return ''; } } elseif ($elements['#access'] === FALSE) { return ''; } } // Do not print elements twice. if (!empty($elements['#printed'])) { return ''; } $context = $this->getCurrentRenderContext(); if (!isset($context)) { throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."); } $context->push(new BubbleableMetadata()); // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely // has these configurable defaults, even when no subtree is render cached. // - this is a render cacheable subtree, to ensure that the cached data has // the configurable defaults (which may affect the ID and invalidation). if ($is_root_call || isset($elements['#cache']['keys'])) { $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; if (isset($elements['#cache']['contexts'])) { $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); } else { $elements['#cache']['contexts'] = $required_cache_contexts; } } // Try to fetch the prerendered element from cache, replace any placeholders // and return the final markup. if (isset($elements['#cache']['keys'])) { $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache // in case of nested elements with #cache set. if ($is_root_call) { $this->replacePlaceholders($elements); } // Mark the element markup as safe if is it a string. if (is_string($elements['#markup'])) { $elements['#markup'] = SafeString::create($elements['#markup']); } // The render cache item contains all the bubbleable rendering metadata // for the subtree. $context->update($elements); // Render cache hit, so rendering is finished, all necessary info // collected! $context->bubble(); return $elements['#markup']; } } // Two-tier caching: track pre-bubbling elements' #cache for later // comparison. // @see \Drupal\Core\Render\RenderCacheInterface::get() // @see \Drupal\Core\Render\RenderCacheInterface::set() $pre_bubbling_elements = []; $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : []; // If the default values for this element have not been loaded yet, populate // them. if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { $elements += $this->elementInfo->getInfo($elements['#type']); } // First validate the usage of #lazy_builder; both of the next if-statements // use it if available. if (isset($elements['#lazy_builder'])) { // @todo Convert to assertions once https://www.drupal.org/node/2408013 // lands. if (!is_array($elements['#lazy_builder'])) { throw new \DomainException('The #lazy_builder property must have an array as a value.'); } if (count($elements['#lazy_builder']) !== 2) { throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.'); } if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function ($v) { return is_null($v) || is_scalar($v); }))) { throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL."); } $children = Element::children($elements); if ($children) { throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children))); } $supported_keys = ['#lazy_builder', '#cache', '#create_placeholder', '#weight', '#printed']; $unsupported_keys = array_diff(array_keys($elements), $supported_keys); if (count($unsupported_keys)) { throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys))); } } // If instructed to create a placeholder, and a #lazy_builder callback is // present (without such a callback, it would be impossible to replace the // placeholder), replace the current element with a placeholder. if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) { if (!isset($elements['#lazy_builder'])) { throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.'); } $elements = $this->createPlaceholder($elements); } // Build the element if it is still empty. if (isset($elements['#lazy_builder'])) { $callable = $elements['#lazy_builder'][0]; $args = $elements['#lazy_builder'][1]; if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $new_elements = call_user_func_array($callable, $args); // Retain the original cacheability metadata, plus cache keys. CacheableMetadata::createFromRenderArray($elements)->merge(CacheableMetadata::createFromRenderArray($new_elements))->applyTo($new_elements); if (isset($elements['#cache']['keys'])) { $new_elements['#cache']['keys'] = $elements['#cache']['keys']; } $elements = $new_elements; $elements['#lazy_builder_built'] = TRUE; } // Make any final changes to the element before it is rendered. This means // that the $element or the children can be altered or corrected before the // element is rendered into the final text. if (isset($elements['#pre_render'])) { foreach ($elements['#pre_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements = call_user_func($callable, $elements); } } // Defaults for bubbleable rendering metadata. $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); // Allow #pre_render to abort rendering. if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. $context->update($elements); // #printed, so rendering is finished, all necessary info collected! $context->bubble(); return ''; } // Add any JavaScript state information associated with the element. if (!empty($elements['#states'])) { drupal_process_states($elements); } // Get the children of the element, sorted by weight. $children = Element::children($elements, TRUE); // Initialize this element's #children, unless a #pre_render callback // already preset #children. if (!isset($elements['#children'])) { $elements['#children'] = ''; } if (!empty($elements['#markup'])) { // @todo Decide how to support non-HTML in the render API in // https://www.drupal.org/node/2501313. $elements['#markup'] = $this->xssFilterAdminIfUnsafe($elements['#markup']); } // Assume that if #theme is set it represents an implemented hook. $theme_is_implemented = isset($elements['#theme']); // Check the elements for insecure HTML and pass through sanitization. if (isset($elements)) { $markup_keys = array('#description', '#field_prefix', '#field_suffix'); foreach ($markup_keys as $key) { if (!empty($elements[$key]) && is_scalar($elements[$key])) { $elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]); } } } // Call the element's #theme function if it is set. Then any children of the // element have to be rendered there. If the internal #render_children // property is set, do not call the #theme function to prevent infinite // recursion. if ($theme_is_implemented && !isset($elements['#render_children'])) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the // hook in #theme was not found in the registry and so we need to update // our flag accordingly. This is common for theme suggestions. $theme_is_implemented = $elements['#children'] !== FALSE; } // If #theme is not implemented or #render_children is set and the element // has an empty #children attribute, render the children now. This is the // same process as Renderer::render() but is inlined for speed. if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } $elements['#children'] = SafeString::create($elements['#children']); } // If #theme is not implemented and the element has raw #markup as a // fallback, prepend the content in #markup to #children. In this case // #children will contain whatever is provided by #pre_render prepended to // what is rendered recursively above. If #theme is implemented then it is // the responsibility of that theme implementation to render #markup if // required. Eventually #theme_wrappers will expect both #markup and // #children to be a single string as #children. if (!$theme_is_implemented && isset($elements['#markup'])) { $elements['#children'] = SafeString::create($elements['#markup'] . $elements['#children']); } // Let the theme functions in #theme_wrappers add markup around the rendered // children. // #states and #attached have to be processed before #theme_wrappers, // because the #type 'page' render array from drupal_prepare_page() would // render the $page and wrap it into the html.html.twig template without the // attached assets otherwise. // If the internal #render_children property is set, do not call the // #theme_wrappers function(s) to prevent infinite recursion. if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { foreach ($elements['#theme_wrappers'] as $key => $value) { // If the value of a #theme_wrappers item is an array then the theme // hook is found in the key of the item and the value contains attribute // overrides. Attribute overrides replace key/value pairs in $elements // for only this ThemeManagerInterface::render() call. This allows // #theme hooks and #theme_wrappers hooks to share variable names // without conflict or ambiguity. $wrapper_elements = $elements; if (is_string($key)) { $wrapper_hook = $key; foreach ($value as $attribute => $override) { $wrapper_elements[$attribute] = $override; } } else { $wrapper_hook = $value; } $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); } } // Filter the outputted content and make any last changes before the content // is sent to the browser. The changes are made on $content which allows the // outputted text to be filtered. if (isset($elements['#post_render'])) { foreach ($elements['#post_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); } } // We store the resulting output in $elements['#markup'], to be consistent // with how render cached output gets stored. This ensures that placeholder // replacement logic gets the same data to work with, no matter if #cache is // disabled, #cache is enabled, there is a cache hit or miss. $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : ''; $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : ''; $elements['#markup'] = $prefix . $elements['#children'] . $suffix; // We've rendered this element (and its subtree!), now update the context. $context->update($elements); // Cache the processed element if both $pre_bubbling_elements and $elements // have the metadata necessary to generate a cache ID. if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); } $this->renderCache->set($elements, $pre_bubbling_elements); } // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache in // case of nested elements with #cache set. // // By running them here, we ensure that: // - they run when #cache is disabled, // - they run when #cache is enabled and there is a cache miss. // Only the case of a cache hit when #cache is enabled, is not handled here, // that is handled earlier in Renderer::render(). if ($is_root_call) { $this->replacePlaceholders($elements); // @todo remove as part of https://www.drupal.org/node/2511330. if ($context->count() !== 1) { throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); } } // Rendering is finished, all necessary info collected! $context->bubble(); $elements['#printed'] = TRUE; return SafeString::create($elements['#markup']); }
/** * Ensures that some cache contexts are present in the current response. * * @param string[] $expected_contexts * The expected cache contexts. * @param string $message * (optional) A verbose message to output. * @param bool $include_default_contexts * (optional) Whether the default contexts should automatically be included. * * @return * TRUE if the assertion succeeded, FALSE otherwise. */ protected function assertCacheContexts(array $expected_contexts, $message = NULL, $include_default_contexts = TRUE) { if ($include_default_contexts) { $default_contexts = ['languages:language_interface', 'theme']; // Add the user.permission context to the list of default contexts except // when user is already there. if (!in_array('user', $expected_contexts)) { $default_contexts[] = 'user.permissions'; } $expected_contexts = Cache::mergeContexts($expected_contexts, $default_contexts); } $actual_contexts = $this->getCacheHeaderValues('X-Drupal-Cache-Contexts'); sort($expected_contexts); sort($actual_contexts); $return = $this->assertIdentical($actual_contexts, $expected_contexts, $message); if (!$return) { debug('Unwanted cache contexts in response: ' . implode(',', array_diff($actual_contexts, $expected_contexts))); debug('Missing cache contexts in response: ' . implode(',', array_diff($expected_contexts, $actual_contexts))); } return $return; }
/** * Tests the cache tags on the front page. * * @param bool $do_assert_views_caches * Whether to check Views' result & output caches. */ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) { $view = Views::getView('frontpage'); $view->setDisplay('page_1'); $cache_contexts = ['user.node_grants:view', 'languages:' . LanguageInterface::TYPE_INTERFACE, 'user.permissions', 'theme', 'url.query_args', 'url.site']; $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags(); // Test before there are any nodes. $empty_node_listing_cache_tags = ['config:views.view.frontpage', 'node_list']; $render_cache_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags); $render_cache_tags = Cache::mergeTags($render_cache_tags, ['config:system.site']); $this->assertViewsCacheTags($view, $empty_node_listing_cache_tags, $do_assert_views_caches, $render_cache_tags); $expected_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags); $expected_tags = Cache::mergeTags($expected_tags, ['rendered', 'config:user.role.anonymous', 'config:system.site']); $this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1'), $cache_contexts, $expected_tags); // Create some nodes on the frontpage view. Add more than 10 nodes in order // to enable paging. $this->drupalCreateContentType(['type' => 'article']); for ($i = 0; $i < 15; $i++) { $node = Node::create(['body' => [['value' => $this->randomMachineName(32), 'format' => filter_default_format()]], 'type' => 'article', 'created' => $i, 'title' => $this->randomMachineName(8), 'nid' => $i + 1]); $node->enforceIsNew(TRUE); $node->save(); } $cache_contexts = Cache::mergeContexts($cache_contexts, ['timezone']); $this->pass('First page'); // First page. $first_page_result_cache_tags = ['config:views.view.frontpage', 'node_list', 'node:6', 'node:7', 'node:8', 'node:9', 'node:10', 'node:11', 'node:12', 'node:13', 'node:14', 'node:15']; $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags(); $first_page_output_cache_tags = Cache::mergeTags($first_page_result_cache_tags, $cache_context_tags); $first_page_output_cache_tags = Cache::mergeTags($first_page_output_cache_tags, ['config:filter.format.plain_text', 'node_view', 'user_view', 'user:0']); $view->setDisplay('page_1'); $view->setCurrentPage(0); $this->assertViewsCacheTags($view, $first_page_result_cache_tags, $do_assert_views_caches, $first_page_output_cache_tags); $this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1'), $cache_contexts, Cache::mergeTags($first_page_output_cache_tags, ['rendered', 'config:user.role.anonymous'])); // Second page. $this->pass('Second page'); $this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1', [], ['query' => ['page' => 1]]), $cache_contexts, ['node:1', 'node:2', 'node:3', 'node:4', 'node:5', 'config:filter.format.plain_text', 'config:views.view.frontpage', 'node_list', 'node_view', 'user_view', 'user:0', 'rendered', 'config:user.role.anonymous']); // Let's update a node title on the first page and ensure that the page // cache entry invalidates. $node = Node::load(10); $title = $node->getTitle() . 'a'; $node->setTitle($title); $node->save(); $this->drupalGet(Url::fromRoute('view.frontpage.page_1')); $this->assertText($title); }
/** * Tests the basic translation workflow. */ protected function doTestBasicTranslation() { // Create a new test entity with original values in the default language. $default_langcode = $this->langcodes[0]; $values[$default_langcode] = $this->getNewEntityValues($default_langcode); // Create the entity with the editor as owner, so that afterwards a new // translation is created by the translator and the translation author is // tested. $this->drupalLogin($this->editor); $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode); $this->drupalLogin($this->translator); $entity = entity_load($this->entityTypeId, $this->entityId, TRUE); $this->assertTrue($entity, 'Entity found in the database.'); $this->drupalGet($entity->urlInfo()); $this->assertResponse(200, 'Entity URL is valid.'); // Ensure that the content language cache context is not yet added to the // page. $this->assertCacheContexts($this->defaultCacheContexts); $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); $this->assertNoText('Source language', 'Source language column correctly hidden.'); $translation = $this->getTranslation($entity, $default_langcode); foreach ($values[$default_langcode] as $property => $value) { $stored_value = $this->getValue($translation, $property, $default_langcode); $value = is_array($value) ? $value[0]['value'] : $value; $message = format_string('@property correctly stored in the default language.', array('@property' => $property)); $this->assertEqual($stored_value, $value, $message); } // Add a content translation. $langcode = 'it'; $language = ConfigurableLanguage::load($langcode); $values[$langcode] = $this->getNewEntityValues($langcode); $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [$entity->getEntityTypeId() => $entity->id(), 'source' => $default_langcode, 'target' => $langcode], array('language' => $language)); $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode)); // Assert that HTML is escaped in "all languages" in UI after SafeMarkup // change. if ($this->testHTMLEscapeForAllLanguages) { $this->assertNoRaw('<span class="translation-entity-all-languages">(all languages)</span>'); $this->assertRaw('<span class="translation-entity-all-languages">(all languages)</span>'); } // Ensure that the content language cache context is not yet added to the // page. $entity = entity_load($this->entityTypeId, $this->entityId, TRUE); $this->drupalGet($entity->urlInfo()); $this->assertCacheContexts(Cache::mergeContexts(['languages:language_content'], $this->defaultCacheContexts)); // Reset the cache of the entity, so that the new translation gets the // updated values. $metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode)); $metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); $author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid'; if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) { $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(), SafeMarkup::format('Author of the target translation @langcode correctly stored for translatable owner field.', array('@langcode' => $langcode))); $this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(), SafeMarkup::format('Author of the target translation @target different from the author of the source translation @source for translatable owner field.', array('@target' => $langcode, '@source' => $default_langcode))); } else { $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->editor->id(), 'Author of the entity remained untouched after translation for non translatable owner field.'); } $created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created'; if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) { $this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(), SafeMarkup::format('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.', array('@target' => $langcode, '@source' => $default_langcode))); } else { $this->assertEqual($metadata_target_translation->getCreatedTime(), $metadata_source_translation->getCreatedTime(), 'Creation timestamp of the entity remained untouched after translation for non translatable created field.'); } if ($this->testLanguageSelector) { $this->assertNoFieldByXPath('//select[@id="edit-langcode-0-value"]', NULL, 'Language selector correctly disabled on translations.'); } $entity = entity_load($this->entityTypeId, $this->entityId, TRUE); $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); $this->assertNoText('Source language', 'Source language column correctly hidden.'); // Switch the source language. $langcode = 'fr'; $language = ConfigurableLanguage::load($langcode); $source_langcode = 'it'; $edit = array('source_langcode[source]' => $source_langcode); $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [$entity->getEntityTypeId() => $entity->id(), 'source' => $default_langcode, 'target' => $langcode], array('language' => $language)); // This does not save anything, it merely reloads the form and fills in the // fields with the values from the different source language. $this->drupalPostForm($add_url, $edit, t('Change')); $this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.'); // Add another translation and mark the other ones as outdated. $values[$langcode] = $this->getNewEntityValues($langcode); $edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE); $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [$entity->getEntityTypeId() => $entity->id(), 'source' => $source_langcode, 'target' => $langcode], array('language' => $language)); $this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode)); $entity = entity_load($this->entityTypeId, $this->entityId, TRUE); $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); $this->assertText('Source language', 'Source language column correctly shown.'); // Check that the entered values have been correctly stored. foreach ($values as $langcode => $property_values) { $translation = $this->getTranslation($entity, $langcode); foreach ($property_values as $property => $value) { $stored_value = $this->getValue($translation, $property, $langcode); $value = is_array($value) ? $value[0]['value'] : $value; $message = format_string('%property correctly stored with language %language.', array('%property' => $property, '%language' => $langcode)); $this->assertEqual($stored_value, $value, $message); } } }
/** * {@inheritdoc} */ public function getCacheContexts() { $contexts = []; // By definition arguments depends on the URL. // @todo Once contexts are properly injected into block views we could pull // the information from there. $contexts[] = 'url'; // Asks all subplugins (argument defaults, argument validator and styles). if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheableDependencyInterface) { $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts()); } if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheableDependencyInterface) { $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts()); } if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheableDependencyInterface) { $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts()); } return $contexts; }
/** * Gets the render cache for a given view. * * @param \Drupal\views\ViewExecutable $view * The view. * * @return array|FALSE * The render cache result or FALSE if not existent. */ protected function getRenderCache(ViewExecutable $view) { /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */ $render_cache = \Drupal::service('render_cache'); $view->element = ['#cache' => []]; $build = $view->buildRenderable(); $build['#cache']['contexts'] = Cache::mergeContexts($build['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']); return $render_cache->get($build); }
/** * {@inheritdoc} */ public function getCacheContexts() { return Cache::mergeContexts(parent::getCacheContexts(), ['route']); }
/** * Tests REST export with views render caching enabled. */ public function testRestRenderCaching() { $this->drupalLogin($this->adminUser); /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */ $render_cache = \Drupal::service('render_cache'); // Enable render caching for the views. /** @var \Drupal\views\ViewEntityInterface $storage */ $storage = View::load('test_serializer_display_entity'); $options =& $storage->getDisplay('default'); $options['display_options']['cache'] = ['type' => 'tag']; $storage->save(); $original = DisplayPluginBase::buildBasicRenderable('test_serializer_display_entity', 'rest_export_1'); // Ensure that there is no corresponding render cache item yet. $original['#cache'] += ['contexts' => []]; $original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']); $cache_tags = ['config:views.view.test_serializer_display_entity', 'entity_test:1', 'entity_test:10', 'entity_test:2', 'entity_test:3', 'entity_test:4', 'entity_test:5', 'entity_test:6', 'entity_test:7', 'entity_test:8', 'entity_test:9', 'entity_test_list']; $cache_contexts = ['entity_test_view_grants', 'languages:language_interface', 'theme', 'request_format']; $this->assertFalse($render_cache->get($original)); // Request the page, once in XML and once in JSON to ensure that the caching // varies by it. $result1 = $this->drupalGetJSON('test/serialize/entity'); $this->addRequestWithFormat('json'); $this->assertHeader('content-type', 'application/json'); $this->assertCacheContexts($cache_contexts); $this->assertCacheTags($cache_tags); $this->assertTrue($render_cache->get($original)); $result_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml'); $this->addRequestWithFormat('xml'); $this->assertHeader('content-type', 'text/xml; charset=UTF-8'); $this->assertCacheContexts($cache_contexts); $this->assertCacheTags($cache_tags); $this->assertTrue($render_cache->get($original)); // Ensure that the XML output is different from the JSON one. $this->assertNotEqual($result1, $result_xml); // Ensure that the cached page works. $result2 = $this->drupalGetJSON('test/serialize/entity'); $this->addRequestWithFormat('json'); $this->assertHeader('content-type', 'application/json'); $this->assertEqual($result2, $result1); $this->assertCacheContexts($cache_contexts); $this->assertCacheTags($cache_tags); $this->assertTrue($render_cache->get($original)); // Create a new entity and ensure that the cache tags are taken over. EntityTest::create(['name' => 'test_11', 'user_id' => $this->adminUser->id()])->save(); $result3 = $this->drupalGetJSON('test/serialize/entity'); $this->addRequestWithFormat('json'); $this->assertHeader('content-type', 'application/json'); $this->assertNotEqual($result3, $result2); // Add the new entity cache tag and remove the first one, because we just // show 10 items in total. $cache_tags[] = 'entity_test:11'; unset($cache_tags[array_search('entity_test:1', $cache_tags)]); $this->assertCacheContexts($cache_contexts); $this->assertCacheTags($cache_tags); $this->assertTrue($render_cache->get($original)); }
/** * See the docs for ::render(). */ protected function doRender(&$elements, $is_root_call = FALSE) { if (!isset($elements['#access']) && isset($elements['#access_callback'])) { if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); } $elements['#access'] = call_user_func($elements['#access_callback'], $elements); } // Early-return nothing if user does not have access. if (empty($elements) || isset($elements['#access']) && !$elements['#access']) { return ''; } // Do not print elements twice. if (!empty($elements['#printed'])) { return ''; } if (!isset(static::$stack)) { static::$stack = new \SplStack(); } static::$stack->push(new BubbleableMetadata()); // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely // has these configurable defaults, even when no subtree is render cached. // - this is a render cacheable subtree, to ensure that the cached data has // the configurable defaults (which may affect the ID and invalidation). if ($is_root_call || isset($elements['#cache']['keys'])) { $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; if (isset($elements['#cache']['contexts'])) { $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); } else { $elements['#cache']['contexts'] = $required_cache_contexts; } } // Try to fetch the prerendered element from cache, run any // #post_render_cache callbacks and return the final markup. if (isset($elements['#cache']['keys'])) { $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; // Only when we're not in a root (non-recursive) drupal_render() call, // #post_render_cache callbacks must be executed, to prevent breaking // the render cache in case of nested elements with #cache set. if ($is_root_call) { $this->processPostRenderCache($elements); } // Mark the element markup as safe. If we have cached children, we need // to mark them as safe too. The parent markup contains the child // markup, so if the parent markup is safe, then the markup of the // individual children must be safe as well. $elements['#markup'] = SafeMarkup::set($elements['#markup']); if (!empty($elements['#cache_properties'])) { foreach (Element::children($cached_element) as $key) { SafeMarkup::set($cached_element[$key]['#markup']); } } // The render cache item contains all the bubbleable rendering metadata // for the subtree. $this->updateStack($elements); // Render cache hit, so rendering is finished, all necessary info // collected! $this->bubbleStack(); return $elements['#markup']; } } // Two-tier caching: track pre-bubbling elements' #cache for later // comparison. // @see \Drupal\Core\Render\RenderCacheInterface::get() // @see \Drupal\Core\Render\RenderCacheInterface::set() $pre_bubbling_elements = []; $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : []; // If the default values for this element have not been loaded yet, populate // them. if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { $elements += $this->elementInfo->getInfo($elements['#type']); } // Make any final changes to the element before it is rendered. This means // that the $element or the children can be altered or corrected before the // element is rendered into the final text. if (isset($elements['#pre_render'])) { foreach ($elements['#pre_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements = call_user_func($callable, $elements); } } // Defaults for bubbleable rendering metadata. $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); // Allow #pre_render to abort rendering. if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. $this->updateStack($elements); // #printed, so rendering is finished, all necessary info collected! $this->bubbleStack(); return ''; } // Add any JavaScript state information associated with the element. if (!empty($elements['#states'])) { drupal_process_states($elements); } // Get the children of the element, sorted by weight. $children = Element::children($elements, TRUE); // Initialize this element's #children, unless a #pre_render callback // already preset #children. if (!isset($elements['#children'])) { $elements['#children'] = ''; } // @todo Simplify after https://www.drupal.org/node/2273925. if (isset($elements['#markup'])) { $elements['#markup'] = SafeMarkup::set($elements['#markup']); } // Assume that if #theme is set it represents an implemented hook. $theme_is_implemented = isset($elements['#theme']); // Check the elements for insecure HTML and pass through sanitization. if (isset($elements)) { $markup_keys = array('#description', '#field_prefix', '#field_suffix'); foreach ($markup_keys as $key) { if (!empty($elements[$key]) && is_scalar($elements[$key])) { $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]); } } } // Call the element's #theme function if it is set. Then any children of the // element have to be rendered there. If the internal #render_children // property is set, do not call the #theme function to prevent infinite // recursion. if ($theme_is_implemented && !isset($elements['#render_children'])) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the // hook in #theme was not found in the registry and so we need to update // our flag accordingly. This is common for theme suggestions. $theme_is_implemented = $elements['#children'] !== FALSE; } // If #theme is not implemented or #render_children is set and the element // has an empty #children attribute, render the children now. This is the // same process as Renderer::render() but is inlined for speed. if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } $elements['#children'] = SafeMarkup::set($elements['#children']); } // If #theme is not implemented and the element has raw #markup as a // fallback, prepend the content in #markup to #children. In this case // #children will contain whatever is provided by #pre_render prepended to // what is rendered recursively above. If #theme is implemented then it is // the responsibility of that theme implementation to render #markup if // required. Eventually #theme_wrappers will expect both #markup and // #children to be a single string as #children. if (!$theme_is_implemented && isset($elements['#markup'])) { $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']); } // Let the theme functions in #theme_wrappers add markup around the rendered // children. // #states and #attached have to be processed before #theme_wrappers, // because the #type 'page' render array from drupal_prepare_page() would // render the $page and wrap it into the html.html.twig template without the // attached assets otherwise. // If the internal #render_children property is set, do not call the // #theme_wrappers function(s) to prevent infinite recursion. if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { foreach ($elements['#theme_wrappers'] as $key => $value) { // If the value of a #theme_wrappers item is an array then the theme // hook is found in the key of the item and the value contains attribute // overrides. Attribute overrides replace key/value pairs in $elements // for only this ThemeManagerInterface::render() call. This allows // #theme hooks and #theme_wrappers hooks to share variable names // without conflict or ambiguity. $wrapper_elements = $elements; if (is_string($key)) { $wrapper_hook = $key; foreach ($value as $attribute => $override) { $wrapper_elements[$attribute] = $override; } } else { $wrapper_hook = $value; } $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); } } // Filter the outputted content and make any last changes before the content // is sent to the browser. The changes are made on $content which allows the // outputted text to be filtered. if (isset($elements['#post_render'])) { foreach ($elements['#post_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); } } // We store the resulting output in $elements['#markup'], to be consistent // with how render cached output gets stored. This ensures that // #post_render_cache callbacks get the same data to work with, no matter if // #cache is disabled, #cache is enabled, there is a cache hit or miss. $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : ''; $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : ''; $elements['#markup'] = $prefix . $elements['#children'] . $suffix; // We've rendered this element (and its subtree!), now update the stack. $this->updateStack($elements); // Cache the processed element if both $pre_bubbling_elements and $elements // have the metadata necessary to generate a cache ID. if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); } $this->renderCache->set($elements, $pre_bubbling_elements); } // Only when we're in a root (non-recursive) drupal_render() call, // #post_render_cache callbacks must be executed, to prevent breaking the // render cache in case of nested elements with #cache set. // // By running them here, we ensure that: // - they run when #cache is disabled, // - they run when #cache is enabled and there is a cache miss. // Only the case of a cache hit when #cache is enabled, is not handled here, // that is handled earlier in Renderer::render(). if ($is_root_call) { // We've already called ::updateStack() earlier, which updated both the // element and current stack frame. However, // Renderer::processPostRenderCache() can both change the element // further and create and render new child elements, so provide a fresh // stack frame to collect those additions, merge them back to the element, // and then update the current frame to match the modified element state. do { static::$stack->push(new BubbleableMetadata()); $this->processPostRenderCache($elements); $post_render_additions = static::$stack->pop(); $elements['#post_render_cache'] = NULL; BubbleableMetadata::createFromRenderArray($elements)->merge($post_render_additions)->applyTo($elements); } while (!empty($elements['#post_render_cache'])); if (static::$stack->count() !== 1) { throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); } } // Rendering is finished, all necessary info collected! $this->bubbleStack(); $elements['#printed'] = TRUE; $elements['#markup'] = SafeMarkup::set($elements['#markup']); return $elements['#markup']; }