Revision 148 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?phpnamespace Doctrine\DBAL\Schema;use BadMethodCallException;use Doctrine\DBAL\Exception;use Doctrine\DBAL\Platforms\AbstractPlatform;use Doctrine\DBAL\Types;use Doctrine\Deprecations\Deprecation;use function array_intersect_key;use function array_key_exists;use function array_keys;use function array_map;use function array_merge;use function array_unique;use function assert;use function count;use function get_class;use function sprintf;use function strtolower;/*** Compares two Schemas and return an instance of SchemaDiff.** @method SchemaDiff compareSchemas(Schema $fromSchema, Schema $toSchema)*/class Comparator{private ?AbstractPlatform $platform;/** @internal The comparator can be only instantiated by a schema manager. */public function __construct(?AbstractPlatform $platform = null){if ($platform === null) {Deprecation::triggerIfCalledFromOutside('doctrine/dbal','https://github.com/doctrine/dbal/pull/4746','Not passing a $platform to %s is deprecated.'. ' Use AbstractSchemaManager::createComparator() to instantiate the comparator.',__METHOD__,);}$this->platform = $platform;}/** @param list<mixed> $args */public function __call(string $method, array $args): SchemaDiff{if ($method !== 'compareSchemas') {throw new BadMethodCallException(sprintf('Unknown method "%s"', $method));}return $this->doCompareSchemas(...$args);}/** @param list<mixed> $args */public static function __callStatic(string $method, array $args): SchemaDiff{if ($method !== 'compareSchemas') {throw new BadMethodCallException(sprintf('Unknown method "%s"', $method));}Deprecation::trigger('doctrine/dbal','https://github.com/doctrine/dbal/pull/4707','Calling %s::%s() statically is deprecated.',self::class,$method,);$comparator = new self();return $comparator->doCompareSchemas(...$args);}/*** Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema.** This method should be called non-statically since it will be declared as non-static in the next major release.** @return SchemaDiff** @throws SchemaException*/private function doCompareSchemas(Schema $fromSchema,Schema $toSchema) {$createdSchemas = [];$droppedSchemas = [];$createdTables = [];$alteredTables = [];$droppedTables = [];$createdSequences = [];$alteredSequences = [];$droppedSequences = [];$orphanedForeignKeys = [];$foreignKeysToTable = [];foreach ($toSchema->getNamespaces() as $namespace) {if ($fromSchema->hasNamespace($namespace)) {continue;}$createdSchemas[$namespace] = $namespace;}foreach ($fromSchema->getNamespaces() as $namespace) {if ($toSchema->hasNamespace($namespace)) {continue;}$droppedSchemas[$namespace] = $namespace;}foreach ($toSchema->getTables() as $table) {$tableName = $table->getShortestName($toSchema->getName());if (! $fromSchema->hasTable($tableName)) {$createdTables[$tableName] = $toSchema->getTable($tableName);} else {$tableDifferences = $this->diffTable($fromSchema->getTable($tableName),$toSchema->getTable($tableName),);if ($tableDifferences !== false) {$alteredTables[$tableName] = $tableDifferences;}}}/* Check if there are tables removed */foreach ($fromSchema->getTables() as $table) {$tableName = $table->getShortestName($fromSchema->getName());$table = $fromSchema->getTable($tableName);if (! $toSchema->hasTable($tableName)) {$droppedTables[$tableName] = $table;}// also remember all foreign keys that point to a specific tableforeach ($table->getForeignKeys() as $foreignKey) {$foreignTable = strtolower($foreignKey->getForeignTableName());if (! isset($foreignKeysToTable[$foreignTable])) {$foreignKeysToTable[$foreignTable] = [];}$foreignKeysToTable[$foreignTable][] = $foreignKey;}}foreach ($droppedTables as $tableName => $table) {if (! isset($foreignKeysToTable[$tableName])) {continue;}foreach ($foreignKeysToTable[$tableName] as $foreignKey) {if (isset($droppedTables[strtolower($foreignKey->getLocalTableName())])) {continue;}$orphanedForeignKeys[] = $foreignKey;}// deleting duplicated foreign keys present on both on the orphanedForeignKey// and the removedForeignKeys from changedTablesforeach ($foreignKeysToTable[$tableName] as $foreignKey) {// strtolower the table name to make if compatible with getShortestName$localTableName = strtolower($foreignKey->getLocalTableName());if (! isset($alteredTables[$localTableName])) {continue;}foreach ($alteredTables[$localTableName]->getDroppedForeignKeys() as $droppedForeignKey) {assert($droppedForeignKey instanceof ForeignKeyConstraint);// We check if the key is from the removed table if not we skip.if ($tableName !== strtolower($droppedForeignKey->getForeignTableName())) {continue;}$alteredTables[$localTableName]->unsetDroppedForeignKey($droppedForeignKey);}}}foreach ($toSchema->getSequences() as $sequence) {$sequenceName = $sequence->getShortestName($toSchema->getName());if (! $fromSchema->hasSequence($sequenceName)) {if (! $this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) {$createdSequences[] = $sequence;}} else {if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) {$alteredSequences[] = $toSchema->getSequence($sequenceName);}}}foreach ($fromSchema->getSequences() as $sequence) {if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) {continue;}$sequenceName = $sequence->getShortestName($fromSchema->getName());if ($toSchema->hasSequence($sequenceName)) {continue;}$droppedSequences[] = $sequence;}$diff = new SchemaDiff($createdTables,$alteredTables,$droppedTables,$fromSchema,$createdSchemas,$droppedSchemas,$createdSequences,$alteredSequences,$droppedSequences,);$diff->orphanedForeignKeys = $orphanedForeignKeys;return $diff;}/*** @deprecated Use non-static call to {@see compareSchemas()} instead.** @return SchemaDiff** @throws SchemaException*/public function compare(Schema $fromSchema, Schema $toSchema){Deprecation::trigger('doctrine/dbal','https://github.com/doctrine/dbal/pull/4707','Method compare() is deprecated. Use a non-static call to compareSchemas() instead.',);return $this->compareSchemas($fromSchema, $toSchema);}/*** @param Schema $schema* @param Sequence $sequence*/private function isAutoIncrementSequenceInSchema($schema, $sequence): bool{foreach ($schema->getTables() as $table) {if ($sequence->isAutoIncrementsFor($table)) {return true;}}return false;}/** @return bool */public function diffSequence(Sequence $sequence1, Sequence $sequence2){if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {return true;}return $sequence1->getInitialValue() !== $sequence2->getInitialValue();}/*** Returns the difference between the tables $fromTable and $toTable.** If there are no differences this method returns the boolean false.** @deprecated Use {@see compareTables()} and, optionally, {@see TableDiff::isEmpty()} instead.** @return TableDiff|false** @throws Exception*/public function diffTable(Table $fromTable, Table $toTable){Deprecation::triggerIfCalledFromOutside('doctrine/dbal','https://github.com/doctrine/dbal/pull/5770','%s is deprecated. Use compareTables() instead.',__METHOD__,);$diff = $this->compareTables($fromTable, $toTable);if ($diff->isEmpty()) {return false;}return $diff;}/*** Compares the tables and returns the difference between them.** @throws Exception*/public function compareTables(Table $fromTable, Table $toTable): TableDiff{$addedColumns = [];$modifiedColumns = [];$droppedColumns = [];$addedIndexes = [];$modifiedIndexes = [];$droppedIndexes = [];$addedForeignKeys = [];$modifiedForeignKeys = [];$droppedForeignKeys = [];$fromTableColumns = $fromTable->getColumns();$toTableColumns = $toTable->getColumns();/* See if all the columns in "from" table exist in "to" table */foreach ($toTableColumns as $columnName => $column) {if ($fromTable->hasColumn($columnName)) {continue;}$addedColumns[$columnName] = $column;}/* See if there are any removed columns in "to" table */foreach ($fromTableColumns as $columnName => $column) {// See if column is removed in "to" table.if (! $toTable->hasColumn($columnName)) {$droppedColumns[$columnName] = $column;continue;}$toColumn = $toTable->getColumn($columnName);// See if column has changed properties in "to" table.$changedProperties = $this->diffColumn($column, $toColumn);if ($this->platform !== null) {if ($this->columnsEqual($column, $toColumn)) {continue;}} elseif (count($changedProperties) === 0) {continue;}$modifiedColumns[$column->getName()] = new ColumnDiff($column->getName(),$toColumn,$changedProperties,$column,);}$renamedColumns = $this->detectRenamedColumns($addedColumns, $droppedColumns);$fromTableIndexes = $fromTable->getIndexes();$toTableIndexes = $toTable->getIndexes();/* See if all the indexes in "from" table exist in "to" table */foreach ($toTableIndexes as $indexName => $index) {if (($index->isPrimary() && $fromTable->getPrimaryKey() !== null) || $fromTable->hasIndex($indexName)) {continue;}$addedIndexes[$indexName] = $index;}/* See if there are any removed indexes in "to" table */foreach ($fromTableIndexes as $indexName => $index) {// See if index is removed in "to" table.if (($index->isPrimary() && $toTable->getPrimaryKey() === null) ||! $index->isPrimary() && ! $toTable->hasIndex($indexName)) {$droppedIndexes[$indexName] = $index;continue;}// See if index has changed in "to" table.$toTableIndex = $index->isPrimary() ? $toTable->getPrimaryKey() : $toTable->getIndex($indexName);assert($toTableIndex instanceof Index);if (! $this->diffIndex($index, $toTableIndex)) {continue;}$modifiedIndexes[$indexName] = $toTableIndex;}$renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes);$fromForeignKeys = $fromTable->getForeignKeys();$toForeignKeys = $toTable->getForeignKeys();foreach ($fromForeignKeys as $fromKey => $fromConstraint) {foreach ($toForeignKeys as $toKey => $toConstraint) {if ($this->diffForeignKey($fromConstraint, $toConstraint) === false) {unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]);} else {if (strtolower($fromConstraint->getName()) === strtolower($toConstraint->getName())) {$modifiedForeignKeys[] = $toConstraint;unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]);}}}}foreach ($fromForeignKeys as $fromConstraint) {$droppedForeignKeys[] = $fromConstraint;}foreach ($toForeignKeys as $toConstraint) {$addedForeignKeys[] = $toConstraint;}return new TableDiff($toTable->getName(),$addedColumns,$modifiedColumns,$droppedColumns,$addedIndexes,$modifiedIndexes,$droppedIndexes,$fromTable,$addedForeignKeys,$modifiedForeignKeys,$droppedForeignKeys,$renamedColumns,$renamedIndexes,);}/*** Try to find columns that only changed their name, rename operations maybe cheaper than add/drop* however ambiguities between different possibilities should not lead to renaming at all.** @param array<string,Column> $addedColumns* @param array<string,Column> $removedColumns** @return array<string,Column>** @throws Exception*/private function detectRenamedColumns(array &$addedColumns, array &$removedColumns): array{$candidatesByName = [];foreach ($addedColumns as $addedColumnName => $addedColumn) {foreach ($removedColumns as $removedColumn) {if (! $this->columnsEqual($addedColumn, $removedColumn)) {continue;}$candidatesByName[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName];}}$renamedColumns = [];foreach ($candidatesByName as $candidates) {if (count($candidates) !== 1) {continue;}[$removedColumn, $addedColumn] = $candidates[0];$removedColumnName = $removedColumn->getName();$addedColumnName = strtolower($addedColumn->getName());if (isset($renamedColumns[$removedColumnName])) {continue;}$renamedColumns[$removedColumnName] = $addedColumn;unset($addedColumns[$addedColumnName],$removedColumns[strtolower($removedColumnName)],);}return $renamedColumns;}/*** Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop* however ambiguities between different possibilities should not lead to renaming at all.** @param array<string,Index> $addedIndexes* @param array<string,Index> $removedIndexes** @return array<string,Index>*/private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array{$candidatesByName = [];// Gather possible rename candidates by comparing each added and removed index based on semantics.foreach ($addedIndexes as $addedIndexName => $addedIndex) {foreach ($removedIndexes as $removedIndex) {if ($this->diffIndex($addedIndex, $removedIndex)) {continue;}$candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName];}}$renamedIndexes = [];foreach ($candidatesByName as $candidates) {// If the current rename candidate contains exactly one semantically equal index,// we can safely rename it.// Otherwise, it is unclear if a rename action is really intended,// therefore we let those ambiguous indexes be added/dropped.if (count($candidates) !== 1) {continue;}[$removedIndex, $addedIndex] = $candidates[0];$removedIndexName = strtolower($removedIndex->getName());$addedIndexName = strtolower($addedIndex->getName());if (isset($renamedIndexes[$removedIndexName])) {continue;}$renamedIndexes[$removedIndexName] = $addedIndex;unset($addedIndexes[$addedIndexName],$removedIndexes[$removedIndexName],);}return $renamedIndexes;}/*** @internal The method should be only used from within the {@see Comparator} class hierarchy.** @return bool*/public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2){if (array_map('strtolower', $key1->getUnquotedLocalColumns())!== array_map('strtolower', $key2->getUnquotedLocalColumns())) {return true;}if (array_map('strtolower', $key1->getUnquotedForeignColumns())!== array_map('strtolower', $key2->getUnquotedForeignColumns())) {return true;}if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {return true;}if ($key1->onUpdate() !== $key2->onUpdate()) {return true;}return $key1->onDelete() !== $key2->onDelete();}/*** Compares the definitions of the given columns** @internal The method should be only used from within the {@see Comparator} class hierarchy.** @throws Exception*/public function columnsEqual(Column $column1, Column $column2): bool{if ($this->platform === null) {return $this->diffColumn($column1, $column2) === [];}return $this->platform->columnsEqual($column1, $column2);}/*** Returns the difference between the columns** If there are differences this method returns the changed properties as a* string array, otherwise an empty array gets returned.** @deprecated Use {@see columnsEqual()} instead.** @return string[]*/public function diffColumn(Column $column1, Column $column2){Deprecation::triggerIfCalledFromOutside('doctrine/dbal','https://github.com/doctrine/dbal/pull/5650','%s is deprecated. Use diffTable() instead.',__METHOD__,);$properties1 = $column1->toArray();$properties2 = $column2->toArray();$changedProperties = [];if (get_class($properties1['type']) !== get_class($properties2['type'])) {$changedProperties[] = 'type';}foreach (['notnull', 'unsigned', 'autoincrement'] as $property) {if ($properties1[$property] === $properties2[$property]) {continue;}$changedProperties[] = $property;}// Null values need to be checked additionally as they tell whether to create or drop a default value.// null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation.if (($properties1['default'] === null) !== ($properties2['default'] === null)|| $properties1['default'] != $properties2['default']) {$changedProperties[] = 'default';}if (($properties1['type'] instanceof Types\StringType && ! $properties1['type'] instanceof Types\GuidType) ||$properties1['type'] instanceof Types\BinaryType) {// check if value of length is set at all, default value assumed otherwise.$length1 = $properties1['length'] ?? 255;$length2 = $properties2['length'] ?? 255;if ($length1 !== $length2) {$changedProperties[] = 'length';}if ($properties1['fixed'] !== $properties2['fixed']) {$changedProperties[] = 'fixed';}} elseif ($properties1['type'] instanceof Types\DecimalType) {if (($properties1['precision'] ?? 10) !== ($properties2['precision'] ?? 10)) {$changedProperties[] = 'precision';}if ($properties1['scale'] !== $properties2['scale']) {$changedProperties[] = 'scale';}}// A null value and an empty string are actually equal for a comment so they should not trigger a change.if ($properties1['comment'] !== $properties2['comment'] &&! ($properties1['comment'] === null && $properties2['comment'] === '') &&! ($properties2['comment'] === null && $properties1['comment'] === '')) {$changedProperties[] = 'comment';}$customOptions1 = $column1->getCustomSchemaOptions();$customOptions2 = $column2->getCustomSchemaOptions();foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) {if (! array_key_exists($key, $properties1) || ! array_key_exists($key, $properties2)) {$changedProperties[] = $key;} elseif ($properties1[$key] !== $properties2[$key]) {$changedProperties[] = $key;}}$platformOptions1 = $column1->getPlatformOptions();$platformOptions2 = $column2->getPlatformOptions();foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) {if ($properties1[$key] === $properties2[$key]) {continue;}$changedProperties[] = $key;}return array_unique($changedProperties);}/*** Finds the difference between the indexes $index1 and $index2.** Compares $index1 with $index2 and returns true if there are any* differences or false in case there are no differences.** @internal The method should be only used from within the {@see Comparator} class hierarchy.** @return bool*/public function diffIndex(Index $index1, Index $index2){return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1));}}