You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

478 lines
19 KiB

<?php
/**
* li₃: the most RAD framework for PHP (http://li3.me)
*
* Copyright 2009, Union of RAD. All rights reserved. This source
* code is distributed under the terms of the BSD 3-Clause License.
* The full license text can be found in the LICENSE.txt file.
*/
namespace lithium\template;
use lithium\aop\Filters;
use lithium\core\Libraries;
use lithium\template\TemplateException;
/**
* As one of the three pillars of the Model-View-Controller design pattern, the `View` class
* (along with other supporting classes) is responsible for taking the data passed from the
* request and/or controller, inserting this into the requested template/layout, and then returning
* the rendered content.
*
* The `View` class interacts with a variety of other classes in order to achieve maximum
* flexibility and configurability at all points in the view rendering and presentation
* process. The `Loader` class is tasked with locating and reading template files which are then
* passed to the `Renderer` adapter subclass.
*
* In the default configuration, the `File` adapter acts as both renderer and loader, loading files
* from paths defined in _process steps_ (described below) and rendering them as plain PHP files,
* augmented with [special syntax](../template).
*
* The `View` class operates on _processes_, which define the steps to render a completed view. For
* example, the default process, which renders a template wrapped in a layout, is comprised of two
* _steps_: the first step renders the main template and captures it to the rendering context, where
* it is embedded in the layout in the second step. See the `$_steps` and `$_processes` properties
* for more information.
*
* Using steps and processes, you can create rendering scenarios to suit very complex needs.
*
* By default, the `View` class is called during the course of the framework's dispatch cycle by the
* `Media` class. However, it is also possible to instantiate and call `View` directly, in cases
* where you wish to bypass all other parts of the framework and simply return rendered content.
*
* A simple example, using the `Simple` renderer/loader for string templates:
*
* ```
* $view = new View(['loader' => 'Simple', 'renderer' => 'Simple']);
* echo $view->render('element', ['name' => "Robert"], ['element' => 'Hello, {:name}!']);
*
* // Output:
* "Hello, Robert!";
* ```
*
* _Note_: This is easily adapted for XML templating.
*
* Another example, this time of something that could be used in an application
* error handler:
*
* ```
* $view = new View([
* 'paths' => [
* 'template' => '{:library}/views/errors/{:template}.{:type}.php',
* 'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
* ]
* ]);
*
* $page = $View->render('all', ['content' => $info], [
* 'template' => '404',
* 'layout' => 'error'
* ]);
* ```
*
* To learn more about processes and process steps, see the `$_processes` and `$_steps` properties,
* respectively.
*
* @see lithium\template\view\Renderer
* @see lithium\template\view\adapter
* @see lithium\net\http\Media
*/
class View extends \lithium\core\ObjectDeprecated {
/**
* Output filters for view rendering.
*
* @var array List of filters.
*/
public $outputFilters = [];
/**
* Holds the details of the current request that originated the call to this view, if
* applicable. May be empty if this does not apply. For example, if the View class is
* created to render an email.
*
* @see lithium\action\Request
* @var object `Request` object instance.
*/
protected $_request = null;
/**
* Holds a reference to the `Response` object that will be returned at the end of the current
* dispatch cycle. Allows headers and other response attributes to be assigned in the templating
* layer.
*
* @see lithium\action\Response
* @var object `Response` object instance.
*/
protected $_response = null;
/**
* The object responsible for loading template files.
*
* @var object Loader object.
*/
protected $_loader = null;
/**
* Object responsible for rendering output.
*
* @var object Renderer object.
*/
protected $_renderer = null;
/**
* Path used to look up view loading and rendering adapters.
*
* @var string
*/
protected $_adapters = 'adapter.template.view';
/**
* View processes are aggregated lists of steps taken to to create a complete, rendered view.
* For example, the default process, `'all'`, renders a template, then renders a layout, using
* the rendered template content. A process can be defined using one or more steps defined in
* the `$_steps` property. Each process definition is a simple array of ordered values, where
* each value is a key in the `$_steps` array.
*
* @see lithium\template\View::$_steps
* @see lithium\template\View::render()
* @var array
*/
protected $_processes = [
'all' => ['template', 'layout'],
'template' => ['template'],
'element' => ['element']
];
/**
* The list of available rendering steps. Each step contains instructions for how to render one
* piece of a multi-step view rendering. The `View` class combines multiple steps into
* _processes_ to create the final output.
*
* Each step is named by its key in the `$_steps` array, and can have the following options:
*
* - `'path'` _string_: Indicates the set of paths to use when loading templates.
*
* - `'conditions'` _mixed_: Make the step dependent on a value being present, or on some other
* arbitrary condition. If a `'conditions'` is a string, it indicates that a key with that
* name must be present in the `$options` passed to `render()`, and must be set to a
* non-empty value. If a closure, it will be executed with the rendering parameters, and must
* return `true` or `false`. In either case, if the condition is satisfied, the step is
* processed. Otherwise, it is skipped. See the `_conditions()` method for more information.
*
* - `'capture'` _array_: If specified, allows the results of this rendering step to be assigned
* to a template variable used in subsequent steps, or to the templating context for use in
* subsequent steps. If can be specified in the form of `array('context' => '<var-name>')` or
* `array('data' => '<var-name>')`. If the `'context'` key is used, the results are captured
* to the rendering context. Likewise with the `'data'` key, results are captured to a
* template variable.
*
* - `'multi'` _boolean_: If set to `true`, the rendering parameter matching the name of this
* step can be an array containing multiple values, in which case this step is executed
* multiple times, once for each value of the array.
*
* @see lithium\template\View::$_processes
* @see lithium\template\View::render()
* @var array
*/
protected $_steps = [
'template' => ['path' => 'template', 'capture' => ['context' => 'content']],
'layout' => [
'path' => 'layout', 'conditions' => 'layout', 'multi' => true, 'capture' => [
'context' => 'content'
]
],
'element' => ['path' => 'element']
];
/**
* Auto-configuration parameters.
*
* @var array Objects to auto-configure.
*/
protected $_autoConfig = [
'request', 'response', 'processes' => 'merge', 'steps' => 'merge'
];
/**
* Constructor.
*
* @see lithium\template\View::$_steps
* @see lithium\template\View::$_processes
* @param array $config Class configuration parameters The available options are:
* - `'loader'` _mixed_: Instance or name of the class used for locating and reading
* template content. Defaults to `File`, which reads template content from PHP files.
* - `'renderer'` _mixed_: Instance or name of the class that populates template
* content with the data passed in to the view layer, typically from a controller.
* Defaults to `'File'`, which executes templates as standard PHP files, using path
* information returned from the `loader` class. Both `loader` and `renderer`
* classes are looked up using the `'adapter.template.view'` path, which locates
* classes in the `extensions\adapter\template\view` sub-namespace of an application
* or plugin.
* - `'request'`: The `Request` object to be made available in the templates.
* Defaults to `null`.
* - `'steps'` _array_: The array of step configurations to add to the built-in
* configurations. Will be merged with the defaults, with any configurations passed
* in overwriting built-in steps. See the `$_steps` property for more information.
* - `'processes'` _array_: The array of process steps to add to the built-in
* configurations. Will be merged with the defaults, with any configurations passed
* in overwriting built-in processes. See the `$_processes` property for more
* information.
* - `'outputFilters'` _array_: An array of filters to be used when handling output. By
* default, the class is initialized with one filter, `h`, which is used in automatic
* output escaping.
* @return void
*/
public function __construct(array $config = []) {
$defaults = [
'request' => null,
'response' => null,
'loader' => 'File',
'renderer' => 'File',
'steps' => [],
'processes' => [],
'outputFilters' => []
];
parent::__construct($config + $defaults);
}
/**
* Perform initialization of the View.
*
* Looks up and initializes loader and renderer classes, and initializes the output escape
* handler, matching the encoding from the `Response` object.
*
* @return void
*/
protected function _init() {
parent::_init();
$encoding = 'UTF-8';
if ($this->_response) {
$encoding =& $this->_response->encoding;
}
$h = function($data) use (&$encoding) {
return htmlspecialchars((string) $data, ENT_QUOTES, $encoding);
};
$this->outputFilters += compact('h') + $this->_config['outputFilters'];
foreach (['loader', 'renderer'] as $key) {
if (is_object($this->_config[$key])) {
$this->{'_' . $key} = $this->_config[$key];
continue;
}
$class = $this->_config[$key];
$config = ['view' => $this] + $this->_config;
$this->{'_' . $key} = Libraries::instance($this->_adapters, $class, $config);
}
}
/**
* Executes a named rendering process by running each process step in sequence and aggregating
* the results. The `View` class comes with 3 built-in processes: `'all'`, `'template'`, and
* `'element'`. The `'all'` process is the default two-step rendered view, where a template is
* wrapped in a layout containing a header and footer.
*
* @see lithium\template\View::_conditions()
* @see lithium\template\View::$_processes
* @see lithium\template\View::$_steps
* @param string $process A named set of rendering steps defined in the `$_processes` array.
* @param array $data An associative array of data to be rendered in the set of templates.
* @param array $options Options used when rendering. Available keys are as follows:
* - `'type'` _string_: The type of content to render. Defaults to `'html'`.
* - `'layout'` _string_: The name of the layout to use in the default two-step
* rendering process. Defaults to `null`.
* - `'template'` _string_: The name of the template to render. Defaults to `null`.
* - `'context'` _array_: An associative array of information to inject into the
* rendering context.
* - `'paths'` _array_: A nested array of paths to use for rendering steps. The
* top-level keys should match the `'path'` key in a step configuration (i.e.:
* `'template'`, `'layout'`, or `'element'`), and the second level is an array
* of path template strings to search (can be a string if there's only one path).
* These path strings generally take the following form:
* `'{:library}/views/{:controller}/{:template}.{:type}.php'`. These template
* strings are specific to the `File` loader, but can take any form useful to the
* template loader being used.
* @return string Returns the result of the rendering process, typically by rendering a template
* first, then rendering a layout (using the default configuration of the `'all'`
* process).
*/
public function render($process, array $data = [], array $options = []) {
$defaults = [
'type' => 'html',
'layout' => null,
'template' => null,
'context' => [],
'paths' => [],
'data' => []
];
$options += $defaults;
$data += $options['data'];
$paths = $options['paths'];
unset($options['data'], $options['paths']);
$params = array_filter($options, function($val) { return $val && is_string($val); });
$result = null;
foreach ($this->_process($process, $params) as $name => $step) {
if (isset($paths[$name]) && $paths[$name] === false) {
continue;
}
if (!$this->_conditions($step, $params, $data, $options)) {
continue;
}
if ($step['multi'] && isset($options[$name])) {
foreach ((array) $options[$name] as $value) {
$params[$name] = $value;
$result = $this->_step($step, $params, $data, $options);
}
continue;
}
$result = $this->_step((array) $step, $params, $data, $options);
}
return $result;
}
/**
* Evaluates a step condition to determine if the step should be executed.
*
* @see lithium\template\View::$_steps
* @param array $step The array of instructions that define a rendering step.
* @param array $params The parameters associated with this rendering operation, as passed to
* `render()` (filtered from the `$options` parameter).
* @param array $data The associative array of template variables passed to `render()`.
* @param array $options The `$options` parameter, as passed to `render()`.
* @return boolean Returns `true` if the step should be executed, or `false` if the step should
* be skipped. If the step array has a `'conditions'` key which is a string, it checks
* to see if the rendering options (`$options`) contain a key of the same name, and if
* that key evaluates to `true`. If `'conditions'` is a closure, that closure is
* executed with the rendering parameters (`$params`, `$data`, and `$options`), and the
* result is determined by the return value of the closure. If a step definition has no
* `'conditions'` key, it is always executed.
*/
protected function _conditions(array $step, array $params, array $data, array $options) {
if (!$conditions = $step['conditions']) {
return true;
}
$isCallable = is_callable($conditions) && !is_string($conditions);
if ($isCallable && !$conditions($params, $data, $options)) {
return false;
}
if (is_string($conditions) && !(isset($options[$conditions]) && $options[$conditions])) {
return false;
}
return true;
}
/**
* Performs a rendering step.
*
* @see lithium\template\view\adapter\File::template()
* @see lithium\template\view\Renderer::render()
* @see lithium\template\view\adapter\File::render()
* @param array $step The array defining the step configuration to render.
* @param array $params An associative array of string values used in the template lookup
* process. See the `$params` argument of `File::template()`.
* @param array $data associative array for template data.
* @param array $options An associative array of options to pass to the renderer. See the
* `$options` parameter of `Renderer::render()` or `File::render()`.
* @return string
* @filter
*/
protected function _step(array $step, array $params, array &$data, array &$options = []) {
$step += ['path' => null, 'capture' => null];
$_renderer = $this->_renderer;
$_loader = $this->_loader;
$filters = $this->outputFilters;
$params = compact('step', 'params', 'options') + [
'data' => $data + $filters,
'loader' => $_loader,
'renderer' => $_renderer
];
$result = Filters::run($this, __FUNCTION__, $params, function($params) {
$template = $params['loader']->template($params['step']['path'], $params['params']);
return $params['renderer']->render($template, $params['data'], $params['options']);
});
if (is_array($step['capture'])) {
switch (key($step['capture'])) {
case 'context':
$options['context'][current($step['capture'])] = $result;
break;
case 'data':
$data[current($step['capture'])] = $result;
break;
}
}
return $result;
}
/**
* Converts a process name to an array containing the rendering steps to be executed for each
* process.
*
* @param string $process A named set of rendering steps.
* @param array $params
* @return array A 2-dimensional array that defines the rendering process. The first dimension
* is a numerically-indexed array containing each rendering step. The second dimension
* represents the parameters for each step.
*/
protected function _process($process, &$params) {
$defaults = ['conditions' => null, 'multi' => false];
if (!is_array($process)) {
if (!isset($this->_processes[$process])) {
throw new TemplateException("Undefined rendering process '{$process}'.");
}
$process = $this->_processes[$process];
}
if (is_string(key($process))) {
return $this->_convertSteps($process, $params, $defaults);
}
$result = [];
foreach ($process as $step) {
if (is_array($step)) {
$result[] = $step + $defaults;
continue;
}
if (!isset($this->_steps[$step])) {
throw new TemplateException("Undefined rendering step '{$step}'.");
}
$result[$step] = $this->_steps[$step] + $defaults;
}
return $result;
}
/* Deprecated / BC */
/**
* Handles API backward compatibility by converting an array-based rendering instruction passed
* to `render()` as a process, to a set of rendering steps, rewriting any associated rendering
* parameters as necessary.
*
* @deprecated
* @param array $command A deprecated rendering instruction, i.e.
* `array('template' => '/path/to/template')`.
* @param array $params The array of associated rendering parameters, passed by reference.
* @param array $defaults Default step rendering options to be merged with the passed rendering
* instruction information.
* @return array Returns a converted set of rendering steps, to be executed in `render()`.
*/
protected function _convertSteps(array $command, array &$params, $defaults) {
$message = "Deprecated rendering instructions (`['template' => '/path/to/tmpl']`) found. ";
$message .= "Please use long syntax (`['template' => ['path' => '/path/to/tmpl']]`). ";
$message .= "Rendering instruction was: " . var_export($command, true);
trigger_error($message, E_USER_DEPRECATED);
if (count($command) === 1) {
$params['template'] = current($command);
return [['path' => key($command)] + $defaults];
}
return $command;
}
}
?>