Diff.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Setup\Declaration\Schema\Diff;
  7. use Magento\Framework\Component\ComponentRegistrar;
  8. use Magento\Framework\Setup\Declaration\Schema\Dto\Constraint;
  9. use Magento\Framework\Setup\Declaration\Schema\Dto\Constraints\Reference;
  10. use Magento\Framework\Setup\Declaration\Schema\Dto\ElementInterface;
  11. use Magento\Framework\Setup\Declaration\Schema\Dto\Index;
  12. use Magento\Framework\Setup\Declaration\Schema\Dto\Table;
  13. use Magento\Framework\Setup\Declaration\Schema\Dto\TableElementInterface;
  14. use Magento\Framework\Setup\Declaration\Schema\ElementHistory;
  15. use Magento\Framework\Setup\Declaration\Schema\ElementHistoryFactory;
  16. use Magento\Framework\Setup\Declaration\Schema\Operations\DropReference;
  17. /**
  18. * Holds information about all changes between 2 schemas: db and declaration XML.
  19. * Holds 2 items:
  20. * - new (Should be changed to)
  21. * - old ()
  22. * @api
  23. * @since 102.0.0
  24. */
  25. class Diff implements DiffInterface
  26. {
  27. /**
  28. * Whitelist file name.
  29. */
  30. const GENERATED_WHITELIST_FILE_NAME = 'db_schema_whitelist.json';
  31. /**
  32. * @var array
  33. */
  34. private $changes;
  35. /**
  36. * This changes created only for debug reasons.
  37. *
  38. * @var array
  39. * @since 102.0.0
  40. */
  41. public $debugChanges;
  42. /**
  43. * @var array
  44. */
  45. private $whiteListTables = [];
  46. /**
  47. * @var ComponentRegistrar
  48. */
  49. private $componentRegistrar;
  50. /**
  51. * @var ElementHistoryFactory
  52. */
  53. private $elementHistoryFactory;
  54. /**
  55. * This indexes is needed to ensure that sort order in which table operations
  56. * will be executed is correct.
  57. *
  58. * @var array
  59. */
  60. private $tableIndexes;
  61. /**
  62. * List of operations that are destructive from the point of declarative setup
  63. * and can make system unstable, for example DropTable.
  64. *
  65. * @var string[]
  66. */
  67. private $destructiveOperations;
  68. /**
  69. * Constructor.
  70. *
  71. * @param ComponentRegistrar $componentRegistrar
  72. * @param ElementHistoryFactory $elementHistoryFactory
  73. * @param array $tableIndexes
  74. * @param array $destructiveOperations
  75. */
  76. public function __construct(
  77. ComponentRegistrar $componentRegistrar,
  78. ElementHistoryFactory $elementHistoryFactory,
  79. array $tableIndexes,
  80. array $destructiveOperations
  81. ) {
  82. $this->componentRegistrar = $componentRegistrar;
  83. $this->elementHistoryFactory = $elementHistoryFactory;
  84. $this->tableIndexes = $tableIndexes;
  85. $this->destructiveOperations = $destructiveOperations;
  86. }
  87. /**
  88. * We return all sorted changes.
  89. *
  90. * All changes are sorted because there are dependencies between tables, like foreign keys.
  91. *
  92. * @inheritdoc
  93. * @since 102.0.0
  94. */
  95. public function getAll()
  96. {
  97. if ($this->changes) {
  98. ksort($this->changes);
  99. }
  100. return $this->changes;
  101. }
  102. /**
  103. * Retrieve all changes for specific table.
  104. *
  105. * @param string $table
  106. * @param string $operation
  107. * @return ElementHistory[]
  108. * @since 102.0.0
  109. */
  110. public function getChange($table, $operation)
  111. {
  112. $tableIndex = $this->tableIndexes[$table];
  113. return $this->changes[$tableIndex][$operation] ?? [];
  114. }
  115. /**
  116. * Retrieve array of whitelisted tables.
  117. * Whitelist tables should have JSON format and should be added through
  118. * CLI command: should be done in next story.
  119. *
  120. * @return array
  121. */
  122. private function getWhiteListTables()
  123. {
  124. if (!$this->whiteListTables) {
  125. foreach ($this->componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $path) {
  126. $whiteListPath = $path . DIRECTORY_SEPARATOR . 'etc' .
  127. DIRECTORY_SEPARATOR . 'db_schema_whitelist.json';
  128. if (file_exists($whiteListPath)) {
  129. $this->whiteListTables = array_replace_recursive(
  130. $this->whiteListTables,
  131. json_decode(file_get_contents($whiteListPath), true)
  132. );
  133. }
  134. }
  135. }
  136. return $this->whiteListTables;
  137. }
  138. /**
  139. * Check whether element can be registered.
  140. *
  141. * For example, if element is not in db_schema_whitelist.json it cant
  142. * be registered due to backward incompatibility
  143. * Extensibility point: if you want to add some dynamic rules of applying or ignoring any schema elements
  144. * you can do this by pluginizing this method
  145. *
  146. * @param ElementInterface | Table $object
  147. * @param string $operation
  148. * @return bool
  149. * @since 102.0.0
  150. */
  151. public function canBeRegistered(ElementInterface $object, $operation): bool
  152. {
  153. if (!isset($this->destructiveOperations[$operation])) {
  154. return true;
  155. }
  156. $checkResult = false;
  157. $whiteList = $this->getWhiteListTables();
  158. if ($object instanceof TableElementInterface) {
  159. $tableNameWithoutPrefix = $object->getTable()->getNameWithoutPrefix();
  160. $type = $object->getElementType();
  161. if ($this->isElementHaveAutoGeneratedName($object)) {
  162. $checkResult =
  163. isset($whiteList[$tableNameWithoutPrefix][$type][$object->getNameWithoutPrefix()]);
  164. } else {
  165. $checkResult = isset($whiteList[$tableNameWithoutPrefix][$type][$object->getName()]);
  166. }
  167. } elseif ($object instanceof Table) {
  168. $checkResult = isset($whiteList[$object->getNameWithoutPrefix()]);
  169. }
  170. return $checkResult;
  171. }
  172. /**
  173. * Check if the element has an auto-generated name.
  174. *
  175. * @param ElementInterface $element
  176. * @return bool
  177. */
  178. private function isElementHaveAutoGeneratedName(ElementInterface $element): bool
  179. {
  180. return in_array($element->getElementType(), [Index::TYPE, Constraint::TYPE], true);
  181. }
  182. /**
  183. * Register DTO object.
  184. *
  185. * @param TableElementInterface $dtoObject
  186. * @inheritdoc
  187. * @since 102.0.0
  188. */
  189. public function register(
  190. ElementInterface $dtoObject,
  191. $operation,
  192. ElementInterface $oldDtoObject = null
  193. ) {
  194. if (!$this->canBeRegistered($dtoObject, $operation)) {
  195. return $this;
  196. }
  197. $historyData = ['new' => $dtoObject, 'old' => $oldDtoObject];
  198. $history = $this->elementHistoryFactory->create($historyData);
  199. //dtoObjects can have 4 types: column, constraint, index, table
  200. $this->changes[$this->findTableIndex($dtoObject, $operation)][$operation][] = $history;
  201. $this->debugChanges[$operation][] = $history;
  202. return $this;
  203. }
  204. /**
  205. * As tables can references to each other, we need to take into account
  206. * that they should goes in specific structure: parent table -> child table
  207. * Also we should take into account, that first of all in any case we need to remove all foreign keys
  208. * from tables and only then modify that tables
  209. *
  210. * @param ElementInterface $element
  211. * @param string $operation
  212. * @return int
  213. */
  214. private function findTableIndex(ElementInterface $element, string $operation) : int
  215. {
  216. $elementName = $element instanceof TableElementInterface ?
  217. $element->getTable()->getName() : $element->getName();
  218. //We use not real tables but table indexes in order to be sure that order of table is correct
  219. $tableIndex = $this->tableIndexes[$elementName] ?? INF;
  220. return $operation === DropReference::OPERATION_NAME ? 0 : (int) $tableIndex;
  221. }
  222. }