View.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\template;
  9. use lithium\core\Libraries;
  10. use lithium\template\TemplateException;
  11. /**
  12. * As one of the three pillars of the Model-View-Controller design pattern, the `View` class
  13. * (along with other supporting classes) is responsible for taking the data passed from the
  14. * request and/or controller, inserting this into the requested template/layout, and then returning
  15. * the rendered content.
  16. *
  17. * The `View` class interacts with a variety of other classes in order to achieve maximum
  18. * flexibility and configurability at all points in the view rendering and presentation
  19. * process. The `Loader` class is tasked with locating and reading template files which are then
  20. * passed to the `Renderer` adapter subclass.
  21. *
  22. * In the default configuration, the `File` adapter acts as both renderer and loader, loading files
  23. * from paths defined in _process steps_ (described below) and rendering them as plain PHP files,
  24. * augmented with [special syntax](../template).
  25. *
  26. * The `View` class operates on _processes_, which define the steps to render a completed view. For
  27. * example, the default process, which renders a template wrapped in a layout, is comprised of two
  28. * _steps_: the first step renders the main template and captures it to the rendering context, where
  29. * it is embedded in the layout in the second step. See the `$_steps` and `$_processes` properties
  30. * for more information.
  31. *
  32. * Using steps and processes, you can create rendering scenarios to suit very complex needs.
  33. *
  34. * By default, the `View` class is called during the course of the framework's dispatch cycle by the
  35. * `Media` class. However, it is also possible to instantiate and call `View` directly, in cases
  36. * where you wish to bypass all other parts of the framework and simply return rendered content.
  37. *
  38. * A simple example, using the `Simple` renderer/loader for string templates:
  39. *
  40. * {{{
  41. * $view = new View(array('loader' => 'Simple', 'renderer' => 'Simple'));
  42. * echo $view->render('element', array('name' => "Robert"), array('element' => 'Hello, {:name}!'));
  43. *
  44. * // Output:
  45. * "Hello, Robert!";
  46. * }}}
  47. *
  48. * _Note_: This is easily adapted for XML templating.
  49. *
  50. * Another example, this time of something that could be used in an application
  51. * error handler:
  52. *
  53. * {{{
  54. * $view = new View(array(
  55. * 'paths' => array(
  56. * 'template' => '{:library}/views/errors/{:template}.{:type}.php',
  57. * 'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
  58. * )
  59. * ));
  60. *
  61. * $page = $View->render('all', array('content' => $info), array(
  62. * 'template' => '404',
  63. * 'layout' => 'error'
  64. * ));
  65. * }}}
  66. *
  67. * To learn more about processes and process steps, see the `$_processes` and `$_steps` properties,
  68. * respectively.
  69. *
  70. * @see lithium\template\view\Renderer
  71. * @see lithium\template\view\adapter
  72. * @see lithium\net\http\Media
  73. */
  74. class View extends \lithium\core\Object {
  75. /**
  76. * Output filters for view rendering.
  77. *
  78. * @var array List of filters.
  79. */
  80. public $outputFilters = array();
  81. /**
  82. * Holds the details of the current request that originated the call to this view, if
  83. * applicable. May be empty if this does not apply. For example, if the View class is
  84. * created to render an email.
  85. *
  86. * @see lithium\action\Request
  87. * @var object `Request` object instance.
  88. */
  89. protected $_request = null;
  90. /**
  91. * Holds a reference to the `Response` object that will be returned at the end of the current
  92. * dispatch cycle. Allows headers and other response attributes to be assigned in the templating
  93. * layer.
  94. *
  95. * @see lithium\action\Response
  96. * @var object `Response` object instance.
  97. */
  98. protected $_response = null;
  99. /**
  100. * The object responsible for loading template files.
  101. *
  102. * @var object Loader object.
  103. */
  104. protected $_loader = null;
  105. /**
  106. * Object responsible for rendering output.
  107. *
  108. * @var objet Renderer object.
  109. */
  110. protected $_renderer = null;
  111. /**
  112. * Path used to look up view loading and rendering adapters.
  113. *
  114. * @var string
  115. */
  116. protected $_adapters = 'adapter.template.view';
  117. /**
  118. * View processes are aggregated lists of steps taken to to create a complete, rendered view.
  119. * For example, the default process, `'all'`, renders a template, then renders a layout, using
  120. * the rendered template content. A process can be defined using one or more steps defined in
  121. * the `$_steps` property. Each process definition is a simple array of ordered values, where
  122. * each value is a key in the `$_steps` array.
  123. *
  124. * @see lithium\template\View::$_steps
  125. * @see lithium\template\View::render()
  126. * @var array
  127. */
  128. protected $_processes = array(
  129. 'all' => array('template', 'layout'),
  130. 'template' => array('template'),
  131. 'element' => array('element')
  132. );
  133. /**
  134. * The list of available rendering steps. Each step contains instructions for how to render one
  135. * piece of a multi-step view rendering. The `View` class combines multiple steps into
  136. * _processes_ to create the final output.
  137. *
  138. * Each step is named by its key in the `$_steps` array, and can have the following options:
  139. *
  140. * - `'path'` _string_: Indicates the set of paths to use when loading templates.
  141. *
  142. * - `'conditions'` _mixed_: Make the step dependent on a value being present, or on some other
  143. * arbitrary condition. If a `'conditions'` is a string, it indicates that a key with that
  144. * name must be present in the `$options` passed to `render()`, and must be set to a
  145. * non-empty value. If a closure, it will be executed with the rendering parameters, and must
  146. * return `true` or `false`. In either case, if the condition is satisfied, the step is
  147. * processed. Otherwise, it is skipped. See the `_conditions()` method for more information.
  148. *
  149. * - `'capture'` _array_: If specified, allows the results of this rendering step to be assigned
  150. * to a template variable used in subsequent steps, or to the templating context for use in
  151. * subsequent steps. If can be specified in the form of `array('context' => '<var-name>')` or
  152. * `array('data' => '<var-name>')`. If the `'context'` key is used, the results are captured
  153. * to the rendering context. Likewise with the `'data'` key, results are captured to a
  154. * template variable.
  155. *
  156. * - `'multi'` _boolean_: If set to `true`, the rendering parameter matching the name of this
  157. * step can be an array containing multiple values, in which case this step is executed
  158. * multiple times, once for each value of the array.
  159. *
  160. * @see lithium\template\View::$_processes
  161. * @see lithium\template\View::render()
  162. * @var array
  163. */
  164. protected $_steps = array(
  165. 'template' => array('path' => 'template', 'capture' => array('context' => 'content')),
  166. 'layout' => array(
  167. 'path' => 'layout', 'conditions' => 'layout', 'multi' => true, 'capture' => array(
  168. 'context' => 'content'
  169. )
  170. ),
  171. 'element' => array('path' => 'element')
  172. );
  173. /**
  174. * Auto-configuration parameters.
  175. *
  176. * @var array Objects to auto-configure.
  177. */
  178. protected $_autoConfig = array(
  179. 'request', 'response', 'processes' => 'merge', 'steps' => 'merge'
  180. );
  181. /**
  182. * Constructor.
  183. *
  184. * @see lithium\template\View::$_steps
  185. * @see lithium\template\View::$_processes
  186. * @param array $config Class configuration parameters The available options are:
  187. * - `'loader'` _mixed_: Instance or name of the class used for locating and reading
  188. * template content. Defaults to `File`, which reads template content from PHP files.
  189. * - `'renderer'` _mixed_: Instance or name of the class that populates template
  190. * content with the data passed in to the view layer, typically from a controller.
  191. * Defaults to `'File'`, which executes templates as standard PHP files, using path
  192. * information returned from the `loader` class. Both `loader` and `renderer`
  193. * classes are looked up using the `'adapter.template.view'` path, which locates
  194. * classes in the `extensions\adapter\template\view` sub-namespace of an application
  195. * or plugin.
  196. * - `'request'`: The `Request` object to be made available in the templates.
  197. * Defaults to `null`.
  198. * - `'steps'` _array_: The array of step configurations to add to the built-in
  199. * configurations. Will be merged with the defaults, with any configurations passed
  200. * in overwriting built-in steps. See the `$_steps` property for more information.
  201. * - `'processes'` _array_: The array of process steps to add to the built-in
  202. * configurations. Will be merged with the defaults, with any configurations passed
  203. * in overwriting built-in processes. See the `$_processes` property for more
  204. * information.
  205. * - `'outputFilters'` _array_: An array of filters to be used when handling output. By
  206. * default, the class is initialized with one filter, `h`, which is used in automatic
  207. * output escaping.
  208. */
  209. public function __construct(array $config = array()) {
  210. $defaults = array(
  211. 'request' => null,
  212. 'response' => null,
  213. 'loader' => 'File',
  214. 'renderer' => 'File',
  215. 'steps' => array(),
  216. 'processes' => array(),
  217. 'outputFilters' => array()
  218. );
  219. parent::__construct($config + $defaults);
  220. }
  221. /**
  222. * Perform initialization of the View.
  223. *
  224. * Looks up and initializes loader and renderer classes, and initializes the output escape
  225. * handler, matching the encoding from the `Response` object.
  226. *
  227. * @return void
  228. */
  229. protected function _init() {
  230. parent::_init();
  231. $encoding = 'UTF-8';
  232. if ($this->_response) {
  233. $encoding =& $this->_response->encoding;
  234. }
  235. $h = function($data) use (&$encoding) {
  236. return htmlspecialchars((string) $data, ENT_QUOTES, $encoding);
  237. };
  238. $this->outputFilters += compact('h') + $this->_config['outputFilters'];
  239. foreach (array('loader', 'renderer') as $key) {
  240. if (is_object($this->_config[$key])) {
  241. $this->{'_' . $key} = $this->_config[$key];
  242. continue;
  243. }
  244. $class = $this->_config[$key];
  245. $config = array('view' => $this) + $this->_config;
  246. $this->{'_' . $key} = Libraries::instance($this->_adapters, $class, $config);
  247. }
  248. }
  249. /**
  250. * Executes a named rendering process by running each process step in sequence and aggregating
  251. * the results. The `View` class comes with 3 built-in processes: `'all'`, `'template'`, and
  252. * `'element'`. The `'all'` process is the default two-step rendered view, where a template is
  253. * wrapped in a layout containing a header and footer.
  254. *
  255. * @see lithium\template\View::_conditions()
  256. * @see lithium\template\View::$_processes
  257. * @see lithium\template\View::$_steps
  258. * @param string $process A named set of rendering steps defined in the `$_processes` array.
  259. * @param array $data An associative array of data to be rendered in the set of templates.
  260. * @param array $options Options used when rendering. Available keys are as follows:
  261. * - `'type'` _string_: The type of content to render. Defaults to `'html'`.
  262. * - `'layout'` _string_: The name of the layout to use in the default two-step
  263. * rendering process. Defaults to `null`.
  264. * - `'template'` _string_: The name of the template to render. Defaults to `null`.
  265. * - `'context'` _array_: An associative array of information to inject into the
  266. * rendering context.
  267. * - `'paths'` _array_: A nested array of paths to use for rendering steps. The
  268. * top-level keys should match the `'path'` key in a step configuration (i.e.:
  269. * `'template'`, `'layout'`, or `'element'`), and the second level is an array
  270. * of path template strings to search (can be a string if there's only one path).
  271. * These path strings generally take the following form:
  272. * `'{:library}/views/{:controller}/{:template}.{:type}.php'`. These template
  273. * strings are specific to the `File` loader, but can take any form useful to the
  274. * template loader being used.
  275. * @return string Returns the result of the rendering process, typically by rendering a template
  276. * first, then rendering a layout (using the default configuration of the `'all'`
  277. * process).
  278. */
  279. public function render($process, array $data = array(), array $options = array()) {
  280. $defaults = array(
  281. 'type' => 'html',
  282. 'layout' => null,
  283. 'template' => null,
  284. 'context' => array(),
  285. 'paths' => array(),
  286. 'data' => array()
  287. );
  288. $options += $defaults;
  289. $data += $options['data'];
  290. $paths = $options['paths'];
  291. unset($options['data'], $options['paths']);
  292. $params = array_filter($options, function($val) { return $val && is_string($val); });
  293. $result = null;
  294. foreach ($this->_process($process, $params) as $name => $step) {
  295. if (isset($paths[$name]) && $paths[$name] === false) {
  296. continue;
  297. }
  298. if (!$this->_conditions($step, $params, $data, $options)) {
  299. continue;
  300. }
  301. if ($step['multi'] && isset($options[$name])) {
  302. foreach ((array) $options[$name] as $value) {
  303. $params[$name] = $value;
  304. $result = $this->_step($step, $params, $data, $options);
  305. }
  306. continue;
  307. }
  308. $result = $this->_step((array) $step, $params, $data, $options);
  309. }
  310. return $result;
  311. }
  312. /**
  313. * Evaluates a step condition to determine if the step should be executed.
  314. *
  315. * @see lithium\template\View::$_steps
  316. * @param array $step The array of instructions that define a rendering step.
  317. * @param array $params The parameters associated with this rendering operation, as passed to
  318. * `render()` (filtered from the `$options` parameter).
  319. * @param array $data The associative array of template variables passed to `render()`.
  320. * @param array $options The `$options` parameter, as passed to `render()`.
  321. * @return boolean Returns `true` if the step should be executed, or `false` if the step should
  322. * be skipped. If the step array has a `'conditions'` key which is a string, it checks
  323. * to see if the rendering options (`$options`) contain a key of the same name, and if
  324. * that key evaluates to `true`. If `'conditions'` is a closure, that closure is
  325. * executed with the rendering parameters (`$params`, `$data`, and `$options`), and the
  326. * result is determined by the return value of the closure. If a step definition has no
  327. * `'conditions'` key, it is always executed.
  328. */
  329. protected function _conditions(array $step, array $params, array $data, array $options) {
  330. if (!$conditions = $step['conditions']) {
  331. return true;
  332. }
  333. $isCallable = is_callable($conditions) && !is_string($conditions);
  334. if ($isCallable && !$conditions($params, $data, $options)) {
  335. return false;
  336. }
  337. if (is_string($conditions) && !(isset($options[$conditions]) && $options[$conditions])) {
  338. return false;
  339. }
  340. return true;
  341. }
  342. /**
  343. * Performs a rendering step.
  344. *
  345. * @see lithium\template\view\adapter\File::template()
  346. * @see lithium\template\view\Renderer::render()
  347. * @see lithium\template\view\adapter\File::render()
  348. * @param array $step The array defining the step configuration to render.
  349. * @param array $params An associative array of string values used in the template lookup
  350. * process. See the `$params` argument of `File::template()`.
  351. * @param array $data associative array for template data.
  352. * @param array $options An associative array of options to pass to the renderer. See the
  353. * `$options` parameter of `Renderer::render()` or `File::render()`.
  354. * @return string
  355. * @filter
  356. */
  357. protected function _step(array $step, array $params, array &$data, array &$options = array()) {
  358. $step += array('path' => null, 'capture' => null);
  359. $_renderer = $this->_renderer;
  360. $_loader = $this->_loader;
  361. $filters = $this->outputFilters;
  362. $params = compact('step', 'params', 'options') + array(
  363. 'data' => $data + $filters,
  364. 'loader' => $_loader,
  365. 'renderer' => $_renderer
  366. );
  367. $filter = function($self, $params) {
  368. $template = $params['loader']->template($params['step']['path'], $params['params']);
  369. return $params['renderer']->render($template, $params['data'], $params['options']);
  370. };
  371. $result = $this->_filter(__METHOD__, $params, $filter);
  372. if (is_array($step['capture'])) {
  373. switch (key($step['capture'])) {
  374. case 'context':
  375. $options['context'][current($step['capture'])] = $result;
  376. break;
  377. case 'data':
  378. $data[current($step['capture'])] = $result;
  379. break;
  380. }
  381. }
  382. return $result;
  383. }
  384. /**
  385. * Converts a process name to an array containing the rendering steps to be executed for each
  386. * process.
  387. *
  388. * @param string $process A named set of rendering steps.
  389. * @param array $params
  390. * @return array A 2-dimensional array that defines the rendering process. The first dimension
  391. * is a numerically-indexed array containing each rendering step. The second dimension
  392. * represents the parameters for each step.
  393. */
  394. protected function _process($process, &$params) {
  395. $defaults = array('conditions' => null, 'multi' => false);
  396. if (!is_array($process)) {
  397. if (!isset($this->_processes[$process])) {
  398. throw new TemplateException("Undefined rendering process '{$process}'.");
  399. }
  400. $process = $this->_processes[$process];
  401. }
  402. if (is_string(key($process))) {
  403. return $this->_convertSteps($process, $params, $defaults);
  404. }
  405. $result = array();
  406. foreach ($process as $step) {
  407. if (is_array($step)) {
  408. $result[] = $step + $defaults;
  409. continue;
  410. }
  411. if (!isset($this->_steps[$step])) {
  412. throw new TemplateException("Undefined rendering step '{$step}'.");
  413. }
  414. $result[$step] = $this->_steps[$step] + $defaults;
  415. }
  416. return $result;
  417. }
  418. /**
  419. * Handles API backward compatibility by converting an array-based rendering instruction passed
  420. * to `render()` as a process, to a set of rendering steps, rewriting any associated rendering
  421. * parameters as necessary.
  422. *
  423. * @param array $command A deprecated rendering instruction, i.e.
  424. * `array('template' => '/path/to/template')`.
  425. * @param array $params The array of associated rendering parameters, passed by reference.
  426. * @param array $defaults Default step rendering options to be merged with the passed rendering
  427. * instruction information.
  428. * @return array Returns a converted set of rendering steps, to be executed in `render()`.
  429. */
  430. protected function _convertSteps(array $command, array &$params, $defaults) {
  431. if (count($command) === 1) {
  432. $params['template'] = current($command);
  433. return array(array('path' => key($command)) + $defaults);
  434. }
  435. return $command;
  436. }
  437. }
  438. ?>