SchemaPersistor.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Setup;
  7. use Magento\Framework\Component\ComponentRegistrar;
  8. use Magento\Framework\Setup\Declaration\Schema\Sharding;
  9. /**
  10. * Persist listened schema to db_schema.xml file.
  11. */
  12. class SchemaPersistor
  13. {
  14. /**
  15. * @var ComponentRegistrar
  16. */
  17. private $componentRegistrar;
  18. /**
  19. * @var XmlPersistor
  20. */
  21. private $xmlPersistor;
  22. /**
  23. * @param ComponentRegistrar $componentRegistrar
  24. * @param XmlPersistor $xmlPersistor
  25. */
  26. public function __construct(ComponentRegistrar $componentRegistrar, XmlPersistor $xmlPersistor)
  27. {
  28. $this->componentRegistrar = $componentRegistrar;
  29. $this->xmlPersistor = $xmlPersistor;
  30. }
  31. /**
  32. * Initialize bare DOM XML element.
  33. *
  34. * @return \SimpleXMLElement
  35. */
  36. private function initEmptyDom()
  37. {
  38. return new \SimpleXMLElement(
  39. '<schema
  40. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  41. xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
  42. </schema>'
  43. );
  44. }
  45. /**
  46. * Do persist by modules to db_schema.xml file.
  47. *
  48. * @param SchemaListener $schemaListener
  49. */
  50. public function persist(SchemaListener $schemaListener)
  51. {
  52. foreach ($schemaListener->getTables() as $moduleName => $tablesData) {
  53. $path = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName);
  54. if (empty($path)) {
  55. /** Empty path means that module does not exist */
  56. continue;
  57. }
  58. $schemaPatch = sprintf('%s/etc/db_schema.xml', $path);
  59. $dom = $this->processTables($schemaPatch, $tablesData);
  60. $this->persistModule($dom, $schemaPatch);
  61. }
  62. }
  63. /**
  64. * Convert tables data into XML document.
  65. *
  66. * @param string $schemaPatch
  67. * @param array $tablesData
  68. * @return \SimpleXMLElement
  69. */
  70. private function processTables(string $schemaPatch, array $tablesData): \SimpleXMLElement
  71. {
  72. if (file_exists($schemaPatch)) {
  73. $dom = new \SimpleXMLElement(file_get_contents($schemaPatch));
  74. } else {
  75. $dom = $this->initEmptyDom();
  76. }
  77. $defaultAttributesValues = [
  78. 'resource' => Sharding::DEFAULT_CONNECTION,
  79. ];
  80. foreach ($tablesData as $tableName => $tableData) {
  81. $tableData = $this->handleDefinition($tableData);
  82. $table = $dom->xpath("//table[@name='" . $tableName . "']");
  83. if (!$table) {
  84. $table = $dom->addChild('table');
  85. $table->addAttribute('name', $tableName);
  86. } else {
  87. $table = reset($table);
  88. }
  89. $attributeNames = ['disabled', 'resource', 'engine', 'comment'];
  90. foreach ($attributeNames as $attributeName) {
  91. $this->updateElementAttribute(
  92. $table,
  93. $attributeName,
  94. $tableData,
  95. $defaultAttributesValues[$attributeName] ?? null
  96. );
  97. }
  98. $this->processColumns($tableData, $table);
  99. $this->processConstraints($tableData, $table);
  100. $this->processIndexes($tableData, $table);
  101. }
  102. return $dom;
  103. }
  104. /**
  105. * Update element attribute value or create new attribute.
  106. *
  107. * @param \SimpleXMLElement $element
  108. * @param string $attributeName
  109. * @param array $elementData
  110. * @param string|null $defaultValue
  111. */
  112. private function updateElementAttribute(
  113. \SimpleXMLElement $element,
  114. string $attributeName,
  115. array $elementData,
  116. ?string $defaultValue = null
  117. ) {
  118. $attributeValue = $elementData[$attributeName] ?? $defaultValue;
  119. if ($attributeValue !== null) {
  120. if (is_bool($attributeValue)) {
  121. $attributeValue = $this->castBooleanToString($attributeValue);
  122. }
  123. if ($element->attributes()[$attributeName]) {
  124. $element->attributes()->$attributeName = $attributeValue;
  125. } else {
  126. $element->addAttribute($attributeName, $attributeValue);
  127. }
  128. }
  129. }
  130. /**
  131. * If disabled attribute is set to false it remove it at all.
  132. *
  133. * Also handle other generic attributes.
  134. *
  135. * @param array $definition
  136. * @return array
  137. */
  138. private function handleDefinition(array $definition)
  139. {
  140. if (isset($definition['disabled']) && !$definition['disabled']) {
  141. unset($definition['disabled']);
  142. }
  143. return $definition;
  144. }
  145. /**
  146. * Cast boolean types to string.
  147. *
  148. * @param bool $boolean
  149. * @return string
  150. */
  151. private function castBooleanToString($boolean)
  152. {
  153. return $boolean ? 'true' : 'false';
  154. }
  155. /**
  156. * Convert columns from array to XML format.
  157. *
  158. * @param array $tableData
  159. * @param \SimpleXMLElement $table
  160. * @return \SimpleXMLElement
  161. */
  162. private function processColumns(array $tableData, \SimpleXMLElement $table)
  163. {
  164. if (!isset($tableData['columns'])) {
  165. return $table;
  166. }
  167. foreach ($tableData['columns'] as $columnName => $columnData) {
  168. $columnData = $this->handleDefinition($columnData);
  169. $domColumn = $table->xpath("column[@name='" . $columnName . "']");
  170. if (!$domColumn) {
  171. $domColumn = $table->addChild('column');
  172. if (!empty($columnData['xsi:type'])) {
  173. $domColumn->addAttribute('xsi:type', $columnData['xsi:type'], 'xsi');
  174. }
  175. $domColumn->addAttribute('name', $columnName);
  176. } else {
  177. $domColumn = reset($domColumn);
  178. }
  179. $attributeNames = array_diff(array_keys($columnData), ['name', 'xsi:type']);
  180. foreach ($attributeNames as $attributeName) {
  181. $this->updateElementAttribute(
  182. $domColumn,
  183. $attributeName,
  184. $columnData
  185. );
  186. }
  187. }
  188. return $table;
  189. }
  190. /**
  191. * Convert columns from array to XML format.
  192. *
  193. * @param array $tableData
  194. * @param \SimpleXMLElement $table
  195. * @return \SimpleXMLElement
  196. */
  197. private function processIndexes(array $tableData, \SimpleXMLElement $table)
  198. {
  199. if (isset($tableData['indexes'])) {
  200. foreach ($tableData['indexes'] as $indexName => $indexData) {
  201. $indexData = $this->handleDefinition($indexData);
  202. $domIndex = $table->xpath("index[@referenceId='" . $indexName . "']");
  203. if (!$domIndex) {
  204. $domIndex = $this->getUniqueIndexByName($table, $indexName);
  205. }
  206. if (!$domIndex) {
  207. $domIndex = $table->addChild('index');
  208. $domIndex->addAttribute('referenceId', $indexName);
  209. } elseif (is_array($domIndex)) {
  210. $domIndex = reset($domIndex);
  211. }
  212. $attributeNames = array_diff(array_keys($indexData), ['referenceId', 'columns', 'name']);
  213. foreach ($attributeNames as $attributeName) {
  214. $this->updateElementAttribute(
  215. $domIndex,
  216. $attributeName,
  217. $indexData
  218. );
  219. }
  220. if (!empty($indexData['columns'])) {
  221. foreach ($indexData['columns'] as $column) {
  222. $columnXml = $domIndex->addChild('column');
  223. $columnXml->addAttribute('name', $column);
  224. }
  225. }
  226. }
  227. }
  228. return $table;
  229. }
  230. /**
  231. * Convert constraints from array to XML format.
  232. *
  233. * @param array $tableData
  234. * @param \SimpleXMLElement $table
  235. * @return \SimpleXMLElement
  236. */
  237. private function processConstraints(array $tableData, \SimpleXMLElement $table)
  238. {
  239. if (!isset($tableData['constraints'])) {
  240. return $table;
  241. }
  242. foreach ($tableData['constraints'] as $constraintType => $constraints) {
  243. foreach ($constraints as $constraintName => $constraintData) {
  244. $constraintData = $this->handleDefinition($constraintData);
  245. $domConstraint = $table->xpath("constraint[@referenceId='" . $constraintName . "']");
  246. if (!$domConstraint) {
  247. $domConstraint = $table->addChild('constraint');
  248. $domConstraint->addAttribute('xsi:type', $constraintType, 'xsi');
  249. $domConstraint->addAttribute('referenceId', $constraintName);
  250. } else {
  251. $domConstraint = reset($domConstraint);
  252. }
  253. $attributeNames = array_diff(
  254. array_keys($constraintData),
  255. ['referenceId', 'xsi:type', 'disabled', 'columns', 'name', 'type']
  256. );
  257. foreach ($attributeNames as $attributeName) {
  258. $this->updateElementAttribute(
  259. $domConstraint,
  260. $attributeName,
  261. $constraintData
  262. );
  263. }
  264. if (!empty($constraintData['columns'])) {
  265. foreach ($constraintData['columns'] as $column) {
  266. $columnXml = $domConstraint->addChild('column');
  267. $columnXml->addAttribute('name', $column);
  268. }
  269. }
  270. if (!empty($constraintData['disabled'])) {
  271. $this->updateElementAttribute(
  272. $domConstraint,
  273. 'disabled',
  274. $constraintData
  275. );
  276. }
  277. }
  278. }
  279. return $table;
  280. }
  281. /**
  282. * Do schema persistence to specific module.
  283. *
  284. * @param \SimpleXMLElement $simpleXmlElementDom
  285. * @param string $path
  286. * @return void
  287. */
  288. private function persistModule(\SimpleXMLElement $simpleXmlElementDom, $path)
  289. {
  290. $this->xmlPersistor->persist($simpleXmlElementDom, $path);
  291. }
  292. /**
  293. * Retrieve unique index declaration by name.
  294. *
  295. * @param \SimpleXMLElement $table
  296. * @param string $indexName
  297. * @return \SimpleXMLElement|null
  298. */
  299. private function getUniqueIndexByName(\SimpleXMLElement $table, string $indexName): ?\SimpleXMLElement
  300. {
  301. $indexElement = null;
  302. $constraint = $table->xpath("constraint[@referenceId='" . $indexName . "']");
  303. if ($constraint) {
  304. $constraint = reset($constraint);
  305. $type = $constraint->attributes('xsi', true)->type;
  306. if ($type == 'unique') {
  307. $indexElement = $constraint;
  308. }
  309. }
  310. return $indexElement;
  311. }
  312. }