roolith / migration
php simple migration
Requires
- php: >=8.0
- roolith/database: ^2.0
Requires (Dev)
- fakerphp/faker: ^1.24
- phpunit/phpunit: ^9.6 || ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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 withhost,name,user(passoptional). Missing keys throwInvalidArgumentException.table- optional status-table name, defaults tomigrations. 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 withIF NOT EXISTS, and the nullableerrorTEXT 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 doMigrationInterface::up/downandSeederInterface::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