Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Merge.php
Go to the documentation of this file.
1 <?php
7 
14 
20 class Merge implements \Magento\Framework\View\Layout\ProcessorInterface
21 {
25  const DESIGN_ABSTRACTION_CUSTOM = 'custom';
26 
30  const DESIGN_ABSTRACTION_PAGE_LAYOUT = 'page_layout';
31 
35  const XPATH_HANDLE_DECLARATION = '/layout[@design_abstraction]';
36 
40  const TYPE_ATTRIBUTE = 'xsi:type';
41 
45  const PAGE_LAYOUT_CACHE_SUFFIX = 'page_layout';
46 
50  private $theme;
51 
55  private $scope;
56 
63 
69  protected $updates = [];
70 
76  protected $handles = [];
77 
83  protected $pageHandles = [];
84 
90  protected $subst = null;
91 
95  private $fileSource;
96 
100  private $pageLayoutFileSource;
101 
105  private $appState;
106 
112  private $layoutCacheKey;
113 
117  protected $cache;
118 
122  protected $layoutValidator;
123 
127  protected $logger;
128 
132  protected $pageLayout;
133 
137  protected $cacheSuffix;
138 
144  protected $allHandles = [];
145 
151  protected $handleProcessing = 1;
152 
158  protected $handleProcessed = 2;
159 
163  private $readFactory;
164 
182  public function __construct(
183  \Magento\Framework\View\DesignInterface $design,
184  \Magento\Framework\Url\ScopeResolverInterface $scopeResolver,
185  \Magento\Framework\View\File\CollectorInterface $fileSource,
186  \Magento\Framework\View\File\CollectorInterface $pageLayoutFileSource,
187  \Magento\Framework\App\State $appState,
188  \Magento\Framework\Cache\FrontendInterface $cache,
189  \Magento\Framework\View\Model\Layout\Update\Validator $validator,
190  \Psr\Log\LoggerInterface $logger,
191  ReadFactory $readFactory,
192  \Magento\Framework\View\Design\ThemeInterface $theme = null,
193  $cacheSuffix = '',
194  LayoutCacheKeyInterface $layoutCacheKey = null
195  ) {
196  $this->theme = $theme ?: $design->getDesignTheme();
197  $this->scope = $scopeResolver->getScope();
198  $this->fileSource = $fileSource;
199  $this->pageLayoutFileSource = $pageLayoutFileSource;
200  $this->appState = $appState;
201  $this->cache = $cache;
202  $this->layoutValidator = $validator;
203  $this->logger = $logger;
204  $this->readFactory = $readFactory;
205  $this->cacheSuffix = $cacheSuffix;
206  $this->layoutCacheKey = $layoutCacheKey
207  ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class);
208  }
209 
216  public function addUpdate($update)
217  {
218  if (!in_array($update, $this->updates)) {
219  $this->updates[] = $update;
220  }
221  return $this;
222  }
223 
229  public function asArray()
230  {
231  return $this->updates;
232  }
233 
239  public function asString()
240  {
241  return implode('', $this->updates);
242  }
243 
250  public function addHandle($handleName)
251  {
252  if (is_array($handleName)) {
253  foreach ($handleName as $name) {
254  $this->handles[$name] = 1;
255  }
256  } else {
257  $this->handles[$handleName] = 1;
258  }
259  return $this;
260  }
261 
268  public function removeHandle($handleName)
269  {
270  unset($this->handles[$handleName]);
271  return $this;
272  }
273 
279  public function getHandles()
280  {
281  return array_keys($this->handles);
282  }
283 
291  public function addPageHandles(array $handlesToTry)
292  {
293  $handlesAdded = false;
294  foreach ($handlesToTry as $handleName) {
295  if (!$this->pageHandleExists($handleName)) {
296  continue;
297  }
298  $handles[] = $handleName;
299  $this->pageHandles = $handles;
300  $this->addHandle($handles);
301  $handlesAdded = true;
302  }
303  return $handlesAdded;
304  }
305 
312  public function pageHandleExists($handleName)
313  {
314  return (bool)$this->_getPageHandleNode($handleName);
315  }
316 
320  public function getPageLayout()
321  {
322  return $this->pageLayout;
323  }
324 
330  public function isLayoutDefined()
331  {
332  $fullLayoutXml = $this->getFileLayoutUpdatesXml();
333  foreach ($this->getHandles() as $handle) {
334  if ($fullLayoutXml->xpath("layout[@id='{$handle}']")) {
335  return true;
336  }
337  }
338  return false;
339  }
340 
347  protected function _getPageHandleNode($handleName)
348  {
349  /* quick validation for non-existing page types */
350  if (!$handleName) {
351  return null;
352  }
353  $handles = $this->getFileLayoutUpdatesXml()->xpath("handle[@id='{$handleName}']");
354  if (empty($handles)) {
355  return null;
356  }
357  $nodes = $this->getFileLayoutUpdatesXml()->xpath("/layouts/handle[@id=\"{$handleName}\"]");
358  return $nodes ? reset($nodes) : null;
359  }
360 
366  public function getPageHandles()
367  {
368  return $this->pageHandles;
369  }
370 
386  public function getAllDesignAbstractions()
387  {
388  $result = [];
389 
390  $conditions = [
391  '(@design_abstraction="' . self::DESIGN_ABSTRACTION_PAGE_LAYOUT .
392  '" or @design_abstraction="' . self::DESIGN_ABSTRACTION_CUSTOM . '")',
393  ];
394  $xpath = '/layouts/*[' . implode(' or ', $conditions) . ']';
395  $nodes = $this->getFileLayoutUpdatesXml()->xpath($xpath) ?: [];
397  foreach ($nodes as $node) {
398  $name = $node->getAttribute('id');
399  $info = [
400  'name' => $name,
401  'label' => (string)new \Magento\Framework\Phrase((string)$node->getAttribute('label')),
402  'design_abstraction' => $node->getAttribute('design_abstraction'),
403  ];
404  $result[$name] = $info;
405  }
406  return $result;
407  }
408 
415  public function getPageHandleType($handleName)
416  {
417  $node = $this->_getPageHandleNode($handleName);
418  return $node ? $node->getAttribute('type') : null;
419  }
420 
428  public function load($handles = [])
429  {
430  if (is_string($handles)) {
431  $handles = [$handles];
432  } elseif (!is_array($handles)) {
433  throw new \Magento\Framework\Exception\LocalizedException(
434  new \Magento\Framework\Phrase('Invalid layout update handle')
435  );
436  }
437 
438  $this->addHandle($handles);
439 
440  $cacheId = $this->getCacheId();
441  $cacheIdPageLayout = $cacheId . '_' . self::PAGE_LAYOUT_CACHE_SUFFIX;
442  $result = $this->_loadCache($cacheId);
443  if ($result) {
444  $this->addUpdate($result);
445  $this->pageLayout = $this->_loadCache($cacheIdPageLayout);
446  foreach ($this->getHandles() as $handle) {
447  $this->allHandles[$handle] = $this->handleProcessed;
448  }
449  return $this;
450  }
451 
452  foreach ($this->getHandles() as $handle) {
453  $this->_merge($handle);
454  }
455 
456  $layout = $this->asString();
457  $this->_validateMergedLayout($cacheId, $layout);
458  $this->_saveCache($layout, $cacheId, $this->getHandles());
459  $this->_saveCache((string)$this->pageLayout, $cacheIdPageLayout, $this->getHandles());
460  return $this;
461  }
462 
471  protected function _validateMergedLayout($cacheId, $layout)
472  {
473  $layoutStr = '<handle id="handle">' . $layout . '</handle>';
474 
475  try {
476  $this->layoutValidator->isValid($layoutStr, Validator::LAYOUT_SCHEMA_MERGED, false);
477  } catch (\Exception $e) {
478  $messages = $this->layoutValidator->getMessages();
479  //Add first message to exception
480  $message = reset($messages);
481  $this->logger->info(
482  'Cache file with merged layout: ' . $cacheId
483  . ' and handles ' . implode(', ', (array)$this->getHandles()) . ': ' . $message
484  );
485  if ($this->appState->getMode() === \Magento\Framework\App\State::MODE_DEVELOPER) {
486  throw $e;
487  }
488  }
489 
490  return $this;
491  }
492 
498  public function asSimplexml()
499  {
500  $updates = trim($this->asString());
501  $updates = '<?xml version="1.0"?>'
502  . '<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
503  . $updates
504  . '</layout>';
505  return $this->_loadXmlString($updates);
506  }
507 
514  protected function _loadXmlString($xmlString)
515  {
516  return simplexml_load_string($xmlString, \Magento\Framework\View\Layout\Element::class);
517  }
518 
525  protected function _merge($handle)
526  {
527  if (!isset($this->allHandles[$handle])) {
528  $this->allHandles[$handle] = $this->handleProcessing;
529  $this->_fetchPackageLayoutUpdates($handle);
530  $this->_fetchDbLayoutUpdates($handle);
531  $this->allHandles[$handle] = $this->handleProcessed;
532  } elseif ($this->allHandles[$handle] == $this->handleProcessing
533  && $this->appState->getMode() === \Magento\Framework\App\State::MODE_DEVELOPER
534  ) {
535  $this->logger->info('Cyclic dependency in merged layout for handle: ' . $handle);
536  }
537  return $this;
538  }
539 
547  {
548  $_profilerKey = 'layout_package_update:' . $handle;
549  \Magento\Framework\Profiler::start($_profilerKey);
550  $layout = $this->getFileLayoutUpdatesXml();
551  foreach ($layout->xpath("*[self::handle or self::layout][@id='{$handle}']") as $updateXml) {
552  $this->_fetchRecursiveUpdates($updateXml);
553  $updateInnerXml = $updateXml->innerXml();
554  $this->validateUpdate($handle, $updateInnerXml);
555  $this->addUpdate($updateInnerXml);
556  }
557  \Magento\Framework\Profiler::stop($_profilerKey);
558 
559  return true;
560  }
561 
568  protected function _fetchDbLayoutUpdates($handle)
569  {
570  $_profilerKey = 'layout_db_update: ' . $handle;
571  \Magento\Framework\Profiler::start($_profilerKey);
572  $updateStr = $this->getDbUpdateString($handle);
573  if (!$updateStr) {
574  \Magento\Framework\Profiler::stop($_profilerKey);
575  return false;
576  }
577  $updateStr = '<update_xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' .
578  $updateStr .
579  '</update_xml>';
580  $updateStr = $this->_substitutePlaceholders($updateStr);
581  $updateXml = $this->_loadXmlString($updateStr);
582  $this->_fetchRecursiveUpdates($updateXml);
583  $updateInnerXml = $updateXml->innerXml();
584  $this->validateUpdate($handle, $updateInnerXml);
585  $this->addUpdate($updateInnerXml);
586 
587  \Magento\Framework\Profiler::stop($_profilerKey);
588  return (bool)$updateStr;
589  }
590 
603  public function validateUpdate($handle, $updateXml)
604  {
605  return;
606  }
607 
614  protected function _substitutePlaceholders($xmlString)
615  {
616  if ($this->subst === null) {
617  $placeholders = [
618  'baseUrl' => $this->scope->getBaseUrl(),
619  'baseSecureUrl' => $this->scope->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_LINK, true),
620  ];
621  $this->subst = [];
622  foreach ($placeholders as $key => $value) {
623  $this->subst['from'][] = '{{' . $key . '}}';
624  $this->subst['to'][] = $value;
625  }
626  }
627  return str_replace($this->subst['from'], $this->subst['to'], $xmlString);
628  }
629 
638  public function getDbUpdateString($handle)
639  {
640  return null;
641  }
642 
649  protected function _fetchRecursiveUpdates($updateXml)
650  {
651  foreach ($updateXml->children() as $child) {
652  if (strtolower($child->getName()) == 'update' && isset($child['handle'])) {
653  $this->_merge((string)$child['handle']);
654  }
655  }
656  if (isset($updateXml['layout'])) {
657  $this->pageLayout = (string)$updateXml['layout'];
658  }
659  return $this;
660  }
661 
667  public function getFileLayoutUpdatesXml()
668  {
669  if ($this->layoutUpdatesCache) {
671  }
672  $cacheId = $this->generateCacheId($this->cacheSuffix);
673  $result = $this->_loadCache($cacheId);
674  if ($result) {
675  $result = $this->_loadXmlString($result);
676  } else {
677  $result = $this->_loadFileLayoutUpdatesXml();
678  $this->_saveCache($result->asXML(), $cacheId);
679  }
680  $this->layoutUpdatesCache = $result;
681  return $result;
682  }
683 
690  protected function generateCacheId($suffix = '')
691  {
692  return "LAYOUT_{$this->theme->getArea()}_STORE{$this->scope->getId()}_{$this->theme->getId()}{$suffix}";
693  }
694 
701  protected function _loadCache($cacheId)
702  {
703  return $this->cache->load($cacheId);
704  }
705 
714  protected function _saveCache($data, $cacheId, array $cacheTags = [])
715  {
716  $this->cache->save($data, $cacheId, $cacheTags, null);
717  }
718 
725  protected function _loadFileLayoutUpdatesXml()
726  {
727  $layoutStr = '';
728  $theme = $this->_getPhysicalTheme($this->theme);
729  $updateFiles = $this->fileSource->getFiles($theme, '*.xml');
730  $updateFiles = array_merge($updateFiles, $this->pageLayoutFileSource->getFiles($theme, '*.xml'));
731  $useErrors = libxml_use_internal_errors(true);
732  foreach ($updateFiles as $file) {
734  $fileReader = $this->readFactory->create($file->getFilename(), DriverPool::FILE);
735  $fileStr = $fileReader->readAll($file->getName());
736  $fileStr = $this->_substitutePlaceholders($fileStr);
738  $fileXml = $this->_loadXmlString($fileStr);
739  if (!$fileXml instanceof \Magento\Framework\View\Layout\Element) {
740  $xmlErrors = $this->getXmlErrors(libxml_get_errors());
741  $this->_logXmlErrors($file->getFilename(), $xmlErrors);
742  if ($this->appState->getMode() === State::MODE_DEVELOPER) {
743  throw new ValidationException(
744  new \Magento\Framework\Phrase(
745  "Theme layout update file '%1' is not valid.\n%2",
746  [
747  $file->getFilename(),
748  implode("\n", $xmlErrors)
749  ]
750  )
751  );
752  }
753  libxml_clear_errors();
754  continue;
755  }
756  if (!$file->isBase() && $fileXml->xpath(self::XPATH_HANDLE_DECLARATION)) {
757  throw new \Magento\Framework\Exception\LocalizedException(
758  new \Magento\Framework\Phrase(
759  'Theme layout update file \'%1\' must not declare page types.',
760  [$file->getFileName()]
761  )
762  );
763  }
764  $handleName = basename($file->getFilename(), '.xml');
765  $tagName = $fileXml->getName() === 'layout' ? 'layout' : 'handle';
766  $handleAttributes = ' id="' . $handleName . '"' . $this->_renderXmlAttributes($fileXml);
767  $handleStr = '<' . $tagName . $handleAttributes . '>' . $fileXml->innerXml() . '</' . $tagName . '>';
768  $layoutStr .= $handleStr;
769  }
770  libxml_use_internal_errors($useErrors);
771  $layoutStr = '<layouts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' . $layoutStr . '</layouts>';
772  $layoutXml = $this->_loadXmlString($layoutStr);
773  return $layoutXml;
774  }
775 
783  protected function _logXmlErrors($fileName, $xmlErrors)
784  {
785  $this->logger->info(
786  sprintf("Theme layout update file '%s' is not valid.\n%s", $fileName, implode("\n", $xmlErrors))
787  );
788  }
789 
796  private function getXmlErrors($libXmlErrors)
797  {
798  $errors = [];
799  if (count($libXmlErrors)) {
800  foreach ($libXmlErrors as $error) {
801  $errors[] = "{$error->message} Line: {$error->line}";
802  }
803  }
804  return $errors;
805  }
806 
814  protected function _getPhysicalTheme(\Magento\Framework\View\Design\ThemeInterface $theme)
815  {
816  $result = $theme;
817  while ($result !== null && $result->getId() && !$result->isPhysical()) {
818  $result = $result->getParentTheme();
819  }
820  if (!$result) {
821  throw new \Magento\Framework\Exception\LocalizedException(
822  new \Magento\Framework\Phrase(
823  'Unable to find a physical ancestor for a theme \'%1\'.',
824  [$theme->getThemeTitle()]
825  )
826  );
827  }
828  return $result;
829  }
830 
837  protected function _renderXmlAttributes(\SimpleXMLElement $node)
838  {
839  $result = '';
840  foreach ($node->attributes() as $attributeName => $attributeValue) {
841  $result .= ' ' . $attributeName . '="' . $attributeValue . '"';
842  }
843  return $result;
844  }
845 
857  public function getContainers()
858  {
859  $result = [];
860  $containerNodes = $this->asSimplexml()->xpath('//container');
862  foreach ($containerNodes as $oneContainerNode) {
863  $label = $oneContainerNode->getAttribute('label');
864  if ($label) {
865  $result[$oneContainerNode->getAttribute('name')] = (string)new \Magento\Framework\Phrase($label);
866  }
867  }
868  return $result;
869  }
870 
877  public function __destruct()
878  {
879  $this->updates = [];
880  $this->layoutUpdatesCache = null;
881  }
882 
886  public function isCustomerDesignAbstraction(array $abstraction)
887  {
888  if (!isset($abstraction['design_abstraction'])) {
889  return false;
890  }
891  return $abstraction['design_abstraction'] === self::DESIGN_ABSTRACTION_CUSTOM;
892  }
893 
897  public function isPageLayoutDesignAbstraction(array $abstraction)
898  {
899  if (!isset($abstraction['design_abstraction'])) {
900  return false;
901  }
902  return $abstraction['design_abstraction'] === self::DESIGN_ABSTRACTION_PAGE_LAYOUT;
903  }
904 
910  public function getTheme()
911  {
912  return $this->theme;
913  }
914 
920  public function getScope()
921  {
922  return $this->scope;
923  }
924 
930  public function getCacheId()
931  {
932  $layoutCacheKeys = $this->layoutCacheKey->getCacheKeys();
933  return $this->generateCacheId(md5(implode('|', array_merge($this->getHandles(), $layoutCacheKeys))));
934  }
935 }
$suffix
Definition: name.phtml:27
elseif(isset( $params[ 'redirect_parent']))
Definition: iframe.phtml:17
_logXmlErrors($fileName, $xmlErrors)
Definition: Merge.php:783
_getPhysicalTheme(\Magento\Framework\View\Design\ThemeInterface $theme)
Definition: Merge.php:814
$message
validateUpdate($handle, $updateXml)
Definition: Merge.php:603
$fileName
Definition: translate.phtml:15
$label
Definition: details.phtml:21
__construct(\Magento\Framework\View\DesignInterface $design, \Magento\Framework\Url\ScopeResolverInterface $scopeResolver, \Magento\Framework\View\File\CollectorInterface $fileSource, \Magento\Framework\View\File\CollectorInterface $pageLayoutFileSource, \Magento\Framework\App\State $appState, \Magento\Framework\Cache\FrontendInterface $cache, \Magento\Framework\View\Model\Layout\Update\Validator $validator, \Psr\Log\LoggerInterface $logger, ReadFactory $readFactory, \Magento\Framework\View\Design\ThemeInterface $theme=null, $cacheSuffix='', LayoutCacheKeyInterface $layoutCacheKey=null)
Definition: Merge.php:182
$value
Definition: gender.phtml:16
isCustomerDesignAbstraction(array $abstraction)
Definition: Merge.php:886
_saveCache($data, $cacheId, array $cacheTags=[])
Definition: Merge.php:714
_renderXmlAttributes(\SimpleXMLElement $node)
Definition: Merge.php:837
$theme
addPageHandles(array $handlesToTry)
Definition: Merge.php:291
_validateMergedLayout($cacheId, $layout)
Definition: Merge.php:471
isPageLayoutDesignAbstraction(array $abstraction)
Definition: Merge.php:897
foreach( $_productCollection as $_product)() ?>" class $info
Definition: listing.phtml:52
$handle
$errors
Definition: overview.phtml:9
if(!isset($_GET['name'])) $name
Definition: log.php:14