diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php
new file mode 100644
index 00000000..c14e3a03
--- /dev/null
+++ b/src/Command/SeedCommand.php
@@ -0,0 +1,137 @@
+
+ */
+ use EventDispatcherTrait;
+
+ /**
+ * The default name added to the application command list
+ *
+ * @return string
+ */
+ public static function defaultName(): string
+ {
+ return 'migrations seed';
+ }
+
+ /**
+ * Configure the option parser
+ *
+ * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
+ * @return \Cake\Console\ConsoleOptionParser
+ */
+ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ {
+ $parser->setDescription([
+ 'Seed the database with data',
+ '',
+ 'Runs a seeder script that can populate the database with data, or run mutations',
+ '',
+ 'migrations seed --connection secondary --seed UserSeeder',
+ '',
+ 'The `--seed` option can be supplied multiple times to run more than one seeder',
+ ])->addOption('plugin', [
+ 'short' => 'p',
+ 'help' => 'The plugin to run seeders in',
+ ])->addOption('connection', [
+ 'short' => 'c',
+ 'help' => 'The datasource connection to use',
+ 'default' => 'default',
+ ])->addOption('source', [
+ 'short' => 's',
+ 'default' => ConfigInterface::DEFAULT_SEED_FOLDER,
+ 'help' => 'The folder where your seeders are.',
+ ])->addOption('seed', [
+ 'help' => 'The name of the seeder that you want to run.',
+ 'multiple' => true,
+ ]);
+
+ return $parser;
+ }
+
+ /**
+ * Execute the command.
+ *
+ * @param \Cake\Console\Arguments $args The command arguments.
+ * @param \Cake\Console\ConsoleIo $io The console io
+ * @return int|null The exit code or null for success
+ */
+ public function execute(Arguments $args, ConsoleIo $io): ?int
+ {
+ $event = $this->dispatchEvent('Migration.beforeSeed');
+ if ($event->isStopped()) {
+ return $event->getResult() ? self::CODE_SUCCESS : self::CODE_ERROR;
+ }
+ $result = $this->executeSeeds($args, $io);
+ $this->dispatchEvent('Migration.afterSeed');
+
+ return $result;
+ }
+
+ /**
+ * Execute seeders based on console inputs.
+ *
+ * @param \Cake\Console\Arguments $args The command arguments.
+ * @param \Cake\Console\ConsoleIo $io The console io
+ * @return int|null The exit code or null for success
+ */
+ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int
+ {
+ $factory = new ManagerFactory([
+ 'plugin' => $args->getOption('plugin'),
+ 'source' => $args->getOption('source'),
+ 'connection' => $args->getOption('connection'),
+ ]);
+ $manager = $factory->createManager($io);
+ $config = $manager->getConfig();
+ $seeds = (array)$args->getMultipleOption('seed');
+
+ $versionOrder = $config->getVersionOrder();
+ $io->out('using connection ' . (string)$args->getOption('connection'));
+ $io->out('using paths ' . implode(', ', $config->getMigrationPaths()));
+ $io->out('ordering by ' . $versionOrder . ' time');
+
+ $start = microtime(true);
+ if (empty($seeds)) {
+ // run all the seed(ers)
+ $manager->seed();
+ } else {
+ // run seed(ers) specified in a comma-separated list of classes
+ foreach ($seeds as $seed) {
+ $manager->seed(trim($seed));
+ }
+ }
+ $end = microtime(true);
+
+ $io->out('All Done. Took ' . sprintf('%.4fs', $end - $start) . '');
+
+ return self::CODE_SUCCESS;
+ }
+}
diff --git a/src/Config/ConfigInterface.php b/src/Config/ConfigInterface.php
index 9af398f1..b1b2db12 100644
--- a/src/Config/ConfigInterface.php
+++ b/src/Config/ConfigInterface.php
@@ -19,6 +19,7 @@
interface ConfigInterface extends ArrayAccess
{
public const DEFAULT_MIGRATION_FOLDER = 'Migrations';
+ public const DEFAULT_SEED_FOLDER = 'Seeds';
/**
* Returns the configuration for the current environment.
diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php
index d1b781e1..200efe85 100644
--- a/src/Migration/Manager.php
+++ b/src/Migration/Manager.php
@@ -1006,10 +1006,7 @@ public function getSeeds(): array
ksort($seeds);
$this->setSeeds($seeds);
}
-
- assert(!empty($this->seeds), 'seeds must be set');
- $this->seeds = $this->orderSeedsByDependencies($this->seeds);
-
+ $this->seeds = $this->orderSeedsByDependencies((array)$this->seeds);
if (empty($this->seeds)) {
return [];
}
diff --git a/src/Migration/ManagerFactory.php b/src/Migration/ManagerFactory.php
index c37ebbe9..2ccaa6bb 100644
--- a/src/Migration/ManagerFactory.php
+++ b/src/Migration/ManagerFactory.php
@@ -35,7 +35,7 @@ class ManagerFactory
*
* ## Options
*
- * - source - The directory in app/config that migrations should be read from.
+ * - source - The directory in app/config that migrations and seeds should be read from.
* - plugin - The plugin name that migrations are being run on.
* - connection - The connection name.
* - dry-run - Whether or not dry-run mode should be enabled.
@@ -70,7 +70,8 @@ public function createConfig(): ConfigInterface
{
$folder = (string)$this->getOption('source');
- // Get the filepath for migrations and seeds(not implemented yet)
+ // Get the filepath for migrations and seeds.
+ // We rely on factory parameters to define which directory to use.
$dir = ROOT . DS . 'config' . DS . $folder;
if (defined('CONFIG')) {
$dir = CONFIG . $folder;
@@ -110,7 +111,9 @@ public function createConfig(): ConfigInterface
$configData = [
'paths' => [
+ // TODO make paths a simple list.
'migrations' => $dir,
+ 'seeds' => $dir,
],
'templates' => [
'file' => $templatePath . 'Phinx/create.php.template',
diff --git a/src/MigrationsPlugin.php b/src/MigrationsPlugin.php
index 417f778f..4cff3d27 100644
--- a/src/MigrationsPlugin.php
+++ b/src/MigrationsPlugin.php
@@ -36,6 +36,7 @@
use Migrations\Command\MigrationsSeedCommand;
use Migrations\Command\MigrationsStatusCommand;
use Migrations\Command\RollbackCommand;
+use Migrations\Command\SeedCommand;
use Migrations\Command\StatusCommand;
/**
@@ -95,11 +96,12 @@ public function console(CommandCollection $commands): CommandCollection
{
if (Configure::read('Migrations.backend') == 'builtin') {
$classes = [
- StatusCommand::class,
+ DumpCommand::class,
MarkMigratedCommand::class,
MigrateCommand::class,
- DumpCommand::class,
RollbackCommand::class,
+ SeedCommand::class,
+ StatusCommand::class,
];
if (class_exists(SimpleBakeCommand::class)) {
$classes[] = BakeMigrationCommand::class;
diff --git a/tests/TestCase/Command/Phinx/SeedTest.php b/tests/TestCase/Command/Phinx/SeedTest.php
index 81994d92..d80f77c1 100644
--- a/tests/TestCase/Command/Phinx/SeedTest.php
+++ b/tests/TestCase/Command/Phinx/SeedTest.php
@@ -85,8 +85,11 @@ public function setUp(): void
public function tearDown(): void
{
parent::tearDown();
+
$this->connection->execute('DROP TABLE IF EXISTS phinxlog');
$this->connection->execute('DROP TABLE IF EXISTS numbers');
+ $this->connection->execute('DROP TABLE IF EXISTS letters');
+ $this->connection->execute('DROP TABLE IF EXISTS stores');
}
/**
diff --git a/tests/TestCase/Command/RollbackCommandTest.php b/tests/TestCase/Command/RollbackCommandTest.php
index 3c45bbc3..3fdbd08e 100644
--- a/tests/TestCase/Command/RollbackCommandTest.php
+++ b/tests/TestCase/Command/RollbackCommandTest.php
@@ -4,13 +4,13 @@
namespace Migrations\Test\TestCase\Command;
use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
-use Cake\Console\TestSuite\StubConsoleOutput;
use Cake\Core\Configure;
use Cake\Database\Exception\DatabaseException;
use Cake\Event\EventInterface;
use Cake\Event\EventManager;
use Cake\TestSuite\TestCase;
use InvalidArgumentException;
+use ReflectionProperty;
class RollbackCommandTest extends TestCase
{
@@ -46,7 +46,10 @@ public function tearDown(): void
protected function resetOutput(): void
{
- $this->_out = new StubConsoleOutput();
+ if ($this->_out) {
+ $property = new ReflectionProperty($this->_out, '_out');
+ $property->setValue($this->_out, []);
+ }
}
public function testHelp(): void
diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php
new file mode 100644
index 00000000..8a9bedcc
--- /dev/null
+++ b/tests/TestCase/Command/SeedCommandTest.php
@@ -0,0 +1,176 @@
+fetchTable('Phinxlog');
+ try {
+ $table->deleteAll('1=1');
+ } catch (DatabaseException $e) {
+ }
+ }
+
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+
+ $connection->execute('DROP TABLE IF EXISTS numbers');
+ $connection->execute('DROP TABLE IF EXISTS letters');
+ $connection->execute('DROP TABLE IF EXISTS stores');
+ }
+
+ protected function resetOutput(): void
+ {
+ if ($this->_out) {
+ $property = new ReflectionProperty($this->_out, '_out');
+ $property->setValue($this->_out, []);
+ }
+ }
+
+ protected function createTables(): void
+ {
+ $this->exec('migrations migrate -c test -s TestsMigrations --no-lock');
+ $this->assertExitSuccess();
+ $this->resetOutput();
+ }
+
+ public function testHelp(): void
+ {
+ $this->exec('migrations seed --help');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('Seed the database with data');
+ $this->assertOutputContains('migrations seed --connection secondary --seed UserSeeder');
+ }
+
+ public function testSeederEvents(): void
+ {
+ /** @var array $fired */
+ $fired = [];
+ EventManager::instance()->on('Migration.beforeSeed', function (EventInterface $event) use (&$fired): void {
+ $fired[] = $event->getName();
+ });
+ EventManager::instance()->on('Migration.afterSeed', function (EventInterface $event) use (&$fired): void {
+ $fired[] = $event->getName();
+ });
+
+ $this->createTables();
+ $this->exec('migrations seed -c test --seed NumbersSeed');
+ $this->assertExitSuccess();
+
+ $this->assertSame(['Migration.beforeSeed', 'Migration.afterSeed'], $fired);
+ }
+
+ public function testBeforeSeederAbort(): void
+ {
+ /** @var array $fired */
+ $fired = [];
+ EventManager::instance()->on('Migration.beforeSeed', function (EventInterface $event) use (&$fired): void {
+ $fired[] = $event->getName();
+ $event->stopPropagation();
+ });
+ EventManager::instance()->on('Migration.afterSeed', function (EventInterface $event) use (&$fired): void {
+ $fired[] = $event->getName();
+ });
+
+ $this->createTables();
+ $this->exec('migrations seed -c test --seed NumbersSeed');
+ $this->assertExitError();
+
+ $this->assertSame(['Migration.beforeSeed'], $fired);
+ }
+
+ public function testSeederUnknown(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The seed class "NotThere" does not exist');
+ $this->exec('migrations seed -c test --seed NotThere');
+ }
+
+ public function testSeederOne(): void
+ {
+ $this->createTables();
+ $this->exec('migrations seed -c test --seed NumbersSeed');
+
+ $this->assertExitSuccess();
+ $this->assertOutputContains('NumbersSeed: seeding');
+ $this->assertOutputContains('All Done');
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+ $query = $connection->execute('SELECT COUNT(*) FROM numbers');
+ $this->assertEquals(1, $query->fetchColumn(0));
+ }
+
+ public function testSeederImplictAll(): void
+ {
+ $this->createTables();
+ $this->exec('migrations seed -c test');
+
+ $this->assertExitSuccess();
+ $this->assertOutputContains('NumbersSeed: seeding');
+ $this->assertOutputContains('All Done');
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+ $query = $connection->execute('SELECT COUNT(*) FROM numbers');
+ $this->assertEquals(1, $query->fetchColumn(0));
+ }
+
+ public function testSeederMultipleNotFound(): void
+ {
+ $this->createTables();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The seed class "NotThere" does not exist');
+ $this->exec('migrations seed -c test --seed NumbersSeed --seed NotThere');
+ }
+
+ public function testSeederMultiple(): void
+ {
+ $this->createTables();
+ $this->exec('migrations seed -c test --source CallSeeds --seed LettersSeed --seed NumbersCallSeed');
+
+ $this->assertExitSuccess();
+ $this->assertOutputContains('NumbersCallSeed: seeding');
+ $this->assertOutputContains('LettersSeed: seeding');
+ $this->assertOutputContains('All Done');
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+ $query = $connection->execute('SELECT COUNT(*) FROM numbers');
+ $this->assertEquals(1, $query->fetchColumn(0));
+
+ $query = $connection->execute('SELECT COUNT(*) FROM letters');
+ $this->assertEquals(2, $query->fetchColumn(0));
+ }
+
+ public function testSeederSourceNotFound(): void
+ {
+ $this->createTables();
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The seed class "LettersSeed" does not exist');
+
+ $this->exec('migrations seed -c test --source NotThere --seed LettersSeed');
+ }
+}
diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php
index a0a452a9..9e109235 100644
--- a/tests/TestCase/Migration/ManagerTest.php
+++ b/tests/TestCase/Migration/ManagerTest.php
@@ -31,7 +31,7 @@ class ManagerTest extends TestCase
protected $io;
/**
- * @var \Cake\Console\StubConsoleOutput $io
+ * @var \Cake\Console\TestSuite\StubConsoleOutput $io
*/
protected $out;
@@ -601,7 +601,7 @@ public function testGetMigrationsWithDuplicateMigrationVersions()
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessageMatches('/Duplicate migration/');
- $this->expectExceptionMessageMatches('/20120111235330_duplicate_migration_2.php" has the same version as "20120111235330"/');
+ $this->expectExceptionMessageMatches('/20120111235330_duplicate_migration(_2)?.php" has the same version as "20120111235330"/');
$manager->getMigrations();
}