qtools_profiler-8.x-1.x-dev/modules/qtools_cache_profiler/src/Renderer.php
modules/qtools_cache_profiler/src/Renderer.php
<?php
namespace Drupal\qtools_cache_profiler;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Render\Markup;
use Drupal\Core\Render\Renderer as CoreRenderer;
use Drupal\qtools_common\QToolsCacheHelper;
/**
* Decorates core's Renderer to provide the necessary metadata to renderviz' JS.
*/
class Renderer extends CoreRenderer {
const LOG_START = 'START';
const LOG_END = 'END';
/**
* Wrapper to easy bedug.
*/
protected function log($type, $cacheId = NULL, $data = NULL) {
if (TRUE) {
return;
}
// Logger function.
$log = function_exists('dpm') ? 'dpm' : 'print_r';
// Output payload before separator if we closing render.
if ($type == static::LOG_END && !empty($data)) {
$log($data);
}
// Print action and cid.
$prefix = '=============== RENDER ' . $type;
$log($prefix . ': ' . $cacheId);
// Output payload after separator if we opening render.
if ($type == static::LOG_START && !empty($data)) {
$log($data);
}
}
/**
* Check if given content require wraping to correcly place metadata.
*/
protected function requireWrap($elements, $cleaned_content) {
$trimmed_content = trim($elements['#markup']);
// To preserve all cache info we need a target tag, so we have to wrap
// output but we can't wrap some special tags or drupal will break.
$unwrappable_tags = [
'<script',
'<link',
'<style',
'<meta',
'<title',
'<body',
'<html',
'<head',
'<!DOCTYPE',
];
foreach ($unwrappable_tags as $tag) {
if (strpos($cleaned_content, $tag) === 0) {
return FALSE;
}
}
// If template is empty we would see previous render info.
$wrap = strpos($trimmed_content, '<!--RENDERER_START-->') === 0;
// If elements are about to be lazy built we wrap them.
if (!$wrap && !empty($elements['#lazy_builder'])) {
$wrap = TRUE;
}
// If there are more than one child element we need to wrap.
if (!$wrap) {
$dom = new \DOMDocument();
libxml_use_internal_errors(TRUE);
$dom->loadHTML($cleaned_content);
// Structure will be wrapped in <html><body> so we go 2 level deep.
$childNodes = $dom->childNodes[1]->childNodes[0]->childNodes;
if ($childNodes->length > 1) {
$wrap = TRUE;
}
libxml_use_internal_errors(FALSE);
}
return $wrap;
}
/**
* {@inheritdoc}
*/
protected function doRender(&$elements, $is_root_call = FALSE) {
$placeholder = !empty($elements['#create_placeholder']);
$original_elements = [];
$original_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : [];
// Create cache ID and store it alongside with rendered metadata.
$cacheId = QToolsCacheHelper::buildCidElements($elements);
if (!empty($cacheId)) {
$this->log(static::LOG_START, $cacheId, $elements);
$keys = $elements['#cache']['keys'];
};
// Do normal rendering.
$result = parent::doRender($elements, $is_root_call);
// When there is no output, there is also nothing to visualize.
if ($result === '') {
$this->log(static::LOG_END, $cacheId);
return '';
}
// The HTML spec says HTML comments are markup, thus it's not allowed to use
// HTML comments inside HTML element attributes. For example:
// <a <!--title="need to be comment out"-->>a link</a>
// is as wrong as
// <a <span></span>>a link</a>
// So we only wrap $result in a HTML comment with renderviz metadata when
// $result actually contains HTML markup. So, 'This is text.' will cause an
// early return, but 'This is text and <a href="…">a link</a>.' will not.
// The presence of HTML indicates it's valid to have HTML, and hwen it
// valid to have HTML, HTML comments are allowed too.
// Since strip tags always remove comments we need to make sure
// they will not affect our compassing.
$cleaned_result = trim(preg_replace('/<!--(.*)-->/Uis', '', $result));
// Check if this chunk is HTML markup.
if ($cleaned_result == strip_tags($cleaned_result)) {
$this->log(static::LOG_END, $cacheId, $result);
// Returned without debug output.
return $result;
}
// Apply the same default cacheability logic that Renderer::doRender()
// applies.
$pre_bubbling_elements = $original_elements;
$pre_bubbling_elements['#cache']['tags'] = isset($original_elements['#cache']['tags']) ? $original_elements['#cache']['tags'] : [];
$pre_bubbling_elements['#cache']['max-age'] = isset($original_elements['#cache']['max-age']) ? $original_elements['#cache']['max-age'] : Cache::PERMANENT;
// @todo Add these always? That's more accurate for visualization purposes;
// it is only for performance optimization purposes that the wrapped
// function doesn't do that.
if ($is_root_call || isset($elements['#cache']['keys'])) {
$required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
if (isset($pre_bubbling_elements['#cache']['contexts'])) {
$pre_bubbling_elements['#cache']['contexts'] = Cache::mergeContexts($pre_bubbling_elements['#cache']['contexts'], $required_cache_contexts);
}
else {
$pre_bubbling_elements['#cache']['contexts'] = $required_cache_contexts;
}
}
// Prepare data for output.
$result_cache = $elements['#cache'];
$pre_bubbling_cache = $pre_bubbling_elements['#cache'];
// If this render could have been cached we need to get its final cacheId
// as it will be different from previous due to bubbling.
if (!empty($cacheId)) {
$cacheId2 = QToolsCacheHelper::buildCidElements($elements, $keys);
$result_cache['qtools-cache-profiler-cacheable'] = 1;
}
elseif (!empty($elements['#lazy_builder_built'])) {
// Lazy built blocks defy normal rule in a way that they are always
// built on every page load, and must act as ones that
// potentially cacheable.
$cacheId2 = QToolsCacheHelper::buildCidElements($elements);
$result_cache['qtools-cache-profiler-cacheable'] = 1;
$result_cache['qtools-cache-profiler-lazy'] = 1;
}
elseif (strpos($cleaned_result, '<!DOCTYPE') === 0) {
$result_cache['qtools-cache-profiler-cacheable'] = 1;
$result_cache['contexts'][] = 'route';
$result_cache['contexts'][] = 'request_format';
$result_cache['contexts'][] = 'url.query_args:_wrapper_format';
$cacheId2 = QToolsCacheHelper::buildCidElements(['#cache' => $result_cache], ['response']);
}
// Add real cid to the output.
if (!empty($cacheId2)) {
$result_cache['qtools-cache-profiler-cid'] = $cacheId2;
}
// Add debug output.
// @todo: This currently prints the final and pre-bubbling elements.
$interesting_keys = ['keys', 'contexts', 'tags', 'max-age'];
if (array_intersect(array_keys($result_cache), $interesting_keys) || array_intersect(array_keys($pre_bubbling_cache), $interesting_keys)) {
$prefix = '<!--RENDERER_START-->' . '<!--' . Json::encode($result_cache) . '-->' . '<!--' . Json::encode($pre_bubbling_cache) . '-->';
$suffix = '<!--RENDERER_END-->';
// If this render support caching add cid for easy visualise.
if (!empty($cacheId2)) {
$prefix .= '<!--RENDERER_CID ' . $cacheId2 . ' -->';
}
// Some templates does not have any additional tags, therefor
// there is nothing to attach our data to, as we add wrapper if allowed
// and this data is critical.
// Placeholder elements always wrapped as their cache info is yet unknown.
if (!empty($placeholder) || (!empty($cacheId2) && $this->requireWrap($elements, $cleaned_result))) {
$elements['#markup'] = '<div>' . $elements['#markup'] . '</div>';
}
$elements['#markup'] = Markup::create($prefix . $elements['#markup'] . $suffix);
}
// Debug results.
if (!empty($cacheId2)) {
$this->log(static::LOG_END, $cacheId2, $elements);
};
return $elements['#markup'];
}
}
