diff --git a/composer.json b/composer.json index 7eb4f95..072ccca 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "ext-openssl": "*", "ext-pdo_mysql": "*", "ext-fileinfo": "*", + "doctrine/data-fixtures": "^1.7", "doctrine/orm": "~3", "doctrine/sql-formatter": "^1.4", "dragonmantank/cron-expression": "^3.3", @@ -62,7 +63,7 @@ "conflict": { }, "require-dev": { - "squizlabs/php_codesniffer": "^3.10.1", + "squizlabs/php_codesniffer": "^3.10.2", "slevomat/coding-standard": "^8.15.0" }, "scripts": { diff --git a/src/ComposerCreateProject.php b/src/ComposerCreateProject.php index d458fdb..4dc111b 100644 --- a/src/ComposerCreateProject.php +++ b/src/ComposerCreateProject.php @@ -216,6 +216,18 @@ public static function composerDoCreateProject($event) php bin/tray-digita app:generate:scheduler ``` +MD, + 'app/Seeders/README.md' => << <<seederCommand, + 'S', + InputOption::VALUE_NONE, + sprintf( + $this->translateContext( + 'Dump seed data (should execute with %s command)', + 'console' + ), + sprintf( + '--%s', + $this->seederCommand + ) + ), + null + ), ]) ->setHelp( sprintf( @@ -214,11 +234,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $theSchema = $input->getOption($this->schemaCommand); $thePrint = $input->getOption($this->printCommand); $dump = $input->getOption($this->dumpCommand); + $seeding = $input->getOption($this->seederCommand); $optionSchemaDump = $dump && $thePrint; $optionPrintSchema = !$dump && $thePrint; try { if ($theSchema) { $this->databaseSchemaDetect($input, $output); + } elseif ($seeding) { + $this->databaseSeedDump($input, $output); } else { $this->doDatabaseCheck($output, false); } @@ -245,9 +268,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @throws Throwable + * @param OutputInterface $output + * @return Connection|int */ - public function databaseSchemaDetect(InputInterface $input, OutputInterface $output) : int + protected function databaseInit(OutputInterface $output) : Connection|int { $container = $this->getContainer(); if (!$container?->has(Connection::class)) { @@ -284,14 +308,26 @@ public function databaseSchemaDetect(InputInterface $input, OutputInterface $out $database->getEntityManager()->getEventManager() ); } + return $database; + } + /** + * @throws Throwable + */ + public function databaseSchemaDetect(InputInterface $input, OutputInterface $output) : int + { + $database = $this->databaseInit($output); + if (is_int($database)) { + return $database; + } $isExecute = $input->getOption($this->executeCommand); $thePrint = $input->getOption($this->printCommand); $dump = $input->getOption($this->dumpCommand); $optimize = $input->getOption($this->optimizeCommand); + $isSeeding = $input->getOption($this->seederCommand); $optionSchemaDump = $dump && $thePrint; $optionPrintSchema = !$dump && $thePrint; - if (!$thePrint && !$isExecute && !$optimize) { + if (!$thePrint && !$isExecute && !$optimize && !$isSeeding) { $this->doDatabaseCheck($output, true); } @@ -419,939 +455,934 @@ public function databaseSchemaDetect(InputInterface $input, OutputInterface $out } return Command::SUCCESS; } + if ($optimize) { + if (empty($optimizeArray)) { + $output->writeln( + sprintf( + '%s', + $this->translateContext( + 'There are no tables that can be optimized', + 'console' + ) + ) + ); + return Command::SUCCESS; + } + // only mysql + if (!$platform instanceof AbstractMySQLPlatform) { + $className = explode('\\', $platform::class); + $className = end($className); + $className = preg_replace('~Platform$~', '', $className); + $output->writeln( + sprintf( + $this->translateContext( + '%s does not yet support optimization', + 'console' + ), + sprintf( + '%s', + $className + ) + ) + ); + return Command::SUCCESS; + } + foreach ($optimizeArray as $tableName => $freed) { + if (!$currentSchema->hasTable($tableName)) { + unset($optimizeArray[$tableName]); + continue; + } + $table = $currentSchema->getTable($tableName); + $optimizeArray[$tableName] = $table->getName(); + $output->writeln( + sprintf( + $this->translateContext( + 'Table "%s" can be freed up to : (%s)', + 'console' + ), + sprintf( + '%s', + $table->getName(), + ), + sprintf( + '%s', + Consolidation::sizeFormat($freed, 4) + ) + ) + ); + } - if (!$optimize) { - $containOptimize = false; - foreach ($allMetadata as $meta) { - $tableName = $meta->getTableName(); - $table = $schemaManager->tablesExist([$tableName]) - ? $schemaManager->introspectTable($tableName) - : null; - if (!$table) { - $containChange = true; - if (!$isExecute) { - $this->writeIndent( - $output, - sprintf( - $this->translateContext('Table "%s" does not exist!', 'console'), - sprintf( - '%s', - $tableName - ) - ), - mode: self::MODE_DANGER - ); - } else { - $this->write( - $output, - sprintf( - $this->translateContext( - 'Table "%s" does not exist!', - 'console' - ), - sprintf( - '%s', - $tableName - ) - ), - mode: self::MODE_DANGER + /** @noinspection DuplicatedCode */ + $answer = $interactive ? $io->ask( + $this->translateContext( + 'Are you sure to continue (Yes/No)?', + 'console' + ), + null, + function ($e) { + $e = !is_string($e) ? '' : $e; + $e = strtolower(trim($e)); + $ask = match ($e) { + 'yes' => true, + 'no' => false, + default => null + }; + if ($ask === null) { + throw new InteractiveArgumentException( + $this->translateContext( + 'Please enter valid answer! (Yes / No)', + 'console' + ) ); } - continue; - } - $currentTable = $currentSchema->getTable($meta->getTableName()); - $tableDiff = $comparator->compareTables($table, $currentTable); - $tableDiff = $this->compareSchemaTableFix($table, $currentTable, $tableDiff); - $isNeedOptimize = ($optimizeArray[strtolower($currentTable->getName())] ?? 0) > 0; - if ($isNeedOptimize) { - $containOptimize = true; + return $ask; } - if ($tableDiff->isEmpty()) { - if (!$isExecute) { - $this->writeIndent( - $output, + ) : true; + if (!$answer) { + $output->writeln( + sprintf( + '%s', + $this->translateContext('Operation cancelled!', 'console') + ) + ); + return Command::SUCCESS; + } + $output->writeln(''); + $output->writeln( + sprintf( + '%s', + $this->translateContext('PLEASE DO NOT CANCEL OPERATION!', 'console') + ) + ); + $output->writeln(''); + foreach ($optimizeArray as $tableName) { + $output->write( + sprintf( + '%s "%s"', + $this->translateContext('Optimizing table', 'console'), + $tableName + ) + ); + try { + $result = $database + ->executeQuery( sprintf( - $this->translateContext( - 'Table "%s" %s', - 'console' - ), - sprintf( - '%s', - $tableName - ), - sprintf( - '%s%s%s', - sprintf( - '%s', - $this->translateContext('no difference', 'console') - ), - $isNeedOptimize - ? sprintf( - ' [%s]', - $this->translateContext('NEED TO OPTIMIZE', 'console') - ) : '', - ($currentTable->getComment() - ? sprintf(' (%s)', $currentTable->getComment()) - : '' - ) - ) - ), - mode: self::MODE_SUCCESS + 'OPTIMIZE TABLE %s', + $database->quoteIdentifier($tableName) + ) ); + $status = 'OK'; + foreach ($result->fetchAllAssociative() as $stat) { + $stat = array_change_key_case($stat); + if (($stat['msg_type']??null) !== 'status') { + continue; + } + $status = $stat['msg_text']??'OK'; + break; } + $result->free(); + $output->writeln( + sprintf(' [%s]', $status) + ); + } catch (Throwable $e) { + $output->writeln( + sprintf( + ' [%s] %s', + $this->translateContext('FAIL', 'console'), + $e->getMessage() + ) + ); continue; } + } + $output->writeln( + sprintf( + '%s', + $this->translateContext('ALL DONE!', 'console') + ) + ); + return Command::SUCCESS; + } + $containOptimize = false; + foreach ($allMetadata as $meta) { + $tableName = $meta->getTableName(); + $table = $schemaManager->tablesExist([$tableName]) + ? $schemaManager->introspectTable($tableName) + : null; + if (!$table) { $containChange = true; - $message = sprintf( - '%s%s', - sprintf( - $this->translateContext('Table "%s" need to be change', 'console'), - sprintf( - '%s', - $tableName, - ) - ), - $isNeedOptimize ? sprintf( - ' [%s]', - $this->translateContext('NEED TO OPTIMIZE', 'console') - ) : '', - ); if (!$isExecute) { $this->writeIndent( $output, - $message, - mode: self::MODE_WARNING + sprintf( + $this->translateContext('Table "%s" does not exist!', 'console'), + sprintf( + '%s', + $tableName + ) + ), + mode: self::MODE_DANGER ); } else { $this->write( $output, - $message, - mode: self::MODE_WARNING - ); - } - - $typeRegistry = Type::getTypeRegistry(); - - // COLUMNS - // modified - foreach ($tableDiff->getModifiedColumns() as $column) { - $oldColumn = $column->getOldColumn(); - $newColumn = $column->getNewColumn(); - if ($column->hasTypeChanged()) { - $this->writeIndent( - $output, - sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - $this->translateContext('type', 'console'), - sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - $typeRegistry->lookupName($oldColumn->getType()) - ), - sprintf( - '%s', - $typeRegistry->lookupName($newColumn->getType()) - ) - ) - ) - ); - } - if ($column->hasDefaultChanged()) { - $this->writeIndent( - $output, - sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - $this->translateContext('default value', 'console'), - sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - $oldColumn->getDefault() ?? '' - ), - sprintf( - '%s', - $newColumn->getDefault() ?? '' - ) - ) - ) - ); - } - if ($column->hasFixedChanged()) { - $this->writeIndent( - $output, + sprintf( + $this->translateContext( + 'Table "%s" does not exist!', + 'console' + ), sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - 'fixed', - sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - $oldColumn->getFixed() ? 'YES' : 'NO' - ), - sprintf( - '%s', - $newColumn->getFixed() ? 'YES' : 'NO' - ) - ) + '%s', + $tableName ) - ); - } - if ($column->hasAutoIncrementChanged()) { - $this->writeIndent( - $output, + ), + mode: self::MODE_DANGER + ); + } + continue; + } + $currentTable = $currentSchema->getTable($meta->getTableName()); + $tableDiff = $comparator->compareTables($table, $currentTable); + $tableDiff = $this->compareSchemaTableFix($table, $currentTable, $tableDiff); + $isNeedOptimize = ($optimizeArray[strtolower($currentTable->getName())] ?? 0) > 0; + if ($isNeedOptimize) { + $containOptimize = true; + } + if ($tableDiff->isEmpty()) { + if (!$isExecute) { + $this->writeIndent( + $output, + sprintf( + $this->translateContext( + 'Table "%s" %s', + 'console' + ), sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - 'auto increment', - sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - $oldColumn->getAutoincrement() ? 'YES' : 'NO' - ), - sprintf( - '%s', - $newColumn->getAutoincrement() ? 'YES' : 'NO' - ) - ) - ) - ); - } - if ($column->hasLengthChanged()) { - $this->writeIndent( - $output, + '%s', + $tableName + ), sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - 'length', + '%s%s%s', sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - $oldColumn->getLength() ?? '' - ), - sprintf( - '%s', - $newColumn->getLength() ?? '' - ) + '%s', + $this->translateContext('no difference', 'console') + ), + $isNeedOptimize + ? sprintf( + ' [%s]', + $this->translateContext('NEED TO OPTIMIZE', 'console') + ) : '', + ($currentTable->getComment() + ? sprintf(' (%s)', $currentTable->getComment()) + : '' ) ) - ); - } - if ($column->hasPrecisionChanged()) { - $this->writeIndent( - $output, - sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - 'precision', - sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - $oldColumn->getPrecision() ?? '' - ), - sprintf( - '%s', - $newColumn->getPrecision() ?? '' - ) - ) - ) - ); - } - if ($column->hasNotNullChanged()) { - $this->writeIndent( - $output, - sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - 'not null', - sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - !$oldColumn->getNotnull() ? 'YES' : 'NO' - ), - sprintf( - '%s', - !$newColumn->getNotnull() ? 'YES' : 'NO' - ) - ) - ) - ); - } - if ($column->hasScaleChanged()) { - $this->writeIndent( - $output, - sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - $this->translateContext('scale', 'console'), - sprintf( - $this->translateContext( - 'change from: [%s] to [%s]', - 'console' - ), - sprintf( - '%s', - $oldColumn->getScale() ??'' - ), - sprintf( - '%s', - $newColumn->getScale() ??'' - ) - ) - ) - ); - } - if ($column->hasUnsignedChanged()) { - $this->writeIndent( - $output, - sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - 'unsigned', - sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - $oldColumn->getUnsigned() ? 'YES' : 'NO' - ), - sprintf( - '%s', - $newColumn->getUnsigned() ? 'YES' : 'NO' - ) - ) - ) - ); - } - if ($column->hasCommentChanged()) { - $this->writeIndent( - $output, - sprintf( - '- %s %s %s %s', - $this->translateContext('Column', 'console'), - $newColumn->getName(), - $this->translateContext('comment', 'console'), - sprintf( - $this->translateContext('change from: [%s] to [%s]', 'console'), - sprintf( - '%s', - $oldColumn->getComment() ?? '' - ), - sprintf( - '%s', - $newColumn->getComment() ?? '' - ) - ) - ) - ); - } + ), + mode: self::MODE_SUCCESS + ); } + continue; + } + $containChange = true; + $message = sprintf( + '%s%s', + sprintf( + $this->translateContext('Table "%s" need to be change', 'console'), + sprintf( + '%s', + $tableName, + ) + ), + $isNeedOptimize ? sprintf( + ' [%s]', + $this->translateContext('NEED TO OPTIMIZE', 'console') + ) : '', + ); + if (!$isExecute) { + $this->writeIndent( + $output, + $message, + mode: self::MODE_WARNING + ); + } else { + $this->write( + $output, + $message, + mode: self::MODE_WARNING + ); + } - // added - foreach ($tableDiff->getAddedColumns() as $column) { + $typeRegistry = Type::getTypeRegistry(); + + // COLUMNS + // modified + foreach ($tableDiff->getModifiedColumns() as $column) { + $oldColumn = $column->getOldColumn(); + $newColumn = $column->getNewColumn(); + if ($column->hasTypeChanged()) { $this->writeIndent( $output, sprintf( - '- %s [%s]', - $this->translateContext('Added Column', 'console'), - $column->getName() + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + $this->translateContext('type', 'console'), + sprintf( + $this->translateContext('change from: [%s] to [%s]', 'console'), + sprintf( + '%s', + $typeRegistry->lookupName($oldColumn->getType()) + ), + sprintf( + '%s', + $typeRegistry->lookupName($newColumn->getType()) + ) + ) ) ); } - // remove - foreach ($tableDiff->getDroppedColumns() as $column) { + if ($column->hasDefaultChanged()) { $this->writeIndent( $output, sprintf( - '- %s [%s]', - $this->translateContext('Removed Column', 'console'), - $column->getName() + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + $this->translateContext('default value', 'console'), + sprintf( + $this->translateContext('change from: [%s] to [%s]', 'console'), + sprintf( + '%s', + $oldColumn->getDefault() ?? '' + ), + sprintf( + '%s', + $newColumn->getDefault() ?? '' + ) + ) ) ); } - // rename - foreach ($tableDiff->getRenamedColumns() as $columnName => $column) { + if ($column->hasFixedChanged()) { $this->writeIndent( $output, sprintf( - '- %s %s', - $this->translateContext('Renamed Column', 'console'), + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + 'fixed', sprintf( - $this->translateContext('from [%s] to [%s]', 'console'), + $this->translateContext('change from: [%s] to [%s]', 'console'), sprintf( '%s', - $columnName + $oldColumn->getFixed() ? 'YES' : 'NO' ), sprintf( '%s', - $column->getName() + $newColumn->getFixed() ? 'YES' : 'NO' ) ) ) ); } - - // INDEXES - // added - foreach ($tableDiff->getModifiedIndexes() as $indexName => $index) { - $indexName = !is_string($indexName) ? $index->getName() : $indexName; - $oldIndex = $table->getIndex($indexName); + if ($column->hasAutoIncrementChanged()) { $this->writeIndent( $output, sprintf( - '- %s %s', - $this->translateContext('Modify Index', 'console'), + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + 'auto increment', sprintf( - $this->translateContext('from %s to %s', 'console'), + $this->translateContext('change from: [%s] to [%s]', 'console'), sprintf( - '[%s](%s)', - sprintf( - '%s', - $indexName - ), - sprintf( - '%s', - implode(', ', $oldIndex->getColumns()) - ) + '%s', + $oldColumn->getAutoincrement() ? 'YES' : 'NO' ), sprintf( - '[%s](%s)', - sprintf( - '%s', - $index->getName() - ), - sprintf( - '%s', - implode(', ', $index->getColumns()) - ) + '%s', + $newColumn->getAutoincrement() ? 'YES' : 'NO' ) ) ) ); } - - foreach ($tableDiff->getAddedIndexes() as $index) { + if ($column->hasLengthChanged()) { $this->writeIndent( $output, sprintf( - '- %s [%s] %s', - $this->translateContext('Added Index', 'console'), - $index->getName(), + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + 'length', sprintf( - 'with columns [%s]', + $this->translateContext('change from: [%s] to [%s]', 'console'), sprintf( '%s', - implode(', ', $index->getColumns()) + $oldColumn->getLength() ?? '' + ), + sprintf( + '%s', + $newColumn->getLength() ?? '' ) ) ) ); } - // dropped - foreach ($tableDiff->getDroppedIndexes() as $index) { + if ($column->hasPrecisionChanged()) { $this->writeIndent( $output, sprintf( - '- %s [%s] %s', - $this->translateContext('Removed Index', 'console'), - $index->getName(), + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + 'precision', sprintf( - 'contain columns [%s]', + $this->translateContext('change from: [%s] to [%s]', 'console'), sprintf( '%s', - implode(', ', $index->getColumns()) + $oldColumn->getPrecision() ?? '' + ), + sprintf( + '%s', + $newColumn->getPrecision() ?? '' ) ) ) ); } - - // renamed - foreach ($tableDiff->getRenamedIndexes() as $indexName => $index) { - $indexName = !is_string($indexName) ? $index->getName() : $indexName; + if ($column->hasNotNullChanged()) { $this->writeIndent( $output, sprintf( - '- %s %s', - $this->translateContext('Renamed Index', 'console'), + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + 'not null', sprintf( - 'from [%s] to [%s]', - sprintf('%s', $indexName), - sprintf('%s', $index->getName()) + $this->translateContext('change from: [%s] to [%s]', 'console'), + sprintf( + '%s', + !$oldColumn->getNotnull() ? 'YES' : 'NO' + ), + sprintf( + '%s', + !$newColumn->getNotnull() ? 'YES' : 'NO' + ) ) ) ); } - - // RELATIONS - // added - foreach ($tableDiff->getModifiedForeignKeys() as $foreignName => $foreignKey) { - $foreignName = !is_string($foreignName) ? $foreignKey->getName() : $foreignName; - $oldForeign = $table->getForeignKey($foreignName); + if ($column->hasScaleChanged()) { $this->writeIndent( $output, sprintf( - '- %s [%s](%s)' - . ' -> (%s)' - . ' [%s](%s) -> (%s)%s', - $this->translateContext('Modify ForeignKey', 'console'), - $foreignName, - sprintf( - '%s(%s)', - $table->getName(), - implode(', ', $oldForeign->getLocalColumns()) - ), + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + $this->translateContext('scale', 'console'), sprintf( - '%s(%s)', - $oldForeign->getForeignTableName(), - implode(', ', $oldForeign->getForeignColumns()) - ), - $foreignKey->getName(), - sprintf( - '%s(%s)', - $table->getName(), - implode(', ', $foreignKey->getLocalColumns()) - ), - sprintf( - '%s(%s)', - $foreignKey->getForeignTableName(), - implode(', ', $foreignKey->getForeignColumns()) - ), - ( - $foreignKey->onDelete() || $foreignKey->onUpdate() ? sprintf( - '(%s%s)', - $foreignKey->onUpdate() ? sprintf( - 'ON UPDATE %s', - $foreignKey->onUpdate() - ) : '', - $foreignKey->onDelete() ? sprintf( - ' ON DELETE %s', - $foreignKey->onDelete() - ) : '' - ) : '' + $this->translateContext( + 'change from: [%s] to [%s]', + 'console' + ), + sprintf( + '%s', + $oldColumn->getScale() ??'' + ), + sprintf( + '%s', + $newColumn->getScale() ??'' + ) ) ) ); } - - foreach ($tableDiff->getAddedForeignKeys() as $foreignKey) { + if ($column->hasUnsignedChanged()) { $this->writeIndent( $output, sprintf( - '- %s [%s](%s) ' - . '-> (%s)', - $this->translateContext('Added ForeignKey', 'console'), - $foreignKey->getName(), - sprintf( - '%s(%s)', - $table->getName(), - implode(', ', $foreignKey->getLocalColumns()) - ), + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + 'unsigned', sprintf( - '%s(%s)', - $foreignKey->getForeignTableName(), - implode(', ', $foreignKey->getForeignColumns()) - ), + $this->translateContext('change from: [%s] to [%s]', 'console'), + sprintf( + '%s', + $oldColumn->getUnsigned() ? 'YES' : 'NO' + ), + sprintf( + '%s', + $newColumn->getUnsigned() ? 'YES' : 'NO' + ) + ) ) ); } - // drop - foreach ($tableDiff->getDroppedForeignKeys() as $foreignKey) { + if ($column->hasCommentChanged()) { $this->writeIndent( $output, sprintf( - '- %s [%s](%s) ' - . '-> (%s)', - $this->translateContext('Removed ForeignKey', 'console'), - $foreignKey->getName(), - sprintf( - '%s(%s)', - $table->getName(), - implode(', ', $foreignKey->getLocalColumns()) - ), + '- %s %s %s %s', + $this->translateContext('Column', 'console'), + $newColumn->getName(), + $this->translateContext('comment', 'console'), sprintf( - '%s(%s)', - $foreignKey->getForeignTableName(), - implode(', ', $foreignKey->getForeignColumns()) - ), + $this->translateContext('change from: [%s] to [%s]', 'console'), + sprintf( + '%s', + $oldColumn->getComment() ?? '' + ), + sprintf( + '%s', + $newColumn->getComment() ?? '' + ) + ) ) ); } } - if (!$isExecute) { - foreach ($schemaManager->introspectSchema()->getTables() as $table) { - $tableName = $table->getName(); - if ($currentSchema->hasTable($tableName)) { - continue; - } - $lowerTableName = strtolower($tableName); - if (str_contains($lowerTableName, 'translation') - && $table->hasColumn('domain') - && $table->hasColumn('language') - && $table->hasColumn('original') - && $table->hasColumn('translation') - && $table->hasColumn('plural_translation') - ) { - $this->writeIndent( - $output, + // added + foreach ($tableDiff->getAddedColumns() as $column) { + $this->writeIndent( + $output, + sprintf( + '- %s [%s]', + $this->translateContext('Added Column', 'console'), + $column->getName() + ) + ); + } + // remove + foreach ($tableDiff->getDroppedColumns() as $column) { + $this->writeIndent( + $output, + sprintf( + '- %s [%s]', + $this->translateContext('Removed Column', 'console'), + $column->getName() + ) + ); + } + // rename + foreach ($tableDiff->getRenamedColumns() as $columnName => $column) { + $this->writeIndent( + $output, + sprintf( + '- %s %s', + $this->translateContext('Renamed Column', 'console'), + sprintf( + $this->translateContext('from [%s] to [%s]', 'console'), sprintf( - '%s%s', - sprintf( - $this->translateContext( - 'Table "%s" for %s & does not exist in schema', - 'console' - ), - sprintf( - '%s', - $table->getName(), - ), - sprintf( - '%s', - 'translations' - ) - ), - ($table->getComment() - ? sprintf(' (%s)', $table->getComment()) - : '' - ) + '%s', + $columnName ), - mode: self::MODE_WARNING - ); - continue; - } - if (str_contains($lowerTableName, 'cache') - && $table->hasColumn('item_id') - && $table->hasColumn('item_data') - && $table->hasColumn('item_lifetime') - && $table->hasColumn('item_time') - ) { - $this->writeIndent( - $output, sprintf( - '%s%s', - sprintf( - $this->translateContext( - 'Table "%s" for %s & does not exist in schema', - 'console' - ), - sprintf( - '%s', - $table->getName(), - ), - sprintf( - '%s', - 'cache' - ) - ), - ($table->getComment() - ? sprintf(' (%s)', $table->getComment()) - : '' - ) - ), - mode: self::MODE_WARNING - ); - continue; - } + '%s', + $column->getName() + ) + ) + ) + ); + } - if (str_contains($lowerTableName, 'log') - && $table->hasColumn('id') - && $table->hasColumn('channel') - && $table->hasColumn('level') - && $table->hasColumn('message') - ) { - $this->writeIndent( - $output, + // INDEXES + // added + foreach ($tableDiff->getModifiedIndexes() as $indexName => $index) { + $indexName = !is_string($indexName) ? $index->getName() : $indexName; + $oldIndex = $table->getIndex($indexName); + $this->writeIndent( + $output, + sprintf( + '- %s %s', + $this->translateContext('Modify Index', 'console'), + sprintf( + $this->translateContext('from %s to %s', 'console'), sprintf( - '%s%s', + '[%s](%s)', sprintf( - $this->translateContext( - 'Table "%s" for %s & does not exist in schema', - 'console' - ), - sprintf( - '%s', - $table->getName(), - ), - sprintf( - '%s', - 'logs' - ) + '%s', + $indexName ), - ($table->getComment() - ? sprintf(' (%s)', $table->getComment()) - : '' + sprintf( + '%s', + implode(', ', $oldIndex->getColumns()) ) ), - mode: self::MODE_WARNING - ); - continue; - } - - if ($tableMigration === $lowerTableName) { - $this->writeIndent( - $output, sprintf( - '%s%s', + '[%s](%s)', sprintf( - $this->translateContext('Table "%s" for %s', 'console'), - sprintf( - '%s', - $table->getName(), - ), - sprintf( - '%s', - 'migrations' - ) + '%s', + $index->getName() ), - ($table->getComment() - ? sprintf(' (%s)', $table->getComment()) - : '' - ) - ), - mode: self::MODE_SUCCESS - ); - continue; - } - - $this->writeIndent( - $output, - sprintf( - '%s%s', - sprintf( - 'Table "%s" exists in database but not in schema', sprintf( '%s', - $table->getName(), + implode(', ', $index->getColumns()) ) - ), - ($table->getComment() - ? sprintf(' (%s)', $table->getComment()) - : '' ) - ), - mode: self::MODE_WARNING - ); - } - } - - if ($containChange && !$isExecute) { - $output->writeln(''); - $output->writeln( - sprintf( - '%s', - $this->translateContext( - 'Contains changed database schema, you can execute command :', - 'console' ) ) ); - $output->writeln(''); + } + + foreach ($tableDiff->getAddedIndexes() as $index) { $this->writeIndent( $output, sprintf( - '%s %s %s --%s --%s', - PHP_BINARY, - $_SERVER['PHP_SELF'], - $this->getName(), - $this->schemaCommand, - $this->executeCommand + '- %s [%s] %s', + $this->translateContext('Added Index', 'console'), + $index->getName(), + sprintf( + 'with columns [%s]', + sprintf( + '%s', + implode(', ', $index->getColumns()) + ) + ) ) ); - $output->writeln(''); } - if (!$isExecute && $containOptimize) { - $output->writeln(''); - $output->writeln( + // dropped + foreach ($tableDiff->getDroppedIndexes() as $index) { + $this->writeIndent( + $output, sprintf( - '%s', - $this->translateContext( - 'Contains database table that can be optimized, you can execute command :', - 'console' + '- %s [%s] %s', + $this->translateContext('Removed Index', 'console'), + $index->getName(), + sprintf( + 'contain columns [%s]', + sprintf( + '%s', + implode(', ', $index->getColumns()) + ) ) ) ); - $output->writeln(''); + } + + // renamed + foreach ($tableDiff->getRenamedIndexes() as $indexName => $index) { + $indexName = !is_string($indexName) ? $index->getName() : $indexName; $this->writeIndent( $output, sprintf( - '%s %s %s --%s --%s', - PHP_BINARY, - $_SERVER['PHP_SELF'], - $this->getName(), - $this->schemaCommand, - $this->optimizeCommand + '- %s %s', + $this->translateContext('Renamed Index', 'console'), + sprintf( + 'from [%s] to [%s]', + sprintf('%s', $indexName), + sprintf('%s', $index->getName()) + ) ) ); - $output->writeln(''); } - if (!$isExecute) { - return Command::SUCCESS; - } - } else { - if (empty($optimizeArray)) { - $output->writeln( + + // RELATIONS + // added + foreach ($tableDiff->getModifiedForeignKeys() as $foreignName => $foreignKey) { + $foreignName = !is_string($foreignName) ? $foreignKey->getName() : $foreignName; + $oldForeign = $table->getForeignKey($foreignName); + $this->writeIndent( + $output, sprintf( - '%s', - $this->translateContext( - 'There are no tables that can be optimized', - 'console' + '- %s [%s](%s)' + . ' -> (%s)' + . ' [%s](%s) -> (%s)%s', + $this->translateContext('Modify ForeignKey', 'console'), + $foreignName, + sprintf( + '%s(%s)', + $table->getName(), + implode(', ', $oldForeign->getLocalColumns()) + ), + sprintf( + '%s(%s)', + $oldForeign->getForeignTableName(), + implode(', ', $oldForeign->getForeignColumns()) + ), + $foreignKey->getName(), + sprintf( + '%s(%s)', + $table->getName(), + implode(', ', $foreignKey->getLocalColumns()) + ), + sprintf( + '%s(%s)', + $foreignKey->getForeignTableName(), + implode(', ', $foreignKey->getForeignColumns()) + ), + ( + $foreignKey->onDelete() || $foreignKey->onUpdate() ? sprintf( + '(%s%s)', + $foreignKey->onUpdate() ? sprintf( + 'ON UPDATE %s', + $foreignKey->onUpdate() + ) : '', + $foreignKey->onDelete() ? sprintf( + ' ON DELETE %s', + $foreignKey->onDelete() + ) : '' + ) : '' ) ) ); - return Command::SUCCESS; } - // only mysql - if (!$platform instanceof AbstractMySQLPlatform) { - $className = explode('\\', $platform::class); - $className = end($className); - $className = preg_replace('~Platform$~', '', $className); - $output->writeln( + + foreach ($tableDiff->getAddedForeignKeys() as $foreignKey) { + $this->writeIndent( + $output, sprintf( - $this->translateContext( - '%s does not yet support optimization', - 'console' + '- %s [%s](%s) ' + . '-> (%s)', + $this->translateContext('Added ForeignKey', 'console'), + $foreignKey->getName(), + sprintf( + '%s(%s)', + $table->getName(), + implode(', ', $foreignKey->getLocalColumns()) ), sprintf( - '%s', - $className - ) + '%s(%s)', + $foreignKey->getForeignTableName(), + implode(', ', $foreignKey->getForeignColumns()) + ), ) ); - return Command::SUCCESS; } - foreach ($optimizeArray as $tableName => $freed) { - if (!$currentSchema->hasTable($tableName)) { - unset($optimizeArray[$tableName]); - continue; - } - $table = $currentSchema->getTable($tableName); - $optimizeArray[$tableName] = $table->getName(); - $output->writeln( + // drop + foreach ($tableDiff->getDroppedForeignKeys() as $foreignKey) { + $this->writeIndent( + $output, sprintf( - $this->translateContext( - 'Table "%s" can be freed up to : (%s)', - 'console' - ), + '- %s [%s](%s) ' + . '-> (%s)', + $this->translateContext('Removed ForeignKey', 'console'), + $foreignKey->getName(), sprintf( - '%s', + '%s(%s)', $table->getName(), + implode(', ', $foreignKey->getLocalColumns()) ), sprintf( - '%s', - Consolidation::sizeFormat($freed, 4) - ) + '%s(%s)', + $foreignKey->getForeignTableName(), + implode(', ', $foreignKey->getForeignColumns()) + ), ) ); } + } + if (!$isExecute) { + foreach ($schemaManager->introspectSchema()->getTables() as $table) { + $tableName = $table->getName(); + if ($currentSchema->hasTable($tableName)) { + continue; + } + $lowerTableName = strtolower($tableName); + if (str_contains($lowerTableName, 'translation') + && $table->hasColumn('domain') + && $table->hasColumn('language') + && $table->hasColumn('original') + && $table->hasColumn('translation') + && $table->hasColumn('plural_translation') + ) { + $this->writeIndent( + $output, + sprintf( + '%s%s', + sprintf( + $this->translateContext( + 'Table "%s" for %s & does not exist in schema', + 'console' + ), + sprintf( + '%s', + $table->getName(), + ), + sprintf( + '%s', + 'translations' + ) + ), + ($table->getComment() + ? sprintf(' (%s)', $table->getComment()) + : '' + ) + ), + mode: self::MODE_WARNING + ); + continue; + } + if (str_contains($lowerTableName, 'cache') + && $table->hasColumn('item_id') + && $table->hasColumn('item_data') + && $table->hasColumn('item_lifetime') + && $table->hasColumn('item_time') + ) { + $this->writeIndent( + $output, + sprintf( + '%s%s', + sprintf( + $this->translateContext( + 'Table "%s" for %s & does not exist in schema', + 'console' + ), + sprintf( + '%s', + $table->getName(), + ), + sprintf( + '%s', + 'cache' + ) + ), + ($table->getComment() + ? sprintf(' (%s)', $table->getComment()) + : '' + ) + ), + mode: self::MODE_WARNING + ); + continue; + } - /** @noinspection DuplicatedCode */ - $answer = $interactive ? $io->ask( - $this->translateContext( - 'Are you sure to continue (Yes/No)?', - 'console' - ), - null, - function ($e) { - $e = !is_string($e) ? '' : $e; - $e = strtolower(trim($e)); - $ask = match ($e) { - 'yes' => true, - 'no' => false, - default => null - }; - if ($ask === null) { - throw new InteractiveArgumentException( - $this->translateContext( - 'Please enter valid answer! (Yes / No)', - 'console' + if (str_contains($lowerTableName, 'log') + && $table->hasColumn('id') + && $table->hasColumn('channel') + && $table->hasColumn('level') + && $table->hasColumn('message') + ) { + $this->writeIndent( + $output, + sprintf( + '%s%s', + sprintf( + $this->translateContext( + 'Table "%s" for %s & does not exist in schema', + 'console' + ), + sprintf( + '%s', + $table->getName(), + ), + sprintf( + '%s', + 'logs' + ) + ), + ($table->getComment() + ? sprintf(' (%s)', $table->getComment()) + : '' + ) + ), + mode: self::MODE_WARNING + ); + continue; + } + + if ($tableMigration === $lowerTableName) { + $this->writeIndent( + $output, + sprintf( + '%s%s', + sprintf( + $this->translateContext('Table "%s" for %s', 'console'), + sprintf( + '%s', + $table->getName(), + ), + sprintf( + '%s', + 'migrations' + ) + ), + ($table->getComment() + ? sprintf(' (%s)', $table->getComment()) + : '' ) - ); - } - return $ask; + ), + mode: self::MODE_SUCCESS + ); + continue; } - ) : true; - if (!$answer) { - $output->writeln( + + $this->writeIndent( + $output, sprintf( - '%s', - $this->translateContext('Operation cancelled!', 'console') - ) + '%s%s', + sprintf( + 'Table "%s" exists in database but not in schema', + sprintf( + '%s', + $table->getName(), + ) + ), + ($table->getComment() + ? sprintf(' (%s)', $table->getComment()) + : '' + ) + ), + mode: self::MODE_WARNING ); - return Command::SUCCESS; } + } + if ($containChange && !$isExecute) { $output->writeln(''); $output->writeln( sprintf( - '%s', - $this->translateContext('PLEASE DO NOT CANCEL OPERATION!', 'console') + '%s', + $this->translateContext( + 'Contains changed database schema, you can execute command :', + 'console' + ) ) ); $output->writeln(''); - foreach ($optimizeArray as $tableName) { - $output->write( - sprintf( - '%s "%s"', - $this->translateContext('Optimizing table', 'console'), - $tableName - ) - ); - try { - $result = $database - ->executeQuery( - sprintf( - 'OPTIMIZE TABLE %s', - $database->quoteIdentifier($tableName) - ) - ); - $status = 'OK'; - foreach ($result->fetchAllAssociative() as $stat) { - $stat = array_change_key_case($stat); - if (($stat['msg_type']??null) !== 'status') { - continue; - } - $status = $stat['msg_text']??'OK'; - break; - } - $result->free(); - $output->writeln( - sprintf(' [%s]', $status) - ); - } catch (Throwable $e) { - $output->writeln( - sprintf( - ' [%s] %s', - $this->translateContext('FAIL', 'console'), - $e->getMessage() - ) - ); - continue; - } - } + $this->writeIndent( + $output, + sprintf( + '%s %s %s --%s --%s', + PHP_BINARY, + $_SERVER['PHP_SELF'], + $this->getName(), + $this->schemaCommand, + $this->executeCommand + ) + ); + $output->writeln(''); + } + if (!$isExecute && $containOptimize) { + $output->writeln(''); $output->writeln( sprintf( '%s', - $this->translateContext('ALL DONE!', 'console') + $this->translateContext( + 'Contains database table that can be optimized, you can execute command :', + 'console' + ) ) ); + $output->writeln(''); + $this->writeIndent( + $output, + sprintf( + '%s %s %s --%s --%s', + PHP_BINARY, + $_SERVER['PHP_SELF'], + $this->getName(), + $this->schemaCommand, + $this->optimizeCommand + ) + ); + $output->writeln(''); + } + if (!$isExecute) { return Command::SUCCESS; } - $clonedSchema = clone $schemaManager->introspectSchema(); foreach ($clonedSchema->getTables() as $table) { if (!$currentSchema->hasTable($table->getName())) { @@ -1401,7 +1432,13 @@ function ($e) { OutputInterface::OUTPUT_RAW ); } - + $output->writeln(''); + $output->writeln( + $this->translateContext( + 'Executing Schema', + 'console' + ) + ); /** @noinspection DuplicatedCode */ $answer = $interactive ? $io->ask( $this->translateContext( @@ -1541,6 +1578,119 @@ function ($e) { return Command::SUCCESS; } + public function databaseSeedDump(InputInterface $input, OutputInterface $output) : int + { + $database = $this->databaseInit($output); + if (is_int($database)) { + return $database; + } + $seeders = ContainerHelper::getNull(Seeders::class, $this->getContainer()); + if (!$seeders instanceof Seeders) { + $this->writeDanger( + $output, + $this->translateContext( + 'Seeders object is not valid object from container', + 'console' + ), + ); + return self::FAILURE; + } + + $output->writeln(''); + $output->writeln( + $this->translateContext( + 'Dumping seed data', + 'console' + ) + ); + $io = new SymfonyStyle($input, $output); + $interactive = $input->isInteractive(); + /** @noinspection DuplicatedCode */ + $answer = $interactive ? $io->ask( + $this->translateContext( + 'Are you sure to continue (Yes/No)?', + 'console' + ), + null, + function ($e) { + $e = !is_string($e) ? '' : $e; + $e = strtolower(trim($e)); + $ask = match ($e) { + 'yes' => true, + 'no' => false, + default => null + }; + if ($ask === null) { + throw new InteractiveArgumentException( + $this->translateContext( + 'Please enter valid answer! (Yes / No)', + 'console' + ) + ); + } + return $ask; + } + ) : true; + + if (!$answer) { + $output->writeln( + sprintf( + '%s', + $this->translateContext( + 'Operation cancelled!', + 'console' + ) + ) + ); + return Command::SUCCESS; + } + $output->writeln(''); + $output->writeln( + sprintf( + '%s', + $this->translateContext( + 'PLEASE DO NOT CANCEL OPERATION!', + 'console' + ) + ) + ); + + $output->writeln(''); + $progressBar = !$output->isVerbose() ? $io->createProgressBar() : null; + $progressBar?->setMaxSteps(count($seeders->getFixtures())); + try { + $executor = new ORMExecutor($this->entityManager, new ORMPurger()); + foreach ($seeders->getFixtures() as $fixture) { + $executor->execute([$fixture]); + $progressBar?->advance(); + } + $progressBar?->finish(); + $progressBar?->clear(); + } catch (Throwable $e) { + $output->writeln( + sprintf( + '%s', + $this->translateContext('Failed to execute command', 'console') + ) + ); + $output->writeln( + sprintf( + '%s', + $e->getMessage() + ) + ); + return Command::FAILURE; + } + + $output->writeln( + sprintf( + '%s', + $this->translateContext('ALL DONE!', 'console') + ) + ); + return Command::SUCCESS; + } + /** * @throws Throwable */ @@ -1559,7 +1709,16 @@ private function doDatabaseCheck( ); return; } - + if (!$container?->has(Seeders::class)) { + $this->writeDanger( + $output, + $this->translateContext( + 'Can not get seeders object from container', + 'console' + ) + ); + return; + } $database = ContainerHelper::getNull(Connection::class, $container); if (!$database instanceof Connection) { $this->writeDanger( @@ -1571,6 +1730,17 @@ private function doDatabaseCheck( ); return; } + $seeders = ContainerHelper::getNull(Seeders::class, $container); + if (!$seeders instanceof Seeders) { + $this->writeDanger( + $output, + $this->translateContext( + 'Seeders object is not valid object from container', + 'console' + ), + ); + return; + } //$platform = null; $error = null; $config = null; @@ -1813,14 +1983,28 @@ private function doDatabaseCheck( $error ? self::MODE_DANGER : self::MODE_SUCCESS ); if (!$skipChecking) { - $this->doCheckData($output, $error, $allMetadata, $database); + $this->doCheckSchemaData($output, $error, $allMetadata, $database); } - } + $seederLists = $seeders->getFixtures(); + $this->write( + $output, + sprintf( + '%s (%d)', + $this->translateContext('Registered Seeders', 'console'), + count($seederLists) + ), + self::MODE_SUCCESS + ); + if (!$skipChecking) { + $this->doCheckSeederData($output, $seederLists); + } + } + /** * @throws Throwable */ - protected function doCheckData( + protected function doCheckSchemaData( OutputInterface $output, $error, array $allMetadata, @@ -2001,6 +2185,29 @@ protected function doCheckData( } } + /** + * Check the seeders + * + * @param OutputInterface $output + * @param array $fixtures + * @return void + */ + protected function doCheckSeederData( + OutputInterface $output, + array $fixtures + ) : void { + foreach ($fixtures as $fixture) { + $this->writeIndent( + $output, + sprintf( + '- %s [%s]', + Consolidation::classShortName($fixture), + $fixture::class + ) + ); + } + } + private function compareSchemaTableFix(Table $realTable, Table $currentTable, TableDiff $diff): TableDiff { if (count($currentTable->getForeignKeys()) === 0) { diff --git a/src/Console/Command/SeederGenerator.php b/src/Console/Command/SeederGenerator.php new file mode 100644 index 0000000..0fbc9b8 --- /dev/null +++ b/src/Console/Command/SeederGenerator.php @@ -0,0 +1,455 @@ +getContainer() + ); + if ($kernel instanceof BaseKernel) { + $this->seederNamespace = $kernel->getSeederNamespace(); + $this->seederDirectory = $kernel->getRegisteredDirectories()[$this->seederNamespace]??null; + } else { + $namespace = dirname( + str_replace( + '\\', + '/', + __NAMESPACE__ + ) + ); + $appNameSpace = str_replace('/', '\\', dirname($namespace)) . '\\App'; + $this->seederNamespace = "$appNameSpace\\Seeders\\"; + } + + $namespace = rtrim($this->seederNamespace, '\\'); + $this + ->setName('app:generate:seeder') + ->setAliases(['generate-seeder']) + ->setDescription( + $this->translateContext('Generate seeder class.', 'console') + )->setDefinition([ + new InputOption( + 'print', + 'p', + InputOption::VALUE_NONE, + $this->translateContext('Print generated class file only', 'console') + ) + ])->setHelp( + sprintf( + $this->translateContext( + "The %s help you to create %s object.\n\n" + . "Seeder will use prefix namespace with %s\n" + . "Seeder only support single class name.\n\n", + 'console' + ), + '%command.name%', + 'seeder', + sprintf( + '%s', + $namespace + ) + ) + ); + } + + /** + * @param string $name + * @return ?array{name: string, className:string} + */ + protected function filterNames(string $name) : ?array + { + /** @noinspection DuplicatedCode */ + $name = trim($name); + $name = ltrim(str_replace(['/', '_', '-'], '\\', $name), '\\'); + $name = preg_replace('~\\\+~', '\\', $name); + $name = ucwords($name, '\\'); + $name = preg_replace_callback('~_([a-z])~', static function ($e) { + return ucwords($e[1]); + }, $name); + $name = trim($name, '_'); + $seederName = preg_replace('~[\\\_]+~', '_', $name); + $seederName = preg_replace_callback('~(_[a-zA-Z0-9]|[A-Z0-9])~', static function ($e) { + return "_".trim($e[1], '_'); + }, $seederName); + $seederName = strtolower(trim($seederName, '_')); + return $name !== '' && Consolidation::isValidClassName($name) + ? [ + 'identity' => $seederName, + 'className' => $name, + ] + : null; + } + + private ?array $seederList = null; + + private function isFileExists(string $className) : bool + { + $className = trim($className, '\\'); + $lowerClassName = strtolower($className); + if ($this->seederList === null) { + $this->seederList = []; + $seederDirectory = $this->seederDirectory; + $lengthStart = strlen($seederDirectory) + 1; + foreach (Finder::create() + ->in($seederDirectory) + ->ignoreVCS(true) + ->ignoreDotFiles(true) + // depth <= 10 + ->depth('<= 10') + ->name('/^[_A-za-z]([a-zA-Z0-9]+)?\.php$/') + ->files() as $file + ) { + $realPath = $file->getRealPath(); + $baseClassName = substr( + $realPath, + $lengthStart, + -4 + ); + $class = str_replace('/', '\\', $baseClassName); + $this->seederList[strtolower($class)] = $baseClassName; + } + } + return isset($this->seederList[$lowerClassName]); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return ?array{name: string, className:string} + */ + protected function askClassName(InputInterface $input, OutputInterface $output) : ?array + { + $io = new SymfonyStyle($input, $output); + return $io->ask( + $this->translateContext('Please enter seeder class name', 'console'), + null, + function ($name) { + $definitions = $this->filterNames($name); + if ($definitions === null) { + throw new InteractiveArgumentException( + $this->translateContext( + 'Please enter valid seeder class name!', + 'console' + ) + ); + } + $seederName = $definitions['identity']; + $className = $definitions['className']; + if (!Consolidation::allowedClassName($className)) { + throw new InteractiveArgumentException( + sprintf( + $this->translateContext( + 'Seeder [%s] is invalid! class name contain reserved keyword!', + 'console' + ), + $className + ) + ); + } + if (count(explode('\\', $className)) > 1) { + throw new InteractiveArgumentException( + sprintf( + $this->translateContext( + 'Seeder [%s] is invalid! Seeder only contain single class name, not namespaced!', + 'console' + ), + $className + ) + ); + } + if (strlen($seederName) > 128) { + throw new InteractiveArgumentException( + sprintf( + $this->translateContext( + 'Seeder [%s] is too long! Must be less or equal 128 characters', + 'console' + ), + $seederName + ) + ); + } + if ($this->isFileExists($className)) { + throw new InteractiveArgumentException( + sprintf( + $this->translateContext( + 'Seeder [%s] exist', + 'console' + ), + $this->seederNamespace . $className + ) + ); + } + return $definitions; + } + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if ($output->isQuiet()) { + return self::INVALID; + } + + $input->setInteractive(true); + $container = $this->getContainer(); + if (!$this->seederDirectory) { + $config = ContainerHelper::use(Config::class, $container) + ?? new Config(); + $path = $config->get('path'); + $path = $path instanceof Config ? $path : null; + $seederDirectory = $path?->get('seeder'); + if (is_string($seederDirectory) && is_dir($seederDirectory)) { + $this->seederDirectory = realpath($seederDirectory) ?? $seederDirectory; + } + } + + if (!$this->seederDirectory) { + $output->writeln( + sprintf( + $this->translateContext( + '%s Could not detect seeder directory', + 'console' + ), + '[X]' + ) + ); + return self::FAILURE; + } + + $named = $this->askClassName($input, $output); + if (!$named && !$input->isInteractive()) { + $output->writeln( + sprintf( + $this->translateContext( + '%s generator only support in interactive mode', + 'console' + ), + '[X]' + ) + ); + return self::FAILURE; + } + + $fileName = $this->seederDirectory + . DIRECTORY_SEPARATOR + . str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $named['className']) + . '.php'; + + $name = preg_replace_callback('~([A-Z])~', function ($a) { + return " $a[1]"; + }, $named['className']); + if ($input->getOption('print')) { + $output->writeln( + $this->generateSeederContent($named['className']) + ); + return self::SUCCESS; + } + $output->writeln( + sprintf( + $this->translateContext( + 'Seeder Name : %s', + 'console' + ), + sprintf( + '%s', + $name + ) + ) + ); + $output->writeln( + sprintf( + $this->translateContext( + 'Seeder Identity : %s', + 'console' + ), + sprintf( + '%s', + $named['identity'] + ) + ) + ); + $className = $this->seederNamespace . $named['className']; + $output->writeln( + sprintf( + $this->translateContext( + 'Seeder Class : %s', + 'console' + ), + sprintf( + '%s', + $className + ) + ) + ); + $output->writeln( + sprintf( + $this->translateContext( + 'Seeder File : %s', + 'console' + ), + sprintf( + '%s', + $fileName + ) + ) + ); + /** @noinspection DuplicatedCode */ + $io = new SymfonyStyle($input, $output); + $answer = !$input->isInteractive() || $io->ask( + $this->translateContext('Are you sure to continue (Yes/No)?', 'console'), + null, + function ($e) { + $e = !is_string($e) ? '' : $e; + $e = strtolower(trim($e)); + $ask = match ($e) { + 'yes' => true, + 'no' => false, + default => null + }; + if ($ask === null) { + throw new InteractiveArgumentException( + $this->translateContext( + 'Please enter valid answer! (Yes / No)', + 'console' + ) + ); + } + return $ask; + } + ); + if ($answer) { + if (!is_dir(dirname($fileName))) { + @mkdir(dirname($fileName), 0755, true); + } + $status = @file_put_contents( + $fileName, + $this->generateSeederContent( + $name, + $named['identity'], + $named['className'] + ) + ); + if (!$status) { + $output->writeln( + sprintf( + $this->translateContext( + '%s Could not save seeder!', + 'console' + ), + '[X]' + ) + ); + return self::SUCCESS; + } + $output->writeln( + sprintf( + $this->translateContext( + '%s Seeder successfully created!', + 'console' + ), + '[√]' + ) + ); + return self::FAILURE; + } + $output->writeln( + sprintf( + '%s', + $this->translateContext('Operation cancelled!', 'console') + ) + ); + return self::SUCCESS; + } + + private function generateSeederContent( + string $className + ): string { + $classes = explode('\\', $className); + $baseClassName = array_pop($classes); + $namespace = trim($this->seederNamespace, '\\'); + if (!empty($classes)) { + $namespace .= '\\' . implode('\\', $classes); + } + $time = date('c'); + return <<setValue('value'); + // // do anything + // + // \$manager->persist(\$example); + // \$manager->flush(); + } +} + +PHP; + } +} diff --git a/src/Container/Factory/ContainerFactory.php b/src/Container/Factory/ContainerFactory.php index 4a9d820..59facc2 100644 --- a/src/Container/Factory/ContainerFactory.php +++ b/src/Container/Factory/ContainerFactory.php @@ -27,6 +27,7 @@ use ArrayAccess\TrayDigita\Container\Interfaces\ContainerFactoryInterface; use ArrayAccess\TrayDigita\Database\Connection; use ArrayAccess\TrayDigita\Database\DatabaseEventsCollector; +use ArrayAccess\TrayDigita\Database\Seeders; use ArrayAccess\TrayDigita\Event\Interfaces\ManagerInterface; use ArrayAccess\TrayDigita\Event\Manager; use ArrayAccess\TrayDigita\Handler\ErrorHandler; @@ -66,6 +67,7 @@ use ArrayAccess\TrayDigita\Uploader\Chunk; use ArrayAccess\TrayDigita\View\Interfaces\ViewInterface; use ArrayAccess\TrayDigita\View\View; +use Doctrine\Common\DataFixtures\Loader; use Doctrine\Common\EventManager; use Doctrine\DBAL\Configuration as DoctrineConfiguration; use Doctrine\DBAL\Connection as DoctrineConnection; @@ -158,6 +160,7 @@ class ContainerFactory implements ContainerFactoryInterface UserAuth::class => UserAuth::class, // Assets AssetsJsCssQueue::class => AssetsJsCssQueue::class, + Seeders::class => Seeders::class, ]; /** @@ -255,7 +258,11 @@ class ContainerFactory implements ContainerFactoryInterface // user auth 'userAuth' => UserAuth::class, // assets - 'assetsQueue' => AssetsJsCssQueue::class + 'assetsQueue' => AssetsJsCssQueue::class, + // fixtures + Loader::class => Seeders::class, + 'fixtures' => Seeders::class, + 'seeders' => Seeders::class, ]; /** diff --git a/src/Database/Seeders.php b/src/Database/Seeders.php new file mode 100644 index 0000000..d20f282 --- /dev/null +++ b/src/Database/Seeders.php @@ -0,0 +1,34 @@ +setContainer($connection->getContainer()); + $manager = $this->connection->getManager(); + if (!$manager) { + $manager = ContainerHelper::service( + ManagerInterface::class, + $this->getContainer() + ); + } + if ($manager instanceof ManagerInterface) { + $this->setManager($manager); + } + } +} diff --git a/src/HttpKernel/BaseKernel.php b/src/HttpKernel/BaseKernel.php index 3d4dd20..518bd8c 100644 --- a/src/HttpKernel/BaseKernel.php +++ b/src/HttpKernel/BaseKernel.php @@ -17,6 +17,7 @@ use ArrayAccess\TrayDigita\HttpKernel\Helper\KernelMiddlewareLoader; use ArrayAccess\TrayDigita\HttpKernel\Helper\KernelModuleLoader; use ArrayAccess\TrayDigita\HttpKernel\Helper\KernelSchedulerLoader; +use ArrayAccess\TrayDigita\HttpKernel\Helper\KernelSeederLoader; use ArrayAccess\TrayDigita\HttpKernel\Interfaces\HttpKernelInterface; use ArrayAccess\TrayDigita\HttpKernel\Interfaces\TerminableInterface; use ArrayAccess\TrayDigita\Kernel\Interfaces\KernelInterface; @@ -378,6 +379,7 @@ public function __call(string $name, array $arguments) protected ?string $databaseEventNameSpace = null; protected ?string $commandNameSpace = null; protected ?string $schedulerNamespace = null; + protected ?string $seederNamespace = null; /*! STATUS */ @@ -485,6 +487,7 @@ final public function init() : static 'databaseEvent' => $appDirectory . '/DatabaseEvents', 'scheduler' => $appDirectory . '/Schedulers', 'command' => $appDirectory . '/Commands', + 'seeder' => $appDirectory . '/Seeders', 'storage' => $root . '/storage', 'data' => $root . '/data', 'public' => $publicDirectory, @@ -720,6 +723,7 @@ final public function init() : static $container->setParameter('migrationsDirectory', $path->get('migration')); $container->setParameter('modulesDirectory', $path->get('module')); $container->setParameter('entitiesDirectory', $path->get('entity')); + $container->setParameter('seedersDirectory', $path->get('seeder')); // $container->setParameter('repositoriesDirectory', $path->get('repository')); $container->setParameter('databaseEventsDirectory', $path->get('databaseEvent')); $container->setParameter('schedulersDirectory', $path->get('scheduler')); @@ -805,7 +809,7 @@ final public function init() : static $namespace = dirname(str_replace('\\', '/', __NAMESPACE__)); // app $this->appNameSpace = str_replace('/', '\\', $namespace) . '\\App'; - $appNameSpace = $this->appNameSpace; + $appNameSpace = $this->getAppNameSpace(); $this->controllerNameSpace = "$appNameSpace\\Controllers\\"; $this->entityNamespace = "$appNameSpace\\Entities\\"; @@ -815,6 +819,7 @@ final public function init() : static $this->databaseEventNameSpace = "$appNameSpace\\DatabaseEvents\\"; $this->schedulerNamespace = "$appNameSpace\\Schedulers\\"; $this->commandNameSpace = "$appNameSpace\\Commands\\"; + $this->seederNamespace = "$appNameSpace\\Seeders\\"; $this->registeredDirectories = [ $this->moduleNameSpace => $path->get('module') ?? $defaultPaths['module'], $this->controllerNameSpace => $path->get('controller') ?? $defaultPaths['controller'], @@ -824,6 +829,7 @@ final public function init() : static $this->migrationNameSpace => $path->get('migration') ?? $defaultPaths['migration'], $this->databaseEventNameSpace => $path->get('databaseEvent') ?? $defaultPaths['databaseEvent'], $this->commandNameSpace => $path->get('command') ?? $defaultPaths['command'], + $this->seederNamespace => $path->get('seeder') ?? $defaultPaths['seeder'], ]; $routing = ContainerHelper::getNull( @@ -883,7 +889,8 @@ final public function init() : static KernelDatabaseEventLoader::register($this); // do register commands KernelCommandLoader::register($this); - + // do register seeders + KernelSeederLoader::register($this); // registering debug middleware at the first middleware $httpKernel->addDeferredMiddleware($debugMiddleware); $httpKernel->addDeferredMiddleware($errorMiddleware); @@ -983,6 +990,11 @@ public function getCommandNameSpace(): ?string return $this->commandNameSpace; } + public function getSeederNamespace(): ?string + { + return $this->seederNamespace; + } + public function getConfigError(): ?string { return $this->configError; diff --git a/src/HttpKernel/Helper/KernelSeederLoader.php b/src/HttpKernel/Helper/KernelSeederLoader.php new file mode 100644 index 0000000..6cdfcd2 --- /dev/null +++ b/src/HttpKernel/Helper/KernelSeederLoader.php @@ -0,0 +1,177 @@ +kernel->getSeederNamespace(); + } + + /** + * @return Finder + */ + protected function getFileLists(): Finder + { + return $this + ->createFinder($this->getDirectory(), 0, '/^[_A-za-z]([a-zA-Z0-9]+)?\.php$/') + ->files(); + } + + protected function getSeeder() : ?Seeders + { + return $this->seeder ??= ContainerHelper::service( + Seeders::class, + $this->kernel->getHttpKernel()->getContainer() + ); + } + + protected function getManager(): ?ManagerInterface + { + return $this->getSeeder() + ?->getManager()??parent::getManager(); + } + + protected function getContainer(): ?ContainerInterface + { + return $this->getSeeder() + ?->getContainer()??parent::getContainer(); + } + + protected function getDirectory(): ?string + { + $namespace = $this->getNameSpace(); + $directory = $namespace + ? $this->kernel->getRegisteredDirectories()[$namespace]??null + : null; + return $directory && is_dir($directory) ? $directory : null; + } + + protected function getMode(): string + { + return 'seeders'; + } + + protected function isProcessable(): bool + { + $processable = ! $this->kernel->getConfigError() + && $this->getNameSpace() + && $this->getDirectory() + && $this->getSeeder(); + if ($processable) { + $canBeProcess = $this + ->getManager() + ->dispatch('kernel.seederLoader', true); + $processable = is_bool($canBeProcess) ? $canBeProcess : true; + } + return $processable; + } + + /** + * @param SplFileInfo $splFileInfo + * @return void + */ + protected function loadService( + SplFileInfo $splFileInfo + ): void { + if (!$splFileInfo->isFile() + || ! ($seeder = $this->getSeeder()) + ) { + return; + } + $realPath = $splFileInfo->getRealPath(); + $manager = $this->getManager(); + // @dispatch(kernel.beforeRegisterSeeder) + $manager?->dispatch( + 'kernel.beforeRegisterSeeder', + $realPath, + $seeder, + $this->kernel + ); + $result = null; + try { + $className = $this->getClasNameFromFile($splFileInfo); + if (!$className) { + // @dispatch(kernel.registerSeeder) + $manager?->dispatch( + 'kernel.registerSeeder', + $realPath, + $seeder, + $this->kernel, + $this + ); + return; + } + try { + if (!class_exists($className)) { + (static function ($realPath) { + require_once $realPath; + })($splFileInfo->getRealPath()); + } + $ref = new ReflectionClass($className); + if ($ref->getName() !== $className) { + $this->saveClasNameFromFile( + $splFileInfo, + $ref->getName() + ); + } + if ($ref->isInstantiable() + && $ref->isSubclassOf(FixtureInterface::class) + && $ref->getFileName() === $splFileInfo->getRealPath() + ) { + $result = new $className(); + $seeder->addFixture($result); + } + } catch (Throwable $e) { + $this->getLogger()?->debug( + $e, + [ + 'loaderMode' => 'seeder', + 'classLoader' => $this::class, + 'className' => $className, + 'file' => $splFileInfo->getRealPath() + ] + ); + } + // @dispatch(kernel.registerSeeder) + $manager?->dispatch( + 'kernel.registerSeeder', + $realPath, + $seeder, + $this->kernel, + $this, + $result + ); + } finally { + if ($result) { + $this->injectDependency($result); + } + // @dispatch(kernel.afterRegisterSeeder) + $manager?->dispatch( + 'kernel.afterRegisterSeeder', + $realPath, + $seeder, + $this->kernel, + $this, + $result + ); + } + } +}