columnGenerator = $columnGenerator; $this->indexGenerator = $indexGenerator; $this->tableOptionsGenerator = $tableOptionsGenerator; $this->constraintGenerator = $constraintLineGenerator; } /** * @param ColumnCollection $columnCollection * @param IndexCollection $indexCollection * * @throws Exception * @return string */ private function generateTableColumns(ColumnCollection $columnCollection, IndexCollection $indexCollection): string { $columnDefinitions = []; $hasAutoIncrement = $columnCollection->hasAutoIncrement(); $autoIncrementColumn = $columnCollection->getAutoIncrementColumn(); foreach ($columnCollection as $column) { $columnDefinitions[] = $this->columnGenerator->generateLine($column); $isFieldAsAutoIncrement = $hasAutoIncrement === true && $autoIncrementColumn->getField( ) === $column->getField(); if ($indexCollection !== null && $isFieldAsAutoIncrement === true && $indexCollection->hasPrimaryKey( ) === false) { $primaryRef = $column->getField(); $indexCollection->add(new Primary([$primaryRef])); $columnCollection->getIterator()->rewind(); } } return implode(",\n", $columnDefinitions); } /** * @param IndexCollection $indexCollection * @param ColumnCollection $targetColumns * * @throws EscapingException * * @return string */ private function generateTableIndexes(IndexCollection $indexCollection, ColumnCollection $targetColumns): string { $asIndexes = []; foreach ($indexCollection as $tableIndex) { /** @var IndexInterface $tableIndex */ if ($this->hasIndexReferencesInSchema($tableIndex->getReferences(), $targetColumns)) { $asIndexes [] = $this->indexGenerator->generateLine($tableIndex); } } return implode(",\n", $asIndexes); } /** * @param TableOptionCollection $tableOptionCollection * * @throws EscapingException * * @return string */ private function generateTableOptions(TableOptionCollection $tableOptionCollection): string { $tableOptions = []; foreach ($tableOptionCollection as $tableOption) { /** @var TableOption $tableOption */ $tableOptions[$tableOption->getOption()] = $tableOption->getValue(); } return $this->tableOptionsGenerator->generate($tableOptions); } /** * @inheritDoc */ public function loadFromTable(string $table): TableSchema { $tableSchema = new TableSchema($table); if ($columns = $this->columnGenerator->fetchColumnsFromDb($table)) { foreach ($columns as $column) { $type = strtolower($column['type']); $type = $type === 'int' ? 'integer' : $type; //Cannot use 'Int' as class name as it is reserved $type = $type === 'float' ? 'double' : $type; //Cannot use 'Float' as class name as it is reserved $classType = '\\Xentral\\Components\\SchemaCreator\\Type\\' . ucfirst($type); if (!class_exists($classType)) { throw new SchemaCreatorInvalidArgumentException(sprintf('%s cannot be found', $classType)); } if (!method_exists($classType, 'fromDBColumn')) { throw new SchemaCreatorInvalidArgumentException( sprintf('Method %s::fromDBColumn not found', $classType) ); } if (is_numeric($column['default']) && $this->hasImplemented($classType, 'NumericTypeInterface')) { $column['default'] = (int)$column['default']; } if ($this->hasImplemented($classType, 'EnumAndSetInterface')) { $column['references'] = explode(',', $column['references']); } $callback = call_user_func($classType . '::fromDBColumn', $column); $tableSchema->addColumn($callback); } } if ($indexes = $this->indexGenerator->fetchIndexesFromDb($table)) { $constraints = $this->constraintGenerator->fetchConstraintsFromDb($table); foreach ($indexes as $index) { if ($index['name'] === PrimaryKeyInterface::INDEX_NAME) { $tableSchema->addIndex(new Primary($index['columns'])); } elseif ( ($constraintKey = array_search($index['name'], array_column($constraints, 'name'), true)) !== false) { $tableIndex = new Constraint( $constraints[$constraintKey]['name'], $constraints[$constraintKey]['columns'], $constraints[$constraintKey]['reference_table'], $constraints[$constraintKey]['reference_columns'] ); $tableSchema->addIndex($tableIndex); } else { $tableIndex = $index['unique'] === true ? new Unique($index['columns'], $index['name']) : new Index( $index['columns'], $index['name'] ); $tableSchema->addIndex($tableIndex); } } } if ($options = $this->tableOptionsGenerator->fetchOptionsFromDb($table)) { $tableSchema->addOption(TableOption::fromEngine($options['engine'])); $tableSchema->addOption(TableOption::fromCharset($options['table_charset'])); $tableSchema->addOption(TableOption::fromCollation($options['collation'])); $tableSchema->addOption(TableOption::fromComment($options['comment'])); } return $tableSchema; } /** * @param string $class * @param string $needleInterface * * @return bool */ private function hasImplemented(string $class, string $needleInterface): bool { if ($interfaces = class_implements($class)) { foreach ($interfaces as $interface) { $interface_exploded = explode('\\', $interface); $name = array_pop($interface_exploded); if ($name === $needleInterface) { return true; } } } return false; } /** * @param array $references * @param ColumnCollection $targetColumns * * @return bool */ private function hasIndexReferencesInSchema(array $references, ColumnCollection $targetColumns): bool { $columns = []; foreach ($targetColumns as $configuredColumns) { $columns[] = $configuredColumns->getField(); } foreach ($references as $reference) { if (!in_array(sprintf('%s', $reference), $columns, true)) { throw new SchemaCreatorInvalidArgumentException( sprintf('Column name `%s` missing in the table', $reference) ); } } return true; } /** * @param array $targetSchema * @param array $currentSchema * * @return bool */ private function checkColumnNeedsUpdate(array $targetSchema, array $currentSchema): bool { $diff = array_diff_assoc($targetSchema, $currentSchema); return count($diff) > 0; } /** * @param TableSchema $currentSchema * @param TableSchema $targetSchema * * @throws EscapingException * * @return string */ private function generateColumnsDiff(TableSchema $currentSchema, TableSchema $targetSchema): string { $alterColumnParts = []; $targetColumns = $targetSchema->getColumns(); $currentColumns = $currentSchema->getColumns(); $currentIndexes = $currentSchema->getIndexes(); $hasAutoIncrement = $currentColumns->hasAutoIncrement(); $autoIncrementColumn = $currentColumns->getAutoIncrementColumn(); foreach ($currentColumns as $schemaKey => $currentColumn) { $isFieldAsAutoIncrement = $hasAutoIncrement === true && $autoIncrementColumn->getField( ) === $currentColumn->getField(); if ($currentIndexes !== null && $isFieldAsAutoIncrement === true && $currentIndexes->hasPrimaryKey( ) === false) { $primaryReference = $currentColumn->getField(); $currentIndexes->add(new Primary([$primaryReference])); } } $targetFields = $targetColumns->getFields(); $currentFields = $currentColumns->getFields(); $newFields = array_diff($targetFields, $currentFields); foreach ($targetColumns as $schemaKey => $schemaColumn) { /** @var ColumnInterface $schemaColumn */ $field = $schemaColumn->getField(); if (in_array($field, $newFields, true)) { $alterColumnParts[] = sprintf('ADD %s', $this->columnGenerator->generateLine($schemaColumn)); } else { $needsUpdate = $this->checkColumnNeedsUpdate( $this->columnGenerator->toArray($schemaColumn), $this->columnGenerator->toArray($currentSchema->getColumnByName($field)) ); if ($needsUpdate) { $alterColumnParts[] = sprintf('MODIFY %s', $this->columnGenerator->generateLine($schemaColumn)); } } } return implode(', ', $alterColumnParts); } /** * @param IndexInterface $targetIndex * @param IndexInterface $currentIndex * * @return bool */ private function checkIndexNeedsUpdate(IndexInterface $targetIndex, IndexInterface $currentIndex): bool { $targetReferences = $targetIndex->getReferences(); $currentReferences = $currentIndex->getReferences(); if ($targetIndex->isUnique() !== $currentIndex->isUnique()) { return true; } $diffColumns = array_diff($targetReferences, $currentReferences); return count($diffColumns) > 0; } /** * @param TableSchema $currentSchema * @param TableSchema $targetSchema * * @throws Exception * @return string */ private function generateIndexesDiff(TableSchema $currentSchema, TableSchema $targetSchema): string { $alterIndexParts = []; $targetIndexes = $targetSchema->getIndexes(); $currentIndexes = $currentSchema->getIndexes(); $targetColumns = $targetSchema->getColumns(); // FIX MISSING PRIMARY KEY HERE $this->generateTableColumns($targetColumns, $targetIndexes); $targetIndexNames = $targetIndexes->getIndexNames(); $currentIndexNames = $currentIndexes->getIndexNames(); $newIndexes = array_diff($targetIndexNames, $currentIndexNames); $removedIndexes = array_diff($currentIndexNames, $targetIndexNames); if (count($removedIndexes) > 0) { foreach ($removedIndexes as $removedIndexName) { if ($removedIndexName !== 'PRIMARY' && !in_array($removedIndexName, $newIndexes, true)) { foreach($currentIndexes as $currentIndex) { if($currentIndex->getName() !== $removedIndexName) { continue; } if($currentIndex->getType() === 'CONSTRAINT') { $alterIndexParts[] = sprintf('DROP FOREIGN KEY %s', $this->indexGenerator->escape($removedIndexName)); break; } else { $alterIndexParts[] = sprintf('DROP INDEX %s', $this->indexGenerator->escape($removedIndexName)); break; } } } } } foreach ($targetIndexes as $schemaKey => $schemaIndex) { $indexName = $schemaIndex->getName(); $indexType = $schemaIndex->getType(); if (in_array($indexName, $newIndexes, true) && $this->hasIndexReferencesInSchema($schemaIndex->getReferences(), $targetColumns) === true) { $alterIndexParts[] = sprintf('ADD %s', $this->indexGenerator->generateLine($schemaIndex)); } else { $needsUpdate = $this->checkIndexNeedsUpdate($schemaIndex, $currentSchema->getIndexByName($indexName)); if ($needsUpdate) { if ($schemaIndex instanceof PrimaryKeyInterface) { $references = $schemaIndex->getReferences(); // CHECK IF one of the reference has AutoIncrement $currentPrimary = $currentSchema->getIndexByName('PRIMARY'); if (null !== $currentPrimary) { $currentReferences = $currentPrimary->getReferences(); $columnModified = null; foreach ($currentReferences as $columnName) { $column = $currentSchema->getColumnByName($columnName); $options = $column->getOptions(); if (array_key_exists('extra', $options) && in_array( $options['extra'], ['ai', 'AUTO_INCREMENT'] )) { $columnModified = sprintf( 'MODIFY %s', $this->columnGenerator->generateLine($column) ); $columnModifiedWithoutAi = str_replace('AUTO_INCREMENT', '', $columnModified); $alterIndexParts[] = trim($columnModifiedWithoutAi); break; } } $alterIndexParts[] = 'DROP PRIMARY KEY'; } $reference = implode( ',', array_map( function ($reference) { return $this->indexGenerator->escape($reference); }, $references ) ); $alterIndexParts[] = sprintf('ADD PRIMARY KEY (%s)', $reference); } else { if($indexType === 'CONSTRAINT') { $alterIndexParts[] = sprintf('DROP FOREIGN KEY %s', $this->indexGenerator->escape($indexName)); } else { $alterIndexParts[] = sprintf('DROP INDEX %s', $this->indexGenerator->escape($indexName)); } $alterIndexParts[] = sprintf('ADD %s', $this->indexGenerator->generateLine($schemaIndex)); } } } } return implode(', ', $alterIndexParts); } /** * @param TableSchema $currentSchema * @param TableSchema $targetSchema * * @throws Exception * @return string */ private function generateOptionsDiff(TableSchema $currentSchema, TableSchema $targetSchema): string { $targetOptions = $targetSchema->getOptions(); if ($targetOptions->getIterator()->count() === 0) { return ''; } $currentOptions = $currentSchema->getOptions(); $sqlOptions = []; $charset = null; foreach ($targetOptions as $option) { $currentTableOption = $currentOptions->getTableOption($option->getOption()); if ($currentTableOption === null || $currentTableOption->getValue() !== $option->getValue()) { if ($option->getOption() === 'collation' && ($charsetOption = $currentOptions->getTableOption( 'charset' ))) { $charset = $charsetOption->getValue(); } $sqlOptions[] = $this->tableOptionsGenerator->alterOptions($option, $charset); } } return implode(' ', $sqlOptions); } /** * @inheritDoc */ public function getTableDefinition(TableSchema $schema): string { $name = $schema->getTable(); $columns = $this->generateTableColumns( $schema->getColumns(), $schema->getIndexes() ); $keys = $this->generateTableIndexes($schema->getIndexes(), $schema->getColumns()); $columns = rtrim($columns, ','); if (!empty($keys)) { $keys = ',' . PHP_EOL . $keys; } $definition = sprintf("(\n%s\n)", $columns . $keys); $defFooter = $this->generateTableOptions($schema->getOptions()); $definition .= $defFooter; return sprintf('CREATE TABLE IF NOT EXISTS %s %s', $this->indexGenerator->escape($name), $definition); } /** * @param string $table * * @throws EscapingException * * @return string */ private function generateAlterTable(string $table): string { return sprintf('ALTER TABLE %s ', $this->indexGenerator->escape($table)); } /** * @inheritDoc */ public function generateTableSchemaDiff(TableSchema $currentSchema, TableSchema $targetSchema): string { $sql = ''; $alterColumns = $this->generateColumnsDiff($currentSchema, $targetSchema); $alterIndexes = $this->generateIndexesDiff($currentSchema, $targetSchema); $alterOptions = $this->generateOptionsDiff($currentSchema, $targetSchema); if (empty($alterColumns) && empty($alterIndexes) && empty($alterOptions)) { return $sql; } if (!empty($alterColumns)) { $sql .= $alterColumns . ', '; } if (!empty($alterIndexes)) { $sql .= $alterIndexes . ', '; } $table = $targetSchema->getTable(); $alterTable = $this->generateAlterTable($table); if (!empty($alterOptions)) { $alterTable .= $alterOptions; if (!empty($sql)) { $alterTable .= ', '; } } $sql = substr_replace($sql, ';', -2); return $alterTable . $sql; } }