diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b4adab4..3357cb5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,10 +4,10 @@ on: [push, pull_request] jobs: run-tests-php7: - name: PHP ${{ matrix.php-versions }} + name: PHP ${{ matrix.php-versions }} with Phalcon ${{ matrix.phalcon-versions }} runs-on: ubuntu-latest env: - extensions: mbstring, intl, json, phalcon-4.0.6, mysql, pgsql + extensions: mbstring, intl, json, phalcon-${{ matrix.phalcon-versions }}, mysql, pgsql key: cache-v2.2~17.05.2020 services: mysql: @@ -27,6 +27,8 @@ jobs: fail-fast: false matrix: php-versions: ['7.3', '7.4'] + # There is no 4.1.1 version due release bug + phalcon-versions: ['4.0.5', '4.0.6', '4.1.0', '4.1.2'] steps: - uses: actions/checkout@v1 - name: Setup cache environment diff --git a/CHANGELOG.md b/CHANGELOG.md index d5ca255..3e9438a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [2.2.2](https://github.com/phalcon/migrations/releases/tag/v2.2.2) (2021-08-08) +- Integrated nette/php-generator, changed algorithm of migrations generation ([#90](https://github.com/phalcon/migrations/issues/90)) + # [2.2.1](https://github.com/phalcon/migrations/releases/tag/v2.2.1) (2021-08-03) - Fixed types and indexes definition on pgsql adapter ([#111](https://github.com/phalcon/migrations/issues/111), [#112](https://github.com/phalcon/migrations/issues/112), [#118](https://github.com/phalcon/migrations/issues/118)) diff --git a/composer.json b/composer.json index f3cf87b..4c524ff 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "require": { "php": ">=7.3", "ext-phalcon": ">=4.0.5", - "phalcon/cli-options-parser": "^1.2" + "phalcon/cli-options-parser": "^1.2", + "nette/php-generator": "^3.5" }, "require-dev": { "ext-pdo": "*", diff --git a/composer.lock b/composer.lock index 87f1400..3c788c1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,161 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9c1e43294793db8b26b31b81d27ff0c5", + "content-hash": "10776056dc7a5c07b8d6e21a20cb3f5b", "packages": [ + { + "name": "nette/php-generator", + "version": "v3.5.4", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "59bb35ed6e8da95854fbf7b7d47dce6156b42915" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/59bb35ed6e8da95854fbf7b7d47dce6156b42915", + "reference": "59bb35ed6e8da95854fbf7b7d47dce6156b42915", + "shasum": "" + }, + "require": { + "nette/utils": "^3.1.2", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "nikic/php-parser": "^4.4", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::withBodiesFrom() & GlobalFunction::withBodyFrom()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.0 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "support": { + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v3.5.4" + }, + "time": "2021-07-05T12:02:42+00:00" + }, + { + "name": "nette/utils", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/967cfc4f9a1acd5f1058d76715a424c53343c20c", + "reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c", + "shasum": "" + }, + "require": { + "php": ">=7.2 <8.1" + }, + "conflict": { + "nette/di": "<3.0.6" + }, + "require-dev": { + "nette/tester": "~2.0", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v3.2.2" + }, + "time": "2021-03-03T22:53:25+00:00" + }, { "name": "phalcon/cli-options-parser", "version": "v1.3.0", @@ -683,16 +836,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.21", + "version": "4.1.22", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "c25f20d842a7e3fa0a8e6abf0828f102c914d419" + "reference": "9777ec3690ceedc4bce2ed13af7af4ca4ee3088f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/c25f20d842a7e3fa0a8e6abf0828f102c914d419", - "reference": "c25f20d842a7e3fa0a8e6abf0828f102c914d419", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/9777ec3690ceedc4bce2ed13af7af4ca4ee3088f", + "reference": "9777ec3690ceedc4bce2ed13af7af4ca4ee3088f", "shasum": "" }, "require": { @@ -703,7 +856,7 @@ "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "guzzlehttp/psr7": "~1.4", + "guzzlehttp/psr7": "^1.4 | ^2.0", "php": ">=5.6.0 <9.0", "symfony/console": ">=2.7 <6.0", "symfony/css-selector": ">=2.7 <6.0", @@ -766,7 +919,7 @@ ], "support": { "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/4.1.21" + "source": "https://github.com/Codeception/Codeception/tree/4.1.22" }, "funding": [ { @@ -774,7 +927,7 @@ "type": "open_collective" } ], - "time": "2021-05-28T17:43:39+00:00" + "time": "2021-08-06T17:15:34+00:00" }, { "name": "codeception/lib-asserts", @@ -1839,29 +1992,32 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.8.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + "reference": "1dc8d9cba3897165e16d12bb13d813afb1eb3fe7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", - "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/1dc8d9cba3897165e16d12bb13d813afb1eb3fe7", + "reference": "1dc8d9cba3897165e16d12bb13d813afb1eb3fe7", "shasum": "" }, "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "ralouphie/getallheaders": "^3.0" }, "provide": { + "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { - "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + "bamarni/composer-bin-plugin": "^1.4.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.8 || ^9.3.10" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1869,16 +2025,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "2.0-dev" } }, "autoload": { "psr-4": { "GuzzleHttp\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1893,6 +2046,11 @@ { "name": "Tobias Schultze", "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" } ], "description": "PSR-7 message implementation that also provides common utility methods", @@ -1908,9 +2066,9 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.2" + "source": "https://github.com/guzzle/psr7/tree/2.0.0" }, - "time": "2021-04-26T09:17:50+00:00" + "time": "2021-06-30T20:03:07+00:00" }, { "name": "humbug/box", @@ -2107,12 +2265,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "fb32da186ac7854b4ad205f12940e10829f709f0" + "reference": "8dc2938edf966af39f7514d4726fa2fba700ac25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/fb32da186ac7854b4ad205f12940e10829f709f0", - "reference": "fb32da186ac7854b4ad205f12940e10829f709f0", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/8dc2938edf966af39f7514d4726fa2fba700ac25", + "reference": "8dc2938edf966af39f7514d4726fa2fba700ac25", "shasum": "" }, "require-dev": { @@ -2148,7 +2306,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2021-08-03T11:33:10+00:00" + "time": "2021-08-03T14:45:11+00:00" }, { "name": "justinrainbow/json-schema", @@ -3809,6 +3967,61 @@ }, "time": "2020-06-29T06:28:15+00:00" }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", diff --git a/src/Generator/Snippet.php b/src/Generator/Snippet.php index 66a0d9d..2f42376 100644 --- a/src/Generator/Snippet.php +++ b/src/Generator/Snippet.php @@ -13,200 +13,53 @@ namespace Phalcon\Migrations\Generator; -use Phalcon\Db\ColumnInterface; - class Snippet { - public function getAttributes( - $type, - $visibility, - ColumnInterface $field, - $annotate = false, - $customFieldName = null - ): string { - $fieldName = $customFieldName ?: $field->getName(); - - if ($annotate) { - $templateAttributes = <<isPrimary() ? PHP_EOL . ' * @Primary' : '', - $field->isAutoIncrement() ? PHP_EOL . ' * @Identity' : '', - $field->getName(), - $type, - $field->getSize() ? ', length=' . $field->getSize() : '', - $field->isNotNull() ? 'false' : 'true', - $visibility, - $fieldName - ) . PHP_EOL; - } else { - $templateAttributes = <<morphTable('%s', [ -%s -EOD; - return sprintf( - $template, - $className, - $className, - $table, - $this->getMigrationDefinition('columns', $tableDefinition) - ); - } - - public function getMigrationUp(): string - { - return <<morphTable('%s', [\n%s]);"; } - public function getMigrationBatchInsert($table, $allFields): string + public function getColumnTemplate(): string { - $template = <<batchInsert('%s', [ + return "new Column( + '%s', + [ %s ] - ); -EOD; - return sprintf($template, $table, join(",\n ", $allFields)); + )"; } - public function getMigrationAfterCreateTable($table, $allFields): string + public function getIndexTemplate(): string { - $template = <<batchInsert('%s', [ - %s - ] - ); - } -EOD; - return sprintf($template, $table, join(",\n ", $allFields)); + return "new Index('%s', [%s], %s)"; } - public function getMigrationBatchDelete($table): string + public function getReferenceTemplate(): string { - $template = <<batchDelete('%s'); -EOD; - return sprintf($template, $table); - } - - public function getMigrationDefinition($name, $definition): string - { - $template = << [ - %s - ], - -EOD; - return sprintf($template, $name, join(",\n ", $definition)); + return "new Reference( + '%s', + [ + %s + ] + )"; } - public function getColumnDefinition($field, $fieldDefinition): string + public function getOptionTemplate(): string { - $template = <<options = $options; } + public function getEntity(): PhpFile + { + $this->checkEntityExists(); + + return $this->file; + } + + public function createEntity(string $className, bool $recreate = false): self + { + if (null === $this->class || $recreate) { + $this->file = new PhpFile(); + $this->file->addUse(Column::class) + ->addUse(Exception::class) + ->addUse(Index::class) + ->addUse(Reference::class) + ->addUse(Migration::class); + + $this->class = $this->file->addClass($className); + $this->class + ->setExtends(Migration::class) + ->addComment("Class {$className}"); + } + + return $this; + } + + /** + * @throws UnknownColumnTypeException + */ + public function addMorph(Snippet $snippet, string $table, bool $skipRefSchema = false, bool $skipAI = true): self + { + $this->checkEntityExists(); + + $columns = []; + foreach ($this->getColumns() as $columnName => $columnDefinition) { + $definitions = implode(",\n ", $columnDefinition); + $columns[] = sprintf($snippet->getColumnTemplate(), $columnName, $definitions); + } + + $indexes = []; + foreach ($this->getIndexes() as $indexName => $indexDefinition) { + [$fields, $indexType] = $indexDefinition; + $definitions = implode(", ", $fields); + $type = $indexType ? "'$indexType'" : "''"; + $indexes[] = sprintf($snippet->getIndexTemplate(), $indexName, $definitions, $type); + } + + $references = []; + foreach ($this->getReferences($skipRefSchema) as $constraintName => $referenceDefinition) { + $definitions = implode(",\n ", $referenceDefinition); + $references[] = sprintf($snippet->getReferenceTemplate(), $constraintName, $definitions); + } + + $options = []; + foreach ($this->getOptions($skipAI) as $option) { + $options[] = sprintf($snippet->getOptionTemplate(), $option); + } + + $body = sprintf( + $snippet->getMorphTemplate(), + $table, + $snippet->definitionToString('columns', $columns) + . $snippet->definitionToString('indexes', $indexes) + . $snippet->definitionToString('references', $references) + . $snippet->definitionToString('options', $options) + ); + + $this->class->addMethod('morph') + ->addComment("Define the table structure\n") + ->addComment('@return void') + ->addComment('@throws Exception') + ->setReturnType('void') + ->setBody($body); + + return $this; + } + + public function addUp(string $table, $exportData = null, bool $shouldExportDataFromTable = false): self + { + $this->checkEntityExists(); + + $body = "\n"; + if ($exportData === 'always' || $shouldExportDataFromTable) { + $quoteWrappedColumns = "\n"; + foreach ($this->quoteWrappedColumns as $quoteWrappedColumn) { + $quoteWrappedColumns .= " $quoteWrappedColumn,\n"; + } + $body = "\$this->batchInsert('$table', [{$quoteWrappedColumns}]);"; + } + + $this->class->addMethod('up') + ->addComment("Run the migrations\n") + ->addComment('@return void') + ->setReturnType('void') + ->setBody($body); + + return $this; + } + + public function addDown(string $table, $exportData = null, bool $shouldExportDataFromTable = false): self + { + $this->checkEntityExists(); + + $body = "\n"; + if ($exportData === 'always' || $shouldExportDataFromTable) { + $body = "\$this->batchDelete('$table');"; + } + + $this->class->addMethod('down') + ->addComment("Reverse the migrations\n") + ->addComment('@return void') + ->setReturnType('void') + ->setBody($body); + + return $this; + } + + public function addAfterCreateTable(string $table, $exportData = null): self + { + $this->checkEntityExists(); + + if ($exportData === 'oncreate') { + $quoteWrappedColumns = "\n"; + foreach ($this->quoteWrappedColumns as $quoteWrappedColumn) { + $quoteWrappedColumns .= " $quoteWrappedColumn,\n"; + } + $body = "\$this->batchInsert('$table', [{$quoteWrappedColumns}]);"; + + $this->class->addMethod('afterCreateTable') + ->addComment("This method is called after the table was created\n") + ->addComment('@return void') + ->setReturnType('void') + ->setBody($body); + } + + return $this; + } + + public function createDumpFiles( + string $table, + string $migrationPath, + AbstractAdapter $connection, + ItemInterface $version, + $exportData = null, + bool $shouldExportDataFromTable = false + ): self { + $numericColumns = $this->getNumericColumns(); + if ($exportData === 'always' || $exportData === 'oncreate' || $shouldExportDataFromTable) { + $fileHandler = fopen($migrationPath . $version->getVersion() . '/' . $table . '.dat', 'w'); + $cursor = $connection->query('SELECT * FROM ' . $connection->escapeIdentifier($table)); + $cursor->setFetchMode(Enum::FETCH_ASSOC); + while ($row = $cursor->fetchArray()) { + $data = []; + foreach ($row as $key => $value) { + if (isset($numericColumns[$key])) { + if ($value === '' || $value === null) { + $data[] = 'NULL'; + } else { + $data[] = $value; + } + } elseif (is_string($value)) { + $data[] = addslashes($value); + } else { + $data[] = $value ?? 'NULL'; + } + + unset($value); + } + + fputcsv($fileHandler, $data); + unset($row, $data); + } + + fclose($fileHandler); + } + + return $this; + } + /** * Prepare table columns * @@ -447,4 +649,11 @@ protected function getColumnSize(ColumnInterface $column) return $size; } + + public function checkEntityExists(): void + { + if (null === $this->file) { + throw new RuntimeException('Migration entity is e,pty. Call Generate::createEntity()'); + } + } } diff --git a/src/Migrations.php b/src/Migrations.php index 5f8b650..6c8c41e 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -27,6 +27,7 @@ use Phalcon\Migrations\Mvc\Model\Migration as ModelMigration; use Phalcon\Migrations\Mvc\Model\Migration\TableAware\ListTablesDb; use Phalcon\Migrations\Mvc\Model\Migration\TableAware\ListTablesIterator; +use Phalcon\Migrations\Utils\Helper; use Phalcon\Migrations\Version\IncrementalItem; use Phalcon\Migrations\Version\ItemCollection as VersionCollection; use Phalcon\Migrations\Version\TimestampedItem; @@ -66,6 +67,7 @@ public static function isConsole(): bool */ public static function generate(array $options) { + $helper = new Helper(); $optionStack = new OptionStack(); $listTables = new ListTablesDb(); $optionStack->setOptions($options); @@ -75,56 +77,23 @@ public static function generate(array $options) $optionStack->setDefaultOption('verbose', false); $optionStack->setDefaultOption('skip-ref-schema', false); - $migrationsDirs = $optionStack->getOption('migrationsDir'); - if (is_array($migrationsDirs)) { - if (count($migrationsDirs) > 1) { - $question = 'Which migrations path would you like to use?' . PHP_EOL; - foreach ($migrationsDirs as $id => $dir) { - $question .= " [{$id}] $dir" . PHP_EOL; - } - - fwrite(STDOUT, Color::info($question)); - $handle = fopen('php://stdin', 'r'); - $line = (int)fgets($handle); - if (!isset($migrationsDirs[$line])) { - echo "ABORTING!\n"; - return false; - } - - fclose($handle); - $migrationsDir = $migrationsDirs[$line]; - } else { - $migrationsDir = $migrationsDirs[0]; - } - } else { - $migrationsDir = $migrationsDirs; - } - // Migrations directory - if ($migrationsDir && !file_exists($migrationsDir)) { - mkdir($migrationsDir, 0755, true); - } + $migrationsDirs = $optionStack->getOption('migrationsDir'); + $migrationsDir = $helper->getMigrationsDir($migrationsDirs); $versionItem = $optionStack->getVersionNameGeneratingMigration(); + $verbose = $optionStack->getOption('verbose'); + $force = $optionStack->getOption('force'); // Path to migration dir - $migrationPath = rtrim($migrationsDir, '\\/') . DIRECTORY_SEPARATOR . $versionItem->getVersion(); - if (!file_exists($migrationPath)) { - if (is_writable(dirname($migrationPath)) && !$optionStack->getOption('verbose')) { - mkdir($migrationPath); - } elseif (!is_writable(dirname($migrationPath))) { - throw new RuntimeException("Unable to write '{$migrationPath}' directory. Permission denied"); - } - } elseif (!$optionStack->getOption('force')) { - throw new RuntimeException('Version ' . $versionItem->getVersion() . ' already exists'); - } + $migrationPath = $helper->getMigrationsPath($versionItem, $migrationsDir, $verbose, $force); // Try to connect to the DB if (!isset($optionStack->getOption('config')->database)) { throw new RuntimeException('Cannot load database configuration'); } - ModelMigration::setup($optionStack->getOption('config')->database, $optionStack->getOption('verbose')); + ModelMigration::setup($optionStack->getOption('config')->database, $verbose); ModelMigration::setSkipAutoIncrement((bool)$optionStack->getOption('noAutoIncrement')); ModelMigration::setMigrationPath($migrationsDir); @@ -137,17 +106,14 @@ public static function generate(array $options) $optionStack->getOption('skip-ref-schema') ); - if (!$optionStack->getOption('verbose')) { + if (!$verbose) { foreach ($migrations as $tableName => $migration) { if ($tableName === self::MIGRATION_LOG_TABLE) { continue; } $tableFile = $migrationPath . DIRECTORY_SEPARATOR . $tableName . '.php'; - $wasMigrated = file_put_contents( - $tableFile, - 'getOption('exportDataFromTables') ?: [], $optionStack->getOption('skip-ref-schema') ); - if (!$optionStack->getOption('verbose')) { + if (!$verbose) { $tableFile = $migrationPath . DIRECTORY_SEPARATOR . $table . '.php'; - $wasMigrated = file_put_contents( - $tableFile, - 'getVersion() . ' was successfully generated') . PHP_EOL; - } elseif (!$optionStack->getOption('verbose')) { + } elseif (!$verbose) { print Color::info('Nothing to generate. You should create tables first.') . PHP_EOL; } } @@ -226,7 +189,6 @@ public static function run(array $options) /** @var IncrementalItem $initialVersion */ $initialVersion = self::getCurrentVersion($optionStack->getOptions()); $completedVersions = self::getCompletedVersions($optionStack->getOptions()); - $migrationsDirs = []; $versionItems = []; $migrationsDirList = $optionStack->getOption('migrationsDir'); if (is_array($migrationsDirList)) { @@ -235,7 +197,6 @@ public static function run(array $options) if (!file_exists($migrationsDir)) { throw new RuntimeException('Migrations directory was not found.'); } - $migrationsDirs[] = $migrationsDir; foreach (ModelMigration::scanForVersions($migrationsDir) as $items) { $items->setPath($migrationsDir); $versionItems[] = $items; @@ -247,7 +208,6 @@ public static function run(array $options) throw new RuntimeException('Migrations directory was not found.'); } - $migrationsDirs[] = $migrationsDir; foreach (ModelMigration::scanForVersions($migrationsDir) as $items) { $items->setPath($migrationsDir); $versionItems[] = $items; @@ -302,15 +262,14 @@ public static function run(array $options) if (ModelMigration::DIRECTION_FORWARD === $direction) { // If we migrate up, we should go from the beginning to run some migrations which may have been missed $versionItemsTmp = VersionCollection::sortAsc(array_merge($versionItems, [$initialVersion])); - $initialVersion = $versionItemsTmp[0]; } else { /* - * If we migrate downs, - * we should go from the last migration to revert some migrations which may have been missed + * If we migrate downs, we should go from the last migration to revert some migrations which may have + * been missed */ $versionItemsTmp = VersionCollection::sortDesc(array_merge($versionItems, [$initialVersion])); - $initialVersion = $versionItemsTmp[0]; } + $initialVersion = $versionItemsTmp[0]; // Run migration $versionsBetween = VersionCollection::between($initialVersion, $finalVersion, $versionItems); diff --git a/src/Mvc/Model/Migration.php b/src/Mvc/Model/Migration.php index 50a24fe..fe00143 100644 --- a/src/Mvc/Model/Migration.php +++ b/src/Mvc/Model/Migration.php @@ -15,6 +15,7 @@ use DirectoryIterator; use Exception; +use Nette\PhpGenerator\PsrPrinter; use Phalcon\Config; use Phalcon\Db\Adapter\AbstractAdapter; use Phalcon\Db\Adapter\Pdo\Mysql as PdoMysql; @@ -217,6 +218,7 @@ public static function generate( array $exportTables = [], bool $skipRefSchema = false ): string { + $printer = new PsrPrinter(); $snippet = new Snippet(); $adapter = strtolower((string)self::$databaseConfig->path('adapter')); $defaultSchema = self::resolveDbSchema(self::$databaseConfig); @@ -225,122 +227,27 @@ public static function generate( $references = self::$connection->describeReferences($table, $defaultSchema); $tableOptions = self::$connection->tableOptions($table, $defaultSchema); - $generateAction = new GenerateAction($adapter, $description, $indexes, $references, $tableOptions); - - /** - * Generate Columns - */ - $tableDefinition = []; - foreach ($generateAction->getColumns() as $columnName => $columnDefinition) { - $tableDefinition[] = $snippet->getColumnDefinition($columnName, $columnDefinition); - } - - /** - * Generate Indexes - */ - $indexesDefinition = []; - foreach ($generateAction->getIndexes() as $indexName => $indexDefinition) { - [$definition, $type] = $indexDefinition; - $indexesDefinition[] = $snippet->getIndexDefinition($indexName, $definition, $type); - } - - /** - * Generate References - */ - $referencesDefinition = []; - foreach ($generateAction->getReferences($skipRefSchema) as $constraintName => $referenceDefinition) { - $referencesDefinition[] = $snippet->getReferenceDefinition($constraintName, $referenceDefinition); - } - - /** - * Generate Options - */ - $optionsDefinition = $generateAction->getOptions(self::$skipAI); - $classVersion = preg_replace('/[^0-9A-Za-z]/', '', (string)$version->getStamp()); $className = Text::camelize($table) . 'Migration_' . $classVersion; + $shouldExportDataFromTable = self::shouldExportDataFromTable($table, $exportTables); - // morph() - $classData = $snippet->getMigrationMorph($className, $table, $tableDefinition); - - if (count($indexesDefinition) > 0) { - $classData .= $snippet->getMigrationDefinition('indexes', $indexesDefinition); - } - - if (count($referencesDefinition) > 0) { - $classData .= $snippet->getMigrationDefinition('references', $referencesDefinition); - } - - if (count($optionsDefinition) > 0) { - $classData .= $snippet->getMigrationDefinition('options', $optionsDefinition); - } - - $classData .= " ]\n );\n }\n"; - - // up() - $classData .= $snippet->getMigrationUp(); - - if ($exportData === 'always' || self::shouldExportDataFromTable($table, $exportTables)) { - $classData .= $snippet->getMigrationBatchInsert($table, $generateAction->getQuoteWrappedColumns()); - } - - $classData .= "\n }\n"; - - // down() - $classData .= $snippet->getMigrationDown(); - - if ($exportData === 'always' || self::shouldExportDataFromTable($table, $exportTables)) { - $classData .= $snippet->getMigrationBatchDelete($table); - } - - $classData .= "\n }\n"; - - // afterCreateTable() - if ($exportData === 'oncreate') { - $classData .= $snippet->getMigrationAfterCreateTable($table, $generateAction->getQuoteWrappedColumns()); - } - - // end of class - $classData .= "\n}\n"; - - $numericColumns = $generateAction->getNumericColumns(); - // dump data - if ( - $exportData === 'always' || - $exportData === 'oncreate' || - self::shouldExportDataFromTable($table, $exportTables) - ) { - $fileHandler = fopen(self::$migrationPath . $version->getVersion() . '/' . $table . '.dat', 'w'); - $cursor = self::$connection->query('SELECT * FROM ' . self::$connection->escapeIdentifier($table)); - $cursor->setFetchMode(Enum::FETCH_ASSOC); - while ($row = $cursor->fetchArray()) { - $data = []; - foreach ($row as $key => $value) { - if (isset($numericColumns[$key])) { - if ($value === '' || $value === null) { - $data[] = 'NULL'; - } else { - $data[] = $value; - } - } else { - if (is_string($value)) { - $data[] = addslashes($value); - } else { - $data[] = $value === null ? 'NULL' : $value; - } - } - - unset($value); - } - - fputcsv($fileHandler, $data); - unset($row, $data); - } + $generateAction = new GenerateAction($adapter, $description, $indexes, $references, $tableOptions); + $generateAction->createEntity($className) + ->addMorph($snippet, $table, $skipRefSchema, self::$skipAI) + ->addUp($table, $exportData, $shouldExportDataFromTable) + ->addDown($table, $exportData, $shouldExportDataFromTable) + ->addAfterCreateTable($table, $exportData) + ->createDumpFiles( + $table, + self::$migrationPath, + self::$connection, + $version, + $exportData, + $shouldExportDataFromTable + ); - fclose($fileHandler); - } - return $classData; + return $printer->printFile($generateAction->getEntity()); } public static function shouldExportDataFromTable(string $table, array $exportTables): bool diff --git a/src/Utils/Helper.php b/src/Utils/Helper.php new file mode 100644 index 0000000..b35ec9c --- /dev/null +++ b/src/Utils/Helper.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Migrations\Utils; + +use Phalcon\Migrations\Console\Color; +use Phalcon\Migrations\Exception\RuntimeException; +use Phalcon\Migrations\Version\ItemInterface; + +class Helper +{ + public function getMigrationsDir($migrationsDirs) + { + if (is_array($migrationsDirs)) { + if (count($migrationsDirs) > 1) { + $question = 'Which migrations path would you like to use?' . PHP_EOL; + foreach ($migrationsDirs as $id => $dir) { + $question .= " [{$id}] $dir" . PHP_EOL; + } + + fwrite(STDOUT, Color::info($question)); + $handle = fopen('php://stdin', 'r'); + $line = (int)fgets($handle); + if (!isset($migrationsDirs[$line])) { + echo "ABORTING!\n"; + return false; + } + + fclose($handle); + $migrationsDir = $migrationsDirs[$line]; + } else { + $migrationsDir = $migrationsDirs[0]; + } + } else { + $migrationsDir = $migrationsDirs; + } + + if (!$migrationsDir) { + throw new RuntimeException('Migrations directory is not defined. Cannot proceed'); + } + + if (!file_exists($migrationsDir)) { + mkdir($migrationsDir, 0755, true); + } + + return $migrationsDir; + } + + public function getMigrationsPath(ItemInterface $versionItem, $migrationsDir, $verbose, $force): string + { + // Path to migration dir + $migrationPath = rtrim($migrationsDir, '\\/') . DIRECTORY_SEPARATOR . $versionItem->getVersion(); + if (!file_exists($migrationPath)) { + $dirIsWritable = is_writable(dirname($migrationPath)); + if (!$verbose && $dirIsWritable) { + mkdir($migrationPath); + } elseif (!$dirIsWritable) { + throw new RuntimeException("Unable to write '{$migrationPath}' directory. Permission denied"); + } + } elseif (!$force) { + throw new RuntimeException('Version ' . $versionItem->getVersion() . ' already exists'); + } + + return $migrationPath; + } +}