<?php
/**
* The standard render pipeline for a Panels display object.
*
* Given a fully-loaded panels_display object, this class will turn its
* combination of layout, panes, and styles into HTML, invoking caching
* appropriately along the way. Interacting with the renderer externally is
* very simple - just pass it the display object and call the render() method:
*
* @code
* // given that $display is a fully loaded Panels display object
* $renderer = panels_get_renderer_handler('standard', $display)
* $html_output = $renderer->render();
* @endcode
*
* Internally, the render pipeline is divided into two phases, prepare and
* render:
* - The prepare phase transforms the skeletal data on the provided
* display object into a structure that is expected by the render phase.
* It is divided into a series of discrete sub-methods and operates
* primarily by passing parameters, all with the intention of making
* subclassing easier.
* - The render phase relies primarily on data stored in the renderer object's
* properties, presumably set in the prepare phase. It iterates through the
* rendering of each pane, pane styling, placement in panel regions, region
* styling, and finally the arrangement of rendered regions in the layout.
* Caching, if in use, is triggered per pane, or on the entire display.
*
* In short: prepare builds conf, render renders conf. Subclasses should respect
* this separation of responsibilities by adhering to these loose guidelines,
* given a loaded display object:
* - If your renderer needs to modify the datastructure representing what is
* to be rendered (panes and their conf, styles, caching, etc.), it should
* use the prepare phase.
* - If your renderer needs to modify the manner in which that renderable
* datastructure data is rendered, it should use the render phase.
*
* In the vast majority of use cases, this standard renderer will be sufficient
* and need not be switched out/subclassed; style and/or layout plugins can
* accommodate nearly every use case. If you think you might need a custom
* renderer, consider the following criteria/examples:
* - Some additional markup needs to be added to EVERY SINGLE panel.
* - Given a full display object, just render one pane.
* - Show a Panels admin interface.
*
* The system is almost functionally identical to the old procedural approach,
* with some exceptions (@see panels_renderer_legacy for details). The approach
* here differs primarily in its friendliness to tweaking in subclasses.
*/
class panels_renderer_standard {
/**
* The fully-loaded Panels display object that is to be rendered. "Fully
* loaded" is defined as:
* 1. Having been produced by panels_load_displays(), whether or this page
* request or at some time in the past and the object was exported.
* 2. Having had some external code attach context data ($display->context),
* in the exact form expected by panes. Context matching is delicate,
* typically relying on exact string matches, so special attention must
* be taken.
*
* @var panels_display
*/
var $display;
/**
* An associative array of loaded plugins. Used primarily as a central
* location for storing plugins that require additional loading beyond
* reading the plugin definition, which is already statically cached by
* ctools_get_plugins(). An example is layout plugins, which can optionally
* have a callback that determines the set of panel regions available at
* runtime.
*
* @var array
*/
var $plugins = array();
/**
* A multilevel array of rendered data. The first level of the array
* indicates the type of rendered data, typically with up to three keys:
* 'layout', 'regions', and 'panes'. The relevant rendered data is stored as
* the value for each of these keys as it is generated:
* - 'panes' are an associative array of rendered output, keyed on pane id.
* - 'regions' are an associative array of rendered output, keyed on region
* name.
* - 'layout' is the whole of the rendered output.
*
* @var array
*/
var $rendered = array();
/**
* A multilevel array of data prepared for rendering. The first level of the
* array indicates the type of prepared data. The standard renderer populates
* and uses two top-level keys, 'panes' and 'regions':
* - 'panes' are an associative array of pane objects to be rendered, keyed
* on pane id and sorted into proper rendering order.
* - 'regions' are an associative array of regions, keyed on region name,
* each of which is itself an indexed array of pane ids in the order in
* which those panes appear in that region.
*
* @var array
*/
var $prepared = array();
/**
* Boolean state variable, indicating whether or not the prepare() method has
* been run.
*
* This state is checked in panels_renderer_standard::render_layout() to
* determine whether the prepare method should be automatically triggered.
*
* @var bool
*/
var $prep_run = FALSE;
/**
* The plugin that defines this handler.
*/
var $plugin = FALSE;
/**
* TRUE if this renderer is rendering in administrative mode
* which will allow layouts to have extra functionality.
*
* @var bool
*/
var $admin = FALSE;
/**
* Where to add standard meta information. There are three possibilities:
* - standard: Put the meta information in the normal location. Default.
* - inline: Put the meta information directly inline. This will
* not work for javascript.
*
* @var string
*/
var $meta_location = 'standard';
/**
* Include rendered HTML prior to the layout.
*
* @var string
*/
var $prefix = '';
/**
* Include rendered HTML after the layout.
*
* @var string
*/
var $suffix = '';
/**
* Receive and store the display object to be rendered.
*
* This is a psuedo-constructor that should typically be called immediately
* after object construction.
*
* @param array $plugin
* The definition of the renderer plugin.
* @param panels_display $display
* The panels display object to be rendered.
*/
function init($plugin, &$display) {
$this->plugin = $plugin;
$layout = panels_get_layout($display->layout);
$this->display = &$display;
$this->plugins['layout'] = $layout;
if (!isset($layout['regions'])) {
$this->plugins['layout']['regions'] = panels_get_regions($layout, $display);
}
if (empty($this->plugins['layout'])) {
watchdog('panels', "Layout: @layout couldn't been found, maybe the theme is disabled.", array('@layout' => $display->layout));
}
}
/**
* Prepare the attached display for rendering.
*
* This is the outermost prepare method. It calls several sub-methods as part
* of the overall preparation process. This compartmentalization is intended
* to ease the task of modifying renderer behavior in child classes.
*
* If you override this method, it is important that you either call this
* method via parent::prepare(), or manually set $this->prep_run = TRUE.
*
* @param mixed $external_settings
* An optional parameter allowing external code to pass in additional
* settings for use in the preparation process. Not used in the default
* renderer, but included for interface consistency.
*/
function prepare($external_settings = NULL) {
$this->prepare_panes($this->display->content);
$this->prepare_regions($this->display->panels, $this->display->panel_settings);
$this->prep_run = TRUE;
}
/**
* Prepare the list of panes to be rendered, accounting for visibility/access
* settings and rendering order.
*
* This method represents the standard approach for determining the list of
* panes to be rendered that is compatible with all parts of the Panels
* architecture. It first applies visibility & access checks, then sorts panes
* into their proper rendering order, and returns the result as an array.
*
* Inheriting classes should override this method if that renderer needs to
* regularly make additions to the set of panes that will be rendered.
*
* @param array $panes
* An associative array of pane data (stdClass objects), keyed on pane id.
* @return array
* An associative array of panes to be rendered, keyed on pane id and sorted
* into proper rendering order.
*/
function prepare_panes($panes) {
ctools_include('content');
// Use local variables as writing to them is very slightly faster
$first = $normal = $last = array();
// Prepare the list of panes to be rendered
foreach ($panes as $pid => $pane) {
if (empty($this->admin)) {
// TODO remove in 7.x and ensure the upgrade path weeds out any stragglers; it's been long enough
$pane->shown = !empty($pane->shown); // guarantee this field exists.
// If this pane is not visible to the user, skip out and do the next one
if (!$pane->shown || !panels_pane_access($pane, $this->display)) {
continue;
}
}
// If the pane's subtype is unique, get it so that
// hook_ctools_content_subtype_alter() and/or
// hook_ctools_block_info() will be called.
if ($pane->type != $pane->subtype) {
$content_type = ctools_content_get_subtype($pane->type, $pane->subtype);
}
else {
$content_type = ctools_get_content_type($pane->type);
}
// If this pane wants to render last, add it to the $last array. We allow
// this because some panes need to be rendered after other panes,
// primarily so they can do things like the leftovers of forms.
if (!empty($content_type['render last'])) {
$last[$pid] = $pane;
}
// If it wants to render first, add it to the $first array. This is used
// by panes that need to do some processing before other panes are
// rendered.
else if (!empty($content_type['render first'])) {
$first[$pid] = $pane;
}
// Otherwise, render it in the normal order.
else {
$normal[$pid] = $pane;
}
}
$this->prepared['panes'] = $first + $normal + $last;
// Allow other modules the alter the prepared panes array.
drupal_alter('panels_panes_prepared', $this->prepared['panes'], $this);
return $this->prepared['panes'];
}
/**
* Prepare the list of regions to be rendered.
*
* This method is primarily about properly initializing the style plugin that
* will be used to render the region. This is crucial as regions cannot be
* rendered without a style plugin (in keeping with Panels' philosophy of
* hardcoding none of its output), but for most regions no style has been
* explicitly set. The logic here is what accommodates that situation:
* - If a region has had its style explicitly set, then we fetch that plugin
* and continue.
* - If the region has no explicit style, but a style was set at the display
* level, then inherit the style from the display.
* - If neither the region nor the dispay have explicitly set styles, then
* fall back to the hardcoded 'default' style, a very minimal style.
*
* The other important task accomplished by this method is ensuring that even
* regions without any panes are still properly prepared for the rendering
* process. This is essential because the way Panels loads display objects
* (@see panels_load_displays) results only in a list of regions that
* contain panes - not necessarily all the regions defined by the layout
* plugin, which can only be determined by asking the plugin at runtime. This
* method consults that retrieved list of regions and prepares all of those,
* ensuring none are inadvertently skipped.
*
* @param array $region_pane_list
* An associative array of pane ids, keyed on the region to which those pids
* are assigned. In the default case, this is $display->panels.
* @param array $settings
* All known region style settings, including both the top-level display's
* settings (if any) and all region-specific settings (if any).
* @return array
* An array of regions prepared for rendering.
*/
function prepare_regions($region_pane_list, $settings) {
// Initialize defaults to be used for regions without their own explicit
// settings. Use display settings if they exist, else hardcoded defaults.
$default = array(
'style' => panels_get_style(!empty($settings['style']) ? $settings['style'] : 'default'),
'style settings' => isset($settings['style_settings']['default']) ? $settings['style_settings']['default'] : array(),
);
$regions = array();
if (empty($settings)) {
// No display/panel region settings exist, init all with the defaults.
foreach ($this->plugins['layout']['regions'] as $region_id => $title) {
// Ensure this region has at least an empty panes array.
$panes = !empty($region_pane_list[$region_id]) ? $region_pane_list[$region_id] : array();
$regions[$region_id] = $default;
$regions[$region_id]['pids'] = $panes;
}
}
else {
// Some settings exist; iterate through each region and set individually.
foreach ($this->plugins['layout']['regions'] as $region_id => $title) {
// Ensure this region has at least an empty panes array.
$panes = !empty($region_pane_list[$region_id]) ? $region_pane_list[$region_id] : array();
if (empty($settings[$region_id]['style']) || $settings[$region_id]['style'] == -1) {
$regions[$region_id] = $default;
}
else {
$regions[$region_id]['style'] = panels_get_style($settings[$region_id]['style']);
$regions[$region_id]['style settings'] = isset($settings['style_settings'][$region_id]) ? $settings['style_settings'][$region_id] : array();
}
$regions[$region_id]['pids'] = $panes;
}
}
$this->prepared['regions'] = $regions;
return $this->prepared['regions'];
}
/**
* Build inner content, then hand off to layout-specified theme function for
* final render step.
*
* This is the outermost method in the Panels render pipeline. It calls the
* inner methods, which return a content array, which is in turn passed to the
* theme function specified in the layout plugin.
*
* @return string
* Themed & rendered HTML output.
*/
function render() {
// Let the display refer back to the renderer.
$this->display->renderer_handler = $this;
// Attach out-of-band data first.
$this->add_meta();
if (empty($this->display->cache['method']) || !empty($this->display->skip_cache)) {
return $this->render_layout();
}
else {
$cache = panels_get_cached_content($this->display, $this->display->args, $this->display->context);
if ($cache === FALSE) {
$cache = new panels_cache_object();
$cache->set_content($this->render_layout());
panels_set_cached_content($cache, $this->display, $this->display->args, $this->display->context);
}
return $cache->content;
}
}
/**
* Perform display/layout-level render operations.
*
* This method triggers all the inner pane/region rendering processes, passes
* that to the layout plugin's theme callback, and returns the rendered HTML.
*
* If display-level caching is enabled and that cache is warm, this method
* will not be called.
*
* @return string
* The HTML string representing the entire rendered, themed panel.
*/
function render_layout() {
if (empty($this->prep_run)) {
$this->prepare();
}
$this->render_panes();
$this->render_regions();
if ($this->admin && !empty($this->plugins['layout']['admin theme'])) {
$theme = $this->plugins['layout']['admin theme'];
}
else {
$theme = $this->plugins['layout']['theme'];
}
$this->rendered['layout'] = theme($theme, array('css_id' => check_plain($this->display->css_id), 'content' => $this->rendered['regions'], 'settings' => $this->display->layout_settings, 'display' => $this->display, 'layout' => $this->plugins['layout'], 'renderer' => $this));
return $this->prefix . $this->rendered['layout'] . $this->suffix;
}
/**
* Attach out-of-band page metadata (e.g., CSS and JS).
*
* This must be done before render, because panels-within-panels must have
* their CSS added in the right order: inner content before outer content.
*/
function add_meta() {
if (!empty($this->plugins['layout']['css'])) {
$css = $this->plugins['layout']['css'];
if (!is_array($css)) {
$css = array($css);
}
foreach($css as $file) {
if (file_exists(path_to_theme() . '/' . $file)) {
$this->add_css(path_to_theme() . '/' . $file);
}
else {
$this->add_css($this->plugins['layout']['path'] . '/' . $file);
}
}
}
if ($this->admin && isset($this->plugins['layout']['admin css'])) {
$admin_css = $this->plugins['layout']['admin css'];
if (!is_array($admin_css)) {
$admin_css = array($admin_css);
}
foreach($admin_css as $file) {
$this->add_css($this->plugins['layout']['path'] . '/' . $file);
}
}
}
/**
* Add CSS information to the renderer.
*
* To facilitate previews over Views, CSS can now be added in a manner
* that does not necessarily mean just using drupal_add_css. Therefore,
* during the panel rendering process, this method can be used to add
* css and make certain that ti gets to the proper location.
*
* The arguments should exactly match drupal_add_css().
*
* @see drupal_add_css
*/
function add_css($filename) {
switch ($this->meta_location) {
case 'standard':
drupal_add_css($filename);
break;
case 'inline':
$url = base_path() . $filename;
$this->prefix .= '<link type="text/css" rel="stylesheet" href="' . file_create_url($url) . '" />'."\n";
break;
}
}
/**
* Render all prepared panes, first by dispatching to their plugin's render
* callback, then handing that output off to the pane's style plugin.
*
* @return array
* The array of rendered panes, keyed on pane pid.
*/
function render_panes() {
ctools_include('content');
// First, render all the panes into little boxes.
$this->rendered['panes'] = array();
foreach ($this->prepared['panes'] as $pid => $pane) {
$content = $this->render_pane($pane);
if ($content) {
$this->rendered['panes'][$pid] = $content;
}
}
return $this->rendered['panes'];
}
/**
* Render a pane using its designated style.
*
* This method also manages 'title pane' functionality, where the title from
* an individual pane can be bubbled up to take over the title for the entire
* display.
*
* @param stdClass $pane
* A Panels pane object, as loaded from the database.
*/
function render_pane(&$pane) {
module_invoke_all('panels_pane_prerender', $pane);
$content = $this->render_pane_content($pane);
if ($this->display->hide_title == PANELS_TITLE_PANE && !empty($this->display->title_pane) && $this->display->title_pane == $pane->pid) {
// If the user selected to override the title with nothing, and selected
// this as the title pane, assume the user actually wanted the original
// title to bubble up to the top but not actually be used on the pane.
if (empty($content->title) && !empty($content->original_title)) {
$this->display->stored_pane_title = $content->original_title;
}
else {
$this->display->stored_pane_title = !empty($content->title) ? $content->title : '';
}
}
if (!empty($content->content)) {
if (!empty($pane->style['style'])) {
$style = panels_get_style($pane->style['style']);
if (isset($style) && isset($style['render pane'])) {
$output = theme($style['render pane'], array('content' => $content, 'pane' => $pane, 'display' => $this->display, 'style' => $style, 'settings' => $pane->style['settings']));
// This could be null if no theme function existed.
if (isset($output)) {
return $output;
}
}
}
// fallback
return theme('panels_pane', array('content' => $content, 'pane' => $pane, 'display' => $this->display));
}
}
/**
* Render the interior contents of a single pane.
*
* This method retrieves pane content and produces a ready-to-render content
* object. It also manages pane-specific caching.
*
* @param stdClass $pane
* A Panels pane object, as loaded from the database.
* @return stdClass $content
* A renderable object, containing a subject, content, etc. Based on the
* renderable objects used by the block system.
*/
function render_pane_content(&$pane) {
ctools_include('context');
// TODO finally safe to remove this check?
if (!is_array($this->display->context)) {
watchdog('panels', 'renderer::render_pane_content() hit with a non-array for the context', $this->display, WATCHDOG_DEBUG);
$this->display->context = array();
}
$caching = !empty($pane->cache['method']) && empty($this->display->skip_cache);
if ($caching && ($cache = panels_get_cached_content($this->display, $this->display->args, $this->display->context, $pane))) {
$content = $cache->content;
}
else {
if ($caching) {
// This is created before rendering so that calls to drupal_add_js
// and drupal_add_css will be captured.
$cache = new panels_cache_object();
}
$content = ctools_content_render($pane->type, $pane->subtype, $pane->configuration, array(), $this->display->args, $this->display->context);
foreach (module_implements('panels_pane_content_alter') as $module) {
$function = $module . '_panels_pane_content_alter';
$function($content, $pane, $this->display->args, $this->display->context, $this, $this->display);
}
if ($caching && isset($cache)) {
$cache->set_content($content);
panels_set_cached_content($cache, $this->display, $this->display->args, $this->display->context, $pane);
$content = $cache->content;
}
}
// If there's content, check if we've css configuration to add.
if (!empty($content)) {
// Pass long the css_id that is usually available.
if (!empty($pane->css['css_id'])) {
$content->css_id = check_plain($pane->css['css_id']);
}
// Pass long the css_class that is usually available.
if (!empty($pane->css['css_class'])) {
$content->css_class = check_plain($pane->css['css_class']);
}
}
return $content;
}
/**
* Render all prepared regions, placing already-rendered panes into their
* appropriate positions therein.
*
* @return array
* An array of rendered panel regions, keyed on the region name.
*/
function render_regions() {
$this->rendered['regions'] = array();
// Loop through all panel regions, put all panes that belong to the current
// region in an array, then render the region. Primarily this ensures that
// the panes are arranged in the proper order.
$content = array();
foreach ($this->prepared['regions'] as $region_id => $conf) {
$region_panes = array();
foreach ($conf['pids'] as $pid) {
// Only include panes for region rendering if they had some output.
if (!empty($this->rendered['panes'][$pid])) {
$region_panes[$pid] = $this->rendered['panes'][$pid];
}
}
$this->rendered['regions'][$region_id] = $this->render_region($region_id, $region_panes);
}
return $this->rendered['regions'];
}
/**
* Render a single panel region.
*
* Primarily just a passthrough to the panel region rendering callback
* specified by the style plugin that is attached to the current panel region.
*
* @param $region_id
* The ID of the panel region being rendered
* @param $panes
* An array of panes that are assigned to the panel that's being rendered.
*
* @return string
* The rendered, HTML string output of the passed-in panel region.
*/
function render_region($region_id, $panes) {
$style = $this->prepared['regions'][$region_id]['style'];
$style_settings = $this->prepared['regions'][$region_id]['style settings'];
// Retrieve the pid (can be a panel page id, a mini panel id, etc.), this
// might be used (or even necessary) for some panel display styles.
// TODO: Got to fix this to use panel page name instead of pid, since pid is
// no longer guaranteed. This needs an API to be able to set the final id.
$owner_id = 0;
if (isset($this->display->owner) && is_object($this->display->owner) && isset($this->display->owner->id)) {
$owner_id = $this->display->owner->id;
}
$output = theme($style['render region'], array('display' => $this->display, 'owner_id' => $owner_id, 'panes' => $panes, 'settings' => $style_settings, 'region_id' => $region_id, 'style' => $style));
return $output;
}
}