Why I Built Migrun

Published: (March 28, 2026 at 04:17 PM EDT)
5 min read
Source: Dev.to

Source: Dev.to

The Problem with Existing PHP Migration Tools

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 (Phinx, Phpmig, Phoenix) are available, but they come with problems that have bothered me for years.

Phinx isn’t really standalone

  • It pulls in the Cake framework’s core.
  • Service‑container integration is a hack.
    • Wiring services into Phinx migrations is not possible.
    • People resort to global static access or other work‑arounds to get their own DB connection or a logger into a 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 unnecessary complexity

Migration systems often provide table builders—APIs for creating columns, indexes, and tables in a database‑agnostic way, usually based on the framework’s 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 on ever did. They introduce new databases, but do not replace one with another.
  • When they do switch, they have to deal with database‑specific features, data types, and behaviours anyway.
  • Builder APIs cannot cover every database‑specific case (e.g., the classic “tiny integer with custom size” 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’m not saying builders are useless, but for many projects they are unnecessary overhead.

Inheritance‑based design

All the tools use inheritance. Your migration class must extend an AbstractMigration base class, which locks you 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.

Rigid naming conventions and directory structures

The tools impose naming conventions and a rigid directory structure on adopters. Archiving migrations after hundreds have accumulated? Not an option—or a PITA.

Outdated codebase

Migration tools in the PHP ecosystem have not evolved with the language. PHP now has readonly classes, enums, named arguments, union types, fibers, pipelines, etc., but migration tools still use patterns from the PHP 5 era.

I/O integration is a hack

Phinx only works within Symfony Console, and the runner is tightly coupled to it. Trying to create an HTML page with the list of migrations? Good luck.

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.
  • Works with 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 (or a closure) from a PHP file. All parameters are auto‑resolved from your service container.

Simple migration (anonymous class)

// 20260326_181000_a_migration.php
exec(debug('Users table created.');
    }
};

Reversible migration (implements ReversibleMigration)

// 20260326_181002_reversible_migration.php
exec('CREATE INDEX idx_users_email ON users (email)');
    }

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

Closure‑based migration

// 20260326_181008_closure_migration.php
exec(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 migration

That’s it—simple, composable, and framework‑agnostic.

$migrun->status(); // see what's applied and what's pending

See the readme for fully functional CLI examples (e.g. 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 straightforward interfaces that:

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

Extensibility

  • Pure SQL files? Implement a new executor (a single method) and a new finder (two methods).
  • Different DI container? Implement a single invoker method.
  • Store history in Redis? Implement a new storage class.

There is no inheritance anywhere in the library, no static methods, and no global state.

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

Repository:
Install: composer require dakujem/migrun
(or prompt “Install dakujem/migrun from Packagist using the recommended setup and add CLI scripts called via Composer.”)

0 views
Back to Blog

Related posts

Read more »