Merge.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\View\Model\Layout;
  7. use Magento\Framework\App\State;
  8. use Magento\Framework\Config\Dom\ValidationException;
  9. use Magento\Framework\Filesystem\DriverPool;
  10. use Magento\Framework\Filesystem\File\ReadFactory;
  11. use Magento\Framework\View\Layout\LayoutCacheKeyInterface;
  12. use Magento\Framework\View\Model\Layout\Update\Validator;
  13. /**
  14. * Layout merge model
  15. * @SuppressWarnings(PHPMD.TooManyFields)
  16. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  17. */
  18. class Merge implements \Magento\Framework\View\Layout\ProcessorInterface
  19. {
  20. /**
  21. * Layout abstraction based on designer prerogative.
  22. */
  23. const DESIGN_ABSTRACTION_CUSTOM = 'custom';
  24. /**
  25. * Layout generalization guaranteed to load into View
  26. */
  27. const DESIGN_ABSTRACTION_PAGE_LAYOUT = 'page_layout';
  28. /**
  29. * XPath of handles originally declared in layout updates
  30. */
  31. const XPATH_HANDLE_DECLARATION = '/layout[@design_abstraction]';
  32. /**
  33. * Name of an attribute that stands for data type of node values
  34. */
  35. const TYPE_ATTRIBUTE = 'xsi:type';
  36. /**
  37. * Cache id suffix for page layout
  38. */
  39. const PAGE_LAYOUT_CACHE_SUFFIX = 'page_layout';
  40. /**
  41. * @var \Magento\Framework\View\Design\ThemeInterface
  42. */
  43. private $theme;
  44. /**
  45. * @var \Magento\Framework\Url\ScopeInterface
  46. */
  47. private $scope;
  48. /**
  49. * In-memory cache for loaded layout updates
  50. *
  51. * @var \Magento\Framework\View\Layout\Element
  52. */
  53. protected $layoutUpdatesCache;
  54. /**
  55. * Cumulative array of update XML strings
  56. *
  57. * @var array
  58. */
  59. protected $updates = [];
  60. /**
  61. * Handles used in this update
  62. *
  63. * @var array
  64. */
  65. protected $handles = [];
  66. /**
  67. * Page handle names sorted by from parent to child
  68. *
  69. * @var array
  70. */
  71. protected $pageHandles = [];
  72. /**
  73. * Substitution values in structure array('from' => array(), 'to' => array())
  74. *
  75. * @var array|null
  76. */
  77. protected $subst = null;
  78. /**
  79. * @var \Magento\Framework\View\File\CollectorInterface
  80. */
  81. private $fileSource;
  82. /**
  83. * @var \Magento\Framework\View\File\CollectorInterface
  84. */
  85. private $pageLayoutFileSource;
  86. /**
  87. * @var \Magento\Framework\App\State
  88. */
  89. private $appState;
  90. /**
  91. * Cache keys to be able to generate different cache id for same handles
  92. *
  93. * @var LayoutCacheKeyInterface
  94. */
  95. private $layoutCacheKey;
  96. /**
  97. * @var \Magento\Framework\Cache\FrontendInterface
  98. */
  99. protected $cache;
  100. /**
  101. * @var \Magento\Framework\View\Model\Layout\Update\Validator
  102. */
  103. protected $layoutValidator;
  104. /**
  105. * @var \Psr\Log\LoggerInterface
  106. */
  107. protected $logger;
  108. /**
  109. * @var string
  110. */
  111. protected $pageLayout;
  112. /**
  113. * @var string
  114. */
  115. protected $cacheSuffix;
  116. /**
  117. * All processed handles used in this update
  118. *
  119. * @var array
  120. */
  121. protected $allHandles = [];
  122. /**
  123. * Status for handle being processed
  124. *
  125. * @var int
  126. */
  127. protected $handleProcessing = 1;
  128. /**
  129. * Status for processed handle
  130. *
  131. * @var int
  132. */
  133. protected $handleProcessed = 2;
  134. /**
  135. * @var ReadFactory
  136. */
  137. private $readFactory;
  138. /**
  139. * Init merge model
  140. *
  141. * @param \Magento\Framework\View\DesignInterface $design
  142. * @param \Magento\Framework\Url\ScopeResolverInterface $scopeResolver
  143. * @param \Magento\Framework\View\File\CollectorInterface $fileSource
  144. * @param \Magento\Framework\View\File\CollectorInterface $pageLayoutFileSource
  145. * @param \Magento\Framework\App\State $appState
  146. * @param \Magento\Framework\Cache\FrontendInterface $cache
  147. * @param \Magento\Framework\View\Model\Layout\Update\Validator $validator
  148. * @param \Psr\Log\LoggerInterface $logger
  149. * @param ReadFactory $readFactory ,
  150. * @param \Magento\Framework\View\Design\ThemeInterface $theme Non-injectable theme instance
  151. * @param string $cacheSuffix
  152. * @param LayoutCacheKeyInterface $layoutCacheKey
  153. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  154. */
  155. public function __construct(
  156. \Magento\Framework\View\DesignInterface $design,
  157. \Magento\Framework\Url\ScopeResolverInterface $scopeResolver,
  158. \Magento\Framework\View\File\CollectorInterface $fileSource,
  159. \Magento\Framework\View\File\CollectorInterface $pageLayoutFileSource,
  160. \Magento\Framework\App\State $appState,
  161. \Magento\Framework\Cache\FrontendInterface $cache,
  162. \Magento\Framework\View\Model\Layout\Update\Validator $validator,
  163. \Psr\Log\LoggerInterface $logger,
  164. ReadFactory $readFactory,
  165. \Magento\Framework\View\Design\ThemeInterface $theme = null,
  166. $cacheSuffix = '',
  167. LayoutCacheKeyInterface $layoutCacheKey = null
  168. ) {
  169. $this->theme = $theme ?: $design->getDesignTheme();
  170. $this->scope = $scopeResolver->getScope();
  171. $this->fileSource = $fileSource;
  172. $this->pageLayoutFileSource = $pageLayoutFileSource;
  173. $this->appState = $appState;
  174. $this->cache = $cache;
  175. $this->layoutValidator = $validator;
  176. $this->logger = $logger;
  177. $this->readFactory = $readFactory;
  178. $this->cacheSuffix = $cacheSuffix;
  179. $this->layoutCacheKey = $layoutCacheKey
  180. ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class);
  181. }
  182. /**
  183. * Add XML update instruction
  184. *
  185. * @param string $update
  186. * @return $this
  187. */
  188. public function addUpdate($update)
  189. {
  190. if (!in_array($update, $this->updates)) {
  191. $this->updates[] = $update;
  192. }
  193. return $this;
  194. }
  195. /**
  196. * Get all registered updates as array
  197. *
  198. * @return array
  199. */
  200. public function asArray()
  201. {
  202. return $this->updates;
  203. }
  204. /**
  205. * Get all registered updates as string
  206. *
  207. * @return string
  208. */
  209. public function asString()
  210. {
  211. return implode('', $this->updates);
  212. }
  213. /**
  214. * Add handle(s) to update
  215. *
  216. * @param array|string $handleName
  217. * @return $this
  218. */
  219. public function addHandle($handleName)
  220. {
  221. if (is_array($handleName)) {
  222. foreach ($handleName as $name) {
  223. $this->handles[$name] = 1;
  224. }
  225. } else {
  226. $this->handles[$handleName] = 1;
  227. }
  228. return $this;
  229. }
  230. /**
  231. * Remove handle from update
  232. *
  233. * @param string $handleName
  234. * @return $this
  235. */
  236. public function removeHandle($handleName)
  237. {
  238. unset($this->handles[$handleName]);
  239. return $this;
  240. }
  241. /**
  242. * Get handle names array
  243. *
  244. * @return array
  245. */
  246. public function getHandles()
  247. {
  248. return array_keys($this->handles);
  249. }
  250. /**
  251. * Add the first existing (declared in layout updates) page handle along with all parents to the update.
  252. * Return whether any page handles have been added or not.
  253. *
  254. * @param string[] $handlesToTry
  255. * @return bool
  256. */
  257. public function addPageHandles(array $handlesToTry)
  258. {
  259. $handlesAdded = false;
  260. foreach ($handlesToTry as $handleName) {
  261. if (!$this->pageHandleExists($handleName)) {
  262. continue;
  263. }
  264. $handles[] = $handleName;
  265. $this->pageHandles = $handles;
  266. $this->addHandle($handles);
  267. $handlesAdded = true;
  268. }
  269. return $handlesAdded;
  270. }
  271. /**
  272. * Whether a page handle is declared in the system or not
  273. *
  274. * @param string $handleName
  275. * @return bool
  276. */
  277. public function pageHandleExists($handleName)
  278. {
  279. return (bool)$this->_getPageHandleNode($handleName);
  280. }
  281. /**
  282. * @return string|null
  283. */
  284. public function getPageLayout()
  285. {
  286. return $this->pageLayout;
  287. }
  288. /**
  289. * Check current handles if layout was defined on it
  290. *
  291. * @return bool
  292. */
  293. public function isLayoutDefined()
  294. {
  295. $fullLayoutXml = $this->getFileLayoutUpdatesXml();
  296. foreach ($this->getHandles() as $handle) {
  297. if ($fullLayoutXml->xpath("layout[@id='{$handle}']")) {
  298. return true;
  299. }
  300. }
  301. return false;
  302. }
  303. /**
  304. * Get handle xml node by handle name
  305. *
  306. * @param string $handleName
  307. * @return \Magento\Framework\View\Layout\Element|null
  308. */
  309. protected function _getPageHandleNode($handleName)
  310. {
  311. /* quick validation for non-existing page types */
  312. if (!$handleName) {
  313. return null;
  314. }
  315. $handles = $this->getFileLayoutUpdatesXml()->xpath("handle[@id='{$handleName}']");
  316. if (empty($handles)) {
  317. return null;
  318. }
  319. $nodes = $this->getFileLayoutUpdatesXml()->xpath("/layouts/handle[@id=\"{$handleName}\"]");
  320. return $nodes ? reset($nodes) : null;
  321. }
  322. /**
  323. * Retrieve used page handle names sorted from parent to child
  324. *
  325. * @return array
  326. */
  327. public function getPageHandles()
  328. {
  329. return $this->pageHandles;
  330. }
  331. /**
  332. * Retrieve all design abstractions that exist in the system.
  333. *
  334. * Result format:
  335. * array(
  336. * 'handle_name_1' => array(
  337. * 'name' => 'handle_name_1',
  338. * 'label' => 'Handle Name 1',
  339. * 'design_abstraction' => self::DESIGN_ABSTRACTION_PAGE_LAYOUT or self::DESIGN_ABSTRACTION_CUSTOM
  340. * ),
  341. * // ...
  342. * )
  343. *
  344. * @return array
  345. */
  346. public function getAllDesignAbstractions()
  347. {
  348. $result = [];
  349. $conditions = [
  350. '(@design_abstraction="' . self::DESIGN_ABSTRACTION_PAGE_LAYOUT .
  351. '" or @design_abstraction="' . self::DESIGN_ABSTRACTION_CUSTOM . '")',
  352. ];
  353. $xpath = '/layouts/*[' . implode(' or ', $conditions) . ']';
  354. $nodes = $this->getFileLayoutUpdatesXml()->xpath($xpath) ?: [];
  355. /** @var $node \Magento\Framework\View\Layout\Element */
  356. foreach ($nodes as $node) {
  357. $name = $node->getAttribute('id');
  358. $info = [
  359. 'name' => $name,
  360. 'label' => (string)new \Magento\Framework\Phrase((string)$node->getAttribute('label')),
  361. 'design_abstraction' => $node->getAttribute('design_abstraction'),
  362. ];
  363. $result[$name] = $info;
  364. }
  365. return $result;
  366. }
  367. /**
  368. * Retrieve the type of a page handle
  369. *
  370. * @param string $handleName
  371. * @return string|null
  372. */
  373. public function getPageHandleType($handleName)
  374. {
  375. $node = $this->_getPageHandleNode($handleName);
  376. return $node ? $node->getAttribute('type') : null;
  377. }
  378. /**
  379. * Load layout updates by handles
  380. *
  381. * @param array|string $handles
  382. * @throws \Magento\Framework\Exception\LocalizedException
  383. * @return $this
  384. */
  385. public function load($handles = [])
  386. {
  387. if (is_string($handles)) {
  388. $handles = [$handles];
  389. } elseif (!is_array($handles)) {
  390. throw new \Magento\Framework\Exception\LocalizedException(
  391. new \Magento\Framework\Phrase('Invalid layout update handle')
  392. );
  393. }
  394. $this->addHandle($handles);
  395. $cacheId = $this->getCacheId();
  396. $cacheIdPageLayout = $cacheId . '_' . self::PAGE_LAYOUT_CACHE_SUFFIX;
  397. $result = $this->_loadCache($cacheId);
  398. if ($result) {
  399. $this->addUpdate($result);
  400. $this->pageLayout = $this->_loadCache($cacheIdPageLayout);
  401. foreach ($this->getHandles() as $handle) {
  402. $this->allHandles[$handle] = $this->handleProcessed;
  403. }
  404. return $this;
  405. }
  406. foreach ($this->getHandles() as $handle) {
  407. $this->_merge($handle);
  408. }
  409. $layout = $this->asString();
  410. $this->_validateMergedLayout($cacheId, $layout);
  411. $this->_saveCache($layout, $cacheId, $this->getHandles());
  412. $this->_saveCache((string)$this->pageLayout, $cacheIdPageLayout, $this->getHandles());
  413. return $this;
  414. }
  415. /**
  416. * Validate merged layout
  417. *
  418. * @param string $cacheId
  419. * @param string $layout
  420. * @return $this
  421. * @throws \Exception
  422. */
  423. protected function _validateMergedLayout($cacheId, $layout)
  424. {
  425. $layoutStr = '<handle id="handle">' . $layout . '</handle>';
  426. try {
  427. $this->layoutValidator->isValid($layoutStr, Validator::LAYOUT_SCHEMA_MERGED, false);
  428. } catch (\Exception $e) {
  429. $messages = $this->layoutValidator->getMessages();
  430. //Add first message to exception
  431. $message = reset($messages);
  432. $this->logger->info(
  433. 'Cache file with merged layout: ' . $cacheId
  434. . ' and handles ' . implode(', ', (array)$this->getHandles()) . ': ' . $message
  435. );
  436. if ($this->appState->getMode() === \Magento\Framework\App\State::MODE_DEVELOPER) {
  437. throw $e;
  438. }
  439. }
  440. return $this;
  441. }
  442. /**
  443. * Get layout updates as \Magento\Framework\View\Layout\Element object
  444. *
  445. * @return \SimpleXMLElement
  446. */
  447. public function asSimplexml()
  448. {
  449. $updates = trim($this->asString());
  450. $updates = '<?xml version="1.0"?>'
  451. . '<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
  452. . $updates
  453. . '</layout>';
  454. return $this->_loadXmlString($updates);
  455. }
  456. /**
  457. * Return object representation of XML string
  458. *
  459. * @param string $xmlString
  460. * @return \SimpleXMLElement
  461. */
  462. protected function _loadXmlString($xmlString)
  463. {
  464. return simplexml_load_string($xmlString, \Magento\Framework\View\Layout\Element::class);
  465. }
  466. /**
  467. * Merge layout update by handle
  468. *
  469. * @param string $handle
  470. * @return $this
  471. */
  472. protected function _merge($handle)
  473. {
  474. if (!isset($this->allHandles[$handle])) {
  475. $this->allHandles[$handle] = $this->handleProcessing;
  476. $this->_fetchPackageLayoutUpdates($handle);
  477. $this->_fetchDbLayoutUpdates($handle);
  478. $this->allHandles[$handle] = $this->handleProcessed;
  479. } elseif ($this->allHandles[$handle] == $this->handleProcessing
  480. && $this->appState->getMode() === \Magento\Framework\App\State::MODE_DEVELOPER
  481. ) {
  482. $this->logger->info('Cyclic dependency in merged layout for handle: ' . $handle);
  483. }
  484. return $this;
  485. }
  486. /**
  487. * Add updates for the specified handle
  488. *
  489. * @param string $handle
  490. * @return bool
  491. */
  492. protected function _fetchPackageLayoutUpdates($handle)
  493. {
  494. $_profilerKey = 'layout_package_update:' . $handle;
  495. \Magento\Framework\Profiler::start($_profilerKey);
  496. $layout = $this->getFileLayoutUpdatesXml();
  497. foreach ($layout->xpath("*[self::handle or self::layout][@id='{$handle}']") as $updateXml) {
  498. $this->_fetchRecursiveUpdates($updateXml);
  499. $updateInnerXml = $updateXml->innerXml();
  500. $this->validateUpdate($handle, $updateInnerXml);
  501. $this->addUpdate($updateInnerXml);
  502. }
  503. \Magento\Framework\Profiler::stop($_profilerKey);
  504. return true;
  505. }
  506. /**
  507. * Fetch & add layout updates for the specified handle from the database
  508. *
  509. * @param string $handle
  510. * @return bool
  511. */
  512. protected function _fetchDbLayoutUpdates($handle)
  513. {
  514. $_profilerKey = 'layout_db_update: ' . $handle;
  515. \Magento\Framework\Profiler::start($_profilerKey);
  516. $updateStr = $this->getDbUpdateString($handle);
  517. if (!$updateStr) {
  518. \Magento\Framework\Profiler::stop($_profilerKey);
  519. return false;
  520. }
  521. $updateStr = '<update_xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' .
  522. $updateStr .
  523. '</update_xml>';
  524. $updateStr = $this->_substitutePlaceholders($updateStr);
  525. $updateXml = $this->_loadXmlString($updateStr);
  526. $this->_fetchRecursiveUpdates($updateXml);
  527. $updateInnerXml = $updateXml->innerXml();
  528. $this->validateUpdate($handle, $updateInnerXml);
  529. $this->addUpdate($updateInnerXml);
  530. \Magento\Framework\Profiler::stop($_profilerKey);
  531. return (bool)$updateStr;
  532. }
  533. /**
  534. * Validate layout update content, throw exception on failure.
  535. *
  536. * This method is used as a hook for plugins.
  537. *
  538. * @param string $handle
  539. * @param string $updateXml
  540. * @return void
  541. * @throws \Exception
  542. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  543. * @codeCoverageIgnore
  544. */
  545. public function validateUpdate($handle, $updateXml)
  546. {
  547. return;
  548. }
  549. /**
  550. * Substitute placeholders {{placeholder_name}} with their values in XML string
  551. *
  552. * @param string $xmlString
  553. * @return string
  554. */
  555. protected function _substitutePlaceholders($xmlString)
  556. {
  557. if ($this->subst === null) {
  558. $placeholders = [
  559. 'baseUrl' => $this->scope->getBaseUrl(),
  560. 'baseSecureUrl' => $this->scope->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_LINK, true),
  561. ];
  562. $this->subst = [];
  563. foreach ($placeholders as $key => $value) {
  564. $this->subst['from'][] = '{{' . $key . '}}';
  565. $this->subst['to'][] = $value;
  566. }
  567. }
  568. return str_replace($this->subst['from'], $this->subst['to'], $xmlString);
  569. }
  570. /**
  571. * Get update string
  572. *
  573. * @param string $handle
  574. * @return string
  575. *
  576. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  577. */
  578. public function getDbUpdateString($handle)
  579. {
  580. return null;
  581. }
  582. /**
  583. * Add handles declared as '<update handle="handle_name"/>' directives
  584. *
  585. * @param \SimpleXMLElement $updateXml
  586. * @return $this
  587. */
  588. protected function _fetchRecursiveUpdates($updateXml)
  589. {
  590. foreach ($updateXml->children() as $child) {
  591. if (strtolower($child->getName()) == 'update' && isset($child['handle'])) {
  592. $this->_merge((string)$child['handle']);
  593. }
  594. }
  595. if (isset($updateXml['layout'])) {
  596. $this->pageLayout = (string)$updateXml['layout'];
  597. }
  598. return $this;
  599. }
  600. /**
  601. * Retrieve already merged layout updates from files for specified area/theme/package/store
  602. *
  603. * @return \Magento\Framework\View\Layout\Element
  604. */
  605. public function getFileLayoutUpdatesXml()
  606. {
  607. if ($this->layoutUpdatesCache) {
  608. return $this->layoutUpdatesCache;
  609. }
  610. $cacheId = $this->generateCacheId($this->cacheSuffix);
  611. $result = $this->_loadCache($cacheId);
  612. if ($result) {
  613. $result = $this->_loadXmlString($result);
  614. } else {
  615. $result = $this->_loadFileLayoutUpdatesXml();
  616. $this->_saveCache($result->asXML(), $cacheId);
  617. }
  618. $this->layoutUpdatesCache = $result;
  619. return $result;
  620. }
  621. /**
  622. * Generate cache identifier taking into account current area/package/theme/store
  623. *
  624. * @param string $suffix
  625. * @return string
  626. */
  627. protected function generateCacheId($suffix = '')
  628. {
  629. return "LAYOUT_{$this->theme->getArea()}_STORE{$this->scope->getId()}_{$this->theme->getId()}{$suffix}";
  630. }
  631. /**
  632. * Retrieve data from the cache, if the layout caching is allowed, or FALSE otherwise
  633. *
  634. * @param string $cacheId
  635. * @return string|bool
  636. */
  637. protected function _loadCache($cacheId)
  638. {
  639. return $this->cache->load($cacheId);
  640. }
  641. /**
  642. * Save data to the cache, if the layout caching is allowed
  643. *
  644. * @param string $data
  645. * @param string $cacheId
  646. * @param array $cacheTags
  647. * @return void
  648. */
  649. protected function _saveCache($data, $cacheId, array $cacheTags = [])
  650. {
  651. $this->cache->save($data, $cacheId, $cacheTags, null);
  652. }
  653. /**
  654. * Collect and merge layout updates from files
  655. *
  656. * @return \Magento\Framework\View\Layout\Element
  657. * @throws \Magento\Framework\Exception\LocalizedException
  658. */
  659. protected function _loadFileLayoutUpdatesXml()
  660. {
  661. $layoutStr = '';
  662. $theme = $this->_getPhysicalTheme($this->theme);
  663. $updateFiles = $this->fileSource->getFiles($theme, '*.xml');
  664. $updateFiles = array_merge($updateFiles, $this->pageLayoutFileSource->getFiles($theme, '*.xml'));
  665. $useErrors = libxml_use_internal_errors(true);
  666. foreach ($updateFiles as $file) {
  667. /** @var $fileReader \Magento\Framework\Filesystem\File\Read */
  668. $fileReader = $this->readFactory->create($file->getFilename(), DriverPool::FILE);
  669. $fileStr = $fileReader->readAll($file->getName());
  670. $fileStr = $this->_substitutePlaceholders($fileStr);
  671. /** @var $fileXml \Magento\Framework\View\Layout\Element */
  672. $fileXml = $this->_loadXmlString($fileStr);
  673. if (!$fileXml instanceof \Magento\Framework\View\Layout\Element) {
  674. $xmlErrors = $this->getXmlErrors(libxml_get_errors());
  675. $this->_logXmlErrors($file->getFilename(), $xmlErrors);
  676. if ($this->appState->getMode() === State::MODE_DEVELOPER) {
  677. throw new ValidationException(
  678. new \Magento\Framework\Phrase(
  679. "Theme layout update file '%1' is not valid.\n%2",
  680. [
  681. $file->getFilename(),
  682. implode("\n", $xmlErrors)
  683. ]
  684. )
  685. );
  686. }
  687. libxml_clear_errors();
  688. continue;
  689. }
  690. if (!$file->isBase() && $fileXml->xpath(self::XPATH_HANDLE_DECLARATION)) {
  691. throw new \Magento\Framework\Exception\LocalizedException(
  692. new \Magento\Framework\Phrase(
  693. 'Theme layout update file \'%1\' must not declare page types.',
  694. [$file->getFileName()]
  695. )
  696. );
  697. }
  698. $handleName = basename($file->getFilename(), '.xml');
  699. $tagName = $fileXml->getName() === 'layout' ? 'layout' : 'handle';
  700. $handleAttributes = ' id="' . $handleName . '"' . $this->_renderXmlAttributes($fileXml);
  701. $handleStr = '<' . $tagName . $handleAttributes . '>' . $fileXml->innerXml() . '</' . $tagName . '>';
  702. $layoutStr .= $handleStr;
  703. }
  704. libxml_use_internal_errors($useErrors);
  705. $layoutStr = '<layouts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' . $layoutStr . '</layouts>';
  706. $layoutXml = $this->_loadXmlString($layoutStr);
  707. return $layoutXml;
  708. }
  709. /**
  710. * Log xml errors to system log
  711. *
  712. * @param string $fileName
  713. * @param array $xmlErrors
  714. * @return void
  715. */
  716. protected function _logXmlErrors($fileName, $xmlErrors)
  717. {
  718. $this->logger->info(
  719. sprintf("Theme layout update file '%s' is not valid.\n%s", $fileName, implode("\n", $xmlErrors))
  720. );
  721. }
  722. /**
  723. * Get formatted xml errors
  724. *
  725. * @param array $libXmlErrors
  726. * @return array
  727. */
  728. private function getXmlErrors($libXmlErrors)
  729. {
  730. $errors = [];
  731. if (count($libXmlErrors)) {
  732. foreach ($libXmlErrors as $error) {
  733. $errors[] = "{$error->message} Line: {$error->line}";
  734. }
  735. }
  736. return $errors;
  737. }
  738. /**
  739. * Find the closest physical theme among ancestors and a theme itself
  740. *
  741. * @param \Magento\Framework\View\Design\ThemeInterface $theme
  742. * @return \Magento\Theme\Model\Theme
  743. * @throws \Magento\Framework\Exception\LocalizedException
  744. */
  745. protected function _getPhysicalTheme(\Magento\Framework\View\Design\ThemeInterface $theme)
  746. {
  747. $result = $theme;
  748. while ($result !== null && $result->getId() && !$result->isPhysical()) {
  749. $result = $result->getParentTheme();
  750. }
  751. if (!$result) {
  752. throw new \Magento\Framework\Exception\LocalizedException(
  753. new \Magento\Framework\Phrase(
  754. 'Unable to find a physical ancestor for a theme \'%1\'.',
  755. [$theme->getThemeTitle()]
  756. )
  757. );
  758. }
  759. return $result;
  760. }
  761. /**
  762. * Return attributes of XML node rendered as a string
  763. *
  764. * @param \SimpleXMLElement $node
  765. * @return string
  766. */
  767. protected function _renderXmlAttributes(\SimpleXMLElement $node)
  768. {
  769. $result = '';
  770. foreach ($node->attributes() as $attributeName => $attributeValue) {
  771. $result .= ' ' . $attributeName . '="' . $attributeValue . '"';
  772. }
  773. return $result;
  774. }
  775. /**
  776. * Retrieve containers from the update handles that have been already loaded
  777. *
  778. * Result format:
  779. * array(
  780. * 'container_name' => 'Container Label',
  781. * // ...
  782. * )
  783. *
  784. * @return array
  785. */
  786. public function getContainers()
  787. {
  788. $result = [];
  789. $containerNodes = $this->asSimplexml()->xpath('//container');
  790. /** @var $oneContainerNode \Magento\Framework\View\Layout\Element */
  791. foreach ($containerNodes as $oneContainerNode) {
  792. $label = $oneContainerNode->getAttribute('label');
  793. if ($label) {
  794. $result[$oneContainerNode->getAttribute('name')] = (string)new \Magento\Framework\Phrase($label);
  795. }
  796. }
  797. return $result;
  798. }
  799. /**
  800. * Cleanup circular references
  801. *
  802. * Destructor should be called explicitly in order to work around the PHP bug
  803. * https://bugs.php.net/bug.php?id=62468
  804. */
  805. public function __destruct()
  806. {
  807. $this->updates = [];
  808. $this->layoutUpdatesCache = null;
  809. }
  810. /**
  811. * @inheritdoc
  812. */
  813. public function isCustomerDesignAbstraction(array $abstraction)
  814. {
  815. if (!isset($abstraction['design_abstraction'])) {
  816. return false;
  817. }
  818. return $abstraction['design_abstraction'] === self::DESIGN_ABSTRACTION_CUSTOM;
  819. }
  820. /**
  821. * @inheritdoc
  822. */
  823. public function isPageLayoutDesignAbstraction(array $abstraction)
  824. {
  825. if (!isset($abstraction['design_abstraction'])) {
  826. return false;
  827. }
  828. return $abstraction['design_abstraction'] === self::DESIGN_ABSTRACTION_PAGE_LAYOUT;
  829. }
  830. /**
  831. * Retrieve theme
  832. *
  833. * @return \Magento\Framework\View\Design\ThemeInterface
  834. */
  835. public function getTheme()
  836. {
  837. return $this->theme;
  838. }
  839. /**
  840. * Retrieve current scope
  841. *
  842. * @return \Magento\Framework\Url\ScopeInterface
  843. */
  844. public function getScope()
  845. {
  846. return $this->scope;
  847. }
  848. /**
  849. * Return cache ID based current area/package/theme/store and handles
  850. *
  851. * @return string
  852. */
  853. public function getCacheId()
  854. {
  855. $layoutCacheKeys = $this->layoutCacheKey->getCacheKeys();
  856. return $this->generateCacheId(md5(implode('|', array_merge($this->getHandles(), $layoutCacheKeys))));
  857. }
  858. }