diff --git a/Console/Command/MigrationShell.php b/Console/Command/MigrationShell.php index 5858ad4d..b0fe18db 100644 --- a/Console/Command/MigrationShell.php +++ b/Console/Command/MigrationShell.php @@ -16,10 +16,11 @@ App::uses('String', 'Utility'); App::uses('ClassRegistry', 'Utility'); App::uses('ConnectionManager', 'Model'); +App::uses('Folder', 'Utility'); +App::uses('File', 'Utility'); /** * Migration shell. - * */ class MigrationShell extends AppShell { @@ -156,44 +157,48 @@ public function getOptionParser() { '') ->addOption('plugin', array( 'short' => 'p', - 'help' => __('Plugin name to be used'))) + 'help' => __d('migrations', 'Plugin name to be used'))) ->addOption('precheck', array( 'short' => 'm', 'default' => 'Migrations.PrecheckException', - 'help' => __('Precheck migrations'))) + 'help' => __d('migrations', 'Precheck migrations'))) ->addOption('force', array( 'short' => 'f', 'boolean' => true, - 'help' => __('Force \'generate\' to compare all tables.'))) + 'help' => __d('migrations', 'Force \'generate\' to compare all tables.'))) + ->addOption('overwrite', array( + 'short' => 'o', + 'boolean' => true, + 'help' => __d('migrations', 'Overwrite the schema.php file after generated a migration.'))) ->addOption('connection', array( 'short' => 'c', 'default' => null, - 'help' => __('Overrides the \'default\' connection of the MigrationVersion'))) + 'help' => __d('migrations', 'Overrides the \'default\' connection of the MigrationVersion'))) ->addOption('migrationConnection', array( 'short' => 'i', 'default' => null, - 'help' => __('Overrides the \'default\' connection of the CakeMigrations that are applied'))) + 'help' => __d('migrations', 'Overrides the \'default\' connection of the CakeMigrations that are applied'))) ->addOption('dry', array( 'short' => 'd', 'boolean' => true, 'default' => false, - 'help' => __('Output the raw SQL queries rather than applying them to the database.'))) + 'help' => __d('migrations', 'Output the raw SQL queries rather than applying them to the database.'))) ->addOption('no-auto-init', array( 'short' => 'n', 'boolean' => true, 'default' => false, - 'help' => __('Disables automatic creation of migrations table and running any internal plugin migrations'))) + 'help' => __d('migrations', 'Disables automatic creation of migrations table and running any internal plugin migrations'))) ->addOption('schema-class', array( 'short' => 's', 'boolean' => false, 'default' => false, - 'help' => __('CamelCased Classname without the `Schema` suffix to use when reading or generating schema files. See `Console/cake schema generate --help`.'))) + 'help' => __d('migrations', 'CamelCased Classname without the `Schema` suffix to use when reading or generating schema files. See `Console/cake schema generate --help`.'))) ->addSubcommand('status', array( - 'help' => __('Displays a status of all plugin and app migrations.'))) + 'help' => __d('migrations', 'Displays a status of all plugin and app migrations.'))) ->addSubcommand('run', array( - 'help' => __('Run a migration to given direction or version.'))) + 'help' => __d('migrations', 'Run a migration to given direction or version.'))) ->addSubcommand('generate', array( - 'help' => __('Generates a migration file.'))); + 'help' => __d('migrations', 'Generates a migration file.'))); } /** @@ -267,7 +272,7 @@ public function run() { $result = $this->_execute($options, $once); if ($result !== true) { - $this->out(__d('migrations', $result)); + $this->out($result); } $this->out(__d('migrations', 'All migrations have completed.')); @@ -427,12 +432,15 @@ public function generate() { $response = $this->in(__d('migrations', 'Do you want to compare the schema.php file to the database?'), array('y', 'n'), 'y'); if (strtolower($response) === 'y') { $this->_generateFromComparison($migration, $oldSchema, $comparison); - if (empty($comparison)) { - $this->hr(); - $this->out(__d('migrations', 'No database changes detected.')); - return $this->_stop(); - } + $this->_migrationChanges($migration); $fromSchema = true; + } else { + $response = $this->in(__d('migrations', 'Do you want to compare the database to the schema.php file?'), array('y', 'n'), 'y'); + if(strtolower($response) === 'y') { + $this->_generateFromInverseComparison($migration, $oldSchema, $comparison); + $this->_migrationChanges($migration); + $fromSchema = false; + } } } else { $response = $this->in(__d('migrations', 'Do you want to generate a dump from the current database?'), array('y', 'n'), 'y'); @@ -445,6 +453,10 @@ public function generate() { $this->_finalizeGeneratedMigration($migration, $migrationName, $fromSchema); + if ($this->params['overwrite'] === true) { + $this->_overwriteSchema(); + } + if ($fromSchema && isset($comparison)) { $response = $this->in(__d('migrations', 'Do you want to update the schema.php file?'), array('y', 'n'), 'y'); if (strtolower($response) === 'y') { @@ -453,6 +465,14 @@ public function generate() { } } + protected function _migrationChanges($migration) { + if (empty($migration)) { + $this->hr(); + $this->out(__d('migrations', 'No database changes detected.')); + return $this->_stop(); + } + } + /** * Generate a migration by comparing schema.php with the database. * @@ -473,6 +493,26 @@ protected function _generateFromComparison(&$migration, &$oldSchema, &$compariso $migration = $this->_fromComparison($migration, $comparison, $oldSchema->tables, $newSchema['tables']); } +/** + * Generate a migration by comparing the database with schema.php. + * + * @param array &$migration Reference to variable of the same name in generate() method + * @param array &$oldSchema Reference to variable of the same name in generate() method + * @param array &$comparison Reference to variable of the same name in generate() method + * @return void (The variables passed by reference are changed; nothing is returned) + */ + protected function _generateFromInverseComparison(&$migration, &$oldSchema, &$comparison) { + $this->hr(); + $this->out(__d('migrations', 'Comparing database to the schema.php...')); + + if ($this->type !== 'migrations') { + unset($oldSchema->tables['schema_migrations']); + } + $database = $this->_readSchema(); + $comparison = $this->Schema->compare($database, $oldSchema); + $migration = $this->_fromComparison($migration, $comparison, $oldSchema->tables, $database['tables']); + } + /** * Generate a migration from arguments passed in at the command line * @@ -780,24 +820,49 @@ protected function _getSchema($type = null) { $plugin = ($this->type === 'app') ? null : $this->type; return new CakeSchema(array('connection' => $this->connection, 'plugin' => $plugin)); } - $file = $this->_getPath($type) . 'Config' . DS . 'Schema' . DS . 'schema.php'; - if (!file_exists($file)) { + + $folder = new Folder($this->_getPath($type) . 'Config' . DS . 'Schema'); + $schema_files = $folder->find('.*schema.*.php'); + + if (count($schema_files) === 0) { return false; } - require_once $file; $name = $this->_getSchemaClassName($type); + $file = $this->_findSchemaFile($folder, $schema_files, $name); - if ($type === 'app' && !class_exists($name)) { + if ($type === 'app' && empty($file)) { $appDir = preg_replace('/[^a-zA-Z0-9]/', '', APP_DIR); $name = Inflector::camelize($appDir) . 'Schema'; + $file = $this->_getPath($type) . 'Config' . DS . 'Schema' . DS . 'schema.php'; } + require_once $file; + $plugin = ($type === 'app') ? null : $type; $schema = new $name(array('connection' => $this->connection, 'plugin' => $plugin)); return $schema; } +/** + * Finds schema file + * + * @param Folder $folder Folder object with schema folder path. + * @param string $schema_files Schema files inside schema folder. + * @param string $name Schema-class name. + * @return mixed null in case of no file found, schema file. + */ + protected function _findSchemaFile($folder, $schema_files, $name) { + foreach ($schema_files as $schema_file) { + $file = new File($folder->pwd() . DS . $schema_file); + $content = $file->read(); + if (strpos($content, $name) !== false) { + return $file->path; + } + } + return null; + } + /** * Reads the schema data * @@ -828,6 +893,43 @@ protected function _updateSchema() { $this->dispatchShell($command); } +/** + * Overwrite the schema.php file + * + * @return void + */ + + protected function _overwriteSchema() { + $options = array(); + if ($this->params['force']) { + $options['models'] = false; + } elseif (!empty($this->params['models'])) { + $options['models'] = String::tokenize($this->params['models']); + } + + $cacheDisable = Configure::read('Cache.disable'); + Configure::write('Cache.disable', true); + + $content = $this->Schema->read($options); + $file = 'schema.php'; + + Configure::write('Cache.disable', $cacheDisable); + + if (!empty($this->params['exclude']) && !empty($content)) { + $excluded = String::tokenize($this->params['exclude']); + foreach ($excluded as $table) { + unset($content['tables'][$table]); + } + } + + if ($this->Schema->write($content)) { + $this->out(__d('cake_console', 'Schema file: %s generated', $file)); + return $this->_stop(); + } + $this->err(__d('cake_console', 'Schema file: %s generated')); + return $this->_stop(); + } + /** * Parse fields from the command line for use with generating new migration files * diff --git a/Console/Command/MigrationsShell.php b/Console/Command/MigrationsShell.php new file mode 100644 index 00000000..43f79da3 --- /dev/null +++ b/Console/Command/MigrationsShell.php @@ -0,0 +1,19 @@ +getMessage(), $latestVersionName)); + $errorMessage = __d('migrations', "There was an error during a migration. \n The error was: '%s' \n You must resolve the issue manually and try again.", $exception->getMessage(), $latestVersionName); return $errorMessage; } @@ -492,7 +491,6 @@ protected function _enumerateOldMigrations($type) { /** * Usually used when migrations file/class or map files are not found - * */ class MigrationVersionException extends Exception { diff --git a/Lib/Panel/MigrationsPanel.php b/Lib/Panel/MigrationsPanel.php index 897f63d2..e98a4d87 100644 --- a/Lib/Panel/MigrationsPanel.php +++ b/Lib/Panel/MigrationsPanel.php @@ -22,7 +22,6 @@ * 'panels' => array('Migrations.migrations') * )); * @@@ - * */ class MigrationsPanel extends DebugPanel { diff --git a/Test/Case/Console/Command/MigrationShellTest.php b/Test/Case/Console/Command/MigrationShellTest.php index 3800c6d6..ac12ecea 100644 --- a/Test/Case/Console/Command/MigrationShellTest.php +++ b/Test/Case/Console/Command/MigrationShellTest.php @@ -14,7 +14,6 @@ /** * TestMigrationShell - * */ class TestMigrationShell extends MigrationShell { @@ -64,7 +63,6 @@ public function writeMigration($name, $class, $migration) { /** * MigrationShellTest - * */ class MigrationShellTest extends CakeTestCase { @@ -751,8 +749,10 @@ public function testWriteMigrationIndexesOnly() { public function testGenerate() { $this->Shell->expects($this->at(0))->method('in')->will($this->returnValue('n')); $this->Shell->expects($this->at(1))->method('in')->will($this->returnValue('n')); - $this->Shell->expects($this->at(2))->method('in')->will($this->returnValue('Initial Schema')); + $this->Shell->expects($this->at(2))->method('in')->will($this->returnValue('n')); + $this->Shell->expects($this->at(3))->method('in')->will($this->returnValue('Initial Schema')); + $this->Shell->params['overwrite'] = false; $this->Shell->generate(); $files = glob(TMP . 'tests' . DS . '*initial_schema.php'); @@ -769,10 +769,12 @@ public function testGenerate2() { $this->Shell->expects($this->atLeastOnce())->method('err'); $this->Shell->expects($this->at(0))->method('in')->will($this->returnValue('n')); $this->Shell->expects($this->at(1))->method('in')->will($this->returnValue('n')); - $this->Shell->expects($this->at(2))->method('in')->will($this->returnValue('002 invalid name')); - $this->Shell->expects($this->at(4))->method('in')->will($this->returnValue('invalid-name')); - $this->Shell->expects($this->at(6))->method('in')->will($this->returnValue('create some sample_data')); + $this->Shell->expects($this->at(2))->method('in')->will($this->returnValue('n')); + $this->Shell->expects($this->at(3))->method('in')->will($this->returnValue('002 invalid name')); + $this->Shell->expects($this->at(5))->method('in')->will($this->returnValue('invalid-name')); + $this->Shell->expects($this->at(7))->method('in')->will($this->returnValue('create some sample_data')); + $this->Shell->params['overwrite'] = false; $this->Shell->generate(); $files = glob(TMP . 'tests' . DS . '*create_some_sample_data.php'); @@ -796,7 +798,10 @@ public function testGenerateComparison() { $this->Shell->Version->expects($this->any())->method('getMapping')->will($this->returnCallback(array($this, 'returnMapping'))); $this->assertEmpty(glob(TMP . 'tests' . DS . '*drop_slug_field.php')); - $this->Shell->params['force'] = true; + $this->Shell->params = array( + 'force' => true, + 'overwrite' => false + ); $this->Shell->generate(); $files = glob(TMP . 'tests' . DS . '*drop_slug_field.php'); $this->assertNotEmpty($files); @@ -828,6 +833,50 @@ public function returnMapping() { ); } +/** + * TestGenerateInverseComparison method + * + * @return void + */ + public function testGenerateInverseComparison() { + $this->Shell->type = 'TestMigrationPlugin4'; + $this->Shell->expects($this->at(0))->method('in')->will($this->returnValue('n')); + $this->Shell->expects($this->at(1))->method('in')->will($this->returnValue('y')); + $this->Shell->expects($this->at(3))->method('in')->will($this->returnValue('n')); + $this->Shell->expects($this->at(4))->method('in')->will($this->returnValue('create slug field')); + + $this->Shell->Version->expects($this->any())->method('getMapping')->will($this->returnCallback(array($this, 'returnMapping'))); + + $this->assertEmpty(glob(TMP . 'tests' . DS . '*create_slug_field.php')); + $this->Shell->params = array( + 'force' => true, + 'overwrite' => false + ); + $this->Shell->generate(); + $files = glob(TMP . 'tests' . DS . '*create_slug_field.php'); + $this->assertNotEmpty($files); + + $result = $this->_getMigrationVariable(current($files)); + $this->_unlink($files); + $this->assertNotRegExp('/\'schema_migrations\'/', $result); + + $pattern = << array\( + 'articles' => array\( + 'slug' => array\('type' => 'string', 'null' => false, 'after' => 'title'\), + \), + \),/ +TEXT; + $this->assertRegExp(str_replace("\r\n", "\n", $pattern), $result); + + $pattern = << array\( + 'articles' => array\('slug'\), + \),/ +TEXT; + $this->assertRegExp(str_replace("\r\n", "\n", $pattern), $result); + } + /** * testGenerateFromCliParamsCreateTable method * test the case of using a command such as: @@ -840,7 +889,10 @@ public function testGenerateFromCliParamsCreateTable() { $this->assertEmpty(glob(TMP . 'tests' . DS . '*create_products.php')); $this->Shell->args = array('create_products', 'id', 'created', 'modified', 'name', 'description:text', 'in_stock:boolean', 'price:float', 'stock_count:integer'); - $this->Shell->params['force'] = true; + $this->Shell->params = array( + 'force' => true, + 'overwrite' => false + ); $this->Shell->generate(); $files = glob(TMP . 'tests' . DS . '*create_products.php'); $this->assertNotEmpty($files); @@ -863,7 +915,10 @@ public function testGenerateFromCliParamsDropTable() { $this->assertEmpty(glob(TMP . 'tests' . DS . '*drop_products.php')); $this->Shell->args = array('drop_products'); - $this->Shell->params['force'] = true; + $this->Shell->params = array( + 'force' => true, + 'overwrite' => false + ); $this->Shell->generate(); $files = glob(TMP . 'tests' . DS . '*drop_products.php'); $this->assertNotEmpty($files); @@ -886,7 +941,10 @@ public function testGenerateFromCliParamsAddFields() { $this->assertEmpty(glob(TMP . 'tests' . DS . '*add_all_fields_to_products.php')); $this->Shell->args = array('add_all_fields_to_products', 'id', 'created', 'modified', 'name', 'description:text', 'in_stock:boolean', 'price:float', 'stock_count:integer'); - $this->Shell->params['force'] = true; + $this->Shell->params = array( + 'force' => true, + 'overwrite' => false + ); $this->Shell->generate(); $files = glob(TMP . 'tests' . DS . '*add_all_fields_to_products.php'); $this->assertNotEmpty($files); @@ -909,7 +967,10 @@ public function testGenerateFromCliParamsRemoveFields() { $this->assertEmpty(glob(TMP . 'tests' . DS . '*remove_name_and_desc_from_products.php')); $this->Shell->args = array('remove_name_and_desc_from_products', 'name', 'description'); - $this->Shell->params['force'] = true; + $this->Shell->params = array( + 'force' => true, + 'overwrite' => false + ); $this->Shell->generate(); $files = glob(TMP . 'tests' . DS . '*remove_name_and_desc_from_products.php'); $this->assertNotEmpty($files); @@ -934,9 +995,12 @@ public function testGenerateDump() { $this->assertEmpty(glob(TMP . 'tests' . DS . '*schema_dump.php')); $this->Shell->type = 'TestMigrationPlugin2'; - $this->Shell->params['force'] = true; - $this->Shell->params['dry'] = false; - $this->Shell->params['precheck'] = 'Migrations.PrecheckException'; + $this->Shell->params = array( + 'force' => true, + 'dry' => false, + 'precheck' => 'Migrations.PrecheckException', + 'overwrite' => false + ); $this->Shell->generate(); $files = glob(TMP . 'tests' . DS . '*schema_dump.php'); $this->assertNotEmpty($files); diff --git a/Test/Case/Lib/Migration/PrecheckConditionTest.php b/Test/Case/Lib/Migration/PrecheckConditionTest.php index 51cf0124..71c317d7 100644 --- a/Test/Case/Lib/Migration/PrecheckConditionTest.php +++ b/Test/Case/Lib/Migration/PrecheckConditionTest.php @@ -14,7 +14,6 @@ /** * TestPrecheckCakeMigration - * */ class TestPrecheckCakeMigration extends CakeMigration { diff --git a/Test/Case/Lib/Model/CakeMigrationTest.php b/Test/Case/Lib/Model/CakeMigrationTest.php index d7bc15a1..78113be0 100644 --- a/Test/Case/Lib/Model/CakeMigrationTest.php +++ b/Test/Case/Lib/Model/CakeMigrationTest.php @@ -14,7 +14,6 @@ /** * TestCakeMigration - * */ class TestCakeMigration extends CakeMigration { @@ -28,7 +27,6 @@ class TestCakeMigration extends CakeMigration { /** * TestCallbackCakeMigration - * */ class TestCallbackCakeMigration { @@ -89,7 +87,6 @@ public function afterAction(&$Migration, $type, $data) { /** * CakeMigrationTest - * */ class CakeMigrationTest extends CakeTestCase {