<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\Ui\Config\Reader;
use Magento\Framework\Config\SchemaLocatorInterface;
use Magento\Framework\Config\ValidationStateInterface;
use Magento\Ui\Config\Converter;
use Magento\Framework\Config\Dom as ConfigDom;
/**
* UI Component configuration file DOM object representation
*/
class Dom extends ConfigDom
{
/**
* Id attribute list
*
* @var array
*/
private $idAttributes = [];
/**
* @var \DOMXPath
*/
private $domXPath;
/**
* Path to corresponding XSD file with validation rules for separate config files
*
* @var string
*/
private $schemaFile;
/**
* @var SchemaLocatorInterface
*/
private $schemaLocator;
/**
* Dom constructor
*
* @param string $xml
* @param ValidationStateInterface $validationState
* @param SchemaLocatorInterface $schemaLocator
* @param array $idAttributes
* @param string|null $typeAttributeName
* @param string $errorFormat
*/
public function __construct(
$xml,
ValidationStateInterface $validationState,
SchemaLocatorInterface $schemaLocator,
array $idAttributes = [],
$typeAttributeName = null,
$errorFormat = ConfigDom::ERROR_FORMAT_DEFAULT
) {
$this->idAttributes = array_values($idAttributes);
$this->schemaFile = $schemaLocator->getPerFileSchema() && $validationState->isValidationRequired()
? $schemaLocator->getPerFileSchema() : null;
parent::__construct($xml, $validationState, $idAttributes, $typeAttributeName, $this->schemaFile, $errorFormat);
$this->schemaLocator = $schemaLocator;
}
/**
* Merge $xml into DOM document
*
* @param string $xml
* @return void
*/
public function merge($xml)
{
$dom = $this->_initDom($xml);
$this->domXPath = new \DOMXPath($this->getDom());
$this->nestedMerge($this->getDom()->documentElement, $dom->childNodes);
}
/**
* Merge nested xml nodes
*
* @param \DOMNode $contextNode
* @param \DOMNodeList $insertedNodes
* @return void
*/
private function nestedMerge(\DOMNode $contextNode, \DOMNodeList $insertedNodes)
{
foreach ($insertedNodes as $insertedItem) {
switch ($insertedItem->nodeType) {
case XML_COMMENT_NODE:
break;
case XML_TEXT_NODE:
case XML_CDATA_SECTION_NODE:
if (trim($insertedItem->textContent) !== '') {
$importNode = $this->getDom()->importNode($insertedItem, true);
$contextNode->insertBefore($importNode);
}
break;
default:
$insertedXPath = $this->createXPath($insertedItem);
$rootMatchList = $this->domXPath->query($insertedXPath);
$jLength = $rootMatchList->length;
if ($jLength > 0) {
$this->processMatchedNodes($rootMatchList, $insertedItem);
} else {
$this->appendNode($insertedItem, $contextNode);
}
break;
}
}
}
/**
* Merge node to matched root elements
*
* @param \DOMNodeList $rootMatchList
* @param \DOMElement $insertedItem
* @return void
*/
private function processMatchedNodes(\DOMNodeList $rootMatchList, \DOMElement $insertedItem)
{
foreach ($rootMatchList as $rootItem) {
if ($this->_isTextNode($insertedItem) && $this->_isTextNode($rootItem)) {
$rootItem->nodeValue = $insertedItem->nodeValue;
} else {
$this->nestedMerge($rootItem, $insertedItem->childNodes);
$this->_mergeAttributes($rootItem, $insertedItem);
}
}
}
/**
* Create XPath from node
*
* @param \DOMNode $node
* @return string
*/
private function createXPath(\DOMNode $node)
{
$parentXPath = '';
$currentXPath = $node->getNodePath() ?? '';
if ($node->parentNode !== null && !$node->isSameNode($node->parentNode)) {
$parentXPath = $this->createXPath($node->parentNode);
$pathParts = explode('/', $currentXPath);
$currentXPath = '/' . end($pathParts);
}
$attributesXPath = '';
if ($node->hasAttributes()) {
$attributes = [];
foreach ($node->attributes as $name => $attribute) {
if (in_array($name, $this->idAttributes)) {
$attributes[] = sprintf('@%s="%s"', $name, $attribute->value);
break;
}
}
if (!empty($attributes)) {
if (substr($currentXPath, -1) === ']') {
$currentXPath = substr($currentXPath, 0, strrpos($currentXPath, '['));
}
$attributesXPath = '[' . implode(' and ', $attributes) . ']';
}
}
return '/' . trim($parentXPath . $currentXPath . $attributesXPath, '/');
}
/**
* Append $insertedNode to $contextNode
*
* @param \DOMNode $insertedNode
* @param \DOMNode $contextNode
* @return void
*/
private function appendNode(\DOMNode $insertedNode, \DOMNode $contextNode)
{
$importNode = $this->getDom()->importNode($insertedNode, true);
if (in_array($importNode->localName, [Converter::ARGUMENT_KEY, Converter::SETTINGS_KEY])) {
$this->appendNodeToContext($contextNode, $importNode);
} else {
$contextNode->appendChild($importNode);
}
}
/**
* Append node to context node in correct position
*
* @param \DOMNode $contextNode
* @param \DOMNode $importNode
* @return void
*/
private function appendNodeToContext(\DOMNode $contextNode, \DOMNode $importNode)
{
if (!$contextNode->hasChildNodes()) {
$contextNode->appendChild($importNode);
return;
}
$childContextNode = null;
/** @var \DOMNode $child */
foreach ($contextNode->childNodes as $child) {
if ($child->nodeType != XML_ELEMENT_NODE) {
continue;
}
switch ($child->localName) {
case Converter::ARGUMENT_KEY:
$childContextNode = $child->nextSibling;
break;
case Converter::SETTINGS_KEY:
$childContextNode = $child;
break;
default:
if (!$childContextNode) {
$childContextNode = $child;
}
break;
}
}
$contextNode->insertBefore($importNode, $childContextNode);
}
}
|