1
2/*
3 +------------------------------------------------------------------------+
4 | Phalcon Framework                                                      |
5 +------------------------------------------------------------------------+
6 | Copyright (c) 2011-2017 Phalcon Team (https://phalconphp.com)          |
7 +------------------------------------------------------------------------+
8 | This source file is subject to the New BSD License that is bundled     |
9 | with this package in the file LICENSE.txt.                             |
10 |                                                                        |
11 | If you did not receive a copy of the license and are unable to         |
12 | obtain it through the world-wide-web, please send an email             |
13 | to license@phalconphp.com so we can send you a copy immediately.       |
14 +------------------------------------------------------------------------+
15 | Authors: Andres Gutierrez <andres@phalconphp.com>                      |
16 |          Eduar Carvajal <eduar@phalconphp.com>                         |
17 +------------------------------------------------------------------------+
18 */
19
20namespace Phalcon\Mvc\View;
21
22use Phalcon\Di\Injectable;
23use Phalcon\Mvc\View\Exception;
24use Phalcon\Mvc\ViewBaseInterface;
25use Phalcon\Cache\BackendInterface;
26use Phalcon\Mvc\View\EngineInterface;
27use Phalcon\Mvc\View\Engine\Php as PhpEngine;
28
29/**
30 * Phalcon\Mvc\View\Simple
31 *
32 * This component allows to render views without hierarchical levels
33 *
34 *<code>
35 * use Phalcon\Mvc\View\Simple as View;
36 *
37 * $view = new View();
38 *
39 * // Render a view
40 * echo $view->render(
41 *     "templates/my-view",
42 *     [
43 *         "some" => $param,
44 *     ]
45 * );
46 *
47 * // Or with filename with extension
48 * echo $view->render(
49 *     "templates/my-view.volt",
50 *     [
51 *         "parameter" => $here,
52 *     ]
53 * );
54 *</code>
55 */
56class Simple extends Injectable implements ViewBaseInterface
57{
58
59	protected _options;
60
61	protected _viewsDir;
62
63	protected _partialsDir;
64
65	protected _viewParams;
66
67	/**
68	 * @var \Phalcon\Mvc\View\EngineInterface[]|false
69	 */
70	protected _engines = false;
71
72	/**
73	 * @var array|null
74	 */
75	protected _registeredEngines { get };
76
77	protected _activeRenderPath;
78
79	protected _content;
80
81	protected _cache = false;
82
83	protected _cacheOptions;
84
85	/**
86	 * Phalcon\Mvc\View\Simple constructor
87	 */
88	public function __construct(array options = [])
89	{
90		let this->_options = options;
91	}
92
93	/**
94	 * Sets views directory. Depending of your platform, always add a trailing slash or backslash
95	 */
96	public function setViewsDir(string! viewsDir)
97	{
98		let this->_viewsDir = viewsDir;
99	}
100
101	/**
102	 * Gets views directory
103	 */
104	public function getViewsDir() -> string
105	{
106		return this->_viewsDir;
107	}
108
109	/**
110	 * Register templating engines
111	 *
112	 *<code>
113	 * $this->view->registerEngines(
114	 *     [
115	 *         ".phtml" => "Phalcon\\Mvc\\View\\Engine\\Php",
116	 *         ".volt"  => "Phalcon\\Mvc\\View\\Engine\\Volt",
117	 *         ".mhtml" => "MyCustomEngine",
118	 *     ]
119	 * );
120	 *</code>
121	 */
122	public function registerEngines(array! engines)
123	{
124		let this->_registeredEngines = engines;
125	}
126
127	/**
128	 * Loads registered template engines, if none is registered it will use Phalcon\Mvc\View\Engine\Php
129	 *
130	 * @return array
131	 */
132	protected function _loadTemplateEngines()
133	{
134		var engines, dependencyInjector, registeredEngines, arguments, extension,
135			engineService, engineObject;
136
137		/**
138		 * If the engines aren't initialized 'engines' is false
139		 */
140		let engines = this->_engines;
141		if engines === false {
142
143			let dependencyInjector = this->_dependencyInjector;
144
145			let engines = [];
146
147			let registeredEngines = this->_registeredEngines;
148			if typeof registeredEngines != "array" {
149
150				/**
151				 * We use Phalcon\Mvc\View\Engine\Php as default
152				 * Use .phtml as extension for the PHP engine
153				 */
154				let engines[".phtml"] = new PhpEngine(this, dependencyInjector);
155
156			} else {
157
158				if typeof dependencyInjector != "object" {
159					throw new Exception("A dependency injector container is required to obtain the application services");
160				}
161
162				/**
163				 * Arguments for instantiated engines
164				 */
165				let arguments = [this, dependencyInjector];
166
167				for extension, engineService in registeredEngines {
168
169					if typeof engineService == "object" {
170						/**
171						 * Engine can be a closure
172						 */
173						if engineService instanceof \Closure {
174							let engineObject = call_user_func_array(engineService, arguments);
175						} else {
176							let engineObject = engineService;
177						}
178					} else {
179						/**
180						 * Engine can be a string representing a service in the DI
181						 */
182						if typeof engineService == "string" {
183							let engineObject = dependencyInjector->getShared(engineService, arguments);
184						} else {
185							throw new Exception("Invalid template engine registration for extension: " . extension);
186						}
187					}
188
189					let engines[extension] = engineObject;
190				}
191			}
192
193			let this->_engines = engines;
194		} else {
195			let engines = this->_engines;
196		}
197
198		return engines;
199	}
200
201	/**
202	 * Tries to render the view with every engine registered in the component
203	 *
204	 * @param string path
205	 * @param array  params
206	 */
207	protected final function _internalRender(string! path, params)
208	{
209		var eventsManager, notExists, engines, extension, engine, mustClean, viewEnginePath, viewsDirPath;
210
211		let eventsManager = this->_eventsManager;
212
213		if typeof eventsManager == "object" {
214			let this->_activeRenderPath = path;
215		}
216
217		/**
218		 * Call beforeRender if there is an events manager
219		 */
220		if typeof eventsManager == "object" {
221			if eventsManager->fire("view:beforeRender", this) === false {
222				return null;
223			}
224		}
225
226		let notExists = true,
227			mustClean = true;
228
229		let viewsDirPath =  this->_viewsDir . path;
230
231		/**
232		 * Load the template engines
233		 */
234		let engines = this->_loadTemplateEngines();
235
236		/**
237		 * Views are rendered in each engine
238		 */
239		for extension, engine in engines {
240
241			if file_exists(viewsDirPath . extension) {
242				let viewEnginePath = viewsDirPath . extension;
243			} else {
244
245				/**
246				 * if passed filename with engine extension
247				 */
248				if extension && substr(viewsDirPath, -strlen(extension)) == extension && file_exists(viewsDirPath) {
249					let viewEnginePath = viewsDirPath;
250				} else {
251					let viewEnginePath = "";
252				}
253			}
254
255			if viewEnginePath {
256
257				/**
258				 * Call beforeRenderView if there is an events manager available
259				 */
260				if typeof eventsManager == "object" {
261					if eventsManager->fire("view:beforeRenderView", this, viewEnginePath) === false {
262						continue;
263					}
264				}
265
266				engine->render(viewEnginePath, params, mustClean);
267
268				/**
269				 * Call afterRenderView if there is an events manager available
270				 */
271				let notExists = false;
272				if typeof eventsManager == "object" {
273					eventsManager->fire("view:afterRenderView", this);
274				}
275				break;
276			}
277		}
278
279		/**
280		 * Always throw an exception if the view does not exist
281		 */
282		if notExists === true {
283			throw new Exception("View '" . viewsDirPath . "' was not found in the views directory");
284		}
285
286		/**
287		 * Call afterRender event
288		 */
289		if typeof eventsManager == "object" {
290			eventsManager->fire("view:afterRender", this);
291		}
292
293	}
294
295	/**
296	 * Renders a view
297	 *
298	 * @param  string path
299	 * @param  array  params
300	 */
301	public function render(string! path, params = null) -> string
302	{
303		var cache, key, lifetime, cacheOptions, content, viewParams, mergedParams;
304
305		/**
306		 * Create/Get a cache
307		 */
308		let cache = this->getCache();
309
310		if typeof cache == "object" {
311
312			/**
313			 * Check if the cache is started, the first time a cache is started we start the cache
314			 */
315			if cache->isStarted() === false {
316
317				let key = null, lifetime = null;
318
319				/**
320				 * Check if the user has defined a different options to the default
321				 */
322				let cacheOptions = this->_cacheOptions;
323				if typeof cacheOptions == "array" {
324					fetch key, cacheOptions["key"];
325					fetch lifetime, cacheOptions["lifetime"];
326				}
327
328				/**
329				 * If a cache key is not set we create one using a md5
330				 */
331				if key === null {
332					let key = md5(path);
333				}
334
335				/**
336				 * We start the cache using the key set
337				 */
338				let content = cache->start(key, lifetime);
339				if content !== null {
340					let this->_content = content;
341					return content;
342				}
343			}
344
345		}
346
347		/**
348		 * Create a virtual symbol table
349		 */
350		create_symbol_table();
351
352		ob_start();
353
354		let viewParams = this->_viewParams;
355
356		/**
357		 * Merge parameters
358		 */
359		if typeof params == "array" {
360			if typeof viewParams == "array" {
361				let mergedParams = array_merge(viewParams, params);
362			} else {
363				let mergedParams = params;
364			}
365		} else {
366			let mergedParams = viewParams;
367		}
368
369		/**
370		 * internalRender is also reused by partials
371		 */
372		this->_internalRender(path, mergedParams);
373
374		/**
375		 * Store the data in output into the cache
376		 */
377		if typeof cache == "object" {
378			if cache->isStarted() && cache->isFresh() {
379				cache->save();
380			} else {
381				cache->stop();
382			}
383		}
384
385		ob_end_clean();
386
387		return this->_content;
388	}
389
390	/**
391	 * Renders a partial view
392	 *
393	 * <code>
394	 * // Show a partial inside another view
395	 * $this->partial("shared/footer");
396	 * </code>
397	 *
398	 * <code>
399	 * // Show a partial inside another view with parameters
400	 * $this->partial(
401	 *     "shared/footer",
402	 *     [
403	 *         "content" => $html,
404	 *     ]
405	 * );
406	 * </code>
407	 */
408	public function partial(string! partialPath, var params = null)
409	{
410		var viewParams, mergedParams;
411
412		/**
413		 * Start output buffering
414		 */
415		ob_start();
416
417		/**
418		 * If the developer pass an array of variables we create a new virtual symbol table
419		 */
420		if typeof params == "array" {
421
422			let viewParams = this->_viewParams;
423
424			/**
425			 * Merge or assign the new params as parameters
426			 */
427			if typeof viewParams == "array" {
428				let mergedParams = array_merge(viewParams, params);
429			} else {
430				let mergedParams = params;
431			}
432
433			/**
434			 * Create a virtual symbol table
435			 */
436			create_symbol_table();
437
438		} else {
439			let mergedParams = params;
440		}
441
442		/**
443		 * Call engine render, this checks in every registered engine for the partial
444		 */
445		this->_internalRender(partialPath, mergedParams);
446
447		/**
448		 * Now we need to restore the original view parameters
449		 */
450		if typeof params == "array" {
451			/**
452			 * Restore the original view params
453			 */
454			let this->_viewParams = viewParams;
455		}
456
457		ob_end_clean();
458
459		/**
460		 * Content is output to the parent view
461		 */
462		echo this->_content;
463	}
464
465	/**
466	 * Sets the cache options
467	 */
468	public function setCacheOptions(array options) -> <Simple>
469	{
470		let this->_cacheOptions = options;
471		return this;
472	}
473
474	/**
475	 * Returns the cache options
476	 *
477	 * @return array
478	 */
479	public function getCacheOptions()
480	{
481		return this->_cacheOptions;
482	}
483
484	/**
485	 * Create a Phalcon\Cache based on the internal cache options
486	 */
487	protected function _createCache() -> <BackendInterface>
488	{
489		var dependencyInjector, cacheService, cacheOptions, viewCache;
490
491		let dependencyInjector = this->_dependencyInjector;
492		if typeof dependencyInjector != "object" {
493			throw new Exception("A dependency injector container is required to obtain the view cache services");
494		}
495
496		let cacheService = "viewCache";
497
498		let cacheOptions = this->_cacheOptions;
499		if typeof cacheOptions == "array" {
500			if isset cacheOptions["service"] {
501				fetch cacheService, cacheOptions["service"];
502			}
503		}
504
505		/**
506		 * The injected service must be an object
507		 */
508		let viewCache = <BackendInterface> dependencyInjector->getShared(cacheService);
509		if typeof viewCache != "object" {
510			throw new Exception("The injected caching service is invalid");
511		}
512
513		return viewCache;
514	}
515
516	/**
517	 * Returns the cache instance used to cache
518	 */
519	public function getCache() -> <BackendInterface>
520	{
521		if this->_cache && typeof this->_cache != "object" {
522			let this->_cache = this->_createCache();
523		}
524
525		return this->_cache;
526	}
527
528	/**
529	 * Cache the actual view render to certain level
530	 *
531	 *<code>
532	 * $this->view->cache(
533	 *     [
534	 *         "key"      => "my-key",
535	 *         "lifetime" => 86400,
536	 *     ]
537	 * );
538	 *</code>
539	 */
540	public function cache(var options = true) -> <Simple>
541	{
542		if typeof options == "array" {
543			let this->_cache = true,
544				this->_cacheOptions = options;
545		} else {
546			if options {
547				let this->_cache = true;
548			} else {
549				let this->_cache = false;
550			}
551		}
552		return this;
553	}
554
555	/**
556	 * Adds parameters to views (alias of setVar)
557	 *
558	 *<code>
559	 * $this->view->setParamToView("products", $products);
560	 *</code>
561	 */
562	public function setParamToView(string! key, var value) -> <Simple>
563	{
564		let this->_viewParams[key] = value;
565		return this;
566	}
567
568	/**
569	 * Set all the render params
570	 *
571	 *<code>
572	 * $this->view->setVars(
573	 *     [
574	 *         "products" => $products,
575	 *     ]
576	 * );
577	 *</code>
578	 */
579	public function setVars(array! params, boolean merge = true) -> <Simple>
580	{
581		if merge && typeof this->_viewParams == "array" {
582			let this->_viewParams = array_merge(this->_viewParams, params);
583		} else {
584			let this->_viewParams = params;
585		}
586
587		return this;
588	}
589
590	/**
591	 * Set a single view parameter
592	 *
593	 *<code>
594	 * $this->view->setVar("products", $products);
595	 *</code>
596	 */
597	public function setVar(string! key, var value) -> <Simple>
598	{
599		let this->_viewParams[key] = value;
600		return this;
601	}
602
603	/**
604	 * Returns a parameter previously set in the view
605	 */
606	public function getVar(string! key) -> var | null
607	{
608		var	value;
609		if fetch value, this->_viewParams[key] {
610			return value;
611		}
612		return null;
613	}
614
615	/**
616	 * Returns parameters to views
617	 *
618	 * @return array
619	 */
620	public function getParamsToView() -> array
621	{
622		return this->_viewParams;
623	}
624
625	/**
626	 * Externally sets the view content
627	 *
628	 *<code>
629	 * $this->view->setContent("<h1>hello</h1>");
630	 *</code>
631	 */
632	public function setContent(string! content) -> <Simple>
633	{
634		let this->_content = content;
635		return this;
636	}
637
638	/**
639	 * Returns cached output from another view stage
640	 */
641	public function getContent() -> string
642	{
643		return this->_content;
644	}
645
646	/**
647	 * Returns the path of the view that is currently rendered
648	 *
649	 * @return string
650	 */
651	public function getActiveRenderPath()
652	{
653		return this->_activeRenderPath;
654	}
655
656	/**
657	 * Magic method to pass variables to the views
658	 *
659	 *<code>
660	 * $this->view->products = $products;
661	 *</code>
662	 */
663	public function __set(string! key, var value)
664	{
665		let this->_viewParams[key] = value;
666	}
667
668	/**
669	 * Magic method to retrieve a variable passed to the view
670	 *
671	 *<code>
672	 * echo $this->view->products;
673	 *</code>
674	 */
675	public function __get(string! key) -> var | null
676	{
677		var value;
678		if fetch value, this->_viewParams[key] {
679			return value;
680		}
681
682		return null;
683	}
684}
685