/** * {@inheritdoc} * * @see ::prepareView() * @see ::getEntitiestoView() */ public function view(FieldItemListInterface $items, $langcode = NULL) { $elements = parent::view($items, $langcode); $field_level_access_cacheability = new CacheableMetadata(); // Try to map the cacheability of the access result that was set at // _accessCacheability in getEntitiesToView() to the corresponding render // subtree. If no such subtree is found, then merge it with the field-level // access cacheability. foreach ($items as $delta => $item) { // Ignore items for which access cacheability could not be determined in // prepareView(). if (!empty($item->_accessCacheability)) { if (isset($elements[$delta])) { CacheableMetadata::createFromRenderArray($elements[$delta])->merge($item->_accessCacheability)->applyTo($elements[$delta]); } else { $field_level_access_cacheability = $field_level_access_cacheability->merge($item->_accessCacheability); } } } // Apply the cacheability metadata for the inaccessible entities and the // entities for which the corresponding render subtree could not be found. // This causes the field to be rendered (and cached) according to the cache // contexts by which the access results vary, to ensure only users with // access to this field can view it. It also tags this field with the cache // tags on which the access results depend, to ensure users that cannot view // this field at the moment will gain access once any of those cache tags // are invalidated. $field_level_access_cacheability->applyTo($elements); return $elements; }
/** * Creates a bubbleable metadata object with values taken from a render array. * * @param array $build * A render array. * * @return static */ public static function createFromRenderArray(array $build) { $meta = parent::createFromRenderArray($build); $meta->attached = isset($build['#attached']) ? $build['#attached'] : []; $meta->postRenderCache = isset($build['#post_render_cache']) ? $build['#post_render_cache'] : []; return $meta; }
/** * {@inheritdoc} */ public function setContent($content) { // A render array can automatically be converted to a string and set the // necessary metadata. if (is_array($content) && isset($content['#markup'])) { $this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content)); $this->setAttachments($content['#attached']); $content = $content['#markup']; } parent::setContent($content); }
/** * {@inheritdoc} */ public function setContent($content) { // A render array can automatically be converted to a string and set the // necessary metadata. if (is_array($content) && isset($content['#markup'])) { $content += ['#attached' => ['html_response_attachment_placeholders' => [], 'placeholders' => []]]; $this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content)); $this->setAttachments($content['#attached']); $content = $content['#markup']; } return parent::setContent($content); }
/** * {@inheritdoc} */ public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { $json = []; $json['content'] = (string) $this->renderer->renderRoot($main_content); if (!empty($main_content['#title'])) { $json['title'] = (string) $main_content['#title']; } else { $json['title'] = (string) $this->titleResolver->getTitle($request, $route_match->getRouteObject()); } $response = new CacheableJsonResponse($json, 200); $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($main_content)); return $response; }
/** * {@inheritdoc} */ public function buildSelectorForm(array $form, FormStateInterface $form_state) { $form = parent::buildSelectorForm($form, $form_state); $available_plugins = []; $cacheability_metadata = CacheableMetadata::createFromRenderArray($form); foreach (array_keys($this->selectablePluginDiscovery->getDefinitions()) as $plugin_id) { $available_plugin = $this->selectablePluginFactory->createInstance($plugin_id); $available_plugins[] = $available_plugin; $cacheability_metadata = $cacheability_metadata->merge(CacheableMetadata::createFromObject($available_plugin)); } $cacheability_metadata->applyTo($form); $plugin_selector_form_state_key = static::setPluginSelector($form_state, $this); $form['container'] = array('#attributes' => array('class' => array('plugin-selector-' . Html::getClass($this->getPluginId()))), '#available_plugins' => $available_plugins, '#plugin_selector_form_state_key' => $plugin_selector_form_state_key, '#process' => [[get_class(), 'processBuildSelectorForm']], '#tree' => TRUE, '#type' => 'container'); return $form; }
/** * {@inheritdoc} */ public static function buildResponse($view_id, $display_id, array $args = []) { $build = static::buildBasicRenderable($view_id, $display_id, $args); // Set up an empty response, so for example RSS can set the proper // Content-Type header. $response = new CacheableResponse('', 200); $build['#response'] = $response; /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); $output = (string) $renderer->renderRoot($build); if (empty($output)) { throw new NotFoundHttpException(); } $response->setContent($output); $cache_metadata = CacheableMetadata::createFromRenderArray($build); $response->addCacheableDependency($cache_metadata); return $response; }
/** * Pre-render callback: Renders a link into #markup. * * Doing so during pre_render gives modules a chance to alter the link parts. * * @param array $element * A structured array whose keys form the arguments to _l(): * - #title: The link text to pass as argument to _l(). * - #url: The URL info either pointing to a route or a non routed path. * - #options: (optional) An array of options to pass to _l() or the link * generator. * * @return array * The passed-in element containing a rendered link in '#markup'. */ public static function preRenderLink($element) { // By default, link options to pass to _l() are normally set in #options. $element += array('#options' => array()); // However, within the scope of renderable elements, #attributes is a valid // way to specify attributes, too. Take them into account, but do not override // attributes from #options. if (isset($element['#attributes'])) { $element['#options'] += array('attributes' => array()); $element['#options']['attributes'] += $element['#attributes']; } // This #pre_render callback can be invoked from inside or outside of a Form // API context, and depending on that, a HTML ID may be already set in // different locations. #options should have precedence over Form API's #id. // #attributes have been taken over into #options above already. if (isset($element['#options']['attributes']['id'])) { $element['#id'] = $element['#options']['attributes']['id']; } elseif (isset($element['#id'])) { $element['#options']['attributes']['id'] = $element['#id']; } // Conditionally invoke self::preRenderAjaxForm(), if #ajax is set. if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) { // If no HTML ID was found above, automatically create one. if (!isset($element['#id'])) { $element['#id'] = $element['#options']['attributes']['id'] = HtmlUtility::getUniqueId('ajax-link'); } $element = static::preRenderAjaxForm($element); } if (!empty($element['#url']) && $element['#url'] instanceof CoreUrl) { $options = NestedArray::mergeDeep($element['#url']->getOptions(), $element['#options']); /** @var \Drupal\Core\Utility\LinkGenerator $link_generator */ $link_generator = \Drupal::service('link_generator'); $generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), TRUE); $element['#markup'] = $generated_link->getGeneratedLink(); $generated_link->merge(CacheableMetadata::createFromRenderArray($element))->applyTo($element); } return $element; }
/** * Embeds a Response object in a render array so that RenderCache can cache it. * * @param \Drupal\Core\Cache\CacheableResponseInterface $response * A cacheable response. * * @return array * A render array that embeds the given cacheable response object, with the * cacheability metadata of the response object present in the #cache * property of the render array. * * @see renderArrayToResponse() * * @todo Refactor/remove once https://www.drupal.org/node/2551419 lands. */ protected function responseToRenderArray(CacheableResponseInterface $response) { $response_as_render_array = $this->dynamicPageCacheRedirectRenderArray + ['#response' => $response, '#cache_properties' => ['#response'], '#markup' => '', '#attached' => '']; // Merge the response's cacheability metadata, so that RenderCache can take // care of cache redirects for us. CacheableMetadata::createFromObject($response->getCacheableMetadata())->merge(CacheableMetadata::createFromRenderArray($response_as_render_array))->applyTo($response_as_render_array); return $response_as_render_array; }
/** * {@inheritdoc} */ public static function buildResponse($view_id, $display_id, array $args = []) { $build = static::buildBasicRenderable($view_id, $display_id, $args); // Setup an empty response so headers can be added as needed during views // rendering and processing. $response = new CacheableResponse('', 200); $build['#response'] = $response; /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); $output = (string) $renderer->renderRoot($build); $response->setContent($output); $cache_metadata = CacheableMetadata::createFromRenderArray($build); $response->addCacheableDependency($cache_metadata); $response->headers->set('Content-type', $build['#content_type']); return $response; }
/** * Prior to set the response it check if we can redirect. * * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event * The event object. * @param \Drupal\Core\Url $url * The Url where we want to redirect. */ protected function setResponse(GetResponseEvent $event, Url $url) { $request = $event->getRequest(); $this->context->fromRequest($request); parse_str($request->getQueryString(), $query); $url->setOption('query', $query); $url->setAbsolute(TRUE); // We can only check access for routed URLs. if (!$url->isRouted() || $this->checker->canRedirect($request, $url->getRouteName())) { // Add the 'rendered' cache tag, so that we can invalidate all responses // when settings are changed. $response = new TrustedRedirectResponse($url->toString(), 301); $response->addCacheableDependency(CacheableMetadata::createFromRenderArray([])->addCacheTags(['rendered'])); $event->setResponse($response); } }
/** * Prepares search results for rendering. * * @param \Drupal\Core\Database\StatementInterface $found * Results found from a successful search query execute() method. * * @return array * Array of search result item render arrays (empty array if no results). */ protected function prepareResults(StatementInterface $found) { $results = array(); $node_storage = $this->entityManager->getStorage('node'); $node_render = $this->entityManager->getViewBuilder('node'); $keys = $this->keywords; foreach ($found as $item) { // Render the node. /** @var \Drupal\node\NodeInterface $node */ $node = $node_storage->load($item->sid)->getTranslation($item->langcode); $build = $node_render->view($node, 'search_result', $item->langcode); /** @var \Drupal\node\NodeTypeInterface $type*/ $type = $this->entityManager->getStorage('node_type')->load($node->bundle()); unset($build['#theme']); $build['#pre_render'][] = array($this, 'removeSubmittedInfo'); // Fetch comments for snippet. $rendered = $this->renderer->renderPlain($build); $this->addCacheableDependency(CacheableMetadata::createFromRenderArray($build)); $rendered .= ' ' . $this->moduleHandler->invoke('comment', 'node_update_index', [$node]); $extra = $this->moduleHandler->invokeAll('node_search_result', [$node]); $language = $this->languageManager->getLanguage($item->langcode); $username = array('#theme' => 'username', '#account' => $node->getOwner()); $result = array('link' => $node->url('canonical', array('absolute' => TRUE, 'language' => $language)), 'type' => $type->label(), 'title' => $node->label(), 'node' => $node, 'extra' => $extra, 'score' => $item->calculated_score, 'snippet' => search_excerpt($keys, $rendered, $item->langcode), 'langcode' => $node->language()->getId()); $this->addCacheableDependency($node); // We have to separately add the node owner's cache tags because search // module doesn't use the rendering system, it does its own rendering // without taking cacheability metadata into account. So we have to do it // explicitly here. $this->addCacheableDependency($node->getOwner()); if ($type->displaySubmitted()) { $result += array('user' => $this->renderer->renderPlain($username), 'date' => $node->getChangedTime()); } $results[] = $result; } return $results; }
/** * {@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); // The three parts of rendered markup in html.html.twig (page_top, page and // page_bottom) must be rendered with drupal_render_root(), so that their // #post_render_cache callbacks are executed (which may attach additional // assets). // html.html.twig must be able to render the final list of attached assets, // and hence may not execute any #post_render_cache_callbacks (because they // might add yet more assets to be attached), and therefore it must be // rendered with drupal_render(), not drupal_render_root(). $this->renderer->render($html['page'], TRUE); if (isset($html['page_top'])) { $this->renderer->render($html['page_top'], TRUE); } if (isset($html['page_bottom'])) { $this->renderer->render($html['page_bottom'], TRUE); } $content = $this->renderer->render($html); // Set the generator in the HTTP header. list($version) = explode('.', \Drupal::VERSION, 2); $response = new CacheableResponse($content, 200, ['Content-Type' => 'text/html; charset=UTF-8', 'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)']); // Bubble the cacheability metadata associated with the rendered render // arrays to the response. foreach (['page_top', 'page', 'page_bottom'] as $region) { if (isset($html[$region])) { $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($html[$region])); } } // Also associate the "rendered" cache tag. This allows us to invalidate the // entire render cache, regardless of the cache bin. $default = new CacheableMetadata(); $default->setCacheTags(['rendered']); $response->addCacheableDependency($default); return $response; }
/** * @covers ::createFromRenderArray * @dataProvider providerTestCreateFromRenderArray */ public function testCreateFromRenderArray(array $render_array, CacheableMetadata $expected) { $this->assertEquals($expected, CacheableMetadata::createFromRenderArray($render_array)); }
/** * {@inheritdoc} */ public function addCacheableDependency(array &$elements, $dependency) { $meta_a = CacheableMetadata::createFromRenderArray($elements); $meta_b = CacheableMetadata::createFromObject($dependency); $meta_a->merge($meta_b)->applyTo($elements); }
/** * Gets an array of items for the field. * * @param \Drupal\views\ResultRow $values * The result row object containing the values. * * @return array * An array of items for the field. */ public function getItems(ResultRow $values) { $original_entity = $this->getEntity($values); if (!$original_entity) { return array(); } $entity = $this->process_entity($values, $original_entity); if (!$entity) { return array(); } $display = array('type' => $this->options['type'], 'settings' => $this->options['settings'], 'label' => 'hidden'); $render_array = $entity->get($this->definition['field_name'])->view($display); if ($this->options['field_api_classes']) { return array(array('rendered' => $this->renderer->render($render_array))); } $items = array(); foreach (Element::children($render_array) as $delta) { $items[$delta]['rendered'] = $render_array[$delta]; // Merge the cacheability metadata of the top-level render array into // each child because they will most likely be rendered individually. if (isset($render_array['#cache'])) { CacheableMetadata::createFromRenderArray($render_array)->merge(CacheableMetadata::createFromRenderArray($items[$delta]['rendered']))->applyTo($items[$delta]['rendered']); } // Add the raw field items (for use in tokens). $items[$delta]['raw'] = $render_array['#items'][$delta]; } return $items; }
/** * {@inheritdoc} */ public static function buildResponse($view_id, $display_id, array $args = []) { $build = static::buildBasicRenderable($view_id, $display_id, $args); $build['#cache']['contexts'][] = "url.query_args:callback"; /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); $output = $renderer->renderRoot($build); if (isset($build['#jsonp_callback'])) { $response = new CacheableJsonResponse($output, 200); $response->setCallback($build['#jsonp_callback']); } else { $response = new CacheableResponse($output, 200); } $cache_metadata = CacheableMetadata::createFromRenderArray($build); $response->addCacheableDependency($cache_metadata); $response->headers->set('Content-type', $build['#content_type']); return $response; }
/** * {@inheritdoc} */ public static function buildResponse($view_id, $display_id, array $args = []) { $build = static::buildBasicRenderable($view_id, $display_id, $args); /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); $output = $renderer->renderRoot($build); $response = new CacheableResponse($output, 200); $cache_metadata = CacheableMetadata::createFromRenderArray($build); $response->addCacheableDependency($cache_metadata); $response->headers->set('Content-type', $build['#content_type']); return $response; }
/** * Creates a bubbleable metadata object with values taken from a render array. * * @param array $build * A render array. * * @return static */ public static function createFromRenderArray(array $build) { $meta = parent::createFromRenderArray($build); $meta->attachments = isset($build['#attached']) ? $build['#attached'] : []; return $meta; }
/** * Tests the label formatter. */ public function testLabelFormatter() { /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = $this->container->get('renderer'); $formatter = 'entity_reference_label'; // The 'link' settings is TRUE by default. $build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter); $expected_field_cacheability = ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]; $this->assertEqual($build['#cache'], $expected_field_cacheability, 'The field render array contains the entity access cacheability metadata'); $expected_item_1 = array('#type' => 'link', '#title' => $this->referencedEntity->label(), '#url' => $this->referencedEntity->urlInfo(), '#options' => $this->referencedEntity->urlInfo()->getOptions(), '#cache' => array('contexts' => ['user.permissions'], 'tags' => $this->referencedEntity->getCacheTags())); $this->assertEqual($renderer->renderRoot($build[0]), $renderer->renderRoot($expected_item_1), sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter)); $this->assertEqual(CacheableMetadata::createFromRenderArray($build[0]), CacheableMetadata::createFromRenderArray($expected_item_1)); // The second referenced entity is "autocreated", therefore not saved and // lacking any URL info. $expected_item_2 = array('#plain_text' => $this->unsavedReferencedEntity->label(), '#cache' => array('contexts' => ['user.permissions'], 'tags' => $this->unsavedReferencedEntity->getCacheTags(), 'max-age' => Cache::PERMANENT)); $this->assertEqual($build[1], $expected_item_2, sprintf('The render array returned by the %s formatter is correct for an item with a unsaved entity.', $formatter)); // Test with the 'link' setting set to FALSE. $build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter, array('link' => FALSE)); $this->assertEqual($build[0]['#plain_text'], $this->referencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter)); $this->assertEqual($build[1]['#plain_text'], $this->unsavedReferencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter)); // Test an entity type that doesn't have any link templates, which means // \Drupal\Core\Entity\EntityInterface::urlInfo() will throw an exception // and the label formatter will output only the label instead of a link. $field_storage_config = FieldStorageConfig::loadByName($this->entityType, $this->fieldName); $field_storage_config->setSetting('target_type', 'entity_test_label'); $field_storage_config->save(); $referenced_entity_with_no_link_template = EntityTestLabel::create(array('name' => $this->randomMachineName())); $referenced_entity_with_no_link_template->save(); $build = $this->buildRenderArray([$referenced_entity_with_no_link_template], $formatter, array('link' => TRUE)); $this->assertEqual($build[0]['#plain_text'], $referenced_entity_with_no_link_template->label(), sprintf('The markup returned by the %s formatter is correct for an entity type with no valid link template.', $formatter)); }
/** * {@inheritdoc} */ public function execute() { parent::execute(); $output = $this->view->render(); $header = []; $header['Content-type'] = $this->getMimeType(); $response = new CacheableResponse($this->renderer->renderRoot($output), 200); $cache_metadata = CacheableMetadata::createFromRenderArray($output); $response->addCacheableDependency($cache_metadata); return $response; }
/** * #pre_render callback for building a block. * * Renders the content using the provided block plugin, and then: * - if there is no content, aborts rendering, and makes sure the block won't * be rendered. * - if there is content, moves the contextual links from the block content to * the block itself. */ public static function preRender($build) { $content = $build['#block']->getPlugin()->build(); // Remove the block entity from the render array, to ensure that blocks // can be rendered without the block config entity. unset($build['#block']); if ($content !== NULL && !Element::isEmpty($content)) { // Place the $content returned by the block plugin into a 'content' child // element, as a way to allow the plugin to have complete control of its // properties and rendering (for instance, its own #theme) without // conflicting with the properties used above, or alternate ones used by // alternate block rendering approaches in contrib (for instance, Panels). // However, the use of a child element is an implementation detail of this // particular block rendering approach. Semantically, the content returned // by the plugin "is the" block, and in particular, #attributes and // #contextual_links is information about the *entire* block. Therefore, // we must move these properties from $content and merge them into the // top-level element. foreach (array('#attributes', '#contextual_links') as $property) { if (isset($content[$property])) { $build[$property] += $content[$property]; unset($content[$property]); } } $build['content'] = $content; } else { // Abort rendering: render as the empty string and ensure this block is // render cached, so we can avoid the work of having to repeatedly // determine whether the block is empty. For instance, modifying or adding // entities could cause the block to no longer be empty. $build = array('#markup' => '', '#cache' => $build['#cache']); // If $content is not empty, then it contains cacheability metadata, and // we must merge it with the existing cacheability metadata. This allows // blocks to be empty, yet still bubble cacheability metadata, to indicate // why they are empty. if (!empty($content)) { CacheableMetadata::createFromRenderArray($build)->merge(CacheableMetadata::createFromRenderArray($content))->applyTo($build); } } return $build; }
/** * {@inheritdoc} */ public function build() { // Track whether blocks showing the main content and messages are displayed. $main_content_block_displayed = FALSE; $messages_block_displayed = FALSE; $build = ['#cache' => ['tags' => $this->blockListCacheTags]]; // Load all region content assigned via blocks. $cacheable_metadata_list = []; foreach ($this->blockRepository->getVisibleBlocksPerRegion($cacheable_metadata_list) as $region => $blocks) { /** @var $blocks \Drupal\block\BlockInterface[] */ foreach ($blocks as $key => $block) { $block_plugin = $block->getPlugin(); if ($block_plugin instanceof MainContentBlockPluginInterface) { $block_plugin->setMainContent($this->mainContent); $main_content_block_displayed = TRUE; } elseif ($block_plugin instanceof MessagesBlockPluginInterface) { $messages_block_displayed = TRUE; } $build[$region][$key] = $this->blockViewBuilder->view($block); // The main content block cannot be cached: it is a placeholder for the // render array returned by the controller. It should be rendered as-is, // with other placed blocks "decorating" it. if ($block_plugin instanceof MainContentBlockPluginInterface) { unset($build[$region][$key]['#cache']['keys']); } } if (!empty($build[$region])) { // \Drupal\block\BlockRepositoryInterface::getVisibleBlocksPerRegion() // returns the blocks in sorted order. $build[$region]['#sorted'] = TRUE; } } // If no block that shows the main content is displayed, still show the main // content. Otherwise the end user will see all displayed blocks, but not // the main content they came for. if (!$main_content_block_displayed) { $build['content']['system_main'] = $this->mainContent; } // If no block displays status messages, still render them. if (!$messages_block_displayed) { $build['content']['messages'] = ['#weight' => -1000, '#type' => 'status_messages']; } // The access results' cacheability is currently added to the top level of the // render array. This is done to prevent issues with empty regions being // displayed. // This would need to be changed to allow caching of block regions, as each // region must then have the relevant cacheable metadata. $merged_cacheable_metadata = CacheableMetadata::createFromRenderArray($build); foreach ($cacheable_metadata_list as $cacheable_metadata) { $merged_cacheable_metadata = $merged_cacheable_metadata->merge($cacheable_metadata); } $merged_cacheable_metadata->applyTo($build); return $build; }
/** * Recursive helper function for buildOverviewForm(). * * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree * The tree retrieved by \Drupal\Core\Menu\MenuLinkTreeInterface::load(). * @param int $delta * The default number of menu items used in the menu weight selector is 50. * * @return array * The overview tree form. */ protected function buildOverviewTreeForm($tree, $delta) { $form =& $this->overviewTreeForm; $tree_access_cacheability = new CacheableMetadata(); foreach ($tree as $element) { $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access)); // Only render accessible links. if (!$element->access->isAllowed()) { continue; } /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ $link = $element->link; if ($link) { $id = 'menu_plugin_id:' . $link->getPluginId(); $form[$id]['#item'] = $element; $form[$id]['#attributes'] = $link->isEnabled() ? array('class' => array('menu-enabled')) : array('class' => array('menu-disabled')); $form[$id]['title'] = Link::fromTextAndUrl($link->getTitle(), $link->getUrlObject())->toRenderable(); if (!$link->isEnabled()) { $form[$id]['title']['#suffix'] = ' (' . $this->t('disabled') . ')'; } elseif (($url = $link->getUrlObject()) && $url->isRouted() && $url->getRouteName() == 'user.page') { $form[$id]['title']['#suffix'] = ' (' . $this->t('logged in users only') . ')'; } $form[$id]['enabled'] = array('#type' => 'checkbox', '#title' => $this->t('Enable @title menu link', array('@title' => $link->getTitle())), '#title_display' => 'invisible', '#default_value' => $link->isEnabled()); $form[$id]['weight'] = array('#type' => 'weight', '#delta' => $delta, '#default_value' => $link->getWeight(), '#title' => $this->t('Weight for @title', array('@title' => $link->getTitle())), '#title_display' => 'invisible'); $form[$id]['id'] = array('#type' => 'hidden', '#value' => $link->getPluginId()); $form[$id]['parent'] = array('#type' => 'hidden', '#default_value' => $link->getParent()); // Build a list of operations. $operations = array(); $operations['edit'] = array('title' => $this->t('Edit')); // Allow for a custom edit link per plugin. $edit_route = $link->getEditRoute(); if ($edit_route) { $operations['edit']['url'] = $edit_route; // Bring the user back to the menu overview. $operations['edit']['query'] = $this->getDestinationArray(); } else { // Fall back to the standard edit link. $operations['edit'] += array('url' => Url::fromRoute('menu_ui.link_edit', ['menu_link_plugin' => $link->getPluginId()])); } // Links can either be reset or deleted, not both. if ($link->isResettable()) { $operations['reset'] = array('title' => $this->t('Reset'), 'url' => Url::fromRoute('menu_ui.link_reset', ['menu_link_plugin' => $link->getPluginId()])); } elseif ($delete_link = $link->getDeleteRoute()) { $operations['delete']['url'] = $delete_link; $operations['delete']['query'] = $this->getDestinationArray(); $operations['delete']['title'] = $this->t('Delete'); } if ($link->isTranslatable()) { $operations['translate'] = array('title' => $this->t('Translate'), 'url' => $link->getTranslateRoute()); } $form[$id]['operations'] = array('#type' => 'operations', '#links' => $operations); } if ($element->subtree) { $this->buildOverviewTreeForm($element->subtree, $delta); } } $tree_access_cacheability->merge(CacheableMetadata::createFromRenderArray($form))->applyTo($form); return $form; }
/** * #pre_render callback for building a block. * * Renders the content using the provided block plugin, if there is no * content, aborts rendering, and makes sure the block won't be rendered. */ public function buildBlock($build) { $content = $build['#block_plugin']->build(); // Remove the block plugin from the render array. unset($build['#block_plugin']); if ($content !== NULL && !Element::isEmpty($content)) { $build['content'] = $content; } else { // Abort rendering: render as the empty string and ensure this block is // render cached, so we can avoid the work of having to repeatedly // determine whether the block is empty. E.g. modifying or adding entities // could cause the block to no longer be empty. $build = ['#markup' => '', '#cache' => $build['#cache']]; } // If $content is not empty, then it contains cacheability metadata, and // we must merge it with the existing cacheability metadata. This allows // blocks to be empty, yet still bubble cacheability metadata, to indicate // why they are empty. if (!empty($content)) { CacheableMetadata::createFromRenderArray($build)->merge(CacheableMetadata::createFromRenderArray($content))->applyTo($build); } return $build; }
/** * 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); }
/** * Tests bubbling of menu links' outbound route/path processing cacheability. */ public function testOutboundPathAndRouteProcessing() { \Drupal::service('router.builder')->rebuild(); $request_stack = \Drupal::requestStack(); /** @var \Symfony\Component\Routing\RequestContext $request_context */ $request_context = \Drupal::service('router.request_context'); $request = Request::create('/'); $request->attributes->set(RouteObjectInterface::ROUTE_NAME, '<front>'); $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('/')); $request_stack->push($request); $request_context->fromRequest($request); $menu_tree = \Drupal::menuTree(); $renderer = \Drupal::service('renderer'); $default_menu_cacheability = (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheTags(['config:system.menu.tools'])->setCacheContexts(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme']); User::create(['uid' => 1, 'name' => $this->randomString()])->save(); User::create(['uid' => 2, 'name' => $this->randomString()])->save(); // Five test cases, four asserting one outbound path/route processor, and // together covering one of each: // - no cacheability metadata, // - a cache context, // - a cache tag, // - a cache max-age. // Plus an additional test case to verify that multiple links adding // cacheability metadata of the same type is working (two links with cache // tags). $test_cases = [['uri' => 'route:<current>', 'cacheability' => (new CacheableMetadata())->setCacheContexts(['route'])], ['uri' => 'route:outbound_processing_test.route.csrf', 'cacheability' => (new CacheableMetadata())->setCacheMaxAge(0)], ['uri' => 'internal:/', 'cacheability' => new CacheableMetadata()], ['uri' => 'internal:/user/1', 'cacheability' => (new CacheableMetadata())->setCacheTags(User::load(1)->getCacheTags())], ['uri' => 'internal:/user/2', 'cacheability' => (new CacheableMetadata())->setCacheTags(User::load(2)->getCacheTags())]]; // Test each expectation individually. foreach ($test_cases as $expectation) { $menu_link_content = MenuLinkContent::create(['link' => ['uri' => $expectation['uri']], 'menu_name' => 'tools']); $menu_link_content->save(); $tree = $menu_tree->load('tools', new MenuTreeParameters()); $build = $menu_tree->build($tree); $renderer->renderRoot($build); $expected_cacheability = $default_menu_cacheability->merge($expectation['cacheability']); $this->assertEqual($expected_cacheability, CacheableMetadata::createFromRenderArray($build)); $menu_link_content->delete(); } // Now test them all together in one menu: the rendered menu's cacheability // metadata should be the combination of the cacheability of all links, and // thus of all tested outbound path & route processors. $expected_cacheability = new CacheableMetadata(); foreach ($test_cases as $expectation) { $menu_link_content = MenuLinkContent::create(['link' => ['uri' => $expectation['uri']], 'menu_name' => 'tools']); $menu_link_content->save(); $expected_cacheability = $expected_cacheability->merge($expectation['cacheability']); } $tree = $menu_tree->load('tools', new MenuTreeParameters()); $build = $menu_tree->build($tree); $renderer->renderRoot($build); $expected_cacheability = $expected_cacheability->merge($default_menu_cacheability); $this->assertEqual($expected_cacheability, CacheableMetadata::createFromRenderArray($build)); }
/** * Creates the cache ID for a renderable element. * * Creates the cache ID string based on #cache['keys'] + #cache['contexts']. * * @param array &$elements * A renderable array. * * @return string * The cache ID string, or FALSE if the element may not be cached. */ protected function createCacheID(array &$elements) { // If the maximum age is zero, then caching is effectively prohibited. if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) { return FALSE; } if (isset($elements['#cache']['keys'])) { $cid_parts = $elements['#cache']['keys']; if (!empty($elements['#cache']['contexts'])) { $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']); $cid_parts = array_merge($cid_parts, $context_cache_keys->getKeys()); CacheableMetadata::createFromRenderArray($elements)->merge($context_cache_keys)->applyTo($elements); } return implode(':', $cid_parts); } return FALSE; }
/** * Gets an array of items for the field. * * @param \Drupal\views\ResultRow $values * The result row object containing the values. * * @return array * An array of items for the field. */ public function getItems(ResultRow $values) { if (!$this->displayHandler->useGroupBy()) { $build_list = $this->getEntityFieldRenderer()->render($values, $this); } else { // For grouped results we need to retrieve a massaged entity having // grouped field values to ensure that "grouped by" values, especially // those with multiple cardinality work properly. See // \Drupal\views\Tests\QueryGroupByTest::testGroupByFieldWithCardinality. $display = ['type' => $this->options['type'], 'settings' => $this->options['settings'], 'label' => 'hidden']; $build_list = $this->createEntityForGroupBy($this->getEntity($values), $values)->{$this->definition['field_name']}->view($display); } if (!$build_list) { return []; } if ($this->options['field_api_classes']) { return [['rendered' => $this->renderer->render($build_list)]]; } // Render using the formatted data itself. $items = []; foreach (Element::children($build_list) as $delta) { $items[$delta]['rendered'] = $build_list[$delta]; // Merge the cacheability metadata of the top-level render array into // each child because they will most likely be rendered individually. if (isset($build_list['#cache'])) { CacheableMetadata::createFromRenderArray($build_list)->merge(CacheableMetadata::createFromRenderArray($items[$delta]['rendered']))->applyTo($items[$delta]['rendered']); } // Add the raw field items (for use in tokens). $items[$delta]['raw'] = $build_list['#items'][$delta]; } return $items; }