DEV Community

Cover image for Why I Built Migrun
Andrej Rypo
Andrej Rypo

Posted on

Why I Built Migrun

I mostly work with PHP projects that do not live inside a full framework like Laravel or Symfony — frameworks that come with their own ORM-based migration tools.
The existing standalone migration tools like Phinx, Phpmig, Phoenix are available, but they come with problems that kept bothering me over the years.
I would not even call Phinx standalone — it pulls in the Cake framework's core.

Service container integration is a hack. At least in Phinx, which I used the most.
If you use a dependency injection container (and you really should), wiring services into Phinx migrations is not possible.
People resort to global static access or other workarounds to get their own database connection or a service like logger into their migration.
This completely goes against modern DI practices.
I wanted migrations to simply declare their dependencies and have them resolved from the container automatically.

Query builders add complexity you don't need.
Migration systems often provide table builders — APIs for creating columns, indexes, and tables in a database-agnostic way.
Usually based on the particular framework ORM. In theory, this lets you switch databases without rewriting migrations. In practice:

  • Most projects never switch databases. None of the projects I've worked with ever did. They introduced new databases, but did not replace one with another.
  • When they do, they have to deal with database-specific features, data types, and behaviors anyway.
  • The builder APIs cannot cover every database-specific case. Like this classic problem in Laravel. Phinx is even more limited. You end up writing raw SQL for the tricky parts regardless.
  • The builder adds a layer of abstraction and complexity for something that a simple CREATE TABLE statement does just fine.

I am not saying builders are useless. But for many projects, they are unnecessary overhead.

All the tools use inheritance. Your migration class must extend an AbstractMigration base class. This means you are locked into a class hierarchy.
In modern PHP, we prefer composition and interfaces over inheritance. Inheritance-based designs are rigid and harder to integrate with your own architecture.

They all impose naming conventions and rigid directory structure upon the adopters. Archiving migrations after hundreds have accumulated? Not an option or PITA.

The standalone tools feel outdated. Migration tools in the PHP ecosystem have not evolved with the language.
PHP has readonly classes, enums, named arguments, union types, fibers, pipelines.
Meanwhile, migration tools still use patterns from the PHP 5 era.

Any sort of I/O integration is a hack.
Phinx only works within Symfony Console, and the runner is tightly coupled to it. Go and try to create an HTML page with the list of migrations, I dare you.

What I wanted

Something dead simple:

  • An interface for migrations. Implement up(), optionally down(). Done.
  • Autowiring from the service container. Declare PDO $db as a parameter and it gets resolved.
  • No base classes to extend.
  • No config files.
  • No bundled CLI. No Symfony console.
  • No query builder.
  • No file naming constraints.
  • Zero runtime dependencies. Not even utility packages. No Cake core.
  • Any database, any tech stack, any service container.

So I built Migrun.

Who it is for

Migrun is for developers who:

  • Work on PHP projects outside of Laravel or Doctrine ecosystems
  • Want a migration runner that fits into their existing architecture, not one that dictates it
  • Prefer writing SQL directly instead of learning a builder API
  • Use a service container and want their migrations to benefit from it naturally
  • Want something minimal and hackable, not a full framework

If you already use Laravel or Doctrine, their migration systems are deeply integrated and you should keep using them.
Migrun was not built to replace those — it was built for everyone else.

How it works

Return an anonymous class instance from a PHP file. Get all the parameters auto-resolved from your service container.

// 20260326_181000_a_migration.php
<?php

use Dakujem\Migrun\Migration;
use Psr\Log\LoggerInterface;
use PDO;

return new class implements Migration
{
    public function up(?PDO $db = null, ?LoggerInterface $logger = null): void {
        $db->exec(<<<SQL

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE
)

SQL);
        $logger->debug('Users table created.');
    }
};
Enter fullscreen mode Exit fullscreen mode

Need a rollback? Don't like the heredoc syntax? No problem.

// 20260326_181002_reversible_migration.php
<?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');
    }
};
Enter fullscreen mode Exit fullscreen mode

You can also just return a closure:

// 20260326_181008_closure_migration.php
<?php

return function (\PDO $db): void {
    $db->exec(<<<SQL

CREATE INDEX idx_users_email ON users (email)

SQL);
};
Enter fullscreen mode Exit fullscreen mode

You write the SQL on your terms. You run whatever services your app needs. Fully in control. No hacks.

Setting it up takes a few lines of code.

$migrun = new MigrunBuilder()
    ->directory(__DIR__ . '/migrations')
    ->container($container)    // any PSR-11 container for autowiring
    ->pdoStorage($container->get(PDO::class))
    ->build();

$migrun->run();          // run pending migrations
$migrun->rollback(1);    // roll back the last one
$migrun->status();       // see what's applied and what's pending
Enter fullscreen mode Exit fullscreen mode

See the readme for fully functional CLI examples (like composer migrate:up),
or have AI tools set them up for you in a minute.
Symfony console is supported but not required (e.g. php bin/console db:migrate).

The architecture

Migrun is built around four straight-forward interfaces that

  • look for migrations
  • execute migrations
  • resolve migration dependencies
  • store migration history

Want to load pure SQL files? Implement a new "executor" (a single method) and a new "finder" (two methods).
Want to use a different DI container? Implement a single "invoker" method.
Want to store the history in Redis? Implement a new storage class.

No inheritance anywhere in the library. No static methods. No global state.

Give it a try.
It's in beta phase — your feedback may shape the release version.


  • Repository: github.com/dakujem/migrun
  • Install: composer require dakujem/migrun; or prompt "Install dakujem/migrun from Packagist using the recommended setup and add CLI scripts called via Composer."

Top comments (0)