View.php 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656
  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. * @psalm-var class-string<\Cake\View\ViewBlock>
  248. */
  249. protected $_viewBlockClass = ViewBlock::class;
  250. /**
  251. * Constant for view file type 'template'.
  252. *
  253. * @var string
  254. */
  255. public const TYPE_TEMPLATE = 'template';
  256. /**
  257. * Constant for view file type 'element'
  258. *
  259. * @var string
  260. */
  261. public const TYPE_ELEMENT = 'element';
  262. /**
  263. * Constant for view file type 'layout'
  264. *
  265. * @var string
  266. */
  267. public const TYPE_LAYOUT = 'layout';
  268. /**
  269. * Constant for type used for App::path().
  270. *
  271. * @var string
  272. */
  273. public const NAME_TEMPLATE = 'templates';
  274. /**
  275. * Constant for folder name containing files for overriding plugin templates.
  276. *
  277. * @var string
  278. */
  279. public const PLUGIN_TEMPLATE_FOLDER = 'plugin';
  280. /**
  281. * Constructor
  282. *
  283. * @param \Cake\Http\ServerRequest|null $request Request instance.
  284. * @param \Cake\Http\Response|null $response Response instance.
  285. * @param \Cake\Event\EventManager|null $eventManager Event manager instance.
  286. * @param array $viewOptions View options. See View::$_passedVars for list of
  287. * options which get set as class properties.
  288. */
  289. public function __construct(
  290. ?ServerRequest $request = null,
  291. ?Response $response = null,
  292. ?EventManager $eventManager = null,
  293. array $viewOptions = []
  294. ) {
  295. foreach ($this->_passedVars as $var) {
  296. if (isset($viewOptions[$var])) {
  297. $this->{$var} = $viewOptions[$var];
  298. }
  299. }
  300. $this->setConfig(array_diff_key(
  301. $viewOptions,
  302. array_flip($this->_passedVars)
  303. ));
  304. if ($eventManager !== null) {
  305. $this->setEventManager($eventManager);
  306. }
  307. if ($request === null) {
  308. $request = Router::getRequest() ?: new ServerRequest(['base' => '', 'url' => '', 'webroot' => '/']);
  309. }
  310. $this->request = $request;
  311. $this->response = $response ?: new Response();
  312. $this->Blocks = new $this->_viewBlockClass();
  313. $this->initialize();
  314. $this->loadHelpers();
  315. }
  316. /**
  317. * Initialization hook method.
  318. *
  319. * Properties like $helpers etc. cannot be initialized statically in your custom
  320. * view class as they are overwritten by values from controller in constructor.
  321. * So this method allows you to manipulate them as required after view instance
  322. * is constructed.
  323. *
  324. * @return void
  325. */
  326. public function initialize(): void
  327. {
  328. }
  329. /**
  330. * Gets the request instance.
  331. *
  332. * @return \Cake\Http\ServerRequest
  333. * @since 3.7.0
  334. */
  335. public function getRequest(): ServerRequest
  336. {
  337. return $this->request;
  338. }
  339. /**
  340. * Sets the request objects and configures a number of controller properties
  341. * based on the contents of the request. The properties that get set are:
  342. *
  343. * - $this->request - To the $request parameter
  344. * - $this->plugin - To the value returned by $request->getParam('plugin')
  345. *
  346. * @param \Cake\Http\ServerRequest $request Request instance.
  347. * @return $this
  348. */
  349. public function setRequest(ServerRequest $request)
  350. {
  351. $this->request = $request;
  352. $this->plugin = $request->getParam('plugin');
  353. return $this;
  354. }
  355. /**
  356. * Gets the response instance.
  357. *
  358. * @return \Cake\Http\Response
  359. */
  360. public function getResponse(): Response
  361. {
  362. return $this->response;
  363. }
  364. /**
  365. * Sets the response instance.
  366. *
  367. * @param \Cake\Http\Response $response Response instance.
  368. * @return $this
  369. */
  370. public function setResponse(Response $response)
  371. {
  372. $this->response = $response;
  373. return $this;
  374. }
  375. /**
  376. * Get path for templates files.
  377. *
  378. * @return string
  379. */
  380. public function getTemplatePath(): string
  381. {
  382. return $this->templatePath;
  383. }
  384. /**
  385. * Set path for templates files.
  386. *
  387. * @param string $path Path for template files.
  388. * @return $this
  389. */
  390. public function setTemplatePath(string $path)
  391. {
  392. $this->templatePath = $path;
  393. return $this;
  394. }
  395. /**
  396. * Get path for layout files.
  397. *
  398. * @return string
  399. */
  400. public function getLayoutPath(): string
  401. {
  402. return $this->layoutPath;
  403. }
  404. /**
  405. * Set path for layout files.
  406. *
  407. * @param string $path Path for layout files.
  408. * @return $this
  409. */
  410. public function setLayoutPath(string $path)
  411. {
  412. $this->layoutPath = $path;
  413. return $this;
  414. }
  415. /**
  416. * Returns if CakePHP's conventional mode of applying layout files is enabled.
  417. * Disabled means that layouts will not be automatically applied to rendered views.
  418. *
  419. * @return bool
  420. */
  421. public function isAutoLayoutEnabled(): bool
  422. {
  423. return $this->autoLayout;
  424. }
  425. /**
  426. * Turns on or off CakePHP's conventional mode of applying layout files.
  427. * On by default. Setting to off means that layouts will not be
  428. * automatically applied to rendered views.
  429. *
  430. * @param bool $enable Boolean to turn on/off.
  431. * @return $this
  432. */
  433. public function enableAutoLayout(bool $enable = true)
  434. {
  435. $this->autoLayout = $enable;
  436. return $this;
  437. }
  438. /**
  439. * Turns off CakePHP's conventional mode of applying layout files.
  440. * Layouts will not be automatically applied to rendered views.
  441. *
  442. * @return $this
  443. */
  444. public function disableAutoLayout()
  445. {
  446. $this->autoLayout = false;
  447. return $this;
  448. }
  449. /**
  450. * Get the current view theme.
  451. *
  452. * @return string|null
  453. */
  454. public function getTheme(): ?string
  455. {
  456. return $this->theme;
  457. }
  458. /**
  459. * Set the view theme to use.
  460. *
  461. * @param string|null $theme Theme name.
  462. * @return $this
  463. */
  464. public function setTheme(?string $theme)
  465. {
  466. $this->theme = $theme;
  467. return $this;
  468. }
  469. /**
  470. * Get the name of the template file to render. The name specified is the
  471. * filename in /templates/<SubFolder> without the .php extension.
  472. *
  473. * @return string
  474. */
  475. public function getTemplate(): string
  476. {
  477. return $this->template;
  478. }
  479. /**
  480. * Set the name of the template file to render. The name specified is the
  481. * filename in /templates/<SubFolder> without the .php extension.
  482. *
  483. * @param string $name Template file name to set.
  484. * @return $this
  485. */
  486. public function setTemplate(string $name)
  487. {
  488. $this->template = $name;
  489. return $this;
  490. }
  491. /**
  492. * Get the name of the layout file to render the template inside of.
  493. * The name specified is the filename of the layout in /templates/Layout
  494. * without the .php extension.
  495. *
  496. * @return string
  497. */
  498. public function getLayout(): string
  499. {
  500. return $this->layout;
  501. }
  502. /**
  503. * Set the name of the layout file to render the template inside of.
  504. * The name specified is the filename of the layout in /templates/Layout
  505. * without the .php extension.
  506. *
  507. * @param string $name Layout file name to set.
  508. * @return $this
  509. */
  510. public function setLayout(string $name)
  511. {
  512. $this->layout = $name;
  513. return $this;
  514. }
  515. /**
  516. * Get config value.
  517. *
  518. * Currently if config is not set it fallbacks to checking corresponding
  519. * view var with underscore prefix. Using underscore prefixed special view
  520. * vars is deprecated and this fallback will be removed in CakePHP 4.1.0.
  521. *
  522. * @param string|null $key The key to get or null for the whole config.
  523. * @param mixed $default The return value when the key does not exist.
  524. * @return mixed Config value being read.
  525. * @psalm-suppress PossiblyNullArgument
  526. */
  527. public function getConfig(?string $key = null, $default = null)
  528. {
  529. $value = $this->_getConfig($key);
  530. if ($value !== null) {
  531. return $value;
  532. }
  533. if (isset($this->viewVars["_{$key}"])) {
  534. deprecationWarning(sprintf(
  535. 'Setting special view var "_%s" is deprecated. Use ViewBuilder::setOption(\'%s\', $value) instead.',
  536. $key,
  537. $key
  538. ));
  539. return $this->viewVars["_{$key}"];
  540. }
  541. return $default;
  542. }
  543. /**
  544. * Renders a piece of PHP with provided parameters and returns HTML, XML, or any other string.
  545. *
  546. * This realizes the concept of Elements, (or "partial layouts") and the $params array is used to send
  547. * data to be used in the element. Elements can be cached improving performance by using the `cache` option.
  548. *
  549. * @param string $name Name of template file in the /templates/Element/ folder,
  550. * or `MyPlugin.template` to use the template element from MyPlugin. If the element
  551. * is not found in the plugin, the normal view path cascade will be searched.
  552. * @param array $data Array of data to be made available to the rendered view (i.e. the Element)
  553. * @param array $options Array of options. Possible keys are:
  554. *
  555. * - `cache` - Can either be `true`, to enable caching using the config in View::$elementCache. Or an array
  556. * If an array, the following keys can be used:
  557. *
  558. * - `config` - Used to store the cached element in a custom cache configuration.
  559. * - `key` - Used to define the key used in the Cache::write(). It will be prefixed with `element_`
  560. *
  561. * - `callbacks` - Set to true to fire beforeRender and afterRender helper callbacks for this element.
  562. * Defaults to false.
  563. * - `ignoreMissing` - Used to allow missing elements. Set to true to not throw exceptions.
  564. * - `plugin` - setting to false will force to use the application's element from plugin templates, when the
  565. * plugin has element with same name. Defaults to true
  566. * @return string Rendered Element
  567. * @throws \Cake\View\Exception\MissingElementException When an element is missing and `ignoreMissing`
  568. * is false.
  569. */
  570. public function element(string $name, array $data = [], array $options = []): string
  571. {
  572. $options += ['callbacks' => false, 'cache' => null, 'plugin' => null];
  573. if (isset($options['cache'])) {
  574. $options['cache'] = $this->_elementCache($name, $data, $options);
  575. }
  576. $pluginCheck = $options['plugin'] !== false;
  577. $file = $this->_getElementFileName($name, $pluginCheck);
  578. if ($file && $options['cache']) {
  579. return $this->cache(function () use ($file, $data, $options): void {
  580. echo $this->_renderElement($file, $data, $options);
  581. }, $options['cache']);
  582. }
  583. if ($file) {
  584. return $this->_renderElement($file, $data, $options);
  585. }
  586. if (empty($options['ignoreMissing'])) {
  587. [$plugin] = $this->pluginSplit($name, $pluginCheck);
  588. $paths = iterator_to_array($this->getElementPaths($plugin));
  589. throw new MissingElementException($name . $this->_ext, $paths);
  590. }
  591. return '';
  592. }
  593. /**
  594. * Create a cached block of view logic.
  595. *
  596. * This allows you to cache a block of view output into the cache
  597. * defined in `elementCache`.
  598. *
  599. * This method will attempt to read the cache first. If the cache
  600. * is empty, the $block will be run and the output stored.
  601. *
  602. * @param callable $block The block of code that you want to cache the output of.
  603. * @param array $options The options defining the cache key etc.
  604. * @return string The rendered content.
  605. * @throws \RuntimeException When $options is lacking a 'key' option.
  606. */
  607. public function cache(callable $block, array $options = []): string
  608. {
  609. $options += ['key' => '', 'config' => $this->elementCache];
  610. if (empty($options['key'])) {
  611. throw new RuntimeException('Cannot cache content with an empty key');
  612. }
  613. $result = Cache::read($options['key'], $options['config']);
  614. if ($result) {
  615. return $result;
  616. }
  617. ob_start();
  618. $block();
  619. $result = ob_get_clean();
  620. Cache::write($options['key'], $result, $options['config']);
  621. return $result;
  622. }
  623. /**
  624. * Checks if an element exists
  625. *
  626. * @param string $name Name of template file in the /templates/Element/ folder,
  627. * or `MyPlugin.template` to check the template element from MyPlugin. If the element
  628. * is not found in the plugin, the normal view path cascade will be searched.
  629. * @return bool Success
  630. */
  631. public function elementExists(string $name): bool
  632. {
  633. return (bool)$this->_getElementFileName($name);
  634. }
  635. /**
  636. * Renders view for given template file and layout.
  637. *
  638. * Render triggers helper callbacks, which are fired before and after the template are rendered,
  639. * as well as before and after the layout. The helper callbacks are called:
  640. *
  641. * - `beforeRender`
  642. * - `afterRender`
  643. * - `beforeLayout`
  644. * - `afterLayout`
  645. *
  646. * If View::$autoLayout is set to `false`, the template will be returned bare.
  647. *
  648. * Template and layout names can point to plugin templates/layouts. Using the `Plugin.template` syntax
  649. * a plugin template/layout can be used instead of the app ones. If the chosen plugin is not found
  650. * the template will be located along the regular view path cascade.
  651. *
  652. * @param string|null $template Name of template file to use
  653. * @param string|false|null $layout Layout to use. False to disable.
  654. * @return string Rendered content.
  655. * @throws \Cake\Core\Exception\Exception If there is an error in the view.
  656. * @triggers View.beforeRender $this, [$templateFileName]
  657. * @triggers View.afterRender $this, [$templateFileName]
  658. */
  659. public function render(?string $template = null, $layout = null): string
  660. {
  661. $defaultLayout = '';
  662. $defaultAutoLayout = null;
  663. if ($layout === false) {
  664. $defaultAutoLayout = $this->autoLayout;
  665. $this->autoLayout = false;
  666. } elseif ($layout !== null) {
  667. $defaultLayout = $this->layout;
  668. $this->layout = $layout;
  669. }
  670. $templateFileName = $this->_getTemplateFileName($template);
  671. $this->_currentType = static::TYPE_TEMPLATE;
  672. $this->dispatchEvent('View.beforeRender', [$templateFileName]);
  673. $this->Blocks->set('content', $this->_render($templateFileName));
  674. $this->dispatchEvent('View.afterRender', [$templateFileName]);
  675. if ($this->autoLayout) {
  676. if (empty($this->layout)) {
  677. throw new RuntimeException(
  678. 'View::$layout must be a non-empty string.' .
  679. 'To disable layout rendering use method View::disableAutoLayout() instead.'
  680. );
  681. }
  682. $this->Blocks->set('content', $this->renderLayout('', $this->layout));
  683. }
  684. if ($layout !== null) {
  685. $this->layout = $defaultLayout;
  686. }
  687. if ($defaultAutoLayout !== null) {
  688. $this->autoLayout = $defaultAutoLayout;
  689. }
  690. return $this->Blocks->get('content');
  691. }
  692. /**
  693. * Renders a layout. Returns output from _render().
  694. *
  695. * Several variables are created for use in layout.
  696. *
  697. * @param string $content Content to render in a template, wrapped by the surrounding layout.
  698. * @param string|null $layout Layout name
  699. * @return string Rendered output.
  700. * @throws \Cake\Core\Exception\Exception if there is an error in the view.
  701. * @triggers View.beforeLayout $this, [$layoutFileName]
  702. * @triggers View.afterLayout $this, [$layoutFileName]
  703. */
  704. public function renderLayout(string $content, ?string $layout = null): string
  705. {
  706. $layoutFileName = $this->_getLayoutFileName($layout);
  707. if (!empty($content)) {
  708. $this->Blocks->set('content', $content);
  709. }
  710. $this->dispatchEvent('View.beforeLayout', [$layoutFileName]);
  711. $title = $this->Blocks->get('title');
  712. if ($title === '') {
  713. $title = Inflector::humanize(str_replace(DIRECTORY_SEPARATOR, '/', (string)$this->templatePath));
  714. $this->Blocks->set('title', $title);
  715. }
  716. $this->_currentType = static::TYPE_LAYOUT;
  717. $this->Blocks->set('content', $this->_render($layoutFileName));
  718. $this->dispatchEvent('View.afterLayout', [$layoutFileName]);
  719. return $this->Blocks->get('content');
  720. }
  721. /**
  722. * Returns a list of variables available in the current View context
  723. *
  724. * @return string[] Array of the set view variable names.
  725. */
  726. public function getVars(): array
  727. {
  728. return array_keys($this->viewVars);
  729. }
  730. /**
  731. * Returns the contents of the given View variable.
  732. *
  733. * @param string $var The view var you want the contents of.
  734. * @param mixed $default The default/fallback content of $var.
  735. * @return mixed The content of the named var if its set, otherwise $default.
  736. */
  737. public function get(string $var, $default = null)
  738. {
  739. if (!isset($this->viewVars[$var])) {
  740. return $default;
  741. }
  742. return $this->viewVars[$var];
  743. }
  744. /**
  745. * Saves a variable or an associative array of variables for use inside a template.
  746. *
  747. * @param string|array $name A string or an array of data.
  748. * @param mixed $value Value in case $name is a string (which then works as the key).
  749. * Unused if $name is an associative array, otherwise serves as the values to $name's keys.
  750. * @return $this
  751. * @throws \RuntimeException If the array combine operation failed.
  752. */
  753. public function set($name, $value = null)
  754. {
  755. if (is_array($name)) {
  756. if (is_array($value)) {
  757. $data = array_combine($name, $value);
  758. if ($data === false) {
  759. throw new RuntimeException(
  760. 'Invalid data provided for array_combine() to work: Both $name and $value require same count.'
  761. );
  762. }
  763. } else {
  764. $data = $name;
  765. }
  766. } else {
  767. $data = [$name => $value];
  768. }
  769. $this->viewVars = $data + $this->viewVars;
  770. return $this;
  771. }
  772. /**
  773. * Get the names of all the existing blocks.
  774. *
  775. * @return string[] An array containing the blocks.
  776. * @see \Cake\View\ViewBlock::keys()
  777. */
  778. public function blocks(): array
  779. {
  780. return $this->Blocks->keys();
  781. }
  782. /**
  783. * Start capturing output for a 'block'
  784. *
  785. * You can use start on a block multiple times to
  786. * append or prepend content in a capture mode.
  787. *
  788. * ```
  789. * // Append content to an existing block.
  790. * $this->start('content');
  791. * echo $this->fetch('content');
  792. * echo 'Some new content';
  793. * $this->end();
  794. *
  795. * // Prepend content to an existing block
  796. * $this->start('content');
  797. * echo 'Some new content';
  798. * echo $this->fetch('content');
  799. * $this->end();
  800. * ```
  801. *
  802. * @param string $name The name of the block to capture for.
  803. * @return $this
  804. * @see \Cake\View\ViewBlock::start()
  805. */
  806. public function start(string $name)
  807. {
  808. $this->Blocks->start($name);
  809. return $this;
  810. }
  811. /**
  812. * Append to an existing or new block.
  813. *
  814. * Appending to a new block will create the block.
  815. *
  816. * @param string $name Name of the block
  817. * @param mixed $value The content for the block. Value will be type cast
  818. * to string.
  819. * @return $this
  820. * @see \Cake\View\ViewBlock::concat()
  821. */
  822. public function append(string $name, $value = null)
  823. {
  824. $this->Blocks->concat($name, $value);
  825. return $this;
  826. }
  827. /**
  828. * Prepend to an existing or new block.
  829. *
  830. * Prepending to a new block will create the block.
  831. *
  832. * @param string $name Name of the block
  833. * @param mixed $value The content for the block. Value will be type cast
  834. * to string.
  835. * @return $this
  836. * @see \Cake\View\ViewBlock::concat()
  837. */
  838. public function prepend(string $name, $value)
  839. {
  840. $this->Blocks->concat($name, $value, ViewBlock::PREPEND);
  841. return $this;
  842. }
  843. /**
  844. * Set the content for a block. This will overwrite any
  845. * existing content.
  846. *
  847. * @param string $name Name of the block
  848. * @param mixed $value The content for the block. Value will be type cast
  849. * to string.
  850. * @return $this
  851. * @see \Cake\View\ViewBlock::set()
  852. */
  853. public function assign(string $name, $value)
  854. {
  855. $this->Blocks->set($name, $value);
  856. return $this;
  857. }
  858. /**
  859. * Reset the content for a block. This will overwrite any
  860. * existing content.
  861. *
  862. * @param string $name Name of the block
  863. * @return $this
  864. * @see \Cake\View\ViewBlock::set()
  865. */
  866. public function reset(string $name)
  867. {
  868. $this->assign($name, '');
  869. return $this;
  870. }
  871. /**
  872. * Fetch the content for a block. If a block is
  873. * empty or undefined '' will be returned.
  874. *
  875. * @param string $name Name of the block
  876. * @param string $default Default text
  877. * @return string The block content or $default if the block does not exist.
  878. * @see \Cake\View\ViewBlock::get()
  879. */
  880. public function fetch(string $name, string $default = ''): string
  881. {
  882. return $this->Blocks->get($name, $default);
  883. }
  884. /**
  885. * End a capturing block. The compliment to View::start()
  886. *
  887. * @return $this
  888. * @see \Cake\View\ViewBlock::end()
  889. */
  890. public function end()
  891. {
  892. $this->Blocks->end();
  893. return $this;
  894. }
  895. /**
  896. * Check if a block exists
  897. *
  898. * @param string $name Name of the block
  899. * @return bool
  900. */
  901. public function exists(string $name): bool
  902. {
  903. return $this->Blocks->exists($name);
  904. }
  905. /**
  906. * Provides template or element extension/inheritance. Templates can extends a
  907. * parent template and populate blocks in the parent template.
  908. *
  909. * @param string $name The template or element to 'extend' the current one with.
  910. * @return $this
  911. * @throws \LogicException when you extend a template with itself or make extend loops.
  912. * @throws \LogicException when you extend an element which doesn't exist
  913. */
  914. public function extend(string $name)
  915. {
  916. $type = $name[0] === '/' ? static::TYPE_TEMPLATE : $this->_currentType;
  917. switch ($type) {
  918. case static::TYPE_ELEMENT:
  919. $parent = $this->_getElementFileName($name);
  920. if (!$parent) {
  921. [$plugin, $name] = $this->pluginSplit($name);
  922. $paths = $this->_paths($plugin);
  923. $defaultPath = $paths[0] . static::TYPE_ELEMENT . DIRECTORY_SEPARATOR;
  924. throw new LogicException(sprintf(
  925. 'You cannot extend an element which does not exist (%s).',
  926. $defaultPath . $name . $this->_ext
  927. ));
  928. }
  929. break;
  930. case static::TYPE_LAYOUT:
  931. $parent = $this->_getLayoutFileName($name);
  932. break;
  933. default:
  934. $parent = $this->_getTemplateFileName($name);
  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. [$plugin, $name] = $this->pluginSplit($name);
  1262. $name .= $this->_ext;
  1263. foreach ($this->getLayoutPaths($plugin) as $path) {
  1264. if (file_exists($path . $name)) {
  1265. return $this->_checkFilePath($path . $name, $path);
  1266. }
  1267. }
  1268. $paths = iterator_to_array($this->getLayoutPaths($plugin));
  1269. throw new MissingLayoutException($name, $paths);
  1270. }
  1271. /**
  1272. * Get an iterator for layout paths.
  1273. *
  1274. * @param string|null $plugin The plugin to fetch paths for.
  1275. * @return \Generator
  1276. */
  1277. protected function getLayoutPaths(?string $plugin)
  1278. {
  1279. $subDir = '';
  1280. if ($this->layoutPath) {
  1281. $subDir = $this->layoutPath . DIRECTORY_SEPARATOR;
  1282. }
  1283. $layoutPaths = $this->_getSubPaths(static::TYPE_LAYOUT . DIRECTORY_SEPARATOR . $subDir);
  1284. foreach ($this->_paths($plugin) as $path) {
  1285. foreach ($layoutPaths as $layoutPath) {
  1286. yield $path . $layoutPath;
  1287. }
  1288. }
  1289. }
  1290. /**
  1291. * Finds an element filename, returns false on failure.
  1292. *
  1293. * @param string $name The name of the element to find.
  1294. * @param bool $pluginCheck - if false will ignore the request's plugin if parsed plugin is not loaded
  1295. * @return string|false Either a string to the element filename or false when one can't be found.
  1296. */
  1297. protected function _getElementFileName(string $name, bool $pluginCheck = true)
  1298. {
  1299. [$plugin, $name] = $this->pluginSplit($name, $pluginCheck);
  1300. $name .= $this->_ext;
  1301. foreach ($this->getElementPaths($plugin) as $path) {
  1302. if (file_exists($path . $name)) {
  1303. return $path . $name;
  1304. }
  1305. }
  1306. return false;
  1307. }
  1308. /**
  1309. * Get an iterator for element paths.
  1310. *
  1311. * @param string|null $plugin The plugin to fetch paths for.
  1312. * @return \Generator
  1313. */
  1314. protected function getElementPaths(?string $plugin)
  1315. {
  1316. $elementPaths = $this->_getSubPaths(static::TYPE_ELEMENT);
  1317. foreach ($this->_paths($plugin) as $path) {
  1318. foreach ($elementPaths as $subdir) {
  1319. yield $path . $subdir . DIRECTORY_SEPARATOR;
  1320. }
  1321. }
  1322. }
  1323. /**
  1324. * Find all sub templates path, based on $basePath
  1325. * If a prefix is defined in the current request, this method will prepend
  1326. * the prefixed template path to the $basePath, cascading up in case the prefix
  1327. * is nested.
  1328. * This is essentially used to find prefixed template paths for elements
  1329. * and layouts.
  1330. *
  1331. * @param string $basePath Base path on which to get the prefixed one.
  1332. * @return string[] Array with all the templates paths.
  1333. */
  1334. protected function _getSubPaths(string $basePath): array
  1335. {
  1336. $paths = [$basePath];
  1337. if ($this->request->getParam('prefix')) {
  1338. $prefixPath = explode('/', $this->request->getParam('prefix'));
  1339. $path = '';
  1340. foreach ($prefixPath as $prefixPart) {
  1341. $path .= Inflector::camelize($prefixPart) . DIRECTORY_SEPARATOR;
  1342. array_unshift(
  1343. $paths,
  1344. $path . $basePath
  1345. );
  1346. }
  1347. }
  1348. return $paths;
  1349. }
  1350. /**
  1351. * Return all possible paths to find view files in order
  1352. *
  1353. * @param string|null $plugin Optional plugin name to scan for view files.
  1354. * @param bool $cached Set to false to force a refresh of view paths. Default true.
  1355. * @return string[] paths
  1356. */
  1357. protected function _paths(?string $plugin = null, bool $cached = true): array
  1358. {
  1359. if ($cached === true) {
  1360. if ($plugin === null && !empty($this->_paths)) {
  1361. return $this->_paths;
  1362. }
  1363. if ($plugin !== null && isset($this->_pathsForPlugin[$plugin])) {
  1364. return $this->_pathsForPlugin[$plugin];
  1365. }
  1366. }
  1367. $templatePaths = App::path(static::NAME_TEMPLATE);
  1368. $pluginPaths = $themePaths = [];
  1369. if (!empty($plugin)) {
  1370. for ($i = 0, $count = count($templatePaths); $i < $count; $i++) {
  1371. $pluginPaths[] = $templatePaths[$i]
  1372. . static::PLUGIN_TEMPLATE_FOLDER
  1373. . DIRECTORY_SEPARATOR
  1374. . $plugin
  1375. . DIRECTORY_SEPARATOR;
  1376. }
  1377. $pluginPaths[] = Plugin::templatePath($plugin);
  1378. }
  1379. if (!empty($this->theme)) {
  1380. $themePaths[] = Plugin::templatePath(Inflector::camelize($this->theme));
  1381. if ($plugin) {
  1382. for ($i = 0, $count = count($themePaths); $i < $count; $i++) {
  1383. array_unshift(
  1384. $themePaths,
  1385. $themePaths[$i]
  1386. . static::PLUGIN_TEMPLATE_FOLDER
  1387. . DIRECTORY_SEPARATOR
  1388. . $plugin
  1389. . DIRECTORY_SEPARATOR
  1390. );
  1391. }
  1392. }
  1393. }
  1394. $paths = array_merge(
  1395. $themePaths,
  1396. $pluginPaths,
  1397. $templatePaths,
  1398. App::core('templates')
  1399. );
  1400. if ($plugin !== null) {
  1401. return $this->_pathsForPlugin[$plugin] = $paths;
  1402. }
  1403. return $this->_paths = $paths;
  1404. }
  1405. /**
  1406. * Generate the cache configuration options for an element.
  1407. *
  1408. * @param string $name Element name
  1409. * @param array $data Data
  1410. * @param array $options Element options
  1411. * @return array Element Cache configuration.
  1412. * @psalm-param array{cache:(string|array{key:string, config:string}|null), callbacks:mixed, plugin:mixed} $options
  1413. * @psalm-return array{key:string, config:string}
  1414. */
  1415. protected function _elementCache(string $name, array $data, array $options): array
  1416. {
  1417. if (isset($options['cache']['key'], $options['cache']['config'])) {
  1418. $cache = $options['cache'];
  1419. $cache['key'] = 'element_' . $cache['key'];
  1420. return $cache;
  1421. }
  1422. [$plugin, $name] = $this->pluginSplit($name);
  1423. $pluginKey = null;
  1424. if ($plugin) {
  1425. $pluginKey = str_replace('/', '_', Inflector::underscore($plugin));
  1426. }
  1427. $elementKey = str_replace(['\\', '/'], '_', $name);
  1428. $cache = $options['cache'];
  1429. unset($options['cache'], $options['callbacks'], $options['plugin']);
  1430. $keys = array_merge(
  1431. [$pluginKey, $elementKey],
  1432. array_keys($options),
  1433. array_keys($data)
  1434. );
  1435. $config = [
  1436. 'config' => $this->elementCache,
  1437. 'key' => implode('_', $keys),
  1438. ];
  1439. if (is_array($cache)) {
  1440. $config = $cache + $config;
  1441. }
  1442. $config['key'] = 'element_' . $config['key'];
  1443. return $config;
  1444. }
  1445. /**
  1446. * Renders an element and fires the before and afterRender callbacks for it
  1447. * and writes to the cache if a cache is used
  1448. *
  1449. * @param string $file Element file path
  1450. * @param array $data Data to render
  1451. * @param array $options Element options
  1452. * @return string
  1453. * @triggers View.beforeRender $this, [$file]
  1454. * @triggers View.afterRender $this, [$file, $element]
  1455. */
  1456. protected function _renderElement(string $file, array $data, array $options): string
  1457. {
  1458. $current = $this->_current;
  1459. $restore = $this->_currentType;
  1460. $this->_currentType = static::TYPE_ELEMENT;
  1461. if ($options['callbacks']) {
  1462. $this->dispatchEvent('View.beforeRender', [$file]);
  1463. }
  1464. $element = $this->_render($file, array_merge($this->viewVars, $data));
  1465. if ($options['callbacks']) {
  1466. $this->dispatchEvent('View.afterRender', [$file, $element]);
  1467. }
  1468. $this->_currentType = $restore;
  1469. $this->_current = $current;
  1470. return $element;
  1471. }
  1472. }