mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-12-25 14:10:28 +01:00
547 lines
21 KiB
PHP
547 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Xentral\Components\SchemaCreator\Adapter\Driver;
|
|
|
|
use Exception;
|
|
use Xentral\Components\Database\Exception\EscapingException;
|
|
use Xentral\Components\SchemaCreator\Collection\ColumnCollection;
|
|
use Xentral\Components\SchemaCreator\Collection\IndexCollection;
|
|
use Xentral\Components\SchemaCreator\Collection\TableOptionCollection;
|
|
use Xentral\Components\SchemaCreator\Exception\SchemaCreatorInvalidArgumentException;
|
|
use Xentral\Components\SchemaCreator\Index\Constraint;
|
|
use Xentral\Components\SchemaCreator\Index\Index;
|
|
use Xentral\Components\SchemaCreator\Index\Primary;
|
|
use Xentral\Components\SchemaCreator\Index\Unique;
|
|
use Xentral\Components\SchemaCreator\Interfaces\PrimaryKeyInterface;
|
|
use Xentral\Components\SchemaCreator\Interfaces\ColumnInterface;
|
|
use Xentral\Components\SchemaCreator\Interfaces\IndexInterface;
|
|
use Xentral\Components\SchemaCreator\Interfaces\DriverInterface;
|
|
use Xentral\Components\SchemaCreator\LineGenerator\Common\ColumnLineGenerator;
|
|
use Xentral\Components\SchemaCreator\LineGenerator\Common\ConstraintLineGenerator;
|
|
use Xentral\Components\SchemaCreator\LineGenerator\Common\IndexLineGenerator;
|
|
use Xentral\Components\SchemaCreator\LineGenerator\Common\TableOptionsGenerator;
|
|
use Xentral\Components\SchemaCreator\Option\TableOption;
|
|
use Xentral\Components\SchemaCreator\Schema\TableSchema;
|
|
|
|
final class MysqlDriver implements DriverInterface
|
|
{
|
|
|
|
/** @var ColumnLineGenerator $columnGenerator */
|
|
private $columnGenerator;
|
|
|
|
/** @var IndexLineGenerator $indexGenerator */
|
|
private $indexGenerator;
|
|
|
|
/** @var TableOptionsGenerator $tableOptionsGenerator */
|
|
private $tableOptionsGenerator;
|
|
|
|
/** @var ConstraintLineGenerator $constraintGenerator */
|
|
private $constraintGenerator;
|
|
|
|
/**
|
|
* @param ColumnLineGenerator $columnGenerator
|
|
* @param IndexLineGenerator $indexGenerator
|
|
* @param TableOptionsGenerator $tableOptionsGenerator
|
|
* @param ConstraintLineGenerator $constraintLineGenerator
|
|
*/
|
|
public function __construct(
|
|
ColumnLineGenerator $columnGenerator,
|
|
IndexLineGenerator $indexGenerator,
|
|
TableOptionsGenerator $tableOptionsGenerator,
|
|
ConstraintLineGenerator $constraintLineGenerator
|
|
) {
|
|
$this->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;
|
|
}
|
|
|
|
}
|