render_cache.module in Render cache 7
Same filename and directory in other branches
Hook implementations and frequently used functions for render cache module.
File
render_cache.moduleView source
<?php
/**
* @file
* Hook implementations and frequently used functions for render cache module.
*/
/**
* Override entity API rendering callback to add a caching layer.
*
* This callback is registered as $entity_info[$type]['view callback'] with
* hook_entity_info_alter()
*
* @param object[] $entities
* @param string $view_mode
* @param string|null $langcode
* @param string $entity_type
*
* @return array
* @throws \EntityMalformedException
*
* @see entity_view()
* @see render_cache_entity_info_alter()
*/
function render_cache_entity_view_callback($entities, $view_mode, $langcode = NULL, $entity_type) {
// Remove any passed values that are not an object, this can happen with out
// of date Apache Solr search when entities are deleted and probably other
// situations.
foreach ($entities as $key => $entity) {
if (!is_object($entity)) {
unset($entities[$key]);
}
}
$entity_info = entity_get_info($entity_type);
$entity_order = array_keys($entities);
// Prepare context
$context = array(
'entity_type' => $entity_type,
'view_mode' => $view_mode,
'langcode' => $langcode,
);
// Setup drupal_render style cache array.
$cache_info = render_cache_cache_info_defaults();
$cache_info['keys'] = array(
'entity',
$entity_type,
$view_mode,
);
/* @see hook_render_cache_block_default_cache_info_alter() */
drupal_alter('render_cache_entity_default_cache_info', $cache_info, $context);
// Retrieve a list of cache_ids
$cid_map = array();
foreach ($entities as $id => $entity) {
list($entity_id, $entity_revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
$entity_context = $context + array(
'entity_id' => $entity_id,
'entity_revision_id' => $entity_revision_id,
'bundle' => $bundle,
);
$cid_map[$id] = render_cache_get_entity_cid($entity, $cache_info, $entity_context);
}
$cids = array_values($cid_map);
$cached_entities = array();
if (isset($cache_info['granularity']) && $cache_info['granularity'] != DRUPAL_NO_CACHE) {
$cached_entities = cache_get_multiple($cids, 'cache_render');
}
// Calculate remaining entities
$cid_remaining = array_intersect($cid_map, $cids);
$entities = array_intersect_key($entities, $cid_remaining);
// Render non-cached entities.
if (!empty($entities)) {
// If this is a view callback.
if (isset($entity_info['render cache']['callback'])) {
$rendered = $entity_info['render cache']['callback']($entities, $view_mode, $langcode, $entity_type);
}
else {
// We need the $page variable from entity_view() that it does not pass us.
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
// Get only the stack frames we need (PHP 5.4 only).
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
}
elseif (version_compare(PHP_VERSION, '5.3.6', '>=')) {
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
}
else {
// @see http://php.net/manual/en/function.debug-backtrace.php#refsect1-function.debug-backtrace-parameters
$backtrace = debug_backtrace(TRUE);
}
$page = NULL;
// As a safety, do not grab an unexpected arg for $page, check that this
// was called from entity_view().
if (isset($backtrace[1]['function']) && $backtrace[1]['function'] === 'entity_view' && isset($backtrace[1]['args'][4])) {
$page = $backtrace[1]['args'][4];
}
$rendered = entity_get_controller($entity_type)
->view($entities, $view_mode, $langcode, $page);
}
$rendered = reset($rendered);
// Store rendered entities in cache for future views.
foreach (element_children($rendered) as $id) {
// Remove #weight as it will not be accurate, we weight in the cached and
// rendered merge below.
unset($rendered[$id]['#weight']);
// Cache the entity.
$cid = $cid_map[$id];
$render = $rendered[$id];
_render_cache_pre_render($render, $cid, $cache_info);
$cached_entities[$cid] = (object) array(
'data' => $render,
);
}
}
// Not needed in rest of function.
unset($entities, $rendered);
// Return false if no entities are available, matches entity_view()'s
// functionality.
if (empty($cached_entities)) {
return FALSE;
}
// Put entities back in their request order and output.
$return = array();
// Render entities cached as render arrays.
foreach ($entity_order as $weight => $id) {
$cid = $cid_map[$id];
if (!empty($cached_entities[$cid]->data)) {
$render = $cached_entities[$cid]->data;
_render_cache_post_render($render, $id);
$return[$id] = $render;
$return[$id]['#weight'] = $weight;
}
}
// Return $return, wrap with entity type key in array to match
// entity_view()'s functionality.
return array(
$entity_type => $return,
);
}
function _render_cache_pre_render(array &$render, $cid, $cache_info) {
if (isset($cache_info['granularity']) && $cache_info['granularity'] != DRUPAL_NO_CACHE) {
if (empty($cache_info['render_cache_render_to_markup'])) {
cache_set($cid, $render, 'cache_render');
}
else {
// Process markup with drupal_render() caching.
$render['#cache'] = $cache_info;
// Explicitly set cache id.
$render['#cache']['cid'] = $cid;
$render_cache_attached = array();
// Preserve some properties in #attached?
if (!empty($cache_info['render_cache_render_to_markup']['preserve properties']) && is_array($cache_info['render_cache_render_to_markup']['preserve properties'])) {
foreach ($cache_info['render_cache_render_to_markup']['preserve properties'] as $key) {
if (isset($render[$key])) {
$render_cache_attached[$key] = $render[$key];
}
}
}
if (!empty($render_cache_attached)) {
$render['#attached']['render_cache'] = $render_cache_attached;
}
// Do we want to render now?
if (empty($cache_info['render_cache_render_to_markup']['cache late'])) {
// And save things. Also add our preserved properties back.
$render = array(
'#markup' => drupal_render($render),
) + $render_cache_attached;
}
}
}
return $render;
}
function _render_cache_post_render(array &$render, $id) {
// Potentially merge back previously saved properties.
if (!empty($render['#attached']['render_cache'])) {
$render += $render['#attached']['render_cache'];
unset($render['#attached']['render_cache']);
}
// Run any post-render callbacks.
render_cache_process_attached_callbacks($render, $id);
}
/**
* Invokes attached post-render callbacks.
*
* This function can not be named "render_cache_post_render" because that is
* the name of the #attached element. Any function with the name of an
* #attached element will be invoked to process it from
* drupal_process_attached().
*
* @param array &$element
* The renderable element to check for and run post-render callbacks on.
* @param string $id
* An identifier for this render-cacheable elements.
*/
function render_cache_process_attached_callbacks(&$element, $id) {
if (isset($element['#markup']) && !empty($element['#attached']['render_cache_post_render'])) {
foreach ($element['#attached']['render_cache_post_render'] as $function) {
// Fail fatally if the function has not been defined.
$element['#markup'] = call_user_func($function, $element['#markup'], $id);
}
unset($element['#attached']['render_cache_post_render']);
}
}
/**
* Implements hook_entity_info_alter().
*
* We hijack entity rendering, as performed through the Entity API module, to
* provide full entity caching.
*
* @param array $entity_info
*/
function render_cache_entity_info_alter(&$entity_info) {
foreach ($entity_info as $type => $info) {
if (isset($info['view callback'])) {
// Since we are overwriting the view callback we record the original
// callback so that we know how to render.
$entity_info[$type]['render cache']['callback'] = $info['view callback'];
/* @see render_cache_entity_view_callback() */
$entity_info[$type]['view callback'] = 'render_cache_entity_view_callback';
}
elseif (isset($info['controller class']) && in_array('EntityAPIControllerInterface', class_implements($info['controller class']))) {
// We do not set the render cache callback, when it is missing we will
// render using the controller class.
/* @see render_cache_entity_view_callback() */
$entity_info[$type]['view callback'] = 'render_cache_entity_view_callback';
}
}
}
/**
* Returns default values for cache info.
*
* @return array
*/
function render_cache_cache_info_defaults() {
// Setup defaults.
return array(
'bin' => 'cache_render',
'expire' => CACHE_PERMANENT,
'granularity' => DRUPAL_CACHE_PER_ROLE,
// Use per role to support contextual and its safer anyway.
'keys' => array(),
// Special keys that are only related to our implementation.
'render_cache_render_to_markup' => FALSE,
);
}
/**
* Retrieve cache ID to use for entity render caching.
*
* @param object $entity
* The entity object being rendered.
* @param array $cache_info
* A drupal_render style cache array().
* @param array $context
* The context of the entity, with the following keys:
* - entity_type
* - entity_id
* - entity_revision_id
* - bundle
* - langcode
* - view_mode
* If a field is being rendered, the following additional keys are required:
* - field_name
* - deltas
* - display
*
* @return string
*/
function render_cache_get_entity_cid($entity, &$cache_info, $context) {
if (isset($cache_info['cid'])) {
return $cache_info['cid'];
}
$cache_info += render_cache_cache_info_defaults();
/* @see hook_render_cache_entity_cache_info_alter() */
drupal_alter('render_cache_entity_cache_info', $cache_info, $entity, $context);
$cid_parts = array();
$hash = array();
if (!empty($cache_info['keys']) && is_array($cache_info['keys'])) {
$cid_parts = $cache_info['keys'];
}
// Add drupal_render cid_parts based on granularity
$granularity = isset($cache_info['granularity']) ? $cache_info['granularity'] : NULL;
$cid_parts = array_merge($cid_parts, drupal_render_cid_parts($granularity));
// Calculate hash to expire cached items automatically.
$hash['id'] = !empty($context['entity_revision_id']) ? $context['entity_revision_id'] : $context['entity_id'];
$hash['bundle'] = !empty($context['bundle']) ? $context['bundle'] : $context['entity_type'];
$hash['langcode'] = !empty($context['langcode']) ? $context['langcode'] : $GLOBALS['language_content']->language;
$hash['modified'] = entity_modified_last($context['entity_type'], $entity);
$hash['render_method'] = !empty($cache_info['render_cache_render_to_markup']);
if ($hash['render_method']) {
$hash['render_options'] = serialize($cache_info['render_cache_render_to_markup']);
}
if (!empty($context['field_name'])) {
$hash['deltas'] = implode(',', $context['deltas']);
$hash['display'] = serialize(array_intersect_key($context['display'], array(
'type' => '',
'settings' => '',
)));
}
else {
// Generally hash per view_mode - its unlikely they are the same.
$hash['view_mode'] = !empty($context['view_mode']) ? $context['view_mode'] : 'default';
}
// Allow modules to modify $hash for custom invalidating.
/* @see hook_render_cache_entity_hash_alter() */
drupal_alter('render_cache_entity_hash', $hash, $entity, $cache_info, $context);
$cid_parts[] = sha1(implode('-', $hash));
/* @see hook_render_cache_entity_cid_alter() */
drupal_alter('render_cache_entity_cid', $cid_parts, $entity, $cache_info, $context);
return implode(':', $cid_parts);
}
/**
* Implements hook_module_implements_alter().
*
* Moves our hook_entity_info_alter() implementation to occur last so that we
* can consistently hijack the render function of the entity type.
*
* @param array $implementations
* @param string $hook
*/
function render_cache_module_implements_alter(&$implementations, $hook) {
if ($hook === 'entity_info_alter') {
// Move our hook implementation to the bottom.
$group = $implementations['render_cache'];
unset($implementations['render_cache']);
$implementations['render_cache'] = $group;
}
}
/**
* Implements hook_flush_caches().
*/
function render_cache_flush_caches() {
return array(
'cache_render',
);
}
/**
* Helper function to view a single entity.
*
* This can be used to replace node_view(), comment_view(), easier.
*
* @param string $entity_type
* The type of the entity.
* @param object $entity
* The entity to render.
* @param string $view_mode
* A view mode as used by this entity type, e.g. 'full', 'teaser'...
*
* @return array
* A renderable array.
*/
function render_cache_entity_view_single($entity_type, $entity, $view_mode) {
list($entity_id) = entity_extract_ids($entity_type, $entity);
$build = entity_view($entity_type, array(
$entity_id => $entity,
), $view_mode);
// The output needs to be compatible to what the single function would have
// returned.
if (isset($build[$entity_type][$entity_id])) {
return $build[$entity_type][$entity_id];
}
return array();
}
/**
* Implements hook_render_cache_entity_hash_alter().
*
* @param array $hash
* @param object $entity
* @param array $cache_info
* @param array $context
*/
function node_render_cache_entity_hash_alter(&$hash, $entity, $cache_info, $context) {
// We generally cache nodes based on comment count.
if ($context['entity_type'] == 'node' && isset($entity->comment_count)) {
// @todo This is very unreliable if comments can be edited, it would be better
// to directly save a list of entity_modified values but entity_modified
// needs to support multiple get and caching for that first.
$hash['node_comment_count'] = $entity->comment_count;
}
}
/**
* Helper function to view a single entity field.
*
* This can be used to replace field_view_field().
*
* @param string $entity_type
* The type of $entity; e.g., 'node' or 'user'.
* @param object $entity
* The entity containing the field to display. Must at least contain the id
* key and the field data to display.
* @param string $field_name
* The name of the field to display.
* @param string|array $display
* Can be either:
* - The name of a view mode. The field will be displayed according to the
* display settings specified for this view mode in the $instance
* definition for the field in the entity's bundle.
* If no display settings are found for the view mode, the settings for
* the 'default' view mode will be used.
* - An array of display settings, as found in the 'display' entry of
* $instance definitions. The following key/value pairs are allowed:
* - label: (string) Position of the label. The default 'field' theme
* implementation supports the values 'inline', 'above' and 'hidden'.
* Defaults to 'above'.
* - type: (string) The formatter to use. Defaults to the
* 'default_formatter' for the field type, specified in
* hook_field_info(). The default formatter will also be used if the
* requested formatter is not available.
* - settings: (array) Settings specific to the formatter. Defaults to the
* formatter's default settings, specified in
* hook_field_formatter_info().
* - weight: (float) The weight to assign to the renderable element.
* Defaults to 0.
* @param string $langcode
* (Optional) The language the field values are to be shown in. The site's
* current language fallback logic will be applied no values are available
* for the language. If no language is provided the current language will be
* used.
*
* @return
* A renderable array for the field value.
*/
function render_cache_view_field($entity_type, $entity, $field_name, $display = array(), $langcode = NULL) {
// If the entity has no field value, just return nothing.
$items = field_get_items($entity_type, $entity, $field_name);
if (empty($items)) {
return array();
}
// Prepare context
$context = array(
'entity_type' => $entity_type,
'field_name' => $field_name,
'display' => $display,
'langcode' => field_language($entity_type, $entity, $field_name, $langcode),
'deltas' => array_keys($items),
);
// Setup drupal_render style cache array.
$cache_info = render_cache_cache_info_defaults();
$cache_info['keys'] = array(
'field',
$entity_type,
$field_name,
);
drupal_alter('render_cache_field_default_cache_info', $cache_info, $context);
list($entity_id, $entity_revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
$entity_context = $context + array(
'entity_id' => $entity_id,
'entity_revision_id' => $entity_revision_id,
'bundle' => $bundle,
);
$cid = render_cache_get_entity_cid($entity, $cache_info, $entity_context);
$cache = NULL;
if (isset($cache_info['granularity']) && $cache_info['granularity'] != DRUPAL_NO_CACHE) {
$cache = cache_get($cid, 'cache_render');
}
if (!$cache) {
$field_render = field_view_field($entity_type, $entity, $field_name, $display, $langcode);
_render_cache_pre_render($field_render, $cid, $cache_info);
$cache = (object) array(
'data' => $field_render,
);
}
$render = $cache->data;
// @todo Not sure what should be used for the second parameter here?
_render_cache_post_render($render, $cid);
return $render;
}
Functions
Name | Description |
---|---|
node_render_cache_entity_hash_alter | Implements hook_render_cache_entity_hash_alter(). |
render_cache_cache_info_defaults | Returns default values for cache info. |
render_cache_entity_info_alter | Implements hook_entity_info_alter(). |
render_cache_entity_view_callback | Override entity API rendering callback to add a caching layer. |
render_cache_entity_view_single | Helper function to view a single entity. |
render_cache_flush_caches | Implements hook_flush_caches(). |
render_cache_get_entity_cid | Retrieve cache ID to use for entity render caching. |
render_cache_module_implements_alter | Implements hook_module_implements_alter(). |
render_cache_process_attached_callbacks | Invokes attached post-render callbacks. |
render_cache_view_field | Helper function to view a single entity field. |
_render_cache_post_render | |
_render_cache_pre_render |