View.php 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 0.10.0
  14. * @license https://www.opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\View;
  17. use Cake\Cache\Cache;
  18. use Cake\Core\App;
  19. use Cake\Core\InstanceConfigTrait;
  20. use Cake\Core\Plugin;
  21. use Cake\Event\EventDispatcherInterface;
  22. use Cake\Event\EventDispatcherTrait;
  23. use Cake\Event\EventManager;
  24. use Cake\Http\Response;
  25. use Cake\Http\ServerRequest;
  26. use Cake\Log\LogTrait;
  27. use Cake\Routing\Router;
  28. use Cake\Utility\Inflector;
  29. use Cake\View\Exception\MissingElementException;
  30. use Cake\View\Exception\MissingLayoutException;
  31. use Cake\View\Exception\MissingTemplateException;
  32. use InvalidArgumentException;
  33. use LogicException;
  34. use RuntimeException;
  35. /**
  36. * View, the V in the MVC triad. View interacts with Helpers and view variables passed
  37. * in from the controller to render the results of the controller action. Often this is HTML,
  38. * but can also take the form of JSON, XML, PDF's or streaming files.
  39. *
  40. * CakePHP uses a two-step-view pattern. This means that the template content is rendered first,
  41. * and then inserted into the selected layout. This also means you can pass data from the template to the
  42. * layout using `$this->set()`
  43. *
  44. * View class supports using plugins as themes. You can set
  45. *
  46. * ```
  47. * public function beforeRender(\Cake\Event\EventInterface $event)
  48. * {
  49. * $this->viewBuilder()->setTheme('SuperHot');
  50. * }
  51. * ```
  52. *
  53. * in your Controller to use plugin `SuperHot` as a theme. Eg. If current action
  54. * is PostsController::index() then View class will look for template file
  55. * `plugins/SuperHot/templates/Posts/index.php`. If a theme template
  56. * is not found for the current action the default app template file is used.
  57. *
  58. * @property \Cake\View\Helper\BreadcrumbsHelper $Breadcrumbs
  59. * @property \Cake\View\Helper\FlashHelper $Flash
  60. * @property \Cake\View\Helper\FormHelper $Form
  61. * @property \Cake\View\Helper\HtmlHelper $Html
  62. * @property \Cake\View\Helper\NumberHelper $Number
  63. * @property \Cake\View\Helper\PaginatorHelper $Paginator
  64. * @property \Cake\View\Helper\TextHelper $Text
  65. * @property \Cake\View\Helper\TimeHelper $Time
  66. * @property \Cake\View\Helper\UrlHelper $Url
  67. * @property \Cake\View\ViewBlock $Blocks
  68. */
  69. class View implements EventDispatcherInterface
  70. {
  71. use CellTrait {
  72. cell as public;
  73. }
  74. use EventDispatcherTrait;
  75. use InstanceConfigTrait {
  76. getConfig as private _getConfig;
  77. }
  78. use LogTrait;
  79. /**
  80. * Helpers collection
  81. *
  82. * @var \Cake\View\HelperRegistry
  83. */
  84. protected $_helpers;
  85. /**
  86. * ViewBlock instance.
  87. *
  88. * @var \Cake\View\ViewBlock
  89. */
  90. protected $Blocks;
  91. /**
  92. * The name of the plugin.
  93. *
  94. * @var string|null
  95. */
  96. protected $plugin;
  97. /**
  98. * Name of the controller that created the View if any.
  99. *
  100. * @var string
  101. */
  102. protected $name = '';
  103. /**
  104. * An array of names of built-in helpers to include.
  105. *
  106. * @var array
  107. */
  108. protected $helpers = [];
  109. /**
  110. * The name of the subfolder containing templates for this View.
  111. *
  112. * @var string
  113. */
  114. protected $templatePath;
  115. /**
  116. * The name of the template file to render. The name specified
  117. * is the filename in /templates/<SubFolder> without the .php extension.
  118. *
  119. * @var string
  120. */
  121. protected $template;
  122. /**
  123. * The name of the layout file to render the template inside of. The name specified
  124. * is the filename of the layout in /templates/Layout without the .php
  125. * extension.
  126. *
  127. * @var string
  128. */
  129. protected $layout = 'default';
  130. /**
  131. * The name of the layouts subfolder containing layouts for this View.
  132. *
  133. * @var string
  134. */
  135. protected $layoutPath = '';
  136. /**
  137. * Turns on or off CakePHP's conventional mode of applying layout files. On by default.
  138. * Setting to off means that layouts will not be automatically applied to rendered templates.
  139. *
  140. * @var bool
  141. */
  142. protected $autoLayout = true;
  143. /**
  144. * An array of variables
  145. *
  146. * @var array
  147. */
  148. protected $viewVars = [];
  149. /**
  150. * File extension. Defaults to ".php".
  151. *
  152. * @var string
  153. */
  154. protected $_ext = '.php';
  155. /**
  156. * Sub-directory for this template file. This is often used for extension based routing.
  157. * Eg. With an `xml` extension, $subDir would be `xml/`
  158. *
  159. * @var string
  160. */
  161. protected $subDir = '';
  162. /**
  163. * The view theme to use.
  164. *
  165. * @var string|null
  166. */
  167. protected $theme;
  168. /**
  169. * An instance of a \Cake\Http\ServerRequest object that contains information about the current request.
  170. * This object contains all the information about a request and several methods for reading
  171. * additional information about the request.
  172. *
  173. * @var \Cake\Http\ServerRequest
  174. */
  175. protected $request;
  176. /**
  177. * Reference to the Response object
  178. *
  179. * @var \Cake\Http\Response
  180. */
  181. protected $response;
  182. /**
  183. * The Cache configuration View will use to store cached elements. Changing this will change
  184. * the default configuration elements are stored under. You can also choose a cache config
  185. * per element.
  186. *
  187. * @var string
  188. * @see \Cake\View\View::element()
  189. */
  190. protected $elementCache = 'default';
  191. /**
  192. * List of variables to collect from the associated controller.
  193. *
  194. * @var string[]
  195. */
  196. protected $_passedVars = [
  197. 'viewVars', 'autoLayout', 'helpers', 'template', 'layout', 'name', 'theme',
  198. 'layoutPath', 'templatePath', 'plugin',
  199. ];
  200. /**
  201. * Default custom config options.
  202. *
  203. * @var string[]
  204. */
  205. protected $_defaultConfig = [];
  206. /**
  207. * Holds an array of paths.
  208. *
  209. * @var string[]
  210. */
  211. protected $_paths = [];
  212. /**
  213. * Holds an array of plugin paths.
  214. *
  215. * @var string[][]
  216. */
  217. protected $_pathsForPlugin = [];
  218. /**
  219. * The names of views and their parents used with View::extend();
  220. *
  221. * @var string[]
  222. */
  223. protected $_parents = [];
  224. /**
  225. * The currently rendering view file. Used for resolving parent files.
  226. *
  227. * @var string
  228. */
  229. protected $_current;
  230. /**
  231. * Currently rendering an element. Used for finding parent fragments
  232. * for elements.
  233. *
  234. * @var string
  235. */
  236. protected $_currentType = '';
  237. /**
  238. * Content stack, used for nested templates that all use View::extend();
  239. *
  240. * @var string[]
  241. */
  242. protected $_stack = [];
  243. /**
  244. * ViewBlock class.
  245. *
  246. * @var string
  247. */
  248. protected $_viewBlockClass = ViewBlock::class;
  249. /**
  250. * Constant for view file type 'template'.
  251. *
  252. * @var string
  253. */
  254. public const TYPE_TEMPLATE = 'template';
  255. /**
  256. * Constant for view file type 'element'
  257. *
  258. * @var string
  259. */
  260. public const TYPE_ELEMENT = 'element';
  261. /**
  262. * Constant for view file type 'layout'
  263. *
  264. * @var string
  265. */
  266. public const TYPE_LAYOUT = 'layout';
  267. /**
  268. * Constant for type used for App::path().
  269. *
  270. * @var string
  271. */
  272. public const NAME_TEMPLATE = 'templates';
  273. /**
  274. * Constant for folder name containing files for overriding plugin templates.
  275. *
  276. * @var string
  277. */
  278. public const PLUGIN_TEMPLATE_FOLDER = 'plugin';
  279. /**
  280. * Constructor
  281. *
  282. * @param \Cake\Http\ServerRequest|null $request Request instance.
  283. * @param \Cake\Http\Response|null $response Response instance.
  284. * @param \Cake\Event\EventManager|null $eventManager Event manager instance.
  285. * @param array $viewOptions View options. See View::$_passedVars for list of
  286. * options which get set as class properties.
  287. */
  288. public function __construct(
  289. ?ServerRequest $request = null,
  290. ?Response $response = null,
  291. ?EventManager $eventManager = null,
  292. array $viewOptions = []
  293. ) {
  294. foreach ($this->_passedVars as $var) {
  295. if (isset($viewOptions[$var])) {
  296. $this->{$var} = $viewOptions[$var];
  297. }
  298. }
  299. $this->setConfig(array_diff_key(
  300. $viewOptions,
  301. array_flip($this->_passedVars)
  302. ));
  303. if ($eventManager !== null) {
  304. $this->setEventManager($eventManager);
  305. }
  306. if ($request === null) {
  307. $request = Router::getRequest() ?: new ServerRequest(['base' => '', 'url' => '', 'webroot' => '/']);
  308. }
  309. $this->request = $request;
  310. $this->response = $response ?: new Response();
  311. $this->Blocks = new $this->_viewBlockClass();
  312. $this->initialize();
  313. $this->loadHelpers();
  314. }
  315. /**
  316. * Initialization hook method.
  317. *
  318. * Properties like $helpers etc. cannot be initialized statically in your custom
  319. * view class as they are overwritten by values from controller in constructor.
  320. * So this method allows you to manipulate them as required after view instance
  321. * is constructed.
  322. *
  323. * @return void
  324. */
  325. public function initialize(): void
  326. {
  327. }
  328. /**
  329. * Gets the request instance.
  330. *
  331. * @return \Cake\Http\ServerRequest
  332. * @since 3.7.0
  333. */
  334. public function getRequest(): ServerRequest
  335. {
  336. return $this->request;
  337. }
  338. /**
  339. * Sets the request objects and configures a number of controller properties
  340. * based on the contents of the request. The properties that get set are:
  341. *
  342. * - $this->request - To the $request parameter
  343. * - $this->plugin - To the value returned by $request->getParam('plugin')
  344. *
  345. * @param \Cake\Http\ServerRequest $request Request instance.
  346. * @return $this
  347. */
  348. public function setRequest(ServerRequest $request)
  349. {
  350. $this->request = $request;
  351. $this->plugin = $request->getParam('plugin');
  352. return $this;
  353. }
  354. /**
  355. * Gets the response instance.
  356. *
  357. * @return \Cake\Http\Response
  358. */
  359. public function getResponse(): Response
  360. {
  361. return $this->response;
  362. }
  363. /**
  364. * Sets the response instance.
  365. *
  366. * @param \Cake\Http\Response $response Response instance.
  367. * @return $this
  368. */
  369. public function setResponse(Response $response)
  370. {
  371. $this->response = $response;
  372. return $this;
  373. }
  374. /**
  375. * Get path for templates files.
  376. *
  377. * @return string
  378. */
  379. public function getTemplatePath(): string
  380. {
  381. return $this->templatePath;
  382. }
  383. /**
  384. * Set path for templates files.
  385. *
  386. * @param string $path Path for template files.
  387. * @return $this
  388. */
  389. public function setTemplatePath(string $path)
  390. {
  391. $this->templatePath = $path;
  392. return $this;
  393. }
  394. /**
  395. * Get path for layout files.
  396. *
  397. * @return string
  398. */
  399. public function getLayoutPath(): string
  400. {
  401. return $this->layoutPath;
  402. }
  403. /**
  404. * Set path for layout files.
  405. *
  406. * @param string $path Path for layout files.
  407. * @return $this
  408. */
  409. public function setLayoutPath(string $path)
  410. {
  411. $this->layoutPath = $path;
  412. return $this;
  413. }
  414. /**
  415. * Returns if CakePHP's conventional mode of applying layout files is enabled.
  416. * Disabled means that layouts will not be automatically applied to rendered views.
  417. *
  418. * @return bool
  419. */
  420. public function isAutoLayoutEnabled(): bool
  421. {
  422. return $this->autoLayout;
  423. }
  424. /**
  425. * Turns on or off CakePHP's conventional mode of applying layout files.
  426. * On by default. Setting to off means that layouts will not be
  427. * automatically applied to rendered views.
  428. *
  429. * @param bool $enable Boolean to turn on/off.
  430. * @return $this
  431. */
  432. public function enableAutoLayout(bool $enable = true)
  433. {
  434. $this->autoLayout = $enable;
  435. return $this;
  436. }
  437. /**
  438. * Turns off CakePHP's conventional mode of applying layout files.
  439. * Layouts will not be automatically applied to rendered views.
  440. *
  441. * @return $this
  442. */
  443. public function disableAutoLayout()
  444. {
  445. $this->autoLayout = false;
  446. return $this;
  447. }
  448. /**
  449. * Get the current view theme.
  450. *
  451. * @return string|null
  452. */
  453. public function getTheme(): ?string
  454. {
  455. return $this->theme;
  456. }
  457. /**
  458. * Set the view theme to use.
  459. *
  460. * @param string|null $theme Theme name.
  461. * @return $this
  462. */
  463. public function setTheme(?string $theme)
  464. {
  465. $this->theme = $theme;
  466. return $this;
  467. }
  468. /**
  469. * Get the name of the template file to render. The name specified is the
  470. * filename in /templates/<SubFolder> without the .php extension.
  471. *
  472. * @return string
  473. */
  474. public function getTemplate(): string
  475. {
  476. return $this->template;
  477. }
  478. /**
  479. * Set the name of the template file to render. The name specified is the
  480. * filename in /templates/<SubFolder> without the .php extension.
  481. *
  482. * @param string $name Template file name to set.
  483. * @return $this
  484. */
  485. public function setTemplate(string $name)
  486. {
  487. $this->template = $name;
  488. return $this;
  489. }
  490. /**
  491. * Get the name of the layout file to render the template inside of.
  492. * The name specified is the filename of the layout in /templates/Layout
  493. * without the .php extension.
  494. *
  495. * @return string
  496. */
  497. public function getLayout(): string
  498. {
  499. return $this->layout;
  500. }
  501. /**
  502. * Set the name of the layout file to render the template inside of.
  503. * The name specified is the filename of the layout in /templates/Layout
  504. * without the .php extension.
  505. *
  506. * @param string $name Layout file name to set.
  507. * @return $this
  508. */
  509. public function setLayout(string $name)
  510. {
  511. $this->layout = $name;
  512. return $this;
  513. }
  514. /**
  515. * Get config value.
  516. *
  517. * Currently if config is not set it fallbacks to checking corresponding
  518. * view var with underscore prefix. Using underscore prefixed special view
  519. * vars is deprecated and this fallback will be removed in CakePHP 4.1.0.
  520. *
  521. * @param string|null $key The key to get or null for the whole config.
  522. * @param mixed $default The return value when the key does not exist.
  523. * @return mixed Config value being read.
  524. * @psalm-suppress PossiblyNullArgument
  525. */
  526. public function getConfig(?string $key = null, $default = null)
  527. {
  528. $value = $this->_getConfig($key);
  529. if ($value !== null) {
  530. return $value;
  531. }
  532. if (isset($this->viewVars["_{$key}"])) {
  533. deprecationWarning(sprintf(
  534. 'Setting special view var "_%s" is deprecated. Use ViewBuilder::setOption(\'%s\', $value) instead.',
  535. $key,
  536. $key
  537. ));
  538. return $this->viewVars["_{$key}"];
  539. }
  540. return $default;
  541. }
  542. /**
  543. * Renders a piece of PHP with provided parameters and returns HTML, XML, or any other string.
  544. *
  545. * This realizes the concept of Elements, (or "partial layouts") and the $params array is used to send
  546. * data to be used in the element. Elements can be cached improving performance by using the `cache` option.
  547. *
  548. * @param string $name Name of template file in the /templates/Element/ folder,
  549. * or `MyPlugin.template` to use the template element from MyPlugin. If the element
  550. * is not found in the plugin, the normal view path cascade will be searched.
  551. * @param array $data Array of data to be made available to the rendered view (i.e. the Element)
  552. * @param array $options Array of options. Possible keys are:
  553. * - `cache` - Can either be `true`, to enable caching using the config in View::$elementCache. Or an array
  554. * If an array, the following keys can be used:
  555. * - `config` - Used to store the cached element in a custom cache configuration.
  556. * - `key` - Used to define the key used in the Cache::write(). It will be prefixed with `element_`
  557. * - `callbacks` - Set to true to fire beforeRender and afterRender helper callbacks for this element.
  558. * Defaults to false.
  559. * - `ignoreMissing` - Used to allow missing elements. Set to true to not throw exceptions.
  560. * - `plugin` - setting to false will force to use the application's element from plugin templates, when the
  561. * plugin has element with same name. Defaults to true
  562. * @return string Rendered Element
  563. * @throws \Cake\View\Exception\MissingElementException When an element is missing and `ignoreMissing`
  564. * is false.
  565. */
  566. public function element(string $name, array $data = [], array $options = []): string
  567. {
  568. $options += ['callbacks' => false, 'cache' => null, 'plugin' => null];
  569. if (isset($options['cache'])) {
  570. $options['cache'] = $this->_elementCache($name, $data, $options);
  571. }
  572. $pluginCheck = $options['plugin'] !== false;
  573. $file = $this->_getElementFileName($name, $pluginCheck);
  574. if ($file && $options['cache']) {
  575. return $this->cache(function () use ($file, $data, $options): void {
  576. echo $this->_renderElement($file, $data, $options);
  577. }, $options['cache']);
  578. }
  579. if ($file) {
  580. return $this->_renderElement($file, $data, $options);
  581. }
  582. if (empty($options['ignoreMissing'])) {
  583. [$plugin] = $this->pluginSplit($name, $pluginCheck);
  584. throw new MissingElementException($name, $this->_paths($plugin));
  585. }
  586. return '';
  587. }
  588. /**
  589. * Create a cached block of view logic.
  590. *
  591. * This allows you to cache a block of view output into the cache
  592. * defined in `elementCache`.
  593. *
  594. * This method will attempt to read the cache first. If the cache
  595. * is empty, the $block will be run and the output stored.
  596. *
  597. * @param callable $block The block of code that you want to cache the output of.
  598. * @param array $options The options defining the cache key etc.
  599. * @return string The rendered content.
  600. * @throws \RuntimeException When $options is lacking a 'key' option.
  601. */
  602. public function cache(callable $block, array $options = []): string
  603. {
  604. $options += ['key' => '', 'config' => $this->elementCache];
  605. if (empty($options['key'])) {
  606. throw new RuntimeException('Cannot cache content with an empty key');
  607. }
  608. $result = Cache::read($options['key'], $options['config']);
  609. if ($result) {
  610. return $result;
  611. }
  612. ob_start();
  613. $block();
  614. $result = ob_get_clean();
  615. Cache::write($options['key'], $result, $options['config']);
  616. return $result;
  617. }
  618. /**
  619. * Checks if an element exists
  620. *
  621. * @param string $name Name of template file in the /templates/Element/ folder,
  622. * or `MyPlugin.template` to check the template element from MyPlugin. If the element
  623. * is not found in the plugin, the normal view path cascade will be searched.
  624. * @return bool Success
  625. */
  626. public function elementExists(string $name): bool
  627. {
  628. return (bool)$this->_getElementFileName($name);
  629. }
  630. /**
  631. * Renders view for given template file and layout.
  632. *
  633. * Render triggers helper callbacks, which are fired before and after the template are rendered,
  634. * as well as before and after the layout. The helper callbacks are called:
  635. *
  636. * - `beforeRender`
  637. * - `afterRender`
  638. * - `beforeLayout`
  639. * - `afterLayout`
  640. *
  641. * If View::$autoLayout is set to `false`, the template will be returned bare.
  642. *
  643. * Template and layout names can point to plugin templates/layouts. Using the `Plugin.template` syntax
  644. * a plugin template/layout can be used instead of the app ones. If the chosen plugin is not found
  645. * the template will be located along the regular view path cascade.
  646. *
  647. * @param string|null $template Name of template file to use
  648. * @param string|false|null $layout Layout to use. False to disable.
  649. * @return string Rendered content.
  650. * @throws \Cake\Core\Exception\Exception If there is an error in the view.
  651. * @triggers View.beforeRender $this, [$templateFileName]
  652. * @triggers View.afterRender $this, [$templateFileName]
  653. */
  654. public function render(?string $template = null, $layout = null): string
  655. {
  656. $defaultLayout = null;
  657. $defaultAutoLayout = null;
  658. if ($layout === false) {
  659. $defaultAutoLayout = $this->autoLayout;
  660. $this->autoLayout = false;
  661. } elseif ($layout !== null) {
  662. $defaultLayout = $this->layout;
  663. $this->layout = $layout;
  664. }
  665. $templateFileName = $this->_getTemplateFileName($template);
  666. $this->_currentType = static::TYPE_TEMPLATE;
  667. $this->dispatchEvent('View.beforeRender', [$templateFileName]);
  668. $this->Blocks->set('content', $this->_render($templateFileName));
  669. $this->dispatchEvent('View.afterRender', [$templateFileName]);
  670. if ($this->autoLayout) {
  671. if (empty($this->layout)) {
  672. throw new RuntimeException(
  673. 'View::$layout must be a non-empty string.' .
  674. 'To disable layout rendering use method View::disableAutoLayout() instead.'
  675. );
  676. }
  677. $this->Blocks->set('content', $this->renderLayout('', $this->layout));
  678. }
  679. if ($layout !== null) {
  680. /** @var string $defaultLayout */
  681. $this->layout = $defaultLayout;
  682. }
  683. if ($defaultAutoLayout !== null) {
  684. $this->autoLayout = $defaultAutoLayout;
  685. }
  686. return $this->Blocks->get('content');
  687. }
  688. /**
  689. * Renders a layout. Returns output from _render().
  690. *
  691. * Several variables are created for use in layout.
  692. *
  693. * @param string $content Content to render in a template, wrapped by the surrounding layout.
  694. * @param string|null $layout Layout name
  695. * @return string Rendered output.
  696. * @throws \Cake\Core\Exception\Exception if there is an error in the view.
  697. * @triggers View.beforeLayout $this, [$layoutFileName]
  698. * @triggers View.afterLayout $this, [$layoutFileName]
  699. */
  700. public function renderLayout(string $content, ?string $layout = null): string
  701. {
  702. $layoutFileName = $this->_getLayoutFileName($layout);
  703. if (!empty($content)) {
  704. $this->Blocks->set('content', $content);
  705. }
  706. $this->dispatchEvent('View.beforeLayout', [$layoutFileName]);
  707. $title = $this->Blocks->get('title');
  708. if ($title === '') {
  709. $title = Inflector::humanize((string)$this->templatePath);
  710. $this->Blocks->set('title', $title);
  711. }
  712. $this->_currentType = static::TYPE_LAYOUT;
  713. $this->Blocks->set('content', $this->_render($layoutFileName));
  714. $this->dispatchEvent('View.afterLayout', [$layoutFileName]);
  715. return $this->Blocks->get('content');
  716. }
  717. /**
  718. * Returns a list of variables available in the current View context
  719. *
  720. * @return string[] Array of the set view variable names.
  721. */
  722. public function getVars(): array
  723. {
  724. return array_keys($this->viewVars);
  725. }
  726. /**
  727. * Returns the contents of the given View variable.
  728. *
  729. * @param string $var The view var you want the contents of.
  730. * @param mixed $default The default/fallback content of $var.
  731. * @return mixed The content of the named var if its set, otherwise $default.
  732. */
  733. public function get(string $var, $default = null)
  734. {
  735. if (!isset($this->viewVars[$var])) {
  736. return $default;
  737. }
  738. return $this->viewVars[$var];
  739. }
  740. /**
  741. * Saves a variable or an associative array of variables for use inside a template.
  742. *
  743. * @param string|array $name A string or an array of data.
  744. * @param mixed $value Value in case $name is a string (which then works as the key).
  745. * Unused if $name is an associative array, otherwise serves as the values to $name's keys.
  746. * @return $this
  747. * @throws \RuntimeException If the array combine operation failed.
  748. */
  749. public function set($name, $value = null)
  750. {
  751. if (is_array($name)) {
  752. if (is_array($value)) {
  753. $data = array_combine($name, $value);
  754. if ($data === false) {
  755. throw new RuntimeException(
  756. 'Invalid data provided for array_combine() to work: Both $name and $value require same count.'
  757. );
  758. }
  759. } else {
  760. $data = $name;
  761. }
  762. } else {
  763. $data = [$name => $value];
  764. }
  765. $this->viewVars = $data + $this->viewVars;
  766. return $this;
  767. }
  768. /**
  769. * Get the names of all the existing blocks.
  770. *
  771. * @return string[] An array containing the blocks.
  772. * @see \Cake\View\ViewBlock::keys()
  773. */
  774. public function blocks(): array
  775. {
  776. return $this->Blocks->keys();
  777. }
  778. /**
  779. * Start capturing output for a 'block'
  780. *
  781. * You can use start on a block multiple times to
  782. * append or prepend content in a capture mode.
  783. *
  784. * ```
  785. * // Append content to an existing block.
  786. * $this->start('content');
  787. * echo $this->fetch('content');
  788. * echo 'Some new content';
  789. * $this->end();
  790. *
  791. * // Prepend content to an existing block
  792. * $this->start('content');
  793. * echo 'Some new content';
  794. * echo $this->fetch('content');
  795. * $this->end();
  796. * ```
  797. *
  798. * @param string $name The name of the block to capture for.
  799. * @return $this
  800. * @see \Cake\View\ViewBlock::start()
  801. */
  802. public function start(string $name)
  803. {
  804. $this->Blocks->start($name);
  805. return $this;
  806. }
  807. /**
  808. * Append to an existing or new block.
  809. *
  810. * Appending to a new block will create the block.
  811. *
  812. * @param string $name Name of the block
  813. * @param mixed $value The content for the block. Value will be type cast
  814. * to string.
  815. * @return $this
  816. * @see \Cake\View\ViewBlock::concat()
  817. */
  818. public function append(string $name, $value = null)
  819. {
  820. $this->Blocks->concat($name, $value);
  821. return $this;
  822. }
  823. /**
  824. * Prepend to an existing or new block.
  825. *
  826. * Prepending to a new block will create the block.
  827. *
  828. * @param string $name Name of the block
  829. * @param mixed $value The content for the block. Value will be type cast
  830. * to string.
  831. * @return $this
  832. * @see \Cake\View\ViewBlock::concat()
  833. */
  834. public function prepend(string $name, $value)
  835. {
  836. $this->Blocks->concat($name, $value, ViewBlock::PREPEND);
  837. return $this;
  838. }
  839. /**
  840. * Set the content for a block. This will overwrite any
  841. * existing content.
  842. *
  843. * @param string $name Name of the block
  844. * @param mixed $value The content for the block. Value will be type cast
  845. * to string.
  846. * @return $this
  847. * @see \Cake\View\ViewBlock::set()
  848. */
  849. public function assign(string $name, $value)
  850. {
  851. $this->Blocks->set($name, $value);
  852. return $this;
  853. }
  854. /**
  855. * Reset the content for a block. This will overwrite any
  856. * existing content.
  857. *
  858. * @param string $name Name of the block
  859. * @return $this
  860. * @see \Cake\View\ViewBlock::set()
  861. */
  862. public function reset(string $name)
  863. {
  864. $this->assign($name, '');
  865. return $this;
  866. }
  867. /**
  868. * Fetch the content for a block. If a block is
  869. * empty or undefined '' will be returned.
  870. *
  871. * @param string $name Name of the block
  872. * @param string $default Default text
  873. * @return string The block content or $default if the block does not exist.
  874. * @see \Cake\View\ViewBlock::get()
  875. */
  876. public function fetch(string $name, string $default = ''): string
  877. {
  878. return $this->Blocks->get($name, $default);
  879. }
  880. /**
  881. * End a capturing block. The compliment to View::start()
  882. *
  883. * @return $this
  884. * @see \Cake\View\ViewBlock::end()
  885. */
  886. public function end()
  887. {
  888. $this->Blocks->end();
  889. return $this;
  890. }
  891. /**
  892. * Check if a block exists
  893. *
  894. * @param string $name Name of the block
  895. *
  896. * @return bool
  897. */
  898. public function exists(string $name): bool
  899. {
  900. return $this->Blocks->exists($name);
  901. }
  902. /**
  903. * Provides template or element extension/inheritance. Templates can extends a
  904. * parent template and populate blocks in the parent template.
  905. *
  906. * @param string $name The template or element to 'extend' the current one with.
  907. * @return $this
  908. * @throws \LogicException when you extend a template with itself or make extend loops.
  909. * @throws \LogicException when you extend an element which doesn't exist
  910. */
  911. public function extend(string $name)
  912. {
  913. if ($name[0] === '/' || $this->_currentType === static::TYPE_TEMPLATE) {
  914. $parent = $this->_getTemplateFileName($name);
  915. } else {
  916. switch ($this->_currentType) {
  917. case static::TYPE_ELEMENT:
  918. $parent = $this->_getElementFileName($name);
  919. if (!$parent) {
  920. [$plugin, $name] = $this->pluginSplit($name);
  921. $paths = $this->_paths($plugin);
  922. $defaultPath = $paths[0] . static::TYPE_ELEMENT . DIRECTORY_SEPARATOR;
  923. throw new LogicException(sprintf(
  924. 'You cannot extend an element which does not exist (%s).',
  925. $defaultPath . $name . $this->_ext
  926. ));
  927. }
  928. break;
  929. case static::TYPE_LAYOUT:
  930. $parent = $this->_getLayoutFileName($name);
  931. break;
  932. default:
  933. $parent = $this->_getTemplateFileName($name);
  934. }
  935. }
  936. if ($parent === $this->_current) {
  937. throw new LogicException('You cannot have templates extend themselves.');
  938. }
  939. if (isset($this->_parents[$parent]) && $this->_parents[$parent] === $this->_current) {
  940. throw new LogicException('You cannot have templates extend in a loop.');
  941. }
  942. $this->_parents[$this->_current] = $parent;
  943. return $this;
  944. }
  945. /**
  946. * Retrieve the current template type
  947. *
  948. * @return string
  949. */
  950. public function getCurrentType(): string
  951. {
  952. return $this->_currentType;
  953. }
  954. /**
  955. * Magic accessor for helpers.
  956. *
  957. * @param string $name Name of the attribute to get.
  958. * @return mixed
  959. */
  960. public function __get(string $name)
  961. {
  962. $registry = $this->helpers();
  963. if (isset($registry->{$name})) {
  964. $this->{$name} = $registry->{$name};
  965. return $registry->{$name};
  966. }
  967. }
  968. /**
  969. * Interact with the HelperRegistry to load all the helpers.
  970. *
  971. * @return $this
  972. */
  973. public function loadHelpers()
  974. {
  975. $registry = $this->helpers();
  976. $helpers = $registry->normalizeArray($this->helpers);
  977. foreach ($helpers as $properties) {
  978. $this->loadHelper($properties['class'], $properties['config']);
  979. }
  980. return $this;
  981. }
  982. /**
  983. * Renders and returns output for given template filename with its
  984. * array of data. Handles parent/extended templates.
  985. *
  986. * @param string $templateFile Filename of the template
  987. * @param array $data Data to include in rendered view. If empty the current
  988. * View::$viewVars will be used.
  989. * @return string Rendered output
  990. * @throws \LogicException When a block is left open.
  991. * @triggers View.beforeRenderFile $this, [$templateFile]
  992. * @triggers View.afterRenderFile $this, [$templateFile, $content]
  993. */
  994. protected function _render(string $templateFile, array $data = []): string
  995. {
  996. if (empty($data)) {
  997. $data = $this->viewVars;
  998. }
  999. $this->_current = $templateFile;
  1000. $initialBlocks = count($this->Blocks->unclosed());
  1001. $this->dispatchEvent('View.beforeRenderFile', [$templateFile]);
  1002. $content = $this->_evaluate($templateFile, $data);
  1003. $afterEvent = $this->dispatchEvent('View.afterRenderFile', [$templateFile, $content]);
  1004. if ($afterEvent->getResult() !== null) {
  1005. $content = $afterEvent->getResult();
  1006. }
  1007. if (isset($this->_parents[$templateFile])) {
  1008. $this->_stack[] = $this->fetch('content');
  1009. $this->assign('content', $content);
  1010. $content = $this->_render($this->_parents[$templateFile]);
  1011. $this->assign('content', array_pop($this->_stack));
  1012. }
  1013. $remainingBlocks = count($this->Blocks->unclosed());
  1014. if ($initialBlocks !== $remainingBlocks) {
  1015. throw new LogicException(sprintf(
  1016. 'The "%s" block was left open. Blocks are not allowed to cross files.',
  1017. (string)$this->Blocks->active()
  1018. ));
  1019. }
  1020. return $content;
  1021. }
  1022. /**
  1023. * Sandbox method to evaluate a template / view script in.
  1024. *
  1025. * @param string $templateFile Filename of the template.
  1026. * @param array $dataForView Data to include in rendered view.
  1027. * @return string Rendered output
  1028. */
  1029. protected function _evaluate(string $templateFile, array $dataForView): string
  1030. {
  1031. extract($dataForView);
  1032. ob_start();
  1033. include func_get_arg(0);
  1034. return ob_get_clean();
  1035. }
  1036. /**
  1037. * Get the helper registry in use by this View class.
  1038. *
  1039. * @return \Cake\View\HelperRegistry
  1040. */
  1041. public function helpers(): HelperRegistry
  1042. {
  1043. if ($this->_helpers === null) {
  1044. $this->_helpers = new HelperRegistry($this);
  1045. }
  1046. return $this->_helpers;
  1047. }
  1048. /**
  1049. * Loads a helper. Delegates to the `HelperRegistry::load()` to load the helper
  1050. *
  1051. * @param string $name Name of the helper to load.
  1052. * @param array $config Settings for the helper
  1053. * @return \Cake\View\Helper a constructed helper object.
  1054. * @see \Cake\View\HelperRegistry::load()
  1055. */
  1056. public function loadHelper(string $name, array $config = []): Helper
  1057. {
  1058. [, $class] = pluginSplit($name);
  1059. $helpers = $this->helpers();
  1060. return $this->{$class} = $helpers->load($name, $config);
  1061. }
  1062. /**
  1063. * Set sub-directory for this template files.
  1064. *
  1065. * @param string $subDir Sub-directory name.
  1066. * @return $this
  1067. * @see \Cake\View\View::$subDir
  1068. * @since 3.7.0
  1069. */
  1070. public function setSubDir(string $subDir)
  1071. {
  1072. $this->subDir = $subDir;
  1073. return $this;
  1074. }
  1075. /**
  1076. * Get sub-directory for this template files.
  1077. *
  1078. * @return string
  1079. * @see \Cake\View\View::$subDir
  1080. * @since 3.7.0
  1081. */
  1082. public function getSubDir(): string
  1083. {
  1084. return $this->subDir;
  1085. }
  1086. /**
  1087. * Returns the View's controller name.
  1088. *
  1089. * @return string
  1090. * @since 3.7.7
  1091. */
  1092. public function getName(): string
  1093. {
  1094. return $this->name;
  1095. }
  1096. /**
  1097. * Returns the plugin name.
  1098. *
  1099. * @return string|null
  1100. * @since 3.7.0
  1101. */
  1102. public function getPlugin(): ?string
  1103. {
  1104. return $this->plugin;
  1105. }
  1106. /**
  1107. * Sets the plugin name.
  1108. *
  1109. * @param string|null $name Plugin name.
  1110. * @return $this
  1111. * @since 3.7.0
  1112. */
  1113. public function setPlugin(?string $name)
  1114. {
  1115. $this->plugin = $name;
  1116. return $this;
  1117. }
  1118. /**
  1119. * Set The cache configuration View will use to store cached elements
  1120. *
  1121. * @param string $elementCache Cache config name.
  1122. * @return $this
  1123. * @see \Cake\View\View::$elementCache
  1124. * @since 3.7.0
  1125. */
  1126. public function setElementCache(string $elementCache)
  1127. {
  1128. $this->elementCache = $elementCache;
  1129. return $this;
  1130. }
  1131. /**
  1132. * Returns filename of given action's template file as a string.
  1133. * CamelCased action names will be under_scored by default.
  1134. * This means that you can have LongActionNames that refer to
  1135. * long_action_names.php templates. You can change the inflection rule by
  1136. * overriding _inflectTemplateFileName.
  1137. *
  1138. * @param string|null $name Controller action to find template filename for
  1139. * @return string Template filename
  1140. * @throws \Cake\View\Exception\MissingTemplateException when a template file could not be found.
  1141. * @throws \RuntimeException When template name not provided.
  1142. */
  1143. protected function _getTemplateFileName(?string $name = null): string
  1144. {
  1145. $templatePath = $subDir = '';
  1146. if ($this->templatePath) {
  1147. $templatePath = $this->templatePath . DIRECTORY_SEPARATOR;
  1148. }
  1149. if (strlen($this->subDir)) {
  1150. $subDir = $this->subDir . DIRECTORY_SEPARATOR;
  1151. // Check if templatePath already terminates with subDir
  1152. if ($templatePath != $subDir && substr($templatePath, -strlen($subDir)) === $subDir) {
  1153. $subDir = '';
  1154. }
  1155. }
  1156. if ($name === null) {
  1157. $name = $this->template;
  1158. }
  1159. if (empty($name)) {
  1160. throw new RuntimeException('Template name not provided');
  1161. }
  1162. [$plugin, $name] = $this->pluginSplit($name);
  1163. $name = str_replace('/', DIRECTORY_SEPARATOR, $name);
  1164. if (strpos($name, DIRECTORY_SEPARATOR) === false && $name !== '' && $name[0] !== '.') {
  1165. $name = $templatePath . $subDir . $this->_inflectTemplateFileName($name);
  1166. } elseif (strpos($name, DIRECTORY_SEPARATOR) !== false) {
  1167. if ($name[0] === DIRECTORY_SEPARATOR || $name[1] === ':') {
  1168. $name = trim($name, DIRECTORY_SEPARATOR);
  1169. } elseif (!$plugin || $this->templatePath !== $this->name) {
  1170. $name = $templatePath . $subDir . $name;
  1171. } else {
  1172. $name = DIRECTORY_SEPARATOR . $subDir . $name;
  1173. }
  1174. }
  1175. $name .= $this->_ext;
  1176. $paths = $this->_paths($plugin);
  1177. foreach ($paths as $path) {
  1178. if (file_exists($path . $name)) {
  1179. return $this->_checkFilePath($path . $name, $path);
  1180. }
  1181. }
  1182. throw new MissingTemplateException($name, $paths);
  1183. }
  1184. /**
  1185. * Change the name of a view template file into underscored format.
  1186. *
  1187. * @param string $name Name of file which should be inflected.
  1188. * @return string File name after conversion
  1189. */
  1190. protected function _inflectTemplateFileName(string $name): string
  1191. {
  1192. return Inflector::underscore($name);
  1193. }
  1194. /**
  1195. * Check that a view file path does not go outside of the defined template paths.
  1196. *
  1197. * Only paths that contain `..` will be checked, as they are the ones most likely to
  1198. * have the ability to resolve to files outside of the template paths.
  1199. *
  1200. * @param string $file The path to the template file.
  1201. * @param string $path Base path that $file should be inside of.
  1202. * @return string The file path
  1203. * @throws \InvalidArgumentException
  1204. */
  1205. protected function _checkFilePath(string $file, string $path): string
  1206. {
  1207. if (strpos($file, '..') === false) {
  1208. return $file;
  1209. }
  1210. $absolute = realpath($file);
  1211. if (strpos($absolute, $path) !== 0) {
  1212. throw new InvalidArgumentException(sprintf(
  1213. 'Cannot use "%s" as a template, it is not within any view template path.',
  1214. $file
  1215. ));
  1216. }
  1217. return $absolute;
  1218. }
  1219. /**
  1220. * Splits a dot syntax plugin name into its plugin and filename.
  1221. * If $name does not have a dot, then index 0 will be null.
  1222. * It checks if the plugin is loaded, else filename will stay unchanged for filenames containing dot
  1223. *
  1224. * @param string $name The name you want to plugin split.
  1225. * @param bool $fallback If true uses the plugin set in the current Request when parsed plugin is not loaded
  1226. * @return array Array with 2 indexes. 0 => plugin name, 1 => filename.
  1227. * @psalm-return array{string|null, string}
  1228. */
  1229. public function pluginSplit(string $name, bool $fallback = true): array
  1230. {
  1231. $plugin = null;
  1232. [$first, $second] = pluginSplit($name);
  1233. if ($first && Plugin::isLoaded($first)) {
  1234. $name = $second;
  1235. $plugin = $first;
  1236. }
  1237. if (isset($this->plugin) && !$plugin && $fallback) {
  1238. $plugin = $this->plugin;
  1239. }
  1240. return [$plugin, $name];
  1241. }
  1242. /**
  1243. * Returns layout filename for this template as a string.
  1244. *
  1245. * @param string|null $name The name of the layout to find.
  1246. * @return string Filename for layout file.
  1247. * @throws \Cake\View\Exception\MissingLayoutException when a layout cannot be located
  1248. * @throws \RuntimeException
  1249. */
  1250. protected function _getLayoutFileName(?string $name = null): string
  1251. {
  1252. if ($name === null) {
  1253. if (empty($this->layout)) {
  1254. throw new RuntimeException(
  1255. 'View::$layout must be a non-empty string.' .
  1256. 'To disable layout rendering use method View::disableAutoLayout() instead.'
  1257. );
  1258. }
  1259. $name = $this->layout;
  1260. }
  1261. $subDir = '';
  1262. if ($this->layoutPath) {
  1263. $subDir = $this->layoutPath . DIRECTORY_SEPARATOR;
  1264. }
  1265. [$plugin, $name] = $this->pluginSplit($name);
  1266. $layoutPaths = $this->_getSubPaths(static::TYPE_LAYOUT . DIRECTORY_SEPARATOR . $subDir);
  1267. $name .= $this->_ext;
  1268. foreach ($this->_paths($plugin) as $path) {
  1269. foreach ($layoutPaths as $layoutPath) {
  1270. $currentPath = $path . $layoutPath;
  1271. if (file_exists($currentPath . $name)) {
  1272. return $this->_checkFilePath($currentPath . $name, $currentPath);
  1273. }
  1274. }
  1275. }
  1276. // Generate the searched paths so we can give a more helpful error.
  1277. $paths = [];
  1278. foreach ($this->_paths($plugin) as $path) {
  1279. foreach ($layoutPaths as $layoutPath) {
  1280. $paths[] = $path . $layoutPath;
  1281. }
  1282. }
  1283. throw new MissingLayoutException($name, $paths);
  1284. }
  1285. /**
  1286. * Finds an element filename, returns false on failure.
  1287. *
  1288. * @param string $name The name of the element to find.
  1289. * @param bool $pluginCheck - if false will ignore the request's plugin if parsed plugin is not loaded
  1290. * @return string|false Either a string to the element filename or false when one can't be found.
  1291. */
  1292. protected function _getElementFileName(string $name, bool $pluginCheck = true)
  1293. {
  1294. [$plugin, $name] = $this->pluginSplit($name, $pluginCheck);
  1295. $paths = $this->_paths($plugin);
  1296. $elementPaths = $this->_getSubPaths(static::TYPE_ELEMENT);
  1297. foreach ($paths as $path) {
  1298. foreach ($elementPaths as $elementPath) {
  1299. if (file_exists($path . $elementPath . DIRECTORY_SEPARATOR . $name . $this->_ext)) {
  1300. return $path . $elementPath . DIRECTORY_SEPARATOR . $name . $this->_ext;
  1301. }
  1302. }
  1303. }
  1304. return false;
  1305. }
  1306. /**
  1307. * Find all sub templates path, based on $basePath
  1308. * If a prefix is defined in the current request, this method will prepend
  1309. * the prefixed template path to the $basePath, cascading up in case the prefix
  1310. * is nested.
  1311. * This is essentially used to find prefixed template paths for elements
  1312. * and layouts.
  1313. *
  1314. * @param string $basePath Base path on which to get the prefixed one.
  1315. * @return string[] Array with all the templates paths.
  1316. */
  1317. protected function _getSubPaths(string $basePath): array
  1318. {
  1319. $paths = [$basePath];
  1320. if ($this->request->getParam('prefix')) {
  1321. $prefixPath = explode('/', $this->request->getParam('prefix'));
  1322. $path = '';
  1323. foreach ($prefixPath as $prefixPart) {
  1324. $path .= Inflector::camelize($prefixPart) . DIRECTORY_SEPARATOR;
  1325. array_unshift(
  1326. $paths,
  1327. $path . $basePath
  1328. );
  1329. }
  1330. }
  1331. return $paths;
  1332. }
  1333. /**
  1334. * Return all possible paths to find view files in order
  1335. *
  1336. * @param string|null $plugin Optional plugin name to scan for view files.
  1337. * @param bool $cached Set to false to force a refresh of view paths. Default true.
  1338. * @return string[] paths
  1339. */
  1340. protected function _paths(?string $plugin = null, bool $cached = true): array
  1341. {
  1342. if ($cached === true) {
  1343. if ($plugin === null && !empty($this->_paths)) {
  1344. return $this->_paths;
  1345. }
  1346. if ($plugin !== null && isset($this->_pathsForPlugin[$plugin])) {
  1347. return $this->_pathsForPlugin[$plugin];
  1348. }
  1349. }
  1350. $templatePaths = App::path(static::NAME_TEMPLATE);
  1351. $pluginPaths = $themePaths = [];
  1352. if (!empty($plugin)) {
  1353. for ($i = 0, $count = count($templatePaths); $i < $count; $i++) {
  1354. $pluginPaths[] = $templatePaths[$i]
  1355. . static::PLUGIN_TEMPLATE_FOLDER
  1356. . DIRECTORY_SEPARATOR
  1357. . $plugin
  1358. . DIRECTORY_SEPARATOR;
  1359. }
  1360. $pluginPaths[] = Plugin::templatePath($plugin);
  1361. }
  1362. if (!empty($this->theme)) {
  1363. $themePaths[] = Plugin::templatePath(Inflector::camelize($this->theme));
  1364. if ($plugin) {
  1365. for ($i = 0, $count = count($themePaths); $i < $count; $i++) {
  1366. array_unshift(
  1367. $themePaths,
  1368. $themePaths[$i]
  1369. . static::PLUGIN_TEMPLATE_FOLDER
  1370. . DIRECTORY_SEPARATOR
  1371. . $plugin
  1372. . DIRECTORY_SEPARATOR
  1373. );
  1374. }
  1375. }
  1376. }
  1377. $paths = array_merge(
  1378. $themePaths,
  1379. $pluginPaths,
  1380. $templatePaths,
  1381. App::core('templates')
  1382. );
  1383. if ($plugin !== null) {
  1384. return $this->_pathsForPlugin[$plugin] = $paths;
  1385. }
  1386. return $this->_paths = $paths;
  1387. }
  1388. /**
  1389. * Generate the cache configuration options for an element.
  1390. *
  1391. * @param string $name Element name
  1392. * @param array $data Data
  1393. * @param array $options Element options
  1394. * @return array Element Cache configuration.
  1395. * @psalm-param array{cache:string, callbacks?: mixed, plugin?:mixed}|array{cache:array{key:string, config:string}, callbacks?: mixed, plugin?:mixed} $options
  1396. * @psalm-return array{key:string, config:string}
  1397. */
  1398. protected function _elementCache(string $name, array $data, array $options): array
  1399. {
  1400. if (isset($options['cache']['key'], $options['cache']['config'])) {
  1401. $cache = $options['cache'];
  1402. $cache['key'] = 'element_' . $cache['key'];
  1403. return $cache;
  1404. }
  1405. [$plugin, $name] = $this->pluginSplit($name);
  1406. $underscored = null;
  1407. if ($plugin) {
  1408. $underscored = Inflector::underscore($plugin);
  1409. }
  1410. $cache = $options['cache'];
  1411. unset($options['cache'], $options['callbacks'], $options['plugin']);
  1412. $keys = array_merge(
  1413. [$underscored, $name],
  1414. array_keys($options),
  1415. array_keys($data)
  1416. );
  1417. $config = [
  1418. 'config' => $this->elementCache,
  1419. 'key' => implode('_', $keys),
  1420. ];
  1421. if (is_array($cache)) {
  1422. $defaults = [
  1423. 'config' => $this->elementCache,
  1424. 'key' => $config['key'],
  1425. ];
  1426. $config = $cache + $defaults;
  1427. }
  1428. $config['key'] = 'element_' . $config['key'];
  1429. return $config;
  1430. }
  1431. /**
  1432. * Renders an element and fires the before and afterRender callbacks for it
  1433. * and writes to the cache if a cache is used
  1434. *
  1435. * @param string $file Element file path
  1436. * @param array $data Data to render
  1437. * @param array $options Element options
  1438. * @return string
  1439. * @triggers View.beforeRender $this, [$file]
  1440. * @triggers View.afterRender $this, [$file, $element]
  1441. */
  1442. protected function _renderElement(string $file, array $data, array $options): string
  1443. {
  1444. $current = $this->_current;
  1445. $restore = $this->_currentType;
  1446. $this->_currentType = static::TYPE_ELEMENT;
  1447. if ($options['callbacks']) {
  1448. $this->dispatchEvent('View.beforeRender', [$file]);
  1449. }
  1450. $element = $this->_render($file, array_merge($this->viewVars, $data));
  1451. if ($options['callbacks']) {
  1452. $this->dispatchEvent('View.afterRender', [$file, $element]);
  1453. }
  1454. $this->_currentType = $restore;
  1455. $this->_current = $current;
  1456. return $element;
  1457. }
  1458. }