PHP code example of dakujem / migrun

1. Go to this page and download the library: Download dakujem/migrun library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

dakujem / migrun example snippets



// migrations/20240101_120000_create_users.php

use PDO;

return function (PDO $db): void {
    $db->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)');
};


// migrations/20240115_093000_add_email_index.php

use Dakujem\Migrun\ReversibleMigration;
use PDO;

return new class implements ReversibleMigration
{
    public function up(?PDO $db = null): void
    {
        $db->exec('CREATE INDEX idx_users_email ON users (email)');
    }

    public function down(?PDO $db = null): void
    {
        $db->exec('DROP INDEX idx_users_email');
    }
};



akujem\Migrun\MigrunBuilder;

$container = ame database being migrated
$pdo = $container->get(PDO::class);

$orchestrator = (new MigrunBuilder())
    ->directory(__DIR__ . '/migrations')
    ->container($container)   // enables autowiring of migration parameters
    ->pdoStorage($pdo)        // history stored in the same DB, table: migrun_migrations
    ->build();

$executed = $orchestrator->run();
foreach ($executed as $migration) {
    echo "Migrated: {$migration->id()}" . PHP_EOL;
}

$orchestrator = (new MigrunBuilder())
    ->directory(__DIR__ . '/migrations')
    ->build();

use Dakujem\Migrun\MigrunBuilder;

$orchestrator = (new MigrunBuilder())
    ->directory(__DIR__ . '/migrations')           //                          // recommended: history in the same DB as migrations
    ->build();

// Any PDO connection (MySQL, PostgreSQL, SQLite, …) — recommended
->pdoStorage($pdo)                                // default table name (migrun_migrations)
->pdoStorage($pdo, table: 'schema_history')       // custom table name

// mysqli connection (MySQL/MariaDB only)
->mysqliStorage($mysqli)                          // default table name (migrun_migrations)
->mysqliStorage($mysqli, table: 'schema_history') // custom table name

// SQLite database file
->sqliteStorage()                                 // {migrations-dir}/.migrun/migrun.sqlite
->sqliteStorage(__DIR__ . '/var/history.sqlite')  // explicit path
->sqliteStorage(table: 'schema_history')          // default path, custom table name

// JSON file — default when nothing is set
->fileStorage(__DIR__ . '/var/migrun')     // directory → appends /migrun.json
                                           // file path → used as-is
                                           // omit → {migrations-dir}/.migrun/migrun.json



akujem\Migrun\Executor\ContainerInvoker;
use Dakujem\Migrun\Executor\Executor;
use Dakujem\Migrun\Executor\TrivialInvoker;
use Dakujem\Migrun\Finder\DirectoryFinder;
use Dakujem\Migrun\Orchestrator;
use Dakujem\Migrun\Storage\JsonFileStorage;

$container = iner / no autowiring:
// executor: new Executor(new TrivialInvoker()),

$executed = $orchestrator->run();
foreach ($executed as $migration) {
    echo "Migrated: {$migration->id()}" . PHP_EOL;
}

// Roll back the last migration
// $reverted = $orchestrator->rollback(1);



akujem\Migrun\MigrationState;
use Dakujem\Migrun\MigrunBuilder;

$container =    ->container($container)
    ->build();

$command = $argv[1] ?? 'run';

match ($command) {
    'run' => (function () use ($orchestrator) {
        $executed = $orchestrator->run();
        if (empty($executed)) {
            echo "Nothing to run." . PHP_EOL;
            return;
        }
        foreach ($executed as $m) {
            echo "Migrated: {$m->id()}" . PHP_EOL;
        }
    })(),

    'rollback' => (function () use ($orchestrator, $argv) {
        $steps = (int) ($argv[2] ?? 1);
        $reverted = $orchestrator->rollback($steps);
        foreach ($reverted as $m) {
            echo "Reverted: {$m->id()}" . PHP_EOL;
        }
    })(),

    'status' => (function () use ($orchestrator) {
        $entries = $orchestrator->status();

        if (empty($entries)) {
            echo "No migrations found." . PHP_EOL;
            return;
        }

        $idWidth = max(array_map(fn($e) => strlen($e->id), $entries));
        $idWidth = max($idWidth, 2); // minimum column width

        $header = sprintf(
            "%-{$idWidth}s  %-7s  %s",
            'Migration ID',
            'Status',
            'Applied at (UTC)' . '   ',
        );
        echo $header . PHP_EOL;
        echo str_repeat('-', strlen($header)) . PHP_EOL;

        $up = $down = 0;
        $missingSince = null;
        foreach ($entries as $entry) {
            echo sprintf(
                "%-{$idWidth}s  %-7s  %s",
                $entry->id,
                match ($entry->state) {
                    MigrationState::Applied => 'up',
                    MigrationState::Pending => 'down',
                    MigrationState::Missing => 'MISSING',
                },
                $entry->appliedAt?->format('Y-m-d H:i:s') ?? '-',
            ) . PHP_EOL;
            $up += MigrationState::Applied === $entry->state ? 1 : 0;
            $down += MigrationState::Pending === $entry->state ? 1 : 0;
            if (MigrationState::Missing === $entry->state) {
                $missingSince = $entry->appliedAt;
            }
        }

        echo str_repeat('-', strlen($header)) . PHP_EOL;
        if ($missingSince) {
            echo "WARNING! Some migration files missing since {$missingSince->format('Y-m-d H:i:s')}." . PHP_EOL;
        }
        echo "Total: {$up} up, {$down} down" . PHP_EOL;
    })(),

    default => (function () use ($command) {
        echo "Unknown command: {$command}" . PHP_EOL;
        echo "Usage: migrate.php [run|rollback [steps]|status]" . PHP_EOL;
        exit(1);
    })(),
};



use Dakujem\Migrun\MigrationState;
use Dakujem\Migrun\Orchestrator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class MigrateCommand extends Command
{
    public function __construct(private Orchestrator $runner)
    {
        parent::__construct('db:migrate');
    }

    protected function configure(): void
    {
        $this->addArgument('command', InputArgument::OPTIONAL, 'run | rollback | status', 'run');
        $this->addArgument('steps', InputArgument::OPTIONAL, 'Number of migrations to roll back', 1);
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        return match ($input->getArgument('command')) {
            'run' => $this->runMigrations($output),
            'rollback' => $this->rollback($output, (int) $input->getArgument('steps')),
            'status' => $this->status($output),
            default => (function () use ($input, $output) {
                $output->writeln("<error>Unknown command: {$input->getArgument('command')}</error>");
                return Command::FAILURE;
            })(),
        };
    }

    private function runMigrations(OutputInterface $output): int
    {
        $executed = $this->runner->run();
        if (empty($executed)) {
            $output->writeln('Nothing to run.');
        }
        foreach ($executed as $m) {
            $output->writeln("Migrated: {$m->id()}");
        }
        return Command::SUCCESS;
    }

    private function rollback(OutputInterface $output, int $steps): int
    {
        $reverted = $this->runner->rollback($steps);
        foreach ($reverted as $m) {
            $output->writeln("Reverted: {$m->id()}");
        }
        return Command::SUCCESS;
    }

    private function status(OutputInterface $output): int
    {
        $entries = array_filter(
            iterator_to_array($this->runner->status()),
            fn($e) => $e->state !== MigrationState::Missing,
        );

        if (empty($entries)) {
            $output->writeln('No migrations found.');
            return Command::SUCCESS;
        }

        $idWidth = max(array_map(fn($e) => strlen($e->id), $entries));
        $idWidth = max($idWidth, 2);

        $output->writeln(sprintf("%-{$idWidth}s  %-7s  %s", 'ID', 'Status', 'Applied at'));
        $output->writeln(str_repeat('-', $idWidth + 22));

        foreach ($entries as $entry) {
            $output->writeln(sprintf(
                "%-{$idWidth}s  %-7s  %s",
                $entry->id,
                match ($entry->state) {
                    MigrationState::Applied => 'up',
                    MigrationState::Pending => 'down',
                    MigrationState::Missing => 'MISSING',
                },
                $entry->appliedAt?->format('Y-m-d H:i:s') ?? '-',
            ));
        }

        return Command::SUCCESS;
    }
}

use Dakujem\Migrun\Storage\PdoStorage;
use Dakujem\Migrun\Storage\SqliteStorage;

// Any PDO connection — table is created automatically
$storage = new PdoStorage($pdo);
$storage = new PdoStorage($pdo, 'schema_history'); // custom table name

// SQLite convenience wrapper
$storage = new SqliteStorage(__DIR__ . '/var/migrun.sqlite');

(new MigrunBuilder())
    ->directory(__DIR__ . '/migrations')
    ->pdoStorage($pdo)                        // or ->sqliteStorage()
    ->build();

use Dakujem\Migrun\MigrationFile;
use Dakujem\Migrun\MigrationHistoryEntry;
use Dakujem\Migrun\TracksMigrations;

final class RedisStorage implements TracksMigrations
{
    public function __construct(private \Redis $redis, private string $key = 'migrations') {}

    public function getApplied(): iterable
    {
        $entries = [];
        foreach ($this->redis->hGetAll($this->key) as $id => $at) {
            $entries[] = new MigrationHistoryEntry($id, new \DateTimeImmutable($at));
        }
        usort($entries, fn($a, $b) => $b->at() <=> $a->at());
        return $entries;
    }

    public function isApplied(MigrationFile $migration): bool
    {
        return (bool) $this->redis->hExists($this->key, $migration->id());
    }

    public function markApplied(MigrationFile $migration, ?\DateTimeImmutable $at = null): void
    {
        $this->redis->hSet($this->key, $migration->id(), ($at ?? new \DateTimeImmutable())->format(\DateTimeImmutable::ATOM));
    }

    public function markReverted(MigrationFile $migration, ?\DateTimeImmutable $at = null): void
    {
        $this->redis->hDel($this->key, $migration->id());
    }
}

use Dakujem\Migrun\Executor\InvokesCallable;
use Invoker\InvokerInterface;

final readonly class PhpDiInvoker implements InvokesCallable
{
    public function __construct(private InvokerInterface $invoker) {}

    public function invoke(callable $fn): mixed
    {
        return $this->invoker->call($fn);
    }
}

use Invoker\Invoker;
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
use Invoker\ParameterResolver\DefaultValueResolver;
use Invoker\ParameterResolver\ResolverChain;

$invoker = new Invoker(
    new ResolverChain([
        new TypeHintContainerResolver($container),
        new DefaultValueResolver(),
    ]),
);

$orchestrator = new Orchestrator(
    storage:  new JsonFileStorage(__DIR__ . '/storage/migrations.json'),
    finder:   new DirectoryFinder(__DIR__ . '/migrations'),
    executor: new Executor(new PhpDiInvoker($invoker)),
);

use Dakujem\Migrun\Executor\InvokesCallable;
use Dakujem\Wire\Invoker as GenieInvoker;

final readonly class WireGenieInvoker implements InvokesCallable
{
    public function __construct(private GenieInvoker $invoker) {}

    public function invoke(callable $fn): mixed
    {
        return $this->invoker->invoke($fn);
    }
}

use Dakujem\Wire\Genie;

$orchestrator = new Orchestrator(
    storage:  new JsonFileStorage(__DIR__ . '/storage/migrations.json'),
    finder:   new DirectoryFinder(__DIR__ . '/migrations'),
    executor: new Executor(new WireGenieInvoker(new Genie($container))),
);

use Dakujem\Migrun\Direction;
use Dakujem\Migrun\ExecutesMigrations;
use Dakujem\Migrun\Executor\Executor;
use Dakujem\Migrun\MigrationFile;

final class TransactionalExecutor implements ExecutesMigrations
{
    public function __construct(
        private Executor $inner,
        private \PDO $db,
    ) {}

    public function execute(MigrationFile $migration, Direction $direction): void
    {
        $this->db->beginTransaction();
        try {
            $this->inner->execute($migration, $direction);
            $this->db->commit();
        } catch (\Throwable $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
}

$migrations = (new MigrunBuilder())
    ->directory(__DIR__ . '/migrations')
    ->pdoStorage($pdo)                            // table: migrun_migrations
    ->container($container)
    ->build();

$seeders = (new MigrunBuilder())
    ->directory(__DIR__ . '/seeds')
    ->pdoStorage($pdo, table: 'migrun_seeds')     // separate table, same database
    ->container($container)
    ->build();

$migrations->run();
$seeders->run();

use Dakujem\Migrun\Storage\JsonFileStorage;
use Dakujem\Migrun\Storage\PdoStorage;

$old = new JsonFileStorage(__DIR__ . '/migrations/.migrun/migrun.json');
$new = new PdoStorage($pdo);

$applied = $old->getApplied(); //   newest first
$migrationOrder = array_reverse( // oldest first
    is_array($applied) ? $applied : iterator_to_array($applied),
);
foreach ($migrationOrder as $entry) {
    $new->markApplied($entry->id(), $entry->at());
}

YYYYMMDD_HHMMSS_<name>.php
bash
php bin/migrate.php run
php bin/migrate.php rollback
php bin/migrate.php rollback 3
php bin/migrate.php status
json
{
    "scripts": {
        "migrate:up":     "@php bin/migrate.php run",
        "migrate:down":   "@php bin/migrate.php rollback",
        "migrate:status": "@php bin/migrate.php status"
    }
}