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