0

Final Class Yiisoft\Db\Migration\Command\MarkCommand

InheritanceYiisoft\Db\Migration\Command\MarkCommand » Yiisoft\Db\Migration\Command\DatabaseCommand » Symfony\Component\Console\Command\Command

Moves migration history to a version without executing up() or down().

Use this command only after verifying that the database schema already matches the target migration state.

For example,

./yii migrate:mark 'App\Migrations\M260101000002CreateIndex' # record pending migrations through this class
./yii migrate:mark M260101000003AddColumn                    # use an unqualified migration class name
./yii migrate:mark 260101000004                              # select a migration by its timestamp
./yii migrate:mark --clear                                   # remove all migration history entries

A pending target is recorded along with all earlier pending migrations. A recorded target removes newer history entries. Use the full class name when several migrations share a timestamp.

Method Details

Hide inherited methods

__construct() public method

public mixed __construct ( Yiisoft\Db\Migration\Service\MigrationService $migrationService, Yiisoft\Db\Migration\Migrator $migrator, ?\Yiisoft\Db\Migration\DatabaseRegistry $additionalDatabases = null )
$migrationService Yiisoft\Db\Migration\Service\MigrationService
$migrator Yiisoft\Db\Migration\Migrator
$additionalDatabases ?\Yiisoft\Db\Migration\DatabaseRegistry

                public function __construct(
    private readonly MigrationService $migrationService,
    private readonly Migrator $migrator,
    ?DatabaseRegistry $additionalDatabases = null,
) {
    parent::__construct($additionalDatabases);
}

            
configure() protected method

protected void configure ( )

                protected function configure(): void
{
    $this
        ->addArgument('version', InputArgument::OPTIONAL, 'Migration class name or timestamp.')
        ->addOption('clear', 'c', InputOption::VALUE_NONE, 'Remove all migration history entries.')
        ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.');
}

            
createContext() protected method

protected Yiisoft\Db\Migration\Command\Context\MigrationContext createContext ( Yiisoft\Db\Migration\DatabaseSetup $setup )
$setup Yiisoft\Db\Migration\DatabaseSetup

                protected function createContext(DatabaseSetup $setup): MigrationContext
{
    return new MigrationContext($setup->migrator, $setup->migrationService);
}

            
createDefaultContext() protected method

protected Yiisoft\Db\Migration\Command\Context\MigrationContext createDefaultContext ( )

                protected function createDefaultContext(): MigrationContext
{
    return new MigrationContext($this->migrator, $this->migrationService);
}

            
execute() protected method
protected integer execute ( \Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output )
$input \Symfony\Component\Console\Input\InputInterface
$output \Symfony\Component\Console\Output\OutputInterface

                final protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);
    $names = $this->resolveDatabaseNames($input);
    if (is_string($names)) {
        $io->error($names);
        return Command::INVALID;
    }
    $multipleDatabases = $this->isMultipleDatabases();
    $hasResults = false;
    foreach ($names as $name) {
        if (!$multipleDatabases || $name === DatabaseRegistry::DEFAULT_DATABASE) {
            $context = $this->createDefaultContext();
        } else {
            /** @psalm-var DatabaseRegistry $this->additionalDatabases */
            $context = $this->createContext($this->additionalDatabases->createSetup($name));
        }
        if ($multipleDatabases) {
            $io->section('Database: ' . OutputFormatter::escape($name));
        }
        $status = $this->executeForDatabase($input, $output, $context);
        if ($status === null) {
            continue;
        }
        if ($status !== Command::SUCCESS) {
            return $status;
        }
        $hasResults = true;
    }
    return $hasResults ? Command::SUCCESS : Command::FAILURE;
}

            
executeForDatabase() protected method

protected integer executeForDatabase ( \Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output, object $context )
$input \Symfony\Component\Console\Input\InputInterface
$output \Symfony\Component\Console\Output\OutputInterface
$context object

                protected function executeForDatabase(
    InputInterface $input,
    OutputInterface $output,
    object $context,
): int {
    $io = new SymfonyStyle($input, $output);
    $context->migrator->setIo($io);
    $context->migrationService->setIo($io);
    $context->migrationService->databaseConnection();
    /** @var string|null $version */
    $version = $input->getArgument('version');
    $clear = (bool) $input->getOption('clear');
    if ($clear === ($version !== null)) {
        $io->error('Specify either a version or the "--clear" option.');
        return Command::INVALID;
    }
    $history = array_keys($context->migrator->getHistory());
    if ($version === null) {
        if ($history === []) {
            $io->success('The migration history is already empty. Nothing needs to be done.');
            return Command::SUCCESS;
        }
        $this->writeMigrations($output, $history, 'to be removed from history');
        $io->note('Only migration history will change. No migrations will be reverted.');
        if ($input->getOption('force-yes') || $io->confirm('Remove all migration history entries?', false)) {
            $context->migrator->updateHistory([], $history);
            $io->success('The migration history is cleared. No actual migration was performed.');
        }
        return Command::SUCCESS;
    }
    $version = trim($version, '\\');
    $label = OutputFormatter::escape($version);
    $timestamp = preg_match('/^\d{12}$/D', $version) === 1 ? $version : null;
    if ($timestamp === null && preg_match('/^(?:\w+\\\\)*M\d{12}.*$/D', $version) !== 1) {
        $io->error('The version must be a migration class name or a migration timestamp.');
        return Command::INVALID;
    }
    $add = [];
    $remove = [];
    $found = false;
    $isRecordedClass = $timestamp === null && in_array(
        $version,
        array_map(static fn(string $name): string => trim($name, '\\'), $history),
        true,
    );
    // Exact recorded classes do not need source files. Timestamps still select pending targets first.
    if (!$isRecordedClass) {
        $pending = $context->migrationService->getNewMigrations();
        foreach ($pending as $i => $migration) {
            if ($this->matches($migration, $version, $timestamp)) {
                $add = array_slice($pending, 0, $i + 1);
                $found = true;
                break;
            }
        }
    }
    if (!$found) {
        foreach ($history as $i => $migration) {
            if ($this->matches($migration, $version, $timestamp)) {
                $remove = array_slice($history, 0, $i);
                $found = true;
                break;
            }
        }
    }
    if (!$found) {
        $io->error("Unable to find the version '$label'.");
        return Command::INVALID;
    }
    if ($add === [] && $remove === []) {
        $io->success("Already at '$label'. Nothing needs to be done.");
        return Command::SUCCESS;
    }
    $limit = $context->migrator->getMigrationNameLimit();
    foreach ($add as $migration) {
        if ($limit !== null && strlen($migration) > $limit) {
            $io->error('The migration name "' . OutputFormatter::escape($migration) . '" is too long.');
            return Command::INVALID;
        }
    }
    $this->writeMigrations($output, $add, 'to be marked as applied');
    $this->writeMigrations($output, $remove, 'to be removed from history');
    $io->note('Only migration history will change. No migrations will be applied or reverted.');
    if ($input->getOption('force-yes') || $io->confirm("Set migration history at $label?", false)) {
        $context->migrator->updateHistory($add, $remove);
        $io->success("The migration history is set at $label. No actual migration was performed.");
    }
    return Command::SUCCESS;
}

            
getAvailableDatabaseNames() protected method
protected string[] getAvailableDatabaseNames ( )

                protected function getAvailableDatabaseNames(): array
{
    return [DatabaseRegistry::DEFAULT_DATABASE, ...($this->additionalDatabases?->getNames() ?? [])];
}

            
isMultipleDatabases() protected method
protected boolean isMultipleDatabases ( )

                protected function isMultipleDatabases(): bool
{
    return $this->additionalDatabases !== null && $this->additionalDatabases->getNames() !== [];
}

            
resolveDatabaseNamesWithMissingOption() protected method

Defined in: Yiisoft\Db\Migration\Command\DatabaseCommand::resolveDatabaseNamesWithMissingOption()

Resolves the database names or returns a message indicating that an option is missing if multiple databases are configured.

protected string[]|string resolveDatabaseNamesWithMissingOption ( \Symfony\Component\Console\Input\InputInterface $input )
$input \Symfony\Component\Console\Input\InputInterface

The input interface containing the command options and arguments.

                protected function resolveDatabaseNamesWithMissingOption(InputInterface $input): array|string
{
    if (!$this->isMultipleDatabases()) {
        return [DatabaseRegistry::DEFAULT_DATABASE];
    }
    return 'The --db option is required when multiple databases are configured.';
}