Dom.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Ui\Config\Reader;
  7. use Magento\Framework\Config\SchemaLocatorInterface;
  8. use Magento\Framework\Config\ValidationStateInterface;
  9. use Magento\Ui\Config\Converter;
  10. use Magento\Framework\Config\Dom as ConfigDom;
  11. /**
  12. * UI Component configuration file DOM object representation
  13. */
  14. class Dom extends ConfigDom
  15. {
  16. /**
  17. * Id attribute list
  18. *
  19. * @var array
  20. */
  21. private $idAttributes = [];
  22. /**
  23. * @var \DOMXPath
  24. */
  25. private $domXPath;
  26. /**
  27. * Path to corresponding XSD file with validation rules for separate config files
  28. *
  29. * @var string
  30. */
  31. private $schemaFile;
  32. /**
  33. * @var SchemaLocatorInterface
  34. */
  35. private $schemaLocator;
  36. /**
  37. * Dom constructor
  38. *
  39. * @param string $xml
  40. * @param ValidationStateInterface $validationState
  41. * @param SchemaLocatorInterface $schemaLocator
  42. * @param array $idAttributes
  43. * @param null $typeAttributeName
  44. * @param string $errorFormat
  45. */
  46. public function __construct(
  47. $xml,
  48. ValidationStateInterface $validationState,
  49. SchemaLocatorInterface $schemaLocator,
  50. array $idAttributes = [],
  51. $typeAttributeName = null,
  52. $errorFormat = ConfigDom::ERROR_FORMAT_DEFAULT
  53. ) {
  54. $this->idAttributes = array_values($idAttributes);
  55. $this->schemaFile = $schemaLocator->getPerFileSchema() && $validationState->isValidationRequired()
  56. ? $schemaLocator->getPerFileSchema() : null;
  57. parent::__construct($xml, $validationState, $idAttributes, $typeAttributeName, $this->schemaFile, $errorFormat);
  58. $this->schemaLocator = $schemaLocator;
  59. }
  60. /**
  61. * Merge $xml into DOM document
  62. *
  63. * @param string $xml
  64. * @return void
  65. */
  66. public function merge($xml)
  67. {
  68. $dom = $this->_initDom($xml);
  69. $this->domXPath = new \DOMXPath($this->getDom());
  70. $this->nestedMerge($this->getDom()->documentElement, $dom->childNodes);
  71. }
  72. /**
  73. * Merge nested xml nodes
  74. *
  75. * @param \DOMNode $contextNode
  76. * @param \DOMNodeList $insertedNodes
  77. * @return void
  78. */
  79. private function nestedMerge(\DOMNode $contextNode, \DOMNodeList $insertedNodes)
  80. {
  81. foreach ($insertedNodes as $insertedItem) {
  82. switch ($insertedItem->nodeType) {
  83. case XML_COMMENT_NODE:
  84. break;
  85. case XML_TEXT_NODE:
  86. case XML_CDATA_SECTION_NODE:
  87. if (trim($insertedItem->textContent) !== '') {
  88. $importNode = $this->getDom()->importNode($insertedItem, true);
  89. $contextNode->insertBefore($importNode);
  90. }
  91. break;
  92. default:
  93. $insertedXPath = $this->createXPath($insertedItem);
  94. $rootMatchList = $this->domXPath->query($insertedXPath);
  95. $jLength = $rootMatchList->length;
  96. if ($jLength > 0) {
  97. $this->processMatchedNodes($rootMatchList, $insertedItem);
  98. } else {
  99. $this->appendNode($insertedItem, $contextNode);
  100. }
  101. break;
  102. }
  103. }
  104. }
  105. /**
  106. * Merge node to matched root elements
  107. *
  108. * @param \DOMNodeList $rootMatchList
  109. * @param \DOMElement $insertedItem
  110. * @return void
  111. */
  112. private function processMatchedNodes(\DOMNodeList $rootMatchList, \DOMElement $insertedItem)
  113. {
  114. foreach ($rootMatchList as $rootItem) {
  115. if ($this->_isTextNode($insertedItem) && $this->_isTextNode($rootItem)) {
  116. $rootItem->nodeValue = $insertedItem->nodeValue;
  117. } else {
  118. $this->nestedMerge($rootItem, $insertedItem->childNodes);
  119. $this->_mergeAttributes($rootItem, $insertedItem);
  120. }
  121. }
  122. }
  123. /**
  124. * Create XPath from node
  125. *
  126. * @param \DOMNode $node
  127. * @return string
  128. */
  129. private function createXPath(\DOMNode $node)
  130. {
  131. $parentXPath = '';
  132. $currentXPath = $node->getNodePath();
  133. if ($node->parentNode !== null && !$node->isSameNode($node->parentNode)) {
  134. $parentXPath = $this->createXPath($node->parentNode);
  135. $pathParts = explode('/', $currentXPath);
  136. $currentXPath = '/' . end($pathParts);
  137. }
  138. $attributesXPath = '';
  139. if ($node->hasAttributes()) {
  140. $attributes = [];
  141. foreach ($node->attributes as $name => $attribute) {
  142. if (in_array($name, $this->idAttributes)) {
  143. $attributes[] = sprintf('@%s="%s"', $name, $attribute->value);
  144. break;
  145. }
  146. }
  147. if (!empty($attributes)) {
  148. if (substr($currentXPath, -1) === ']') {
  149. $currentXPath = substr($currentXPath, 0, strrpos($currentXPath, '['));
  150. }
  151. $attributesXPath = '[' . implode(' and ', $attributes) . ']';
  152. }
  153. }
  154. return '/' . trim($parentXPath . $currentXPath . $attributesXPath, '/');
  155. }
  156. /**
  157. * Append $insertedNode to $contextNode
  158. *
  159. * @param \DOMNode $insertedNode
  160. * @param \DOMNode $contextNode
  161. * @return void
  162. */
  163. private function appendNode(\DOMNode $insertedNode, \DOMNode $contextNode)
  164. {
  165. $importNode = $this->getDom()->importNode($insertedNode, true);
  166. if (in_array($importNode->localName, [Converter::ARGUMENT_KEY, Converter::SETTINGS_KEY])) {
  167. $this->appendNodeToContext($contextNode, $importNode);
  168. } else {
  169. $contextNode->appendChild($importNode);
  170. }
  171. }
  172. /**
  173. * Append node to context node in correct position
  174. *
  175. * @param \DOMNode $contextNode
  176. * @param \DOMNode $importNode
  177. * @return void
  178. */
  179. private function appendNodeToContext(\DOMNode $contextNode, \DOMNode $importNode)
  180. {
  181. if (!$contextNode->hasChildNodes()) {
  182. $contextNode->appendChild($importNode);
  183. return;
  184. }
  185. $childContextNode = null;
  186. /** @var \DOMNode $child */
  187. foreach ($contextNode->childNodes as $child) {
  188. if ($child->nodeType != XML_ELEMENT_NODE) {
  189. continue;
  190. }
  191. switch ($child->localName) {
  192. case Converter::ARGUMENT_KEY:
  193. $childContextNode = $child->nextSibling;
  194. break;
  195. case Converter::SETTINGS_KEY:
  196. $childContextNode = $child;
  197. break;
  198. default:
  199. if (!$childContextNode) {
  200. $childContextNode = $child;
  201. }
  202. break;
  203. }
  204. }
  205. $contextNode->insertBefore($importNode, $childContextNode);
  206. }
  207. }