function Renderer::doRender

Same name and namespace in other branches
  1. 9 core/lib/Drupal/Core/Render/Renderer.php \Drupal\Core\Render\Renderer::doRender()
  2. 8.9.x core/lib/Drupal/Core/Render/Renderer.php \Drupal\Core\Render\Renderer::doRender()
  3. 10 core/lib/Drupal/Core/Render/Renderer.php \Drupal\Core\Render\Renderer::doRender()

See the docs for ::render().

1 call to Renderer::doRender()
Renderer::render in core/lib/Drupal/Core/Render/Renderer.php
Renders HTML given a structured array tree.

File

core/lib/Drupal/Core/Render/Renderer.php, line 215

Class

Renderer
Turns a render array into an HTML string.

Namespace

Drupal\Core\Render

Code

protected function doRender(&$elements, $is_root_call = FALSE) {
    if (empty($elements)) {
        return '';
    }
    if ($this->rendererConfig['debug'] === TRUE) {
        $render_start = microtime(TRUE);
    }
    if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
        $elements['#access'] = $this->doCallback('#access_callback', $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 its
        // cacheability metadata to the render array.
        if ($elements['#access'] instanceof AccessResultInterface) {
            $this->addCacheableDependency($elements, $elements['#access']);
            if (!$elements['#access']->isAllowed()) {
                // Abort, but bubble new cache metadata from the access result.
                $context = $this->getCurrentRenderContext();
                if (!isset($context)) {
                    throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderInIsolation() call. Use renderInIsolation()/renderRoot() or #lazy_builder/#pre_render instead.");
                }
                $context->push(new BubbleableMetadata());
                $context->update($elements);
                $context->bubble();
                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 renderInIsolation() call. Use renderInIsolation()/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'] = Markup::create($elements['#markup']);
            }
            // Add debug output to the renderable array on cache hit.
            if ($this->rendererConfig['debug'] === TRUE) {
                $elements = $this->addDebugOutput($elements, TRUE);
            }
            // 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, #lazy_builder and
    // #create_placeholder for later comparison.
    // @see \Drupal\Core\Render\RenderCacheInterface::get()
    // @see \Drupal\Core\Render\RenderCacheInterface::set()
    $pre_bubbling_elements = array_intersect_key($elements, [
        '#cache' => TRUE,
        '#lazy_builder' => TRUE,
        '#lazy_builder_preview' => TRUE,
        '#create_placeholder' => TRUE,
    ]);
    // 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'])) {
        assert(is_array($elements['#lazy_builder']), 'The #lazy_builder property must have an array as a value.');
        assert(count($elements['#lazy_builder']) === 2, 'The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
        assert(is_array($elements['#lazy_builder'][1]), 'The #lazy_builder argument for callback must have an array as a value.');
        assert(count($elements['#lazy_builder'][1]) === count(array_filter($elements['#lazy_builder'][1], function ($v) {
            return is_null($v) || is_scalar($v);
        })), "A #lazy_builder callback's context may only contain scalar values or NULL.");
        assert(!Element::children($elements), 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(', ', Element::children($elements))));
        $supported_keys = [
            '#lazy_builder',
            '#cache',
            '#create_placeholder',
            '#lazy_builder_preview',
            '#preview',
            // The keys below are not actually supported, but these are added
            // automatically by the Renderer. Adding them as though they are
            // supported allows us to avoid throwing an exception 100% of the time.
'#weight',
            '#printed',
        ];
        assert(empty(array_diff(array_keys($elements), $supported_keys)), 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(', ', array_diff(array_keys($elements), $supported_keys))));
    }
    // Determine whether to do auto-placeholdering.
    if ($this->placeholderGenerator
        ->canCreatePlaceholder($elements) && $this->placeholderGenerator
        ->shouldAutomaticallyPlaceholder($elements)) {
        $elements['#create_placeholder'] = TRUE;
    }
    // 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. On
    // uncacheable requests, always skip placeholdering - if a form is inside
    // a placeholder, which is likely, we want to render it as soon as possible,
    // so that form submission and redirection can take over before any more
    // content is rendered.
    if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE && $this->requestStack
        ->getCurrentRequest()
        ->isMethodCacheable()) {
        if (!isset($elements['#lazy_builder'])) {
            throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
        }
        $elements = $this->placeholderGenerator
            ->createPlaceholder($elements);
    }
    // Build the element if it is still empty.
    if (isset($elements['#lazy_builder'])) {
        $new_elements = $this->doCallback('#lazy_builder', $elements['#lazy_builder'][0], $elements['#lazy_builder'][1]);
        // Throw an exception if #lazy_builder callback does not return an array;
        // provide helpful details for troubleshooting.
        assert(is_array($new_elements), "#lazy_builder callbacks must return a valid renderable array, got " . gettype($new_elements) . " from " . Variable::callableToString($elements['#lazy_builder'][0]));
        // 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) {
            $elements = $this->doCallback('#pre_render', $callable, [
                $elements,
            ]);
        }
    }
    // All render elements support #markup and #plain_text.
    if (isset($elements['#markup']) || isset($elements['#plain_text'])) {
        $elements = $this->ensureMarkupIsSafe($elements);
    }
    // Defaults for bubbleable rendering metadata.
    $elements['#cache']['tags'] = $elements['#cache']['tags'] ?? [];
    $elements['#cache']['max-age'] = $elements['#cache']['max-age'] ?? Cache::PERMANENT;
    $elements['#attached'] = $elements['#attached'] ?? [];
    // 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'])) {
        FormHelper::processStates($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'] = '';
    }
    // 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 = [
            '#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'] = Markup::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'] = Markup::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) {
            $elements['#children'] = $this->doCallback('#post_render', $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. If
    // #render_children is set the #prefix and #suffix will have already been
    // added.
    if (isset($elements['#render_children'])) {
        $elements['#markup'] = Markup::create($elements['#children']);
    }
    else {
        $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
        $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
        $elements['#markup'] = Markup::create($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);
        // Add debug output to the renderable array on cache miss.
        if ($this->rendererConfig['debug'] === TRUE) {
            $render_stop = microtime(TRUE);
            $elements = $this->addDebugOutput($elements, FALSE, $pre_bubbling_elements, $render_stop - $render_start);
        }
        // Update the render context; the render cache implementation may update
        // the element, and it may have different bubbleable metadata now.
        // @see \Drupal\Core\Render\PlaceholderingRenderCache::set()
        $context->pop();
        $context->push(new BubbleableMetadata());
        $context->update($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 RendererInterface::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 $elements['#markup'];
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.