Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 94 additions & 29 deletions src/Command/SeedCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Core\Configure;
use Cake\Event\EventDispatcherTrait;
use Exception;
use Migrations\Config\ConfigInterface;
use Migrations\Migration\ManagerFactory;

Expand Down Expand Up @@ -50,33 +50,44 @@ public static function defaultName(): string
*/
public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
$description = [
'Seed the database with data',
'',
'Runs a seeder script that can populate the database with data, or run mutations',
'Runs a seeder script that can populate the database with data, or run mutations:',
'',
'<info>migrations seed --connection secondary --seed UserSeed</info>',
'<info>migrations seed Posts</info>',
'<info>migrations seed Users,Posts</info>',
'<info>migrations seed --plugin Demo</info>',
'<info>migrations seed --connection secondary</info>',
'',
'The `--seed` option can be supplied multiple times to run more than one seed',
])->addOption('plugin', [
'short' => 'p',
'help' => 'The plugin to run seeds in',
])->addOption('connection', [
'short' => 'c',
'help' => 'The datasource connection to use',
'default' => 'default',
])->addOption('dry-run', [
'short' => 'x',
'help' => 'Dump queries to stdout instead of executing them',
'boolean' => true,
])->addOption('source', [
'short' => 's',
'default' => ConfigInterface::DEFAULT_SEED_FOLDER,
'help' => 'The folder where your seeds are.',
])->addOption('seed', [
'help' => 'The name of the seed that you want to run.',
'multiple' => true,
]);
'Runs all seeds if no seed names are specified. When running all seeds',
'in an interactive terminal, a confirmation prompt is shown.',
];

$parser->setDescription($description)
->addArgument('seed', [
'help' => 'The name(s) of the seed(s) to run (comma-separated for multiple). Run all seeds if not specified.',
'required' => false,
])
->addOption('plugin', [
'short' => 'p',
'help' => 'The plugin to run seeds in',
])
->addOption('connection', [
'short' => 'c',
'help' => 'The datasource connection to use',
'default' => 'default',
])
->addOption('dry-run', [
'short' => 'd',
'help' => 'Dump queries to stdout instead of executing them',
'boolean' => true,
])
->addOption('source', [
'short' => 's',
'default' => ConfigInterface::DEFAULT_SEED_FOLDER,
'help' => 'The folder where your seeds are.',
]);

return $parser;
}
Expand Down Expand Up @@ -119,10 +130,20 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int
$manager = $factory->createManager($io);
$config = $manager->getConfig();

if (version_compare(Configure::version(), '5.2.0', '>=')) {
$seeds = (array)$args->getArrayOption('seed');
} else {
$seeds = (array)$args->getMultipleOption('seed');
// Get seed names from arguments
$seeds = [];
if ($args->hasArgument('seed')) {
$seedArg = $args->getArgument('seed');
if ($seedArg !== null) {
// Split by comma to support comma-separated list
$seedList = explode(',', $seedArg);
foreach ($seedList as $seed) {
$trimmed = trim($seed);
if ($trimmed !== '') {
$seeds[] = $trimmed;
}
}
}
}

$versionOrder = $config->getVersionOrder();
Expand All @@ -136,10 +157,54 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int

$start = microtime(true);
if (!$seeds) {
// Get all available seeds and ask for confirmation
try {
$availableSeeds = $manager->getSeeds();
} catch (Exception $e) {
$io->error('Failed to load seeds: ' . $e->getMessage());

return static::CODE_ERROR;
}
Comment on lines +161 to +167
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that defaulting to running all seeds is a risky default, that should be an error instead. If there is a need for a 'run all' mode we could add a --all option.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats why there is now a list of what seeds run and the confirmation before doing. The Extra option is not needed this way. You cannot a accidently do it anymore.


if (!$availableSeeds) {
$io->warning('No seeds found.');

return self::CODE_SUCCESS;
}

// Display the seeds that will be run and ask for confirmation
// Skip confirmation in quiet mode or non-interactive environments
$isInteractive = function_exists('posix_isatty') && @posix_isatty(STDIN);
if ($io->level() > ConsoleIo::QUIET && $isInteractive) {
$io->out('');
$io->out('<info>The following seeds will be executed:</info>');
foreach ($availableSeeds as $seed) {
$seedName = $seed->getName();
// Remove 'Seed' suffix for display
if (str_ends_with($seedName, 'Seed')) {
$seedName = substr($seedName, 0, -4);
}
$io->out(' - ' . $seedName);
}
$io->out('');
$io->out('<warning>Note:</warning> Seeds do not track execution state. They will run');
$io->out('regardless of whether they have been executed before. Ensure your');
$io->out('seeds are idempotent or manually verify they should be (re)run.');
$io->out('');

// Ask for confirmation
$continue = $io->askChoice('Do you want to continue?', ['y', 'n'], 'n');
if ($continue !== 'y') {
$io->warning('Seed operation aborted.');

return self::CODE_SUCCESS;
Comment on lines +193 to +197
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could always ask for confirmation. If users don't want to deal with a confirmation prompt they can use the -q parameter or specify which seeds are to be run.

Copy link
Member Author

@dereuromark dereuromark Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats already how its done above with

if ($io->level() > ConsoleIo::QUIET) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was suggesting that we don't have a confirm to go with the comment about having --all and no implicit run all. I get why you don't want to do that though.

}
}

// run all the seed(ers)
$manager->seed();
} else {
// run seed(ers) specified in a comma-separated list of classes
// run seed(ers) specified as arguments
foreach ($seeds as $seed) {
$manager->seed(trim($seed));
}
Expand Down
91 changes: 71 additions & 20 deletions tests/TestCase/Command/SeedCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ 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 UserSeed');
$this->assertOutputContains('migrations seed Posts');
$this->assertOutputContains('migrations seed Users,Posts');
}

public function testSeederEvents(): void
Expand All @@ -73,7 +74,7 @@ public function testSeederEvents(): void
});

$this->createTables();
$this->exec('migrations seed -c test --seed NumbersSeed');
$this->exec('migrations seed -c test NumbersSeed');
$this->assertExitSuccess();

$this->assertSame(['Migration.beforeSeed', 'Migration.afterSeed'], $fired);
Expand All @@ -92,7 +93,7 @@ public function testBeforeSeederAbort(): void
});

$this->createTables();
$this->exec('migrations seed -c test --seed NumbersSeed');
$this->exec('migrations seed -c test NumbersSeed');
$this->assertExitError();

$this->assertSame(['Migration.beforeSeed'], $fired);
Expand All @@ -102,13 +103,13 @@ public function testSeederUnknown(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The seed `NotThere` does not exist');
$this->exec('migrations seed -c test --seed NotThere');
$this->exec('migrations seed -c test NotThere');
}

public function testSeederOne(): void
{
$this->createTables();
$this->exec('migrations seed -c test --seed NumbersSeed');
$this->exec('migrations seed -c test NumbersSeed');

$this->assertExitSuccess();
$this->assertOutputContains('NumbersSeed:</info> <comment>seeding');
Expand All @@ -123,7 +124,7 @@ public function testSeederOne(): void
public function testSeederBaseSeed(): void
{
$this->createTables();
$this->exec('migrations seed -c test --source BaseSeeds --seed MigrationSeedNumbers');
$this->exec('migrations seed -c test --source BaseSeeds MigrationSeedNumbers');
$this->assertExitSuccess();
$this->assertOutputContains('MigrationSeedNumbers:</info> <comment>seeding');
$this->assertOutputContains('AnotherNumbersSeed:</info> <comment>seeding');
Expand Down Expand Up @@ -160,13 +161,13 @@ public function testSeederMultipleNotFound(): void

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The seed `NotThere` does not exist');
$this->exec('migrations seed -c test --seed NumbersSeed --seed NotThere');
$this->exec('migrations seed -c test NumbersSeed,NotThere');
}

public function testSeederMultiple(): void
{
$this->createTables();
$this->exec('migrations seed -c test --source CallSeeds --seed LettersSeed --seed NumbersCallSeed');
$this->exec('migrations seed -c test --source CallSeeds LettersSeed,NumbersCallSeed');

$this->assertExitSuccess();
$this->assertOutputContains('NumbersCallSeed:</info> <comment>seeding');
Expand All @@ -188,13 +189,13 @@ public function testSeederSourceNotFound(): void
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The seed `LettersSeed` does not exist');

$this->exec('migrations seed -c test --source NotThere --seed LettersSeed');
$this->exec('migrations seed -c test --source NotThere LettersSeed');
}

public function testSeederWithTimestampFields(): void
{
$this->createTables();
$this->exec('migrations seed -c test --seed StoresSeed');
$this->exec('migrations seed -c test StoresSeed');

$this->assertExitSuccess();
$this->assertOutputContains('StoresSeed:</info> <comment>seeding');
Expand All @@ -219,7 +220,7 @@ public function testSeederWithTimestampFields(): void
public function testDryRunModeWarning(): void
{
$this->createTables();
$this->exec('migrations seed -c test --seed NumbersSeed --dry-run');
$this->exec('migrations seed -c test NumbersSeed --dry-run');

$this->assertExitSuccess();
$this->assertOutputContains('DRY-RUN mode enabled');
Expand All @@ -230,7 +231,7 @@ public function testDryRunModeWarning(): void
public function testDryRunModeShortOption(): void
{
$this->createTables();
$this->exec('migrations seed -c test --seed NumbersSeed -x');
$this->exec('migrations seed -c test NumbersSeed -d');

$this->assertExitSuccess();
$this->assertOutputContains('DRY-RUN mode enabled');
Expand All @@ -246,7 +247,7 @@ public function testDryRunModeNoDataChanges(): void
$connection = ConnectionManager::get('test');
$initialCount = $connection->execute('SELECT COUNT(*) FROM numbers')->fetchColumn(0);

$this->exec('migrations seed -c test --seed NumbersSeed --dry-run');
$this->exec('migrations seed -c test NumbersSeed --dry-run');
$this->assertExitSuccess();

$finalCount = $connection->execute('SELECT COUNT(*) FROM numbers')->fetchColumn(0);
Expand All @@ -256,7 +257,7 @@ public function testDryRunModeNoDataChanges(): void
public function testDryRunModeMultipleSeeds(): void
{
$this->createTables();
$this->exec('migrations seed -c test --source CallSeeds --seed LettersSeed --seed NumbersCallSeed --dry-run');
$this->exec('migrations seed -c test --source CallSeeds LettersSeed,NumbersCallSeed --dry-run');

$this->assertExitSuccess();
$this->assertOutputContains('DRY-RUN mode enabled');
Expand Down Expand Up @@ -302,7 +303,7 @@ public function testDryRunModeWithEvents(): void
});

$this->createTables();
$this->exec('migrations seed -c test --seed NumbersSeed --dry-run');
$this->exec('migrations seed -c test NumbersSeed --dry-run');
$this->assertExitSuccess();
$this->assertOutputContains('DRY-RUN mode enabled');

Expand All @@ -317,7 +318,7 @@ public function testDryRunModeWithStoresSeed(): void
$connection = ConnectionManager::get('test');
$initialCount = $connection->execute('SELECT COUNT(*) FROM stores')->fetchColumn(0);

$this->exec('migrations seed -c test --seed StoresSeed --dry-run');
$this->exec('migrations seed -c test StoresSeed --dry-run');
$this->assertExitSuccess();
$this->assertOutputContains('DRY-RUN mode enabled');
$this->assertOutputContains('StoresSeed:</info> <comment>seeding');
Expand All @@ -329,7 +330,7 @@ public function testDryRunModeWithStoresSeed(): void
public function testSeederAnonymousClass(): void
{
$this->createTables();
$this->exec('migrations seed -c test --seed AnonymousStoreSeed');
$this->exec('migrations seed -c test AnonymousStoreSeed');

$this->assertExitSuccess();
$this->assertOutputContains('AnonymousStoreSeed:</info> <comment>seeding');
Expand All @@ -348,7 +349,7 @@ public function testSeederAnonymousClass(): void
public function testSeederShortName(): void
{
$this->createTables();
$this->exec('migrations seed -c test --seed Numbers');
$this->exec('migrations seed -c test Numbers');

$this->assertExitSuccess();
$this->assertOutputContains('NumbersSeed:</info> <comment>seeding');
Expand All @@ -363,7 +364,7 @@ public function testSeederShortName(): void
public function testSeederShortNameMultiple(): void
{
$this->createTables();
$this->exec('migrations seed -c test --source CallSeeds --seed Letters --seed NumbersCall');
$this->exec('migrations seed -c test --source CallSeeds Letters,NumbersCall');

$this->assertExitSuccess();
$this->assertOutputContains('NumbersCallSeed:</info> <comment>seeding');
Expand All @@ -382,7 +383,7 @@ public function testSeederShortNameMultiple(): void
public function testSeederShortNameAnonymous(): void
{
$this->createTables();
$this->exec('migrations seed -c test --seed AnonymousStore');
$this->exec('migrations seed -c test AnonymousStore');

$this->assertExitSuccess();
$this->assertOutputContains('AnonymousStoreSeed:</info> <comment>seeding');
Expand All @@ -393,4 +394,54 @@ public function testSeederShortNameAnonymous(): void
$query = $connection->execute('SELECT COUNT(*) FROM stores');
$this->assertEquals(2, $query->fetchColumn(0));
}

public function testSeederAllWithQuietModeSkipsConfirmation(): void
{
$this->createTables();
// In test environment (non-TTY), confirmation is automatically skipped
// This test verifies that seeds run without prompting
$this->exec('migrations seed -c test');

$this->assertExitSuccess();
$this->assertOutputNotContains('The following seeds will be executed:');
$this->assertOutputNotContains('Do you want to continue?');
$this->assertOutputContains('NumbersSeed:</info> <comment>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 testSeederSpecificSeedSkipsConfirmation(): void
{
$this->createTables();
$this->exec('migrations seed -c test NumbersSeed');

$this->assertExitSuccess();
$this->assertOutputNotContains('The following seeds will be executed:');
$this->assertOutputNotContains('Do you want to continue?');
$this->assertOutputContains('NumbersSeed:</info> <comment>seeding');
$this->assertOutputContains('All Done');
}

public function testSeederCommaSeparated(): void
{
$this->createTables();
$this->exec('migrations seed -c test --source CallSeeds Letters,NumbersCall');

$this->assertExitSuccess();
$this->assertOutputContains('NumbersCallSeed:</info> <comment>seeding');
$this->assertOutputContains('LettersSeed:</info> <comment>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));
}
}
Loading