/** * {@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; }
/** * 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) { /** @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->get('weight'), '#configuration' => $configuration, '#plugin_id' => $plugin_id, '#base_plugin_id' => $base_id, '#derivative_plugin_id' => $derivative_id, '#id' => $entity->id(), '#block' => $entity); $build[$entity_id]['#configuration']['label'] = String::checkPlain($configuration['label']); // Set cache tags; these always need to be set, whether the block is // cacheable or not, so that the page cache is correctly informed. $build[$entity_id]['#cache']['tags'] = Cache::mergeTags($this->getCacheTag(), $entity->getCacheTag(), $plugin->getCacheTags()); if ($plugin->isCacheable()) { $build[$entity_id]['#pre_render'][] = array($this, 'buildBlock'); // Generic cache keys, with the block plugin's custom keys appended // (usually cache context keys like 'cache_context.user.roles'). $default_cache_keys = array('entity_view', 'block', $entity->id(), $this->languageManager->getCurrentLanguage()->getId(), 'cache_context.theme'); $max_age = $plugin->getCacheMaxAge(); $build[$entity_id]['#cache'] += array('keys' => array_merge($default_cache_keys, $plugin->getCacheKeys()), 'expire' => $max_age === Cache::PERMANENT ? Cache::PERMANENT : REQUEST_TIME + $max_age); } 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; }
/** * Tests the bubbling of cache tags. */ public function testCacheTags() { /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = $this->container->get('renderer'); // Create the entity that will be commented upon. $commented_entity = entity_create('entity_test', array('name' => $this->randomMachineName())); $commented_entity->save(); // Verify cache tags on the rendered entity before it has comments. $build = \Drupal::entityManager()->getViewBuilder('entity_test')->view($commented_entity); $renderer->renderRoot($build); $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($build['#cache']['contexts'])->getCacheTags(); $expected_cache_tags = Cache::mergeTags($cache_context_tags, ['entity_test_view', 'entity_test:' . $commented_entity->id(), 'comment_list', 'config:core.entity_form_display.comment.comment.default', 'config:field.field.comment.comment.comment_body', 'config:field.field.entity_test.entity_test.comment', 'config:field.storage.comment.comment_body', 'config:user.settings']); sort($expected_cache_tags); $this->assertEqual($build['#cache']['tags'], $expected_cache_tags); // Create a comment on that entity. Comment loading requires that the uid // also exists in the {users} table. $user = $this->createUser(); $user->save(); $comment = entity_create('comment', array('subject' => 'Llama', 'comment_body' => array('value' => 'Llamas are cool!', 'format' => 'plain_text'), 'entity_id' => $commented_entity->id(), 'entity_type' => 'entity_test', 'field_name' => 'comment', 'comment_type' => 'comment', 'status' => CommentInterface::PUBLISHED, 'uid' => $user->id())); $comment->save(); // Load commented entity so comment_count gets computed. // @todo Remove the $reset = TRUE parameter after // https://www.drupal.org/node/597236 lands. It's a temporary work-around. $commented_entity = entity_load('entity_test', $commented_entity->id(), TRUE); // Verify cache tags on the rendered entity when it has comments. $build = \Drupal::entityManager()->getViewBuilder('entity_test')->view($commented_entity); $renderer->renderRoot($build); $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($build['#cache']['contexts'])->getCacheTags(); $expected_cache_tags = Cache::mergeTags($cache_context_tags, ['entity_test_view', 'entity_test:' . $commented_entity->id(), 'comment_list', 'comment_view', 'comment:' . $comment->id(), 'config:filter.format.plain_text', 'user_view', 'user:2', 'config:core.entity_form_display.comment.comment.default', 'config:field.field.comment.comment.comment_body', 'config:field.field.entity_test.entity_test.comment', 'config:field.storage.comment.comment_body', 'config:user.settings']); sort($expected_cache_tags); $this->assertEqual($build['#cache']['tags'], $expected_cache_tags); }
/** * {@inheritdoc} */ public function getDerivativeDefinitions($base_plugin_definition) { $this->derivatives = []; /** @var \Drupal\rng\Entity\EventType[] $event_types */ foreach ($this->eventManager->getEventTypes() as $entity_type => $event_types) { $cache_tags = $this->entityManager->getDefinition($entity_type)->getListCacheTags(); foreach ($event_types as $event_type) { $cache_tags = Cache::mergeTags($cache_tags, $event_type->getCacheTags()); } // Only need one set of tasks task per entity type. if ($this->routeProvider->getRouteByName("entity.{$entity_type}.canonical")) { $event_default = "rng.event.{$entity_type}.event.default"; $this->derivatives[$event_default] = array('title' => t('Event'), 'base_route' => "entity.{$entity_type}.canonical", 'route_name' => "rng.event.{$entity_type}.event", 'weight' => 30, 'cache_tags' => $cache_tags); $this->derivatives["rng.event.{$entity_type}.event.settings"] = array('title' => t('Settings'), 'route_name' => $this->derivatives[$event_default]['route_name'], 'parent_id' => 'rng.local_tasks:' . $event_default, 'weight' => 10, 'cache_tags' => $cache_tags); $this->derivatives["rng.event.{$entity_type}.event.access"] = array('title' => t('Access'), 'route_name' => "rng.event.{$entity_type}.access", 'parent_id' => 'rng.local_tasks:' . $event_default, 'weight' => 20, 'cache_tags' => $cache_tags); $this->derivatives["rng.event.{$entity_type}.event.messages"] = array('title' => t('Messages'), 'route_name' => "rng.event.{$entity_type}.messages", 'parent_id' => 'rng.local_tasks:' . $event_default, 'weight' => 30, 'cache_tags' => $cache_tags); $this->derivatives["rng.event.{$entity_type}.event.group.list"] = array('title' => t('Groups'), 'route_name' => "rng.event.{$entity_type}.group.list", 'parent_id' => 'rng.local_tasks:' . $event_default, 'weight' => 40, 'cache_tags' => $cache_tags); $this->derivatives["rng.event.{$entity_type}.register.type_list"] = array('route_name' => "rng.event.{$entity_type}.register.type_list", 'base_route' => "entity.{$entity_type}.canonical", 'title' => t('Register'), 'weight' => 40, 'cache_tags' => $cache_tags); } } foreach ($this->derivatives as &$entry) { $entry += $base_plugin_definition; } return parent::getDerivativeDefinitions($base_plugin_definition); }
/** * #pre_render callback for building the regions. */ public function buildRegions(array $build) { $cacheability = CacheableMetadata::createFromRenderArray($build)->addCacheableDependency($this); $contexts = $this->getContexts(); foreach ($this->getRegionAssignments() as $region => $blocks) { if (!$blocks) { continue; } $region_name = Html::getClass("block-region-{$region}"); $build[$region]['#prefix'] = '<div class="' . $region_name . '">'; $build[$region]['#suffix'] = '</div>'; /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ $weight = 0; foreach ($blocks as $block_id => $block) { if ($block instanceof ContextAwarePluginInterface) { $this->contextHandler()->applyContextMapping($block, $contexts); } $access = $block->access($this->account, TRUE); $cacheability->addCacheableDependency($access); if (!$access->isAllowed()) { continue; } $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_block_display', $this->id(), 'block', $block_id], 'tags' => Cache::mergeTags($this->getCacheTags(), $block->getCacheTags()), 'contexts' => $block->getCacheContexts(), 'max-age' => $block->getCacheMaxAge()]]; // Merge the cacheability metadata of blocks into the page. This helps // to avoid cache redirects if the blocks have more cache contexts than // the page, which the page must respect as well. $cacheability->addCacheableDependency($block); $build[$region][$block_id] = $block_build; } } $build['#title'] = $this->renderPageTitle($this->configuration['page_title']); $cacheability->applyTo($build); 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; }
/** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { $config = $this->config('system.site'); // Display login form: $form['name'] = array('#type' => 'textfield', '#title' => $this->t('Username'), '#size' => 60, '#maxlength' => USERNAME_MAX_LENGTH, '#description' => $this->t('Enter your @s username.', array('@s' => $config->get('name'))), '#required' => TRUE, '#attributes' => array('autocorrect' => 'off', 'autocapitalize' => 'off', 'spellcheck' => 'false', 'autofocus' => 'autofocus')); $form['pass'] = array('#type' => 'password', '#title' => $this->t('Password'), '#size' => 60, '#description' => $this->t('Enter the password that accompanies your username.'), '#required' => TRUE); $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array('#type' => 'submit', '#value' => $this->t('Log in')); $form['#validate'][] = '::validateName'; $form['#validate'][] = '::validateAuthentication'; $form['#validate'][] = '::validateFinal'; $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [], $config->getCacheTags()); return $form; }
/** * {@inheritdoc} */ public function invalidateTags(array $tags) { // When either an extension (module/theme) is (un)installed, purge // everything. if (in_array('config:core.extension', $tags)) { // @todo Purge everything. Blocked on https://github.com/d8-contrib-modules/cloudflare/issues/16. return; } // Also invalidate the cache tags as hashes, to automatically also work for // responses that exceed CloudFlare's Cache-Tag header limit. $hashes = CloudFlareCacheTagHeaderGenerator::cacheTagsToHashes($tags); $tags = Cache::mergeTags($tags, $hashes); $this->purgeTags($tags); }
/** * Tests comments as part of an RSS feed. */ function testCommentRss() { // Find comment in RSS feed. $this->drupalLogin($this->webUser); $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName()); $this->drupalGet('rss.xml'); $cache_contexts = ['languages:language_interface', 'theme', 'url.site', 'user.node_grants:view', 'user.permissions', 'timezone']; $this->assertCacheContexts($cache_contexts); $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags(); $this->assertCacheTags(Cache::mergeTags($cache_context_tags, ['config:views.view.frontpage', 'node:1', 'node_list', 'node_view', 'user:3'])); $raw = '<comments>' . $this->node->url('canonical', array('fragment' => 'comments', 'absolute' => TRUE)) . '</comments>'; $this->assertRaw($raw, 'Comments as part of RSS feed.'); // Hide comments from RSS feed and check presence. $this->node->set('comment', CommentItemInterface::HIDDEN); $this->node->save(); $this->drupalGet('rss.xml'); $this->assertNoRaw($raw, 'Hidden comments is not a part of RSS feed.'); }
/** * Asserts a view's result & output cache items' cache tags. * * @param \Drupal\views\ViewExecutable $view * The view to test, must have caching enabled. * @param null|string[] $expected_results_cache * NULL when expecting no results cache item, a set of cache tags expected * to be set on the results cache item otherwise. * @param bool $views_caching_is_enabled * Whether to expect an output cache item. If TRUE, the cache tags must * match those in $expected_render_array_cache_tags. * @param string[] $expected_render_array_cache_tags * A set of cache tags expected to be set on the built view's render array. * * @return array * The render array */ protected function assertViewsCacheTags(ViewExecutable $view, $expected_results_cache, $views_caching_is_enabled, array $expected_render_array_cache_tags) { $build = $view->preview(); // 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_stack->push(new Request()); \Drupal::service('renderer')->renderRoot($build); $request_stack->pop(); // 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); if ($views_caching_is_enabled) { $this->pass('Checking Views results cache item cache tags.'); /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ $cache_plugin = $view->display_handler->getPlugin('cache'); // Results cache. $results_cache_item = \Drupal::cache('data')->get($cache_plugin->generateResultsKey()); if (is_array($expected_results_cache)) { $this->assertTrue($results_cache_item, 'Results cache item found.'); if ($results_cache_item) { sort($expected_results_cache); $this->assertEqual($results_cache_item->tags, $expected_results_cache); } } else { $this->assertFalse($results_cache_item, 'Results cache item not found.'); } // Output cache. $this->pass('Checking Views output cache item cache tags.'); $output_cache_item = \Drupal::cache('render')->get($cache_plugin->generateOutputKey()); if ($views_caching_is_enabled === TRUE) { $this->assertTrue($output_cache_item, 'Output cache item found.'); if ($output_cache_item) { $this->assertEqual($output_cache_item->tags, Cache::mergeTags($expected_render_array_cache_tags, ['rendered'])); } } else { $this->assertFalse($output_cache_item, 'Output cache item not found.'); } } $view->destroy(); return $build; }
/** * {@inheritdoc} */ public function getDerivativeDefinitions($base_plugin_definition) { $this->derivatives = []; /** @var \Drupal\rng\Entity\EventType[] $event_types */ foreach ($this->eventManager->getEventTypes() as $entity_type => $event_types) { $cache_tags = $this->entityManager->getDefinition($entity_type)->getListCacheTags(); foreach ($event_types as $event_type) { $cache_tags = Cache::mergeTags($cache_tags, $event_type->getCacheTags()); } // Only need one set of actions per entity type. $this->derivatives["rng.event.{$entity_type}.event.access.reset"] = array('title' => $this->t('Reset/customize access rules'), 'route_name' => "rng.event.{$entity_type}.access.reset", 'class' => '\\Drupal\\rng\\Plugin\\Menu\\LocalAction\\ResetAccessRules', 'appears_on' => array("rng.event.{$entity_type}.access"), 'cache_tags' => $cache_tags); $this->derivatives["rng.event.{$entity_type}.event.message.add"] = array('title' => $this->t('Add message'), 'route_name' => "rng.event.{$entity_type}.messages.add", 'appears_on' => array("rng.event.{$entity_type}.messages"), 'cache_tags' => $cache_tags); $this->derivatives["rng.event.{$entity_type}.event.group.add"] = array('title' => $this->t('Add group'), 'route_name' => "rng.event.{$entity_type}.group.add", 'appears_on' => array("rng.event.{$entity_type}.group.list"), 'cache_tags' => $cache_tags); } foreach ($this->derivatives as &$entry) { $entry += $base_plugin_definition; } return $this->derivatives; }
/** * {@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(); // 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' => $plugin->getCacheContexts(), 'tags' => Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags(), $plugin->getCacheTags()), '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 render(HtmlFragmentInterface $fragment, $status_code = 200) { // Converts the given HTML fragment which represents the main content region // of the page into a render array. $page_content['main'] = array('#markup' => $fragment->getContent()); $page_content['#title'] = $fragment->getTitle(); if ($fragment instanceof CacheableInterface) { $page_content['main']['#cache']['tags'] = $fragment->getCacheTags(); } // Build the full page array by calling drupal_prepare_page(), which invokes // hook_page_build(). This adds the other regions to the page. $page_array = drupal_prepare_page($page_content); // Build the HtmlPage object. $page = new HtmlPage('', array(), $fragment->getTitle()); $page = $this->preparePage($page, $page_array); $page->setBodyTop(drupal_render_root($page_array['page_top'])); $page->setBodyBottom(drupal_render_root($page_array['page_bottom'])); $page->setContent(drupal_render_root($page_array)); $page->setStatusCode($status_code); drupal_process_attached($page_array); if (isset($page_array['page_top'])) { drupal_process_attached($page_array['page_top']); } if (isset($page_array['page_bottom'])) { drupal_process_attached($page_array['page_bottom']); } if ($fragment instanceof CacheableInterface) { // Persist cache tags associated with this page. Also associate the // "rendered" cache tag. This allows us to invalidate the entire render // cache, regardless of the cache bin. $cache_tags = Cache::mergeTags(isset($page_array['page_top']) ? $page_array['page_top']['#cache']['tags'] : [], $page_array['#cache']['tags'], isset($page_array['page_bottom']) ? $page_array['page_bottom']['#cache']['tags'] : [], ['rendered']); // Only keep unique cache tags. We need to prevent duplicates here already // rather than only in the cache layer, because they are also used by // reverse proxies (like Varnish), not only by Drupal's page cache. $page->setCacheTags(array_unique($cache_tags)); } return $page; }
/** * {@inheritdoc} */ public function getCacheTags() { $tags = parent::getCacheTags(); return Cache::mergeTags($tags, ['block_visibility_group:' . $this->id]); }
/** * {@inheritdoc} */ protected function calculateXmlCacheTags() { // Add tags for the entity that this XML comes from. $entity_tags = is_object($this->entity) && $this->entity instanceof CacheableDependencyInterface ? $this->entity->getCacheTags() : array(); // Also fetch the tags from the display configuration as that is where our // gallery-specific settings are stored (so changes there should also // invalidate the XML). $display = entity_get_display($this->entityType, $this->entity->bundle(), $this->displayName); $display_tags = array(); if ($display instanceof CacheableDependencyInterface) { $display_tags = $display->getCacheTags(); // If this is not a custom display then we need to also include the // default display cache tags as Drupal may reference this display // elsewhere by the "default" label. if (!$display->status() || $display->isNew()) { $display_default = entity_get_display($this->entityType, $this->entity->bundle(), 'default'); if ($display_default instanceof CacheableDependencyInterface) { $display_tags = Cache::mergeTags($display_tags, $display_default->getCacheTags()); } } } return Cache::mergeTags($entity_tags, $display_tags); }
/** * Ensures that some cache tags are present in the current response. * * @param string[] $expected_tags * The expected tags. * @param bool $include_default_tags * (optional) Whether the default cache tags should be included. */ protected function assertCacheTags(array $expected_tags, $include_default_tags = TRUE) { // The anonymous role cache tag is only added if the user is anonymous. if ($include_default_tags && \Drupal::currentUser()->isAnonymous()) { $expected_tags = Cache::mergeTags($expected_tags, ['config:user.role.anonymous']); } $actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags'); sort($expected_tags); sort($actual_tags); $this->assertIdentical($actual_tags, $expected_tags); $this->debugCacheTags($actual_tags, $expected_tags); }
/** * 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); }
/** * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items) { $elements = array(); // Check if the formatter involves a link. if ($this->getSetting('image_link') == 'content') { $uri = $items->getEntity()->urlInfo(); // @todo Remove when theme_responsive_image_formatter() has support for route name. $uri['path'] = $items->getEntity()->getSystemPath(); } elseif ($this->getSetting('image_link') == 'file') { $link_file = TRUE; } $fallback_image_style = ''; // Check if the user defined a custom fallback image style. if ($this->getSetting('fallback_image_style')) { $fallback_image_style = $this->getSetting('fallback_image_style'); } // Collect cache tags to be added for each item in the field. $responsive_image_mapping = $this->responsiveImageMappingStorage->load($this->getSetting('responsive_image_mapping')); $image_styles_to_load = array(); if ($fallback_image_style) { $image_styles_to_load[] = $fallback_image_style; } $cache_tags = []; if ($responsive_image_mapping) { $cache_tags = Cache::mergeTags($cache_tags, $responsive_image_mapping->getCacheTags()); foreach ($responsive_image_mapping->getMappings() as $mapping) { // First mapping found is used as fallback. if (empty($fallback_image_style)) { $fallback_image_style = $mapping['image_style']; } $image_styles_to_load[] = $mapping['image_style']; } } $image_styles = entity_load_multiple('image_style', $image_styles_to_load); foreach ($image_styles as $image_style) { $cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags()); } foreach ($items as $delta => $item) { if (isset($link_file)) { $uri = array('path' => file_create_url($item->entity->getFileUri()), 'options' => array()); } $elements[$delta] = array('#theme' => 'responsive_image_formatter', '#attached' => array('library' => array('core/picturefill')), '#item' => $item, '#image_style' => $fallback_image_style, '#mapping_id' => $responsive_image_mapping ? $responsive_image_mapping->id() : '', '#path' => isset($uri) ? $uri : '', '#cache' => array('tags' => $cache_tags)); } return $elements; }
/** * Applies the cacheability of the current display to the given render array. * * @param array $element * The render array with updated cacheability metadata. */ protected function applyDisplayCachablityMetadata(array &$element) { /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache */ $cache = $this->getPlugin('cache'); (new CacheableMetadata())->setCacheTags(Cache::mergeTags($this->view->getCacheTags(), isset($this->display['cache_metadata']['tags']) ? $this->display['cache_metadata']['tags'] : []))->setCacheContexts(isset($this->display['cache_metadata']['contexts']) ? $this->display['cache_metadata']['contexts'] : [])->setCacheMaxAge(Cache::mergeMaxAges($cache->getCacheMaxAge(), isset($this->display['cache_metadata']['max-age']) ? $this->display['cache_metadata']['max-age'] : Cache::PERMANENT))->merge(CacheableMetadata::createFromRenderArray($element))->applyTo($element); }
/** * Pre-render callback: Renders a processed text element into #markup. * * Runs all the enabled filters on a piece of text. * * Note: Because filters can inject JavaScript or execute PHP code, security * is vital here. When a user supplies a text format, you should validate it * using $format->access() before accepting/using it. This is normally done in * the validation stage of the Form API. You should for example never make a * preview of content in a disallowed format. * * @param array $element * A structured array with the following key-value pairs: * - #text: containing the text to be filtered * - #format: containing the machine name of the filter format to be used to * filter the text. Defaults to the fallback format. * - #langcode: the language code of the text to be filtered, e.g. 'en' for * English. This allows filters to be language-aware so language-specific * text replacement can be implemented. Defaults to an empty string. * - #filter_types_to_skip: an array of filter types to skip, or an empty * array (default) to skip no filter types. All of the format's filters * will be applied, except for filters of the types that are marked to be * skipped. FilterInterface::TYPE_HTML_RESTRICTOR is the only type that * cannot be skipped. * * @return array * The passed-in element with the filtered text in '#markup'. * * @ingroup sanitization */ public static function preRenderText($element) { $format_id = $element['#format']; $filter_types_to_skip = $element['#filter_types_to_skip']; $text = $element['#text']; $langcode = $element['#langcode']; if (!isset($format_id)) { $format_id = static::configFactory()->get('filter.settings')->get('fallback_format'); } /** @var \Drupal\filter\Entity\FilterFormat $format **/ $format = FilterFormat::load($format_id); // If the requested text format doesn't exist or its disabled, the text // cannot be filtered. if (!$format || !$format->status()) { $message = !$format ? 'Missing text format: %format.' : 'Disabled text format: %format.'; static::logger('filter')->alert($message, array('%format' => $format_id)); $element['#markup'] = ''; return $element; } $filter_must_be_applied = function (FilterInterface $filter) use($filter_types_to_skip) { $enabled = $filter->status === TRUE; $type = $filter->getType(); // Prevent FilterInterface::TYPE_HTML_RESTRICTOR from being skipped. $filter_type_must_be_applied = $type == FilterInterface::TYPE_HTML_RESTRICTOR || !in_array($type, $filter_types_to_skip); return $enabled && $filter_type_must_be_applied; }; // Convert all Windows and Mac newlines to a single newline, so filters only // need to deal with one possibility. $text = str_replace(array("\r\n", "\r"), "\n", $text); // Get a complete list of filters, ordered properly. /** @var \Drupal\filter\Plugin\FilterInterface[] $filters **/ $filters = $format->filters(); // Give filters a chance to escape HTML-like data such as code or formulas. foreach ($filters as $filter) { if ($filter_must_be_applied($filter)) { $text = $filter->prepare($text, $langcode); } } // Perform filtering. $metadata = BubbleableMetadata::createFromRenderArray($element); foreach ($filters as $filter) { if ($filter_must_be_applied($filter)) { $result = $filter->process($text, $langcode); $metadata = $metadata->merge($result); $text = $result->getProcessedText(); } } // Filtering and sanitizing have been done in // \Drupal\filter\Plugin\FilterInterface. $text is not guaranteed to be // safe, but it has been passed through the filter system and checked with // a text format, so it must be printed as is. (See the note about security // in the method documentation above.) $element['#markup'] = FilteredMarkup::create($text); // Set the updated bubbleable rendering metadata and the text format's // cache tag. $metadata->applyTo($element); $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $format->getCacheTags()); return $element; }
/** * @covers ::render * @covers ::doRender * @covers \Drupal\Core\Render\RenderCache::get * @covers \Drupal\Core\Render\RenderCache::set * @covers \Drupal\Core\Render\RenderCache::createCacheID */ public function testRenderCache() { $this->setUpRequest(); $this->setupMemoryCache(); // Create an empty element. $test_element = [ '#cache' => [ 'keys' => ['render_cache_test'], 'tags' => ['render_cache_tag'], ], '#markup' => '', 'child' => [ '#cache' => [ 'keys' => ['render_cache_test_child'], 'tags' => ['render_cache_tag_child:1', 'render_cache_tag_child:2'], ], '#markup' => '', ], ]; // Render the element and confirm that it goes through the rendering // process (which will set $element['#printed']). $element = $test_element; $this->renderer->renderRoot($element); $this->assertTrue(isset($element['#printed']), 'No cache hit'); // Render the element again and confirm that it is retrieved from the cache // instead (so $element['#printed'] will not be set). $element = $test_element; $this->renderer->renderRoot($element); $this->assertFalse(isset($element['#printed']), 'Cache hit'); // Test that cache tags are correctly collected from the render element, // including the ones from its subchild. $expected_tags = [ 'render_cache_tag', 'render_cache_tag_child:1', 'render_cache_tag_child:2', ]; $this->assertEquals($expected_tags, $element['#cache']['tags'], 'Cache tags were collected from the element and its subchild.'); // The cache item also has a 'rendered' cache tag. $cache_item = $this->cacheFactory->get('render')->get('render_cache_test:en:stark'); $this->assertSame(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags); }
/** * 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); }
/** * Returns the row cache tags. * * @param ResultRow $row * A result row. * * @return string[] * The row cache tags. */ public function getRowCacheTags(ResultRow $row) { $tags = !empty($row->_entity) ? $row->_entity->getCacheTags() : []; if (!empty($row->_relationship_entities)) { foreach ($row->_relationship_entities as $entity) { $tags = Cache::mergeTags($tags, $entity->getCacheTags()); } } return $tags; }
/** * {@inheritdoc} */ public function getCacheTags() { $tags = []; // Add cache tags for each row, if there is an entity associated with it. if (!$this->hasAggregate) { foreach ($this->getAllEntities() as $entity) { $tags = Cache::mergeTags($entity->getCacheTags(), $tags); } } return $tags; }
/** * Attempt to fetch the gallery's XML via a sub-request to another page. * * This assumes that the gallery XML has already been embedded within a normal * HTML page, at the given path, within a <script> block. * * @param string $path * The Drupal path to use for the sub-request. * @param string $id * The id to search for within the sub-request content that will contain * the embedded XML. * @return string * The embedded XML if found or an empty string. */ protected function fetchXmlSubRequest($path, $id) { $xml = ''; // We want to pass-through all details of the master request, but for some // reason the sub-request may fail with a 406 if some server params unique // to an XMLHttpRequest are used. So we reset those to generic values by // just removing them from the request details passed-through. $server = $this->request->server; $server->remove('HTTP_ACCEPT'); $server->remove('HTTP_X_REQUESTED_WITH'); $subRequest = Request::create($this->request->getBaseUrl() . '/' . $path, 'GET', $this->request->query->all(), $this->request->cookies->all(), $this->request->files->all(), $server->all()); // @todo: See if this session check is needed. $session = $this->request->getSession(); if ($session) { $subRequest->setSession($session); } $subResponse = $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); // Search for the XML within the sub-request markup. We could parse the // DOM for this with DOMDocument, but a regex lookup is more lightweight. $matches = array(); preg_match('/<script[^>]*id=\\"' . $id . '\\"[^>]*>(.*)<\\/script>/simU', $subResponse->getContent(), $matches); if (!empty($matches[1]) && strpos($matches[1], '<?xml') === 0) { $xml = $matches[1]; // Set the cache tags directly from the sub-request response. if ($subResponse instanceof CacheableResponseInterface) { $response_cacheability = $subResponse->getCacheableMetadata(); $this->cacheTags = Cache::mergeTags($this->cacheTags, $response_cacheability->getCacheTags()); } } return $xml; }
/** * Checks the behavior of the Serializer callback paths and row plugins. */ public function testSerializerResponses() { // Test the serialize callback. $view = Views::getView('test_serializer_display_field'); $view->initDisplay(); $this->executeView($view); $actual_json = $this->drupalGetWithFormat('test/serialize/field', 'json'); $this->assertResponse(200); $this->assertCacheTags($view->getCacheTags()); $this->assertCacheContexts(['languages:language_interface', 'theme', 'request_format']); // @todo Due to https://www.drupal.org/node/2352009 we can't yet test the // propagation of cache max-age. // Test the http Content-type. $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.'); $expected = array(); foreach ($view->result as $row) { $expected_row = array(); foreach ($view->field as $id => $field) { $expected_row[$id] = $field->render($row); } $expected[] = $expected_row; } $this->assertIdentical($actual_json, json_encode($expected), 'The expected JSON output was found.'); // Test that the rendered output and the preview output are the same. $view->destroy(); $view->setDisplay('rest_export_1'); // Mock the request content type by setting it on the display handler. $view->display_handler->setContentType('json'); $output = $view->preview(); $this->assertIdentical($actual_json, (string) drupal_render_root($output), 'The expected JSON preview output was found.'); // Test a 403 callback. $this->drupalGet('test/serialize/denied'); $this->assertResponse(403); // Test the entity rows. $view = Views::getView('test_serializer_display_entity'); $view->initDisplay(); $this->executeView($view); // Get the serializer service. $serializer = $this->container->get('serializer'); $entities = array(); foreach ($view->result as $row) { $entities[] = $row->_entity; } $expected = $serializer->serialize($entities, 'json'); $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json'); $this->assertResponse(200); $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.'); $expected_cache_tags = $view->getCacheTags(); $expected_cache_tags[] = 'entity_test_list'; /** @var \Drupal\Core\Entity\EntityInterface $entity */ foreach ($entities as $entity) { $expected_cache_tags = Cache::mergeTags($expected_cache_tags, $entity->getCacheTags()); } $this->assertCacheTags($expected_cache_tags); $this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']); $expected = $serializer->serialize($entities, 'hal_json'); $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'hal_json'); $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.'); $this->assertCacheTags($expected_cache_tags); // Change the default format to xml. $view->setDisplay('rest_export_1'); $view->getDisplay()->setOption('style', array('type' => 'serializer', 'options' => array('uses_fields' => FALSE, 'formats' => array('xml' => 'xml')))); $view->save(); $expected = $serializer->serialize($entities, 'xml'); $actual_xml = $this->drupalGet('test/serialize/entity'); $this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.'); $this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']); // Allow multiple formats. $view->setDisplay('rest_export_1'); $view->getDisplay()->setOption('style', array('type' => 'serializer', 'options' => array('uses_fields' => FALSE, 'formats' => array('xml' => 'xml', 'json' => 'json')))); $view->save(); $expected = $serializer->serialize($entities, 'json'); $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json'); $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.'); $expected = $serializer->serialize($entities, 'xml'); $actual_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml'); $this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.'); }
/** * {@inheritdoc} */ public function getCacheTags() { return Cache::mergeTags(parent::getCacheTags(), $this->configFactory->get('system.site')->getCacheTags()); }
/** * Invalidates an entity's cache tags upon delete. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * The entity type definition. * @param \Drupal\Core\Entity\EntityInterface[] $entities * An array of entities. */ protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_type, array $entities) { $tags = $entity_type->getListCacheTags(); foreach ($entities as $entity) { // An entity was deleted: invalidate its own cache tag, but also its list // cache tags. (A deleted entity may cause changes in a paged list on // other pages than the one it's on. The one it's on is handled by its own // cache tag, but subsequent list pages would not be invalidated, hence we // must invalidate its list cache tags as well.) $tags = Cache::mergeTags($tags, $entity->getCacheTags()); } Cache::invalidateTags($tags); }
/** * {@inheritdoc} */ public function getCacheTags() { return Cache::mergeTags(['config:' . $this->name], $this->cacheTags); }