Migrator
Miko's Migrator manages database schema changes through version-controlled migration files.
Migrator Methods
| Method |
Description |
Returns |
run() | Run pending migrations | array |
rollback($steps) | Rollback last batch | array |
reset() | Rollback all migrations | array |
refresh() | Reset + Run (recreate) | array |
status() | Show migration status | array |
Setup
Directory Structure
database/
├── migrations/
│ ├── 2024_01_15_000001_create_users_table.php
│ ├── 2024_01_15_000002_create_products_table.php
│ ├── 2024_01_15_000003_create_orders_table.php
│ └── 2024_01_16_000001_add_phone_to_users.php
└── seeders/
└── DatabaseSeeder.php
Initialize Migrator
use Miko\Database\Migration\Migrator;
use Miko\Database\DB;
$migrator = new Migrator(
DB::connection(),
__DIR__ . '/database/migrations'
);
Creating Migrations
Migration File Structure
<?php
// database/migrations/2024_01_15_000001_create_users_table.php
use Miko\Database\Migration\Migration;
use Miko\Database\Migration\Schema;
use Miko\Database\Migration\TableBuilder;
class CreateUsersTable extends Migration
{
/**
* Run the migration
*/
public function up(): void
{
Schema::create('users', function(TableBuilder $table) {
$table->id('Id');
$table->string('Name', 100);
$table->string('Email', 100)->unique();
$table->string('Password', 255);
$table->boolean('IsActive')->default(true);
$table->string('Role', 50)->default('user');
$table->integer('Age')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migration
*/
public function down(): void
{
Schema::dropIfExists('users');
}
}
Naming Convention
{YYYY}_{MM}_{DD}_{HHMMSS}_{description}.php
Examples:
2024_01_15_000001_create_users_table.php
2024_01_15_000002_create_products_table.php
2024_01_16_100000_add_phone_to_users.php
2024_01_17_143022_create_orders_table.php
Running Migrations
Run All Pending
$result = $migrator->run();
// Result:
// [
// 'migrated' => [
// '2024_01_15_000001_create_users_table',
// '2024_01_15_000002_create_products_table'
// ],
// 'batch' => 1
// ]
echo "Migrated " . count($result['migrated']) . " files";
Check Status
$status = $migrator->status();
foreach ($status as $migration) {
$ran = $migration['ran'] ? 'Yes' : 'No';
echo "{$migration['name']} - Ran: $ran - Batch: {$migration['batch']}\n";
}
// Output:
// 2024_01_15_000001_create_users_table - Ran: Yes - Batch: 1
// 2024_01_15_000002_create_products_table - Ran: Yes - Batch: 1
// 2024_01_16_000001_add_phone_to_users - Ran: No - Batch: null
Rollback Migrations
Rollback Last Batch
$result = $migrator->rollback();
// Rolls back all migrations from the last batch
// [
// 'rolled_back' => [
// '2024_01_15_000002_create_products_table',
// '2024_01_15_000001_create_users_table'
// ]
// ]
Rollback Multiple Steps
// Rollback last 3 batches
$result = $migrator->rollback(3);
Reset All
// Rollback ALL migrations
$result = $migrator->reset();
Refresh (Reset + Run)
// Rollback all and re-run all migrations
$result = $migrator->refresh();
// Useful for development to recreate database
Migration Examples
Create Table
public function up(): void
{
Schema::create('products', function(TableBuilder $table) {
$table->id('Id');
$table->string('Name', 200);
$table->string('SKU', 50)->unique();
$table->text('Description')->nullable();
$table->decimal('Price', 10, 2);
$table->integer('Stock')->default(0);
$table->bigInteger('CategoryId')->unsigned();
$table->boolean('IsActive')->default(true);
$table->timestamps();
$table->foreign('CategoryId')
->references('Id')
->on('categories')
->onDelete('cascade');
$table->index('Name');
$table->index(['CategoryId', 'IsActive']);
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
Add Column
public function up(): void
{
Schema::table('users', function(TableBuilder $table) {
$table->string('Phone', 20)->nullable()->after('Email');
$table->string('Avatar', 255)->nullable();
$table->datetime('EmailVerifiedAt')->nullable();
});
}
public function down(): void
{
Schema::table('users', function(TableBuilder $table) {
$table->dropColumn('Phone');
$table->dropColumn('Avatar');
$table->dropColumn('EmailVerifiedAt');
});
}
Modify Column
public function up(): void
{
Schema::table('users', function(TableBuilder $table) {
$table->string('Name', 200)->change(); // Increase length
$table->renameColumn('Name', 'FullName');
});
}
public function down(): void
{
Schema::table('users', function(TableBuilder $table) {
$table->renameColumn('FullName', 'Name');
$table->string('Name', 100)->change();
});
}
Add Index
public function up(): void
{
Schema::table('orders', function(TableBuilder $table) {
$table->index('UserId');
$table->index('CreatedDate');
$table->index(['Status', 'CreatedDate'], 'orders_status_date_index');
});
}
public function down(): void
{
Schema::table('orders', function(TableBuilder $table) {
$table->dropIndex('orders_userid_index');
$table->dropIndex('orders_createddate_index');
$table->dropIndex('orders_status_date_index');
});
}
Add Foreign Key
public function up(): void
{
Schema::table('orders', function(TableBuilder $table) {
$table->foreign('UserId')
->references('Id')
->on('users')
->onDelete('cascade')
->onUpdate('cascade');
});
}
public function down(): void
{
Schema::table('orders', function(TableBuilder $table) {
$table->dropForeign('orders_userid_foreign');
});
}
Create Pivot Table
public function up(): void
{
Schema::create('user_roles', function(TableBuilder $table) {
$table->bigInteger('UserId')->unsigned();
$table->bigInteger('RoleId')->unsigned();
$table->timestamps();
$table->primary(['UserId', 'RoleId']);
$table->foreign('UserId')
->references('Id')
->on('users')
->onDelete('cascade');
$table->foreign('RoleId')
->references('Id')
->on('roles')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('user_roles');
}
CLI Commands
Migration Script
<?php
// migrate.php
require_once 'Model/Miko/autoload.php';
use Miko\Database\Migration\Migrator;
use Miko\Database\DB;
$migrator = new Migrator(DB::connection(), __DIR__ . '/database/migrations');
$command = $argv[1] ?? 'status';
switch ($command) {
case 'run':
case 'migrate':
echo "Running migrations...\n";
$result = $migrator->run();
foreach ($result['migrated'] as $migration) {
echo " Migrated: $migration\n";
}
echo "Done!\n";
break;
case 'rollback':
$steps = (int)($argv[2] ?? 1);
echo "Rolling back $steps batch(es)...\n";
$result = $migrator->rollback($steps);
foreach ($result['rolled_back'] as $migration) {
echo " Rolled back: $migration\n";
}
echo "Done!\n";
break;
case 'reset':
echo "Resetting all migrations...\n";
$result = $migrator->reset();
foreach ($result['rolled_back'] as $migration) {
echo " Rolled back: $migration\n";
}
echo "Done!\n";
break;
case 'refresh':
echo "Refreshing database...\n";
$migrator->refresh();
echo "Done!\n";
break;
case 'status':
default:
echo "Migration Status:\n";
echo str_repeat('-', 60) . "\n";
foreach ($migrator->status() as $m) {
$status = $m['ran'] ? 'Ran' : 'Pending';
$batch = $m['batch'] ?? '-';
echo sprintf("%-45s %s (Batch: %s)\n", $m['name'], $status, $batch);
}
break;
}
Usage
php migrate.php status # Show migration status
php migrate.php run # Run pending migrations
php migrate.php rollback # Rollback last batch
php migrate.php rollback 3 # Rollback last 3 batches
php migrate.php reset # Rollback all
php migrate.php refresh # Reset and re-run all
Best Practices
| Practice |
Description |
| One change per migration | Keep migrations focused and reversible |
| Always write down() | Ensure migrations can be rolled back |
| Test rollbacks | Verify down() works before deploying |
| Use timestamps | Migrations run in chronological order |
| Backup before production | Always backup database before migrating |
| Don't modify ran migrations | Create new migration for changes |