diff --git a/README.md b/README.md index 7fa93f6c..fd753298 100644 --- a/README.md +++ b/README.md @@ -261,3 +261,47 @@ If no length is specified, lengths for certain columns are defaulted: - *string*: `255` - *integer*: `11` - *biginteger*: `20` + +### Seeding your database + +As of 1.5.5, you can use the ``migrations`` shell to seed your database. This leverages the [Phinx library seed feature](http://docs.phinx.org/en/latest/seeding.html). +By default, seed files will be looked for in the ``config/Seeds`` directory of your application. +Please make sure you follow [Phinx instructions to build your seed files](http://docs.phinx.org/en/latest/seeding.html#creating-a-new-seed-class). + +As for migrations, a ``bake`` interface is provided for seed files: + +```bash +# This will create a ArticlesSeed.php file in the directory config/Seeds of your application +# By default, the table the seed will try to alter is the "tableized" version of the seed filename +bin/cake bake seed Articles + +# You specify the name of the table the seed files will alter by using the ``--table`` option +bin/cake bake seed Articles --table my_articles_table + +# You can specify a plugin to bake into +bin/cake bake seed Articles --plugin PluginName + +# You can specify an alternative connection when generating a seeder. +bin/cake bake seed Articles --connection connection +``` + +To seed your database, you can use the ``seed`` subcommand: + +```bash +# Without parameters, the seed subcommand will run all available seeders in the target directory, in alphabetical order. +bin/cake migrations seed + +# You can specify only one seeder to be run using the `--seed` option +bin/cake migrations seed --seed ArticlesSeed + +# You can run seeders from an alternative directory +bin/cake migrations seed --source AlternativeSeeds + +# You can run seeders from a plugin +bin/cake migrations seed --plugin PluginName + +# You can run seeders from a specific connection +bin/cake migrations seed --connection connection +``` + +Be aware that, as opposed to migrations, seeders are not tracked, which means that the same seeder can be applied multiple times. diff --git a/composer.json b/composer.json index de2f7ecf..35a16ae0 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "require": { "php": ">=5.4", "robmorgan/phinx": "~0.5.0", - "cakephp/cakephp": "~3.1,3.2.*" + "cakephp/cakephp": "~3.1 || 3.2.*" }, "require-dev": { "phpunit/phpunit": "*", diff --git a/src/Command/Seed.php b/src/Command/Seed.php new file mode 100644 index 00000000..371e6d74 --- /dev/null +++ b/src/Command/Seed.php @@ -0,0 +1,60 @@ +setName('seed') + ->setDescription('Seed the database with data') + ->setHelp('runs all available migrations, optionally up to a specific version') + ->addOption('--seed', null, InputOption::VALUE_REQUIRED, 'What is the name of the seeder?') + ->addOption('--plugin', '-p', InputOption::VALUE_REQUIRED, 'The plugin containing the migrations') + ->addOption('--connection', '-c', InputOption::VALUE_REQUIRED, 'The datasource connection to use') + ->addOption('--source', '-s', InputOption::VALUE_REQUIRED, 'The folder where migrations are in'); + } + + /** + * Overrides the action execute method in order to vanish the idea of environments + * from phinx. CakePHP does not believe in the idea of having in-app environments + * + * @param \Symfony\Component\Console\Input\InputInterface $input the input object + * @param \Symfony\Component\Console\Output\OutputInterface $output the output object + * @return mixed + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $event = $this->dispatchEvent('Migration.beforeSeed'); + if ($event->isStopped()) { + return $event->result; + } + $this->parentExecute($input, $output); + $this->dispatchEvent('Migration.afterSeed'); + } +} diff --git a/src/ConfigurationTrait.php b/src/ConfigurationTrait.php index 82251820..10787c8a 100644 --- a/src/ConfigurationTrait.php +++ b/src/ConfigurationTrait.php @@ -14,6 +14,7 @@ use Cake\Core\Plugin; use Cake\Datasource\ConnectionManager; use Cake\Utility\Inflector; +use Migrations\Command\Seed; use Phinx\Config\Config; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -55,21 +56,34 @@ public function getConfig($forceRefresh = false) return $this->configuration; } - $folder = 'Migrations'; - if ($this->input->getOption('source')) { - $folder = $this->input->getOption('source'); + $migrationsFolder = 'Migrations'; + $seedsFolder = 'Seeds'; + + $source = $this->input->getOption('source'); + if ($source) { + if ($this instanceof Seed) { + $seedsFolder = $source; + } else { + $migrationsFolder = $source; + } } - $dir = ROOT . DS . 'config' . DS . $folder; + $migrationsPath = ROOT . DS . 'config' . DS . $migrationsFolder; + $seedsPath = ROOT . DS . 'config' . DS . $seedsFolder; $plugin = null; if ($this->input->getOption('plugin')) { $plugin = $this->input->getOption('plugin'); - $dir = Plugin::path($plugin) . 'config' . DS . $folder; + $migrationsPath = Plugin::path($plugin) . 'config' . DS . $migrationsFolder; + $seedsPath = Plugin::path($plugin) . 'config' . DS . $seedsFolder; + } + + if (!is_dir($migrationsPath)) { + mkdir($migrationsPath, 0777, true); } - if (!is_dir($dir)) { - mkdir($dir, 0777, true); + if (!is_dir($seedsPath)) { + mkdir($seedsPath, 0777, true); } $plugin = $plugin ? Inflector::underscore($plugin) . '_' : ''; @@ -81,7 +95,8 @@ public function getConfig($forceRefresh = false) $adapterName = $this->getAdapterName($connectionConfig['driver']); $config = [ 'paths' => [ - 'migrations' => $dir + 'migrations' => $migrationsPath, + 'seeds' => $seedsPath, ], 'environments' => [ 'default_migration_table' => $plugin . 'phinxlog', diff --git a/src/MigrationsDispatcher.php b/src/MigrationsDispatcher.php index aff31bc2..66d2a1ee 100644 --- a/src/MigrationsDispatcher.php +++ b/src/MigrationsDispatcher.php @@ -36,5 +36,6 @@ public function __construct($version) $this->add(new Command\Rollback()); $this->add(new Command\Status()); $this->add(new Command\MarkMigrated()); + $this->add(new Command\Seed()); } } diff --git a/src/Shell/MigrationsShell.php b/src/Shell/MigrationsShell.php index 8704f8f2..29088148 100644 --- a/src/Shell/MigrationsShell.php +++ b/src/Shell/MigrationsShell.php @@ -55,6 +55,7 @@ public function getOptionParser() ->addOption('target', ['short' => 't']) ->addOption('connection', ['short' => 'c']) ->addOption('source', ['short' => 's']) + ->addOption('seed') ->addOption('ansi') ->addOption('no-ansi') ->addOption('version', ['short' => 'V']) diff --git a/src/Shell/Task/SeedTask.php b/src/Shell/Task/SeedTask.php index a70573be..84c339c5 100644 --- a/src/Shell/Task/SeedTask.php +++ b/src/Shell/Task/SeedTask.php @@ -29,7 +29,7 @@ class SeedTask extends SimpleBakeTask * * @var string */ - public $pathFragment = 'Seed/'; + public $pathFragment = 'config/Seeds/'; /** * {@inheritDoc} @@ -44,7 +44,19 @@ public function name() */ public function fileName($name) { - return Inflector::classify($this->args[0]) . 'Seed.php'; + return Inflector::camelize($name) . 'Seed.php'; + } + + /** + * {@inheritDoc} + */ + public function getPath() + { + $path = ROOT . DS . $this->pathFragment; + if (isset($this->plugin)) { + $path = $this->_pluginPath($this->plugin) . $this->pathFragment; + } + return str_replace('/', DS, $path); } /** diff --git a/src/Template/Bake/Seed/seed.ctp b/src/Template/Bake/Seed/seed.ctp index 1d04db2e..46986a04 100644 --- a/src/Template/Bake/Seed/seed.ctp +++ b/src/Template/Bake/Seed/seed.ctp @@ -14,8 +14,6 @@ */ %> \Seed; - use Phinx\Seed\AbstractSeed; /** diff --git a/tests/TestCase/Command/SeedTest.php b/tests/TestCase/Command/SeedTest.php new file mode 100644 index 00000000..95c2e9d5 --- /dev/null +++ b/tests/TestCase/Command/SeedTest.php @@ -0,0 +1,256 @@ +Connection = ConnectionManager::get('test'); + $application = new MigrationsDispatcher('testing'); + $this->command = $application->find('seed'); + $this->streamOutput = new StreamOutput(fopen('php://memory', 'w', false)); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + unset($this->Connection, $this->command, $this->streamOutput); + } + + /** + * Test executing the "seed" command in a standard way + * + * @return void + */ + public function testExecute() + { + $params = [ + '--connection' => 'test', + ]; + $commandTester = $this->getCommandTester($params); + $migrations = $this->getMigrations(); + $migrations->migrate(); + + $commandTester->execute([ + 'command' => $this->command->getName(), + '--connection' => 'test' + ]); + + $display = $this->getDisplayFromOutput(); + $this->assertTextContains('== NumbersSeed: seeded', $display); + + $result = $this->Connection->newQuery() + ->select(['*']) + ->from('numbers') + ->order('id DESC') + ->limit(1) + ->execute()->fetchAll('assoc'); + $expected = [ + [ + 'id' => '1', + 'number' => '10', + 'radix' => '10' + ] + ]; + $this->assertEquals($expected, $result); + + $migrations->rollback(['target' => 0]); + } + + /** + * Test executing the "seed" command with custom params + * + * @return void + */ + public function testExecuteCustomParams() + { + $params = [ + '--connection' => 'test', + '--source' => 'AltSeeds' + ]; + $commandTester = $this->getCommandTester($params); + $migrations = $this->getMigrations(); + $migrations->migrate(); + + $commandTester->execute([ + 'command' => $this->command->getName(), + '--connection' => 'test', + '--source' => 'AltSeeds' + ]); + + $display = $this->getDisplayFromOutput(); + $this->assertTextContains('== NumbersAltSeed: seeded', $display); + + $result = $this->Connection->newQuery() + ->select(['*']) + ->from('numbers') + ->order('id DESC') + ->limit(1) + ->execute()->fetchAll('assoc'); + $expected = [ + [ + 'id' => '1', + 'number' => '5', + 'radix' => '10' + ] + ]; + $this->assertEquals($expected, $result); + $migrations->rollback(['target' => 0]); + } + + /** + * Test executing the "seed" command with wrong custom params (no seed found) + * + * @return void + */ + public function testExecuteWrongCustomParams() + { + $params = [ + '--connection' => 'test', + '--source' => 'DerpSeeds' + ]; + $commandTester = $this->getCommandTester($params); + $migrations = $this->getMigrations(); + $migrations->migrate(); + + $commandTester->execute([ + 'command' => $this->command->getName(), + '--connection' => 'test', + '--source' => 'DerpSeeds' + ]); + + $display = $this->getDisplayFromOutput(); + $this->assertEmpty($display); + $migrations->rollback(['target' => 0]); + } + + /** + * Gets a pre-configured of a CommandTester object that is initialized for each + * test methods. This is needed in order to define the same PDO connection resource + * between every objects needed during the tests. + * This is mandatory for the SQLite database vendor, so phinx objects interacting + * with the database have the same connection resource as CakePHP objects. + * + * @return \Symfony\Component\Console\Tester\CommandTester + */ + protected function getCommandTester($params) + { + $input = new ArrayInput($params, $this->command->getDefinition()); + $this->command->setInput($input); + $manager = new CakeManager($this->command->getConfig(), $this->streamOutput); + $manager->getEnvironment('default')->getAdapter()->setConnection($this->Connection->driver()->connection()); + $this->command->setManager($manager); + $commandTester = new CommandTester($this->command); + + return $commandTester; + } + + /** + * Gets a Migrations object in order to easily create and drop tables during the + * tests + * + * @return \Migrations\Migrations + */ + protected function getMigrations() + { + $params = [ + 'connection' => 'test', + 'source' => 'TestsMigrations' + ]; + $migrations = new Migrations($params); + $migrations + ->getManager($this->command->getConfig()) + ->getEnvironment('default') + ->getAdapter() + ->setConnection($this->Connection->driver()->connection()); + + $tables = (new Collection($this->Connection))->listTables(); + if (in_array('phinxlog', $tables)) { + $ormTable = TableRegistry::get('phinxlog', ['connection' => $this->Connection]); + $query = $this->Connection->driver()->schemaDialect()->truncateTableSql($ormTable->schema()); + $this->Connection->execute( + $query[0] + ); + } + + return $migrations; + } + + /** + * Extract the content that was stored in self::$streamOutput. + * + * @return string + */ + protected function getDisplayFromOutput() + { + rewind($this->streamOutput->getStream()); + $display = stream_get_contents($this->streamOutput->getStream()); + return str_replace(PHP_EOL, "\n", $display); + } +} diff --git a/tests/TestCase/Shell/Task/SeedTaskTest.php b/tests/TestCase/Shell/Task/SeedTaskTest.php new file mode 100644 index 00000000..f5f1a4fc --- /dev/null +++ b/tests/TestCase/Shell/Task/SeedTaskTest.php @@ -0,0 +1,62 @@ +_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Seeds' . DS; + $inputOutput = $this->getMock('Cake\Console\ConsoleIo', [], [], '', false); + + $this->Task = $this->getMock( + 'Migrations\Shell\Task\SeedTask', + ['in', 'err', 'createFile', '_stop', 'error'], + [$inputOutput] + ); + $this->Task->name = 'Seeds'; + $this->Task->connection = 'test'; + $this->Task->BakeTemplate = new BakeTemplateTask($inputOutput); + $this->Task->BakeTemplate->initialize(); + $this->Task->BakeTemplate->interactive = false; + } + + /** + * Test empty migration. + * + * @return void + */ + public function testBasicBaking() + { + $this->Task->args = [ + 'articles' + ]; + $result = $this->Task->bake('Articles'); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } +} diff --git a/tests/comparisons/Migration/pgsql/test_auto_id_disabled_snapshot_pgsql.php b/tests/comparisons/Migration/pgsql/test_auto_id_disabled_snapshot_pgsql.php index af2e96e8..d8b77dca 100644 --- a/tests/comparisons/Migration/pgsql/test_auto_id_disabled_snapshot_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_auto_id_disabled_snapshot_pgsql.php @@ -181,8 +181,8 @@ public function up() ) ->addIndex( [ - 'category_id', 'id', + 'category_id', ], ['unique' => true] ) diff --git a/tests/comparisons/Migration/pgsql/test_not_empty_snapshot_pgsql.php b/tests/comparisons/Migration/pgsql/test_not_empty_snapshot_pgsql.php index 75be90a9..bad0b718 100644 --- a/tests/comparisons/Migration/pgsql/test_not_empty_snapshot_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_not_empty_snapshot_pgsql.php @@ -149,8 +149,8 @@ public function up() ) ->addIndex( [ - 'category_id', 'id', + 'category_id', ], ['unique' => true] ) diff --git a/tests/comparisons/Seeds/testBasicBaking.php b/tests/comparisons/Seeds/testBasicBaking.php new file mode 100644 index 00000000..04e3d87d --- /dev/null +++ b/tests/comparisons/Seeds/testBasicBaking.php @@ -0,0 +1,24 @@ +table('articles'); + $table->insert($data)->save(); + } +} diff --git a/tests/test_app/config/AltSeeds/NumbersAltSeed.php b/tests/test_app/config/AltSeeds/NumbersAltSeed.php new file mode 100644 index 00000000..415a4f14 --- /dev/null +++ b/tests/test_app/config/AltSeeds/NumbersAltSeed.php @@ -0,0 +1,30 @@ + '1', + 'number' => '5', + 'radix' => '10' + ] + ]; + + $table = $this->table('numbers'); + $table->insert($data)->save(); + } +} diff --git a/tests/test_app/config/Seeds/NumbersSeed.php b/tests/test_app/config/Seeds/NumbersSeed.php new file mode 100644 index 00000000..b71867e4 --- /dev/null +++ b/tests/test_app/config/Seeds/NumbersSeed.php @@ -0,0 +1,30 @@ + '1', + 'number' => '10', + 'radix' => '10' + ] + ]; + + $table = $this->table('numbers'); + $table->insert($data)->save(); + } +}