/ Migrator

Migrator

Miko's Migrator manages database schema changes through version-controlled migration files.


Migrator Methods

Method Description Returns
run()Run pending migrationsarray
rollback($steps)Rollback last batcharray
reset()Rollback all migrationsarray
refresh()Reset + Run (recreate)array
status()Show migration statusarray

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 migrationKeep migrations focused and reversible
Always write down()Ensure migrations can be rolled back
Test rollbacksVerify down() works before deploying
Use timestampsMigrations run in chronological order
Backup before productionAlways backup database before migrating
Don't modify ran migrationsCreate new migration for changes