Search by

roolith / migration

im4aLL

php simple migration

2.0.0 2026-09-08 18:43 UTC

This package is auto-updated.

Last update: 2026-09-08 18:44:15 UTC


README

Simple migration and seeding tool for PHP applications.

Installation

You can install roolith-migration using Composer:

composer require roolith/migration

Usage

Create a PHP file migration.php and add the following code:

use Roolith\Migration\Migration;

require __DIR__ . "/../vendor/autoload.php";

$migration = new Migration();
$exitCode = $migration
    ->settings([
        "folder" => __DIR__ . "/migrations",
        "table" => "migrations", // optional, defaults to "migrations"
        "database" => [
            "host" => "127.0.0.1", // use 127.0.0.1, not localhost, to force TCP and avoid missing-socket errors
            "name" => "db_name",
            "user" => "user",
            "pass" => "pass",
        ],
    ])
    ->run($argv);

exit($exitCode);

For tests you can inject any DatabaseInterface implementation instead of connecting to MySQL:

$migration = new Migration($database);

Commands

Assuming your filename is migration.php, you can run the migration command as follows:

php migration.php migration:create migration_name
php migration.php migration:run # it will run all pending migrations
php migration.php migration:run migration_name
php migration.php migration:rollback migration_name
php migration.php migration:status
php migration.php migration:status migration_name

For seeding data, you can use the following command:

php migration.php seeder:create seed_name
php migration.php seeder:run # it will run all pending seeds
php migration.php seeder:run seed_name
php migration.php seeder:status
php migration.php seeder:status seed_name

migration:status lists migrations only; seeder:status lists seeders only. There is intentionally no seeder:rollback (seed data is generally non-reversible).

Names: timestamped vs short

Created files are timestamped, for example 20250101000000_ab12_create_users_table.migration.php, and the same string (without suffix) is stored in the name column.

Commands that take a name accept either form:

  • full timestamped name: 20250101000000_ab12_create_users_table
  • short logical name: create_users_table (matched by _<logical> suffix)

When a short name matches more than one row, the command prints an ambiguity warning and asks for the full timestamped name.

Exit codes

Every command returns an int, and Migration::run() returns it (echo output is kept for CLI backward compatibility):

  • 0 - success (including "nothing pending" and "already pending" no-ops)
  • 1 - failure, unknown command, invalid name, or missing file/row

Statuses and failure handling

Rows move through pending -> completed. When a migration or seeder throws, that row is marked failed (with the error message stored in the error column when it exists) and the remaining pending items still run. A summary line (completed / failed / skipped) is printed when anything did not succeed. Only pending rows are executed; re-run after fixing the cause.

Orphan rows (database record whose file was deleted) do not abort the batch: run prints a warning and skips them, rollback/status report the missing file with a non-zero exit code.

Settings

  • folder - migrations folder. Created when missing, must be writable.
  • database - connection config array with host, name, user (pass optional). Missing keys throw InvalidArgumentException.
  • table - optional status-table name, defaults to migrations. Must match ^[A-Za-z0-9_]+$.

Name rules

Logical names (for create) allow letters, numbers, underscore and hyphen, starting with a letter, number or underscore. Lookups additionally accept the numeric timestamp prefix form. Paths, .. traversal and null bytes are rejected. Generated class names are prefixed by type (_Migration_..., _Seeder_...) so a migration and a seeder can share a logical name; files created by older versions (_Timestamp_Name) still load.

Database notes

  • DDL is MySQL-specific (ENUM, ENGINE=InnoDB). The status table is created with IF NOT EXISTS, and the nullable error TEXT column is added idempotently, so existing installs keep working.
  • Status writes run as single statements outside user transactions on purpose: wrapping user DDL together with the status update in one DatabaseInterface::transaction() is unsafe because MySQL DDL causes implicit commits and user code may manage its own transactions.
  • Generated classes typehint DatabaseInterface, as do MigrationInterface::up/down and SeederInterface::run.

Notes

  • it will create migrations table if not exists
  • it will create migrations folder if not exists
  • You can change the name of the folder by passing settings

Example of migration file

php migration.php migration:create create_users_table
<?php

use Roolith\Store\Interfaces\DatabaseInterface;
use Roolith\Migration\Interfaces\MigrationInterface;

class _Migration_20250101000000_ab12_CreateUsersTable implements MigrationInterface
{
    public function up(DatabaseInterface $db): void {}

    public function down(DatabaseInterface $db): void {}
}

Example of seed file

php migration.php seeder:create add_users
<?php

use Roolith\Migration\Interfaces\SeederInterface;
use Roolith\Store\Interfaces\DatabaseInterface;

class _Seeder_20250101000000_ab12_AddUsers implements SeederInterface
{
    public function run(DatabaseInterface $db): void {}
}

Development

composer install
vendor/bin/phpunit
composer test