AbstractPlatform.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <?php
  2. namespace Michel\PaperORM\Platform;
  3. use LogicException;
  4. use Michel\PaperORM\Collection\ObjectStorage;
  5. use Michel\PaperORM\Entity\EntityInterface;
  6. use Michel\PaperORM\Mapper\EntityMapper;
  7. use Michel\PaperORM\Mapping\Column\Column;
  8. use Michel\PaperORM\Mapping\Column\JoinColumn;
  9. use Michel\PaperORM\Mapping\Column\PrimaryKeyColumn;
  10. use Michel\PaperORM\Mapping\Index;
  11. use Michel\PaperORM\Metadata\ColumnMetadata;
  12. use Michel\PaperORM\Metadata\DatabaseSchemaDiffMetadata;
  13. use Michel\PaperORM\Metadata\ForeignKeyMetadata;
  14. use Michel\PaperORM\Metadata\IndexMetadata;
  15. abstract class AbstractPlatform implements PlatformInterface
  16. {
  17. final public function mapColumnsToMetadata(string $tableName, $columns): array
  18. {
  19. $columnsMetadata = [];
  20. foreach ($columns as $column) {
  21. if (!$column instanceof Column) {
  22. throw new LogicException(sprintf("The column '%s' is not supported.", is_object($column) ? get_class($column) : gettype($column)));
  23. }
  24. $columnsMetadata[] = $this->mapColumnToMetadata($tableName, $column);
  25. }
  26. return $columnsMetadata;
  27. }
  28. final public function mapColumnToMetadata(string $tableName, Column $column): ColumnMetadata
  29. {
  30. $mappings = $this->getColumnTypeMappings();
  31. $className = get_class($column);
  32. if (!array_key_exists($className, $mappings)) {
  33. throw new LogicException(sprintf("The column type '%s' is not supported.", $className));
  34. }
  35. $mapping = $mappings[$className];
  36. $sqlType = $mapping['type'];
  37. $args = $mapping['args'];
  38. $foreignKeyMetadata = null;
  39. if ($this->getSchema()->supportsForeignKeyConstraints() && $column instanceof JoinColumn) {
  40. $targetEntity = $column->getTargetEntity();
  41. if (is_subclass_of($targetEntity, EntityInterface::class)) {
  42. $referenceTable = EntityMapper::getTable($targetEntity);
  43. $foreignKeyMetadata = ForeignKeyMetadata::fromArray([
  44. 'name' => $this->generateForeignKeyName($tableName, [$column->getName()]),
  45. 'columns' => [$column->getName()],
  46. 'referenceTable' => $referenceTable,
  47. 'referenceColumns' => [$column->getReferencedColumnName()],
  48. 'onDelete' => $column->getOnDelete(),
  49. 'onUpdate' => $column->getOnUpdate(),
  50. ]);
  51. }
  52. }
  53. return ColumnMetadata::fromColumn($column, $sqlType, $foreignKeyMetadata, $args[0] ?? null, $args[1] ?? null);
  54. }
  55. /**
  56. * @param string $tableName
  57. * @param array<Column> $columns
  58. * @param array<Index> $indexes
  59. * @return DatabaseSchemaDiffMetadata
  60. */
  61. final public function diff(string $tableName, array $columns, array $indexes): DatabaseSchemaDiffMetadata
  62. {
  63. foreach ($columns as $column) {
  64. if ($column->isUnique() && $this->autoCreateIndexUniqueColumns()) {
  65. $indexes[] = new Index([$column->getName()], true);
  66. } elseif ($column instanceof JoinColumn && $this->autoCreateIndexJoinColumns()) {
  67. $indexes[] = new Index([$column->getName()], $column->isUnique());
  68. } elseif ($column instanceof PrimaryKeyColumn && $this->autoCreateIndexPrimaryKeys()) {
  69. $indexes[] = new Index([$column->getName()], true);
  70. }
  71. }
  72. list(
  73. $columnsToAdd,
  74. $columnsToUpdate,
  75. $columnsToDrop,
  76. $originalColumns,
  77. $foreignKeyToAdd,
  78. $foreignKeyToDrop,
  79. $originalForeignKeys,
  80. ) = $this->diffColumns($tableName, $columns);
  81. list($indexesToAdd, $indexesToUpdate, $indexesToDrop, $originalIndexes) = $this->diffIndexes($tableName, $indexes);
  82. return new DatabaseSchemaDiffMetadata(
  83. $columnsToAdd,
  84. $columnsToUpdate,
  85. $columnsToDrop,
  86. $originalColumns,
  87. $foreignKeyToAdd,
  88. $foreignKeyToDrop,
  89. $originalForeignKeys,
  90. $indexesToAdd,
  91. $indexesToUpdate,
  92. $indexesToDrop,
  93. $originalIndexes
  94. );
  95. }
  96. /**
  97. * @param string $tableName
  98. * @param array<Column> $columns
  99. * @return array
  100. *
  101. */
  102. private function diffColumns(string $tableName, array $columns): array
  103. {
  104. $columnsFromTable = $this->listTableColumns($tableName);
  105. $columnsExisting = [];
  106. $foreignKeysExisting = [];
  107. foreach ($columnsFromTable as $columnMetadata) {
  108. $columnsExisting[$columnMetadata->getName()] = $columnMetadata;
  109. if ($columnMetadata->getForeignKeyMetadata()) {
  110. $foreignKeysExisting[$columnMetadata->getForeignKeyMetadata()->getName()] = $columnMetadata->getForeignKeyMetadata();
  111. }
  112. }
  113. $columnsToAdd = [];
  114. $columnsToUpdate = [];
  115. $columnsToDrop = [];
  116. $foreignKeyToAdd = [];
  117. $foreignKeyToDrop = [];
  118. $columnsProcessed = [];
  119. $foreignKeysProcessed = [];
  120. foreach ($columns as $column) {
  121. $columnMetadata = $this->mapColumnToMetadata($tableName, $column);
  122. $willBeUpdated = false;
  123. if (isset($columnsExisting[$columnMetadata->getName()])) {
  124. $columnFromTable = $columnsExisting[$columnMetadata->getName()];
  125. if ($columnFromTable->toArray() != $columnMetadata->toArray()) {
  126. $columnsToUpdate[] = $columnMetadata;
  127. $willBeUpdated = true;
  128. }
  129. } else {
  130. $columnsToAdd[] = $columnMetadata;
  131. }
  132. $columnsProcessed[] = $columnMetadata->getName();
  133. if ($columnMetadata->getForeignKeyMetadata()) {
  134. $columnForeignKey = $columnMetadata->getForeignKeyMetadata();
  135. $foreignKeyName = $columnForeignKey->getName();
  136. if (isset($foreignKeysExisting[$foreignKeyName])) {
  137. if ($willBeUpdated || $foreignKeysExisting[$foreignKeyName]->toArray() != $columnForeignKey->toArray()) {
  138. $foreignKeyToDrop[] = $foreignKeysExisting[$foreignKeyName];
  139. $foreignKeyToAdd[] = $columnForeignKey;
  140. }
  141. } else {
  142. $foreignKeyToAdd[] = $columnForeignKey;
  143. }
  144. $foreignKeysProcessed[$foreignKeyName] = true;
  145. }
  146. }
  147. foreach ($columnsExisting as $columnMetadata) {
  148. $willDrop = !in_array($columnMetadata->getName(), $columnsProcessed);
  149. if ($willDrop) {
  150. $columnsToDrop[] = $columnMetadata;
  151. }
  152. if ($columnMetadata->getForeignKeyMetadata()) {
  153. $columnForeignKey = $columnMetadata->getForeignKeyMetadata();
  154. $foreignKeyName = $columnForeignKey->getName();
  155. if (($willDrop && isset($foreignKeysExisting[$foreignKeyName])) || !isset($foreignKeysProcessed[$foreignKeyName])) {
  156. $foreignKeyToDrop[] = $columnForeignKey;
  157. }
  158. }
  159. }
  160. $foreignKeyToAdd = array_values(array_unique($foreignKeyToAdd, SORT_REGULAR));
  161. $foreignKeyToDrop = array_values(array_unique($foreignKeyToDrop, SORT_REGULAR));
  162. return [
  163. $columnsToAdd,
  164. $columnsToUpdate,
  165. $columnsToDrop,
  166. $columnsFromTable,
  167. $foreignKeyToAdd,
  168. $foreignKeyToDrop,
  169. array_values($foreignKeysExisting),
  170. ];
  171. }
  172. /**
  173. * @param string $tableName
  174. * @param array<Index> $indexes
  175. * @return array
  176. */
  177. private function diffIndexes(string $tableName, array $indexes): array
  178. {
  179. if ($this->getSchema()->supportsIndexes() === false) {
  180. return [[], [], [], []];
  181. }
  182. $indexesFromTable = new ObjectStorage($this->listTableIndexes($tableName));
  183. $indexesToAdd = [];
  184. $indexesToUpdate = [];
  185. $indexesToDrop = [];
  186. $indexesExisting = [];
  187. foreach ($indexes as $index) {
  188. $indexMetadata = new IndexMetadata(
  189. $tableName,
  190. $index->getName() ?: $this->generateIndexName($tableName, $index->getColumns(), $index->isUnique()),
  191. $index->getColumns(),
  192. $index->isUnique()
  193. );
  194. $indexFound = $indexesFromTable->findOneByMethod('getName', $indexMetadata->getName());
  195. if ($indexFound) {
  196. if ($indexMetadata->toArray() != $indexFound->toArray()) {
  197. $indexesToUpdate[] = $indexMetadata;
  198. }
  199. } else {
  200. $indexesToAdd[] = $indexMetadata;
  201. }
  202. $indexesExisting[] = $indexMetadata->getName();
  203. }
  204. foreach ($indexesFromTable as $index) {
  205. if (!in_array($index->getName(), $indexesExisting)) {
  206. $indexesToDrop[] = $index;
  207. }
  208. }
  209. return [$indexesToAdd, $indexesToUpdate, $indexesToDrop, $indexesFromTable->toArray()];
  210. }
  211. final protected function generateIndexName(string $tableName, array $columnNames, bool $unique): string
  212. {
  213. $hash = implode('', array_map(static function ($column) {
  214. return dechex(crc32($column));
  215. }, array_merge([$tableName], $columnNames)));
  216. $prefix = $unique ? $this->getPrefixUniqIndexName() : $this->getPrefixIndexName();
  217. return strtoupper(substr($prefix . $hash, 0, $this->getMaxLength()));
  218. }
  219. final protected function generateForeignKeyName(string $tableName, array $columnNames): string
  220. {
  221. $hash = implode('', array_map(static function ($column) {
  222. return dechex(crc32($column));
  223. }, array_merge([$tableName], $columnNames)));
  224. return strtoupper(substr($this->getPrefixForeignKeyName() . $hash, 0, $this->getMaxLength()));
  225. }
  226. }