diff --git a/README.md b/README.md index 66c66cd..c37b9f8 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ $ composer require graze/morphism All commands support the `--help` parameter which give more information on usage. -* **morphism-extract**: Extract schema definitions from a mysqldump file. -* **morphism-dump**: Dump database schema for a named database connection. -* **morphism-lint**: Check database schema files for correctness. -* **morphism-diff**: Show necessary DDL statements to make a given database match the schema files. Optionally apply the changes too. +* **morphism extract**: Extract schema definitions from a mysqldump file. +* **morphism dump**: Dump database schema for a named database connection. +* **morphism lint**: Check database schema files for correctness. +* **morphism diff**: Show necessary DDL statements to make a given database match the schema files. Optionally apply the changes too. ## Config File @@ -66,7 +66,7 @@ databases: unix_socket: '/var/lib/mysql/catalog.sock' # morphism specific options morphism: - # morphism-diff only operates on connections with 'enable: true' + # morphism diff only operates on connections with 'enable: true' enable: true # Path where schema files live. # Defaults to "schema/" @@ -89,12 +89,12 @@ databases: ## Example Usage -This example uses `morphism-dump` to generate schema files from a database, `morphism-lint` for checking the files and `morphism-diff` to apply changes both interactively and automatically. +This example uses `morphism dump` to generate schema files from a database, `morphism lint` for checking the files and `morphism diff` to apply changes both interactively and automatically. ``` (master) $ # create a baseline for the schema (master) $ mkdir schema -(master) $ bin/morphism-dump --write config.yml catalog +(master) $ bin/morphism dump --write config.yml catalog (master) $ git add schema/catalog (master) $ git commit -m "initial checkin of catalog schema" (master) $ @@ -102,17 +102,17 @@ This example uses `morphism-dump` to generate schema files from a database, `mor (master) $ git checkout -b catalog-fixes (catalog-fixes) $ vi schema/catalog/product.sql # edit table definition (catalog-fixes) $ vi schema/catalog/product_dimensions.sql # add new table -(catalog-fixes) $ bin/morphism-lint schema/catalog # check syntax +(catalog-fixes) $ bin/morphism lint schema/catalog # check syntax ERROR schema/catalog/product_dimensions.sql, line 2: unknown datatype 'intt' 1: CREATE TABLE product_dimensions ( 2: `pd_id` intt<>(10) unsigned NOT NULL AUTO_INCREMENT, (catalog-fixes) $ vi schema/catalog/product_dimensions.sql # fix table definition -(catalog-fixes) $ bin/morphism-lint schema/catalog # check syntax +(catalog-fixes) $ bin/morphism lint schema/catalog # check syntax (catalog-fixes) $ git add schema/catalog (catalog-fixes) $ git rm schema/catalog/discontinued.sql # delete a table (catalog-fixes) $ git commit -m "various changes to catalog schema" (catalog-fixes) $ # alter the database to match the schema files -(catalog-fixes) $ bin/morphism-diff --apply-changes=confirm config.yml catalog +(catalog-fixes) $ bin/morphism diff --apply-changes=confirm config.yml catalog -- -------------------------------- -- Connection: catalog -- -------------------------------- @@ -158,7 +158,7 @@ CREATE TABLE `product_dimensions` ( (catalog-fixes) $ # do some work back on master... (catalog-fixes) $ git checkout master (master) $ # restore schema to previous state -(master) $ bin/morphism-diff --apply-changes=yes config.yml catalog +(master) $ bin/morphism diff --apply-changes=yes config.yml catalog ``` ## Testing diff --git a/bin/morphism b/bin/morphism new file mode 100755 index 0000000..60d8065 --- /dev/null +++ b/bin/morphism @@ -0,0 +1,13 @@ +#!/usr/bin/env php +add(new Graze\Morphism\Command\Diff()); +$console->add(new Graze\Morphism\Command\Fastdump()); +$console->add(new Graze\Morphism\Command\Extract()); +$console->add(new Graze\Morphism\Command\Lint()); + +$console->run(); diff --git a/bin/morphism-diff b/bin/morphism-diff deleted file mode 100755 index 459acaa..0000000 --- a/bin/morphism-diff +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env php -argv($argv); - $cmd->run(); -} -catch(\Exception $e) { - fprintf(STDERR, "%s\n", $e->getMessage()); - exit(1); -} - diff --git a/bin/morphism-dump b/bin/morphism-dump deleted file mode 100755 index c38874a..0000000 --- a/bin/morphism-dump +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env php -argv($argv); - $cmd->run(); -} -catch(\Exception $e) { - fprintf(STDERR, "%s\n", $e->getMessage()); - exit(1); -} - diff --git a/bin/morphism-extract b/bin/morphism-extract deleted file mode 100755 index 8790622..0000000 --- a/bin/morphism-extract +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env php -argv($argv); - $cmd->run(); -} -catch(\Exception $e) { - fprintf(STDERR, "%s\n", $e->getMessage()); - exit(1); -} - diff --git a/bin/morphism-lint b/bin/morphism-lint deleted file mode 100755 index a74fb96..0000000 --- a/bin/morphism-lint +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env php -argv($argv); - $success = $cmd->run(); -} -catch(\Exception $e) { - fprintf(STDERR, "%s\n", $e->getMessage()); - exit(1); -} - -exit($success ? 0 : 1); diff --git a/composer.json b/composer.json index 283bd06..c4d8328 100644 --- a/composer.json +++ b/composer.json @@ -34,13 +34,15 @@ ], "require": { "php": ">=5.5", - "symfony/yaml": "~2.6", - "doctrine/dbal": "~2.5" + + "doctrine/dbal": "~2.5", + "symfony/console": "^2.8", + "symfony/yaml": "~2.6" }, "require-dev": { + "graze/standards": "^1.0", "phpunit/phpunit": "~4.5", - "squizlabs/php_codesniffer": "^2.5", - "graze/standards": "^1.0" + "squizlabs/php_codesniffer": "^2.5" }, "autoload": { "psr-4": { @@ -53,9 +55,6 @@ } }, "bin": [ - "./bin/morphism-diff", - "./bin/morphism-dump", - "./bin/morphism-extract", - "./bin/morphism-lint" + "./bin/morphism" ] } diff --git a/composer.lock b/composer.lock index 8dc5bc9..3eb0e5b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "df5ca643e094ad9a4b5ce08e367e1692", + "content-hash": "f8cd74a8a153746dd65794628bfa6560", "packages": [ { "name": "doctrine/annotations", @@ -475,6 +475,230 @@ ], "time": "2014-09-09T13:34:57+00:00" }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "symfony/console", + "version": "v2.8.41", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/e8e59b74ad1274714dad2748349b55e3e6e630c7", + "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/debug": "^2.7.2|~3.0.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1|~3.0.0", + "symfony/process": "~2.1|~3.0.0" + }, + "suggest": { + "psr/log-implementation": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2018-05-15T21:17:45+00:00" + }, + { + "name": "symfony/debug", + "version": "v2.8.41", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "fe8838e11cf7dbaf324bd6f51d065d873ccf78a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/fe8838e11cf7dbaf324bd6f51d065d873ccf78a2", + "reference": "fe8838e11cf7dbaf324bd6f51d065d873ccf78a2", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/class-loader": "~2.2|~3.0.0", + "symfony/http-kernel": "~2.3.24|~2.5.9|^2.6.2|~3.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2018-05-15T21:17:45+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "3296adf6a6454a050679cde90f95350ad604b171" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", + "reference": "3296adf6a6454a050679cde90f95350ad604b171", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2018-04-26T10:06:28+00:00" + }, { "name": "symfony/yaml", "version": "v2.8.34", diff --git a/docker-compose.yml b/docker-compose.yml index 3b4e62c..1fa5ccf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: working_dir: /usr/src/app db: - image: mysql + image: mysql:5.6 volumes: - ./example/schema:/docker-entrypoint-initdb.d environment: diff --git a/example/morphism.conf.example b/example/morphism.conf.example index 3ca4b38..ccd5a02 100644 --- a/example/morphism.conf.example +++ b/example/morphism.conf.example @@ -15,7 +15,7 @@ databases: #unix_socket: '/var/lib/mysql/foo.sock' # morphism specific options morphism: - # morphism-diff only operates on connections with 'enable: true' + # morphism diff only operates on connections with 'enable: true' enable: true # Path where schema files live. # Defaults to "schema/" diff --git a/src/Command/Argv/ConsumerInterface.php b/src/Command/Argv/ConsumerInterface.php deleted file mode 100644 index b017701..0000000 --- a/src/Command/Argv/ConsumerInterface.php +++ /dev/null @@ -1,18 +0,0 @@ -option = $option; - $this->value = $value; - } - - /** - * @return string - */ - public function getOption() - { - return $this->option; - } - - public function noValue() - { - if (!is_null($this->value)) { - throw new Exception("does not allow an argument"); - } - } - - /** - * @return null|string - */ - public function required() - { - if (is_null($this->value)) { - throw new Exception("requires an argument"); - } - return $this->value; - } - - /** - * @param string $default - * @return string|null - */ - public function optional($default = null) - { - return is_null($this->value) ? $default : $this->value; - } - - /** - * @return bool - */ - public function bool() - { - $this->noValue(); - return substr($this->option, 0, 5) === '--no-' ? false : true; - } - - public function unrecognised() - { - throw new Exception("unrecognised option"); - } -} diff --git a/src/Command/Argv/Parser.php b/src/Command/Argv/Parser.php deleted file mode 100644 index 197ebd0..0000000 --- a/src/Command/Argv/Parser.php +++ /dev/null @@ -1,90 +0,0 @@ -prog = basename(array_shift($argv)); - $this->argv = $argv; - } - - /** - * @return string|null - */ - public function getProg() - { - return $this->prog; - } - - /** - * @param ConsumerInterface $consumer - */ - public function consumeWith(ConsumerInterface $consumer) - { - try { - while (count($this->argv) > 0) { - $opt = array_shift($this->argv); - if ($opt == '--') { - $this->args = array_merge($this->args, $argv); - $this->argv = []; - } elseif (in_array($opt, ['-h', '-help', '--help'])) { - $consumer->consumeHelp($this->prog); - exit(0); - } elseif (strlen($opt) > 1 && $opt[0] == '-') { - $eqPos = strpos($opt, '='); - if ($eqPos === false) { - $option = new Option($opt); - } else { - $option = new Option( - substr($opt, 0, $eqPos), - // substr($s, strlen($s)) returns false, so we must - // cast to string to ensure we get '' as any sane - // person would expect - (string)substr($opt, $eqPos + 1) - ); - } - try { - $consumer->consumeOption($option); - } catch (Exception $e) { - throw new Exception("option `{$option->getOption()}`: " . $e->getMessage()); - } - } else { - $this->args[] = $opt; - } - } - - $consumer->consumeArgs($this->args); - } catch (Exception $e) { - $this->usage($e->getMessage()); - exit(1); - } - } - - /** - * @param string $msg - */ - public function usage($msg = null) - { - fprintf( - STDERR, - "%s: %s\n" . - "Try `%s --help' for more information.\n", - $this->prog, - $msg, - $this->prog - ); - } -} diff --git a/src/Command/Diff.php b/src/Command/Diff.php index 3b57b29..59fc872 100644 --- a/src/Command/Diff.php +++ b/src/Command/Diff.php @@ -2,14 +2,46 @@ namespace Graze\Morphism\Command; +use Doctrine\DBAL\Connection; +use Exception; use Graze\Morphism\Parse\TokenStream; use Graze\Morphism\Parse\Token; use Graze\Morphism\Parse\MysqlDump; use Graze\Morphism\Extractor; use Graze\Morphism\Config; - -class Diff implements Argv\ConsumerInterface +use InvalidArgumentException; +use RuntimeException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Diff extends Command { + const COMMAND_NAME = 'diff'; + + // Command line arguments + const ARGUMENT_CONFIG_FILE = 'config-file'; + const ARGUMENT_CONNECTIONS = 'connections'; + + // Command line options + const OPTION_ENGINE = 'engine'; + const OPTION_COLLATION = 'collation'; + const OPTION_APPLY_CHANGES = 'apply-changes'; + const OPTION_LOG_DIR = 'log-dir'; + + const OPTION_QUOTE_NAMES = 'quote-names'; + const OPTION_NO_QUOTE_NAMES = 'no-quote-names'; + const OPTION_CREATE_TABLE = 'create-table'; + const OPTION_NO_CREATE_TABLE = 'no-create-table'; + const OPTION_DROP_TABLE = 'drop-table'; + const OPTION_NO_DROP_TABLE = 'no-drop-table'; + const OPTION_ALTER_ENGINE = 'alter-engine'; + const OPTION_NO_ALTER_ENGINE = 'no-alter-engine'; + const OPTION_LOG_SKIPPED = 'log-skipped'; + const OPTION_NO_LOG_SKIPPED = 'no-log-skipped'; + /** @var string */ private $engine = 'InnoDB'; /** @var string|null */ @@ -33,12 +65,11 @@ class Diff implements Argv\ConsumerInterface /** @var bool */ private $logSkipped = true; - /** - * @param string $prog - */ - public function consumeHelp($prog) + protected function configure() { - printf( + $this->setName(self::COMMAND_NAME); + + $helpText = sprintf( "Usage: %s [OPTION] CONFIG-FILE [CONN] ...\n" . "Extracts schema definitions from the named connections, and outputs the\n" . "necessary ALTER TABLE statements to transform them into what is defined\n" . @@ -62,94 +93,72 @@ public function consumeHelp($prog) "A YAML file mapping connection names to parameters. See the morphism project's\n" . "README.md file for detailed information.\n" . "", - $prog + self::COMMAND_NAME ); - } + $this->setHelp($helpText); - /** - * @param array $argv - */ - public function argv(array $argv) - { - $argvParser = new Argv\Parser($argv); - $argvParser->consumeWith($this); - } + $this->addArgument( + self::ARGUMENT_CONFIG_FILE, + InputArgument::REQUIRED + ); - /** - * @param Argv\Option $option - */ - public function consumeOption(Argv\Option $option) - { - switch ($option->getOption()) { - case '--engine': - $this->engine = $option->required(); - break; - - case '--collation': - $this->collation = $option->required(); - break; - - case '--quote-names': - case '--no-quote-names': - $this->quoteNames = $option->bool(); - break; - - case '--create-table': - case '--no-create-table': - $this->createTable = $option->bool(); - break; - - case '--drop-table': - case '--no-drop-table': - $this->createTable = $option->bool(); - break; - - case '--alter-engine': - case '--no-alter-engine': - $this->alterEngine = $option->bool(); - break; - - case '--apply-changes': - $applyChanges = $option->required(); - if (!in_array($applyChanges, ['yes', 'no', 'confirm'])) { - throw new Argv\Exception("unknown value"); - } - $this->applyChanges = $applyChanges; - break; + $this->addArgument( + self::ARGUMENT_CONNECTIONS, + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + null, + [] + ); - case '--log-dir': - $this->logDir = $option->required(); - break; + $this->addOption( + self::OPTION_ENGINE, + null, + InputOption::VALUE_REQUIRED, + 'Database engine', + 'InnoDB' + ); + $this->addOption( + self::OPTION_COLLATION, + null, + InputOption::VALUE_REQUIRED, + 'Database collation' + ); - case '--log-skipped': - case '--no-log-skipped': - $this->logSkipped = $option->bool(); - break; + $this->addOption(self::OPTION_QUOTE_NAMES); + $this->addOption(self::OPTION_NO_QUOTE_NAMES); - default: - $option->unrecognised(); - break; - } - } + $this->addOption(self::OPTION_CREATE_TABLE); + $this->addOption(self::OPTION_NO_CREATE_TABLE); - /** - * @param array $args - */ - public function consumeArgs(array $args) - { - if (count($args) < 1) { - throw new Argv\Exception("expected CONFIG-FILE"); - } - $this->configFile = array_shift($args); - $this->connectionNames = $args; + $this->addOption(self::OPTION_DROP_TABLE); + $this->addOption(self::OPTION_NO_DROP_TABLE); + + $this->addOption(self::OPTION_ALTER_ENGINE); + $this->addOption(self::OPTION_NO_ALTER_ENGINE); + + $this->addOption( + self::OPTION_APPLY_CHANGES, + null, + InputOption::VALUE_REQUIRED, + null, + "no" + ); + + $this->addOption( + self::OPTION_LOG_DIR, + null, + InputOption::VALUE_REQUIRED + ); + + $this->addOption(self::OPTION_LOG_SKIPPED); + $this->addOption(self::OPTION_NO_LOG_SKIPPED); } /** - * @param string $connection + * @param Connection $connection * @param string $dbName * @return MysqlDump */ - private function getCurrentSchema($connection, $dbName) + private function getCurrentSchema(Connection $connection, $dbName) { $extractor = new Extractor($connection); $extractor->setDatabases([$dbName]); @@ -186,12 +195,12 @@ private function getTargetSchema($schemaDefinitionPath, $dbName) } /** - * @param string $connection + * @param Connection $connection * @param string $connectionName * @param array $diff - * @throws \Exception + * @throws Exception */ - private function applyChanges($connection, $connectionName, array $diff) + private function applyChanges(Connection $connection, $connectionName, array $diff) { if (count($diff) == 0) { return; @@ -229,7 +238,7 @@ private function applyChanges($connection, $connectionName, array $diff) echo "-- Apply this change? [y]es [n]o [a]ll [q]uit: "; $response = fgets(STDIN); if ($response === false) { - throw new \Exception("could not read response"); + throw new Exception("Could not read response"); } $response = rtrim($response); } while (!in_array($response, ['y', 'n', 'a', 'q'])); @@ -274,10 +283,40 @@ private function applyChanges($connection, $connectionName, array $diff) } /** - * @throws \Exception + * @param InputInterface $input + * @param OutputInterface $output + * @throws Exception */ - public function run() + protected function execute(InputInterface $input, OutputInterface $output) { + $this->configFile = $input->getArgument(self::ARGUMENT_CONFIG_FILE); + $this->connectionNames = $input->getArgument(self::ARGUMENT_CONNECTIONS); + + if ($input->getOption(self::OPTION_NO_QUOTE_NAMES)) { + $this->quoteNames = false; + } + + if ($input->getOption(self::OPTION_NO_CREATE_TABLE)) { + $this->createTable = false; + } + + if ($input->getOption(self::OPTION_NO_DROP_TABLE)) { + $this->dropTable = false; + } + + $this->applyChanges = $input->getOption(self::OPTION_APPLY_CHANGES); + if (!in_array($this->applyChanges, ['yes', 'no', 'confirm'])) { + throw new InvalidArgumentException(sprintf( + "Unknown value for --%s: %s", + self::OPTION_APPLY_CHANGES, + $this->applyChanges + )); + } + + if ($input->getOption(self::OPTION_NO_LOG_SKIPPED)) { + $this->logSkipped = false; + } + try { $config = new Config($this->configFile); $config->parse(); @@ -291,7 +330,7 @@ public function run() if (!is_null($this->logDir)) { if (!is_dir($this->logDir)) { - if (!@mkdir($this->logDir, 0777, true)) { + if (!@mkdir($this->logDir, 0755, true)) { fprintf(STDERR, "Could not create log directory: {$this->logDir}\n"); exit(1); } @@ -341,10 +380,10 @@ public function run() $this->applyChanges($connection, $connectionName, $statements); } - } catch (\RuntimeException $e) { + } catch (RuntimeException $e) { throw $e; - } catch (\Exception $e) { - throw new \Exception($e->getMessage() . "\n\n" . $e->getTraceAsString()); + } catch (Exception $e) { + throw new Exception($e->getMessage() . "\n\n" . $e->getTraceAsString()); } } } diff --git a/src/Command/Extract.php b/src/Command/Extract.php index 3fafafd..5f22b45 100644 --- a/src/Command/Extract.php +++ b/src/Command/Extract.php @@ -2,13 +2,29 @@ namespace Graze\Morphism\Command; +use Exception; use Graze\Morphism\Parse\MysqlDump; use Graze\Morphism\Parse\TokenStream; +use RuntimeException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; -class Extract implements Argv\ConsumerInterface +class Extract extends Command { - /** @var bool */ - private $quoteNames = true; + const COMMAND_NAME = 'extract'; + + // Command line arguments + const ARGUMENT_MYSQL_DUMP_FILE = 'mysql-dump-file'; + + // Command line options + const OPTION_SCHEMA_PATH = 'schema-path'; + const OPTION_DATABASE = 'database'; + const OPTION_WRITE = 'write'; + const OPTION_NO_WRITE = 'no-write'; + /** @var string */ private $schemaPath = './schema'; /** @var bool */ @@ -18,12 +34,11 @@ class Extract implements Argv\ConsumerInterface /** @var string|null */ private $databaseName = null; - /** - * @param string $prog - */ - public function consumeHelp($prog) + protected function configure() { - printf( + $this->setName(self::COMMAND_NAME); + + $helpText = sprintf( "Usage: %s [OPTIONS] [MYSQL-DUMP-FILE]\n" . "Extracts schema definition(s) from a mysqldump file. Multiple databases may\n" . "be defined in the dump, and they will be extracted to separate directories.\n" . @@ -32,95 +47,77 @@ public function consumeHelp($prog) "\n" . "OPTIONS\n" . " -h, -help, --help display this message, and exit\n" . - " --[no-]quote-names [do not] quote names with `...`; default: no\n" . " --schema-path=PATH location of schemas; default: ./schema\n" . " --database=NAME name of database if not specified in dump\n" . " --[no-]write write schema files to schema path; default: no\n" . "", - $prog + self::COMMAND_NAME ); - } + $this->setHelp($helpText); - /** - * @param array $argv - */ - public function argv(array $argv) - { - $argvParser = new Argv\Parser($argv); - $argvParser->consumeWith($this); - } + $this->addArgument( + self::ARGUMENT_MYSQL_DUMP_FILE, + InputArgument::OPTIONAL, + null, + 'php://stdin' + ); - /** - * @param Argv\Option $option - */ - public function consumeOption(Argv\Option $option) - { - switch ($option->getOption()) { - case '--quote-names': - case '--no-quote-names': - $this->quoteNames = $option->bool(); - break; - - case '--schema-path': - $this->schemaPath = $option->required(); - break; - - case '--database': - $this->databaseName = $option->required(); - break; - - case '--write': - case '--no-write': - $this->write = $option->bool(); - break; - - default: - $option->unrecognised(); - break; - } + $this->addOption( + self::OPTION_SCHEMA_PATH, + null, + InputOption::VALUE_REQUIRED, + null, + './schema' + ); + + $this->addOption( + self::OPTION_DATABASE, + null, + InputOption::VALUE_REQUIRED + ); + + $this->addOption(self::OPTION_WRITE); + $this->addOption(self::OPTION_NO_WRITE); } /** - * @param array $args + * @param InputInterface $input + * @param OutputInterface $output + * @throws Exception */ - public function consumeArgs(array $args) + protected function execute(InputInterface $input, OutputInterface $output) { - if (count($args) == 0) { - $this->mysqldump = 'php://stdin'; - } elseif (count($args) == 1) { - $this->mysqldump = $args[0]; - } else { - throw new Argv\Exception("expected a mysqldump file"); + $this->mysqldump = $input->getArgument(self::ARGUMENT_MYSQL_DUMP_FILE); + + $this->schemaPath = $input->getOption(self::OPTION_SCHEMA_PATH); + $this->databaseName = $input->getOption(self::OPTION_DATABASE); + + if ($input->getOption(self::OPTION_WRITE)) { + $this->write = true; } - } - /** - * @throws \Exception - */ - public function run() - { $stream = TokenStream::newFromFile($this->mysqldump); $dump = new MysqlDump(); try { $dump->parse($stream); - } catch (\RuntimeException $e) { - throw new \RuntimeException($stream->contextualise($e->getMessage())); - } catch (\Exception $e) { - throw new \Exception($e->getMessage() . "\n\n" . $e->getTraceAsString()); + } catch (RuntimeException $e) { + throw new RuntimeException($stream->contextualise($e->getMessage())); + } catch (Exception $e) { + throw new Exception($e->getMessage() . "\n\n" . $e->getTraceAsString()); } if ($this->write) { foreach ($dump->databases as $database) { $databaseName = ($database->name == '') ? $this->databaseName : $database->name; if ($databaseName == '') { - throw new \RuntimeException("no database name specified in dump - please use --database=NAME to supply one"); + throw new RuntimeException("No database name specified in dump - please use --database=NAME to supply one"); } $output = "{$this->schemaPath}/$databaseName"; if (!is_dir($output)) { - if (!@mkdir($output, 0777, true)) { - throw new \RuntimeException("could not make directory $output"); + if (!@mkdir($output, 0755, true)) { + throw new RuntimeException("Could not make directory $output"); } } foreach ($database->tables as $table) { @@ -130,7 +127,7 @@ public function run() $text .= "$query;\n\n"; } if (false === @file_put_contents($path, $text)) { - throw new \RuntimeException("could not write $path"); + throw new RuntimeException("Could not write $path"); } fprintf(STDERR, "wrote $path\n"); } diff --git a/src/Command/Fastdump.php b/src/Command/Fastdump.php index 9f8d545..816a447 100644 --- a/src/Command/Fastdump.php +++ b/src/Command/Fastdump.php @@ -2,13 +2,31 @@ namespace Graze\Morphism\Command; +use Exception; use Graze\Morphism\Parse\TokenStream; use Graze\Morphism\Parse\MysqlDump; use Graze\Morphism\Extractor; use Graze\Morphism\Config; +use RuntimeException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; -class Fastdump implements Argv\ConsumerInterface +class Fastdump extends Command { + const COMMAND_NAME = 'dump'; + + // Command line arguments + const ARGUMENT_CONFIG_FILE = 'config-file'; + const ARGUMENT_CONNECTIONS = 'connections'; + + // Command line options + const OPTION_QUOTE_NAMES = 'quote-names'; + const OPTION_NO_QUOTE_NAMES = 'no-quote-names'; + const OPTION_WRITE = 'write'; + const OPTION_NO_WRITE = 'no-write'; + /** @var bool */ private $quoteNames = true; /** @var string|null */ @@ -18,81 +36,75 @@ class Fastdump implements Argv\ConsumerInterface /** @var array */ private $connectionNames = []; - /** - * @param string $prog - */ - public function consumeHelp($prog) + protected function configure() { - printf( - "Usage: %s [OPTIONS] CONFIG-FILE CONN [CONN ...]\n" . + $this->setName(self::COMMAND_NAME); + + $helpText = sprintf( + "Usage: %s [OPTIONS] CONFIG-FILE [CONN ...]\n" . "Dumps database schemas for named connections. This tool is considerably faster\n" . "than mysqldump, especially for large schemas. You might use this tool to\n" . "(re-)initalise your project's schema directory from a local database.\n" . + "If no connections are specified, all connections\n" . + "in the config with 'morphism: enable: true' will be used.\n" . "\n" . "OPTIONS\n" . " -h, -help, --help display this message, and exit\n" . - " --[no-]quote-names [do not] quote names with `...`; default: no\n" . + " --[no-]quote-names [do not] quote names with `...`; default: yes\n" . " --[no-]write write schema files to schema path; default: no\n" . "\n" . "CONFIG-FILE\n" . "A YAML file mapping connection names to parameters. See the morphism project's\n" . "README.md file for detailed information.\n" . "", - $prog + self::COMMAND_NAME ); - } + $this->setHelp($helpText); - /** - * @param array $argv - */ - public function argv(array $argv) - { - $argvParser = new Argv\Parser($argv); - $argvParser->consumeWith($this); + $this->addArgument( + self::ARGUMENT_CONFIG_FILE, + InputArgument::REQUIRED + ); + + $this->addArgument( + self::ARGUMENT_CONNECTIONS, + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + null, + [] + ); + + $this->addOption(self::OPTION_QUOTE_NAMES); + $this->addOption(self::OPTION_NO_QUOTE_NAMES); + + $this->addOption(self::OPTION_WRITE); + $this->addOption(self::OPTION_NO_WRITE); } /** - * @param Argv\Option $option + * @param InputInterface $input + * @param OutputInterface $output + * @throws Exception */ - public function consumeOption(Argv\Option $option) + protected function execute(InputInterface $input, OutputInterface $output) { - switch ($option->getOption()) { - case '--quote-names': - case '--no-quote-names': - $this->quoteNames = $option->bool(); - break; - - case '--write': - case '--no-write': - $this->write = $option->bool(); - break; - - default: - $option->unrecognised(); - break; + $this->configFile = $input->getArgument(self::ARGUMENT_CONFIG_FILE); + $this->connectionNames = $input->getArgument(self::ARGUMENT_CONNECTIONS); + + if ($input->getOption(self::OPTION_QUOTE_NAMES)) { + $this->quoteNames = false; } - } - /** - * @param array $args - */ - public function consumeArgs(array $args) - { - if (count($args) < 2) { - throw new Argv\Exception("expected CONFIG-FILE CONN [CONN ...]"); + if ($input->getOption(self::OPTION_WRITE)) { + $this->write = true; } - $this->configFile = array_shift($args); - $this->connectionNames = $args; - } - /** - * @throws \Exception - */ - public function run() - { $config = new Config($this->configFile); $config->parse(); + if (! $this->connectionNames) { + $this->connectionNames = $config->getConnectionNames(); + } + foreach ($this->connectionNames as $connectionName) { $connection = $config->getConnection($connectionName); @@ -120,16 +132,16 @@ public function run() $dump = new MysqlDump(); try { $dump->parse($stream, ['matchTables' => $matchTables]); - } catch (\RuntimeException $e) { - throw new \RuntimeException($stream->contextualise($e->getMessage())); - } catch (\Exception $e) { - throw new \Exception($e->getMessage() . "\n\n" . $e->getTraceAsString()); + } catch (RuntimeException $e) { + throw new RuntimeException($stream->contextualise($e->getMessage())); + } catch (Exception $e) { + throw new Exception($e->getMessage() . "\n\n" . $e->getTraceAsString()); } if ($this->write) { if (!is_dir($schemaDefinitionPath)) { - if (!@mkdir($schemaDefinitionPath, 0777, true)) { - throw new \RuntimeException("could not make directory $schemaDefinitionPath"); + if (!@mkdir($schemaDefinitionPath, 0755, true)) { + throw new RuntimeException("could not make directory $schemaDefinitionPath"); } } $database = reset($dump->databases); @@ -140,7 +152,7 @@ public function run() $text .= "$query;\n\n"; } if (false === @file_put_contents($path, $text)) { - throw new \RuntimeException("could not write $path"); + throw new RuntimeException("could not write $path"); } fprintf(STDERR, "wrote $path\n"); } diff --git a/src/Command/Lint.php b/src/Command/Lint.php index e145198..e4851ec 100644 --- a/src/Command/Lint.php +++ b/src/Command/Lint.php @@ -2,21 +2,37 @@ namespace Graze\Morphism\Command; +use GlobIterator; use Graze\Morphism\Parse\MysqlDump; use Graze\Morphism\Parse\TokenStream; +use RuntimeException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; -class Lint implements Argv\ConsumerInterface +class Lint extends Command { + const COMMAND_NAME = 'lint'; + + // Command line arguments + const ARGUMENT_PATH = 'path'; + + // Command line options + const OPTION_VERBOSE = 'verbose'; + const OPTION_NO_VERBOSE = 'no-verbose'; + /** @var bool */ private $verbose = false; + /** @var array */ + private $paths = []; - /** - * @param string $prog - */ - public function consumeHelp($prog) + protected function configure() { - printf( - "Usage: %s [OPTIONS] PATH ...\n" . + $this->setName(self::COMMAND_NAME); + + $helpText = sprintf( + "Usage: %s [OPTIONS] [PATH ...]\n" . "Checks all schema files below the specified paths for correctness. If no PATH\n" . "is given, checks standard input. By default output is only produced if errors\n" . "are detected.\n" . @@ -28,50 +44,32 @@ public function consumeHelp($prog) "EXIT STATUS\n" . "The exit status will be 1 if any errors were detected, or 0 otherwise.\n" . "", - $prog + self::COMMAND_NAME ); - } + $this->setHelp($helpText); - /** - * @param array $argv - */ - public function argv(array $argv) - { - $argvParser = new Argv\Parser($argv); - $argvParser->consumeWith($this); - } + $this->addArgument( + self::ARGUMENT_PATH, + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + null, + ['php://stdin'] + ); - /** - * @param Argv\Option $option - */ - public function consumeOption(Argv\Option $option) - { - switch ($option->getOption()) { - case '--verbose': - case '--no-verbose': - $this->verbose = $option->bool(); - break; - - default: - $option->unrecognised(); - break; - } + $this->addOption(self::OPTION_NO_VERBOSE); } /** - * @param array $args + * @param InputInterface $input + * @param OutputInterface $output + * @return int */ - public function consumeArgs(array $args) + protected function execute(InputInterface $input, OutputInterface $output) { - $this->paths = count($args) == 0 ? ['php://stdin'] : $args; - } + $this->paths = $input->getArgument(self::ARGUMENT_PATH); - /** - * @return bool - */ - public function run() - { - $success = true; + if ($input->getOption(self::OPTION_VERBOSE)) { + $this->verbose = true; + } $engine = null; $collation = null; @@ -86,7 +84,7 @@ public function run() if ($this->verbose) { echo "$path\n"; } - foreach (new \GlobIterator("$path/*.sql") as $fileInfo) { + foreach (new GlobIterator("$path/*.sql") as $fileInfo) { $files[] = $fileInfo->getPathname(); } } else { @@ -100,7 +98,7 @@ public function run() if ($this->verbose) { echo "OK $file\n"; } - } catch (\RuntimeException $e) { + } catch (RuntimeException $e) { $errorFiles[] = $file; $message = $stream->contextualise($e->getMessage()); echo "ERROR $message\n"; @@ -108,6 +106,6 @@ public function run() } } - return count($errorFiles) == 0; + return count($errorFiles); } } diff --git a/src/Parse/CreateTable.php b/src/Parse/CreateTable.php index 6169b2b..2954f8b 100644 --- a/src/Parse/CreateTable.php +++ b/src/Parse/CreateTable.php @@ -136,7 +136,7 @@ public function getCollation() /** * Returns an array of SQL DDL statements to create the table. * - * @return string + * @return array */ public function getDDL() { diff --git a/src/Parse/TokenStream.php b/src/Parse/TokenStream.php index e934f36..d012b5a 100644 --- a/src/Parse/TokenStream.php +++ b/src/Parse/TokenStream.php @@ -745,7 +745,7 @@ public function expectStringExtended() * * ... this function will produce something like this: * - * schema/morphism-test/foo.sql, line 2: unknown datatype 'bar' + * schema/morphism test/foo.sql, line 2: unknown datatype 'bar' * 1: CREATE TABLE `foo` ( * 2: `a` bar<> DEFAULT NULL *