Overview
The Migration system provides version control for your database schema, allowing you to easily modify and share database structure across environments. It consists of two classes: Migration (base class for migrations) and Migrator (manages migration execution).
Class Reference
Lyger\Database\Migration
Base class for all database migrations.
Location: Lyger/Database/Migration.php
Lyger\Database\Migrator
Manages database migration execution.
Location: Lyger/Database/Migration.php:43
Migration Class
Abstract Methods
up()
abstract public function up(): void
Run the migration to create or modify database structures.
Example:
public function up(): void
{
$this->getSchema()->create('users', function ($table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
down()
abstract public function down(): void
Reverse the migration (rollback changes).
Example:
public function down(): void
{
$this->getSchema()->drop('users');
}
Helper Methods
getConnection()
public function getConnection(): \PDO
Get the database PDO connection.
Returns: PDO instance
Example:
public function up(): void
{
$pdo = $this->getConnection();
$pdo->exec("CREATE INDEX idx_name ON users(name)");
}
getSchema()
public function getSchema(): Schema
Get a Schema builder instance.
Returns: Schema instance
Example:
public function up(): void
{
$schema = $this->getSchema();
$schema->create('posts', function ($table) {
$table->id();
$table->string('title');
});
}
Migrator Class
Constructor
public function __construct(?string $migrationsPath = null)
Create a new Migrator instance.
Path to migrations directory. Defaults to database/migrations/
Example:
$migrator = new Migrator();
// or with custom path
$migrator = new Migrator('/path/to/migrations');
Methods
path()
public function path(string $path): self
Set the migrations directory path.
Path to migrations directory
Returns: $this for method chaining
Example:
$migrator = new Migrator();
$migrator->path('/custom/migrations');
run()
public function run(): void
Run all pending migrations.
Example:
$migrator = new Migrator();
$migrator->run();
// Output:
// Running migration: CreateUsersTable
// Migrated: CreateUsersTable
rollback()
public function rollback(): void
Rollback the last batch of migrations.
Example:
$migrator = new Migrator();
$migrator->rollback();
// Output:
// Rolling back migration: CreateUsersTable
// Rolled back: CreateUsersTable
reset()
public function reset(): void
Rollback all migrations.
Example:
$migrator = new Migrator();
$migrator->reset();
status()
public function status(): void
Display the status of all migrations.
Example:
$migrator = new Migrator();
$migrator->status();
// Output:
// | Migration |
// |-----------|
// | CreateUsersTable | Ran |
// | CreatePostsTable | Pending |
make()
public function make(string $name): void
Create a new migration file.
Name of the migration (e.g., “create_users_table”)
Example:
$migrator = new Migrator();
$migrator->make('create_users_table');
// Output:
// Created migration: 2026_03_08_120000_create_users_table.php
getMigrationsPath()
public function getMigrationsPath(): string
Get the migrations directory path.
Returns: Migration directory path
getMigrations()
public function getMigrations(): array
Get all loaded migrations.
Returns: Array of migration name => file path
loadMigrations()
public function loadMigrations(): void
Load all migration files from the migrations directory.
Creating Migrations
Using make()
The easiest way to create a migration:
$migrator = new Migrator();
$migrator->make('create_users_table');
This generates a timestamped file like 2026_03_08_120000_CreateUsersTable.php:
<?php
declare(strict_types=1);
use Lyger\Database\Migration;
class CreateUsersTable extends Migration
{
public function up(): void
{
$this->getSchema()->create('users', function ($table) {
$table->id();
// Add your columns here
// $table->string('name');
// $table->timestamps();
});
}
public function down(): void
{
$this->getSchema()->drop('users');
}
}
Manual Migration Creation
Create a file in database/migrations/ with the format: YYYY_MM_DD_HHiiss_DescriptiveName.php
<?php
declare(strict_types=1);
use Lyger\Database\Migration;
class CreateProductsTable extends Migration
{
public function up(): void
{
$this->getSchema()->create('products', function ($table) {
$table->id();
$table->string('name');
$table->text('description');
$table->decimal('price', 10, 2);
$table->integer('stock')->default(0);
$table->timestamps();
});
}
public function down(): void
{
$this->getSchema()->drop('products');
}
}
Migration Examples
Creating a Table
<?php
use Lyger\Database\Migration;
class CreateUsersTable extends Migration
{
public function up(): void
{
$this->getSchema()->create('users', function ($table) {
$table->id();
$table->string('name', 100);
$table->string('email')->unique();
$table->string('password');
$table->timestamp('email_verified_at')->nullable();
$table->boolean('is_active')->default(1);
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
$this->getSchema()->drop('users');
}
}
Adding Columns to Existing Table
<?php
use Lyger\Database\Migration;
class AddPhoneToUsersTable extends Migration
{
public function up(): void
{
$this->getSchema()->table('users', function ($table) {
$table->string('phone', 20)->nullable();
$table->string('address')->nullable();
});
}
public function down(): void
{
// Note: SQLite doesn't support dropping columns easily
// You may need to recreate the table without these columns
}
}
<?php
use Lyger\Database\Migration;
class CreatePostsTable extends Migration
{
public function up(): void
{
$this->getSchema()->create('posts', function ($table) {
$table->id();
$table->integer('user_id')->unsigned();
$table->string('title');
$table->text('content');
$table->string('status')->default('draft');
$table->datetime('published_at')->nullable();
$table->timestamps();
$table->index(['user_id']);
$table->index(['status', 'published_at']);
});
}
public function down(): void
{
$this->getSchema()->drop('posts');
}
}
Creating Pivot Tables
<?php
use Lyger\Database\Migration;
class CreateRoleUserTable extends Migration
{
public function up(): void
{
$this->getSchema()->create('role_user', function ($table) {
$table->integer('role_id')->unsigned();
$table->integer('user_id')->unsigned();
$table->timestamps();
// Composite index
$table->index(['role_id', 'user_id']);
});
}
public function down(): void
{
$this->getSchema()->drop('role_user');
}
}
Using Raw SQL
<?php
use Lyger\Database\Migration;
class CreateCustomIndex extends Migration
{
public function up(): void
{
$pdo = $this->getConnection();
$pdo->exec("
CREATE INDEX idx_users_email_active
ON users(email, is_active)
WHERE deleted_at IS NULL
");
}
public function down(): void
{
$pdo = $this->getConnection();
$pdo->exec("DROP INDEX idx_users_email_active");
}
}
Complex Migration
<?php
use Lyger\Database\Migration;
class CreateEcommerceSchema extends Migration
{
public function up(): void
{
$schema = $this->getSchema();
// Categories table
$schema->create('categories', function ($table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->integer('parent_id')->nullable();
$table->timestamps();
});
// Products table
$schema->create('products', function ($table) {
$table->id();
$table->integer('category_id')->unsigned();
$table->string('sku', 50)->unique();
$table->string('name');
$table->text('description');
$table->decimal('price', 10, 2);
$table->integer('stock')->unsigned()->default(0);
$table->boolean('is_active')->default(1);
$table->json('attributes')->nullable();
$table->timestamps();
$table->index(['category_id']);
$table->index(['is_active', 'stock']);
});
// Orders table
$schema->create('orders', function ($table) {
$table->id();
$table->integer('user_id')->unsigned();
$table->string('order_number', 20)->unique();
$table->decimal('subtotal', 10, 2);
$table->decimal('tax', 10, 2);
$table->decimal('total', 10, 2);
$table->string('status')->default('pending');
$table->json('shipping_address');
$table->datetime('shipped_at')->nullable();
$table->timestamps();
$table->index(['user_id']);
$table->index(['status', 'created_at']);
});
// Order items table
$schema->create('order_items', function ($table) {
$table->id();
$table->integer('order_id')->unsigned();
$table->integer('product_id')->unsigned();
$table->integer('quantity')->unsigned();
$table->decimal('price', 10, 2);
$table->decimal('total', 10, 2);
$table->timestamps();
$table->index(['order_id']);
});
}
public function down(): void
{
$schema = $this->getSchema();
$schema->drop('order_items');
$schema->drop('orders');
$schema->drop('products');
$schema->drop('categories');
}
}
Running Migrations
Run All Pending Migrations
$migrator = new Migrator();
$migrator->run();
Check Migration Status
$migrator = new Migrator();
$migrator->status();
Rollback Last Batch
$migrator = new Migrator();
$migrator->rollback();
Reset All Migrations
$migrator = new Migrator();
$migrator->reset();
Fresh Migration (Reset + Run)
$migrator = new Migrator();
$migrator->reset();
$migrator->run();
Migration Batching
Migrations are run in batches. Each time you call run(), all pending migrations are executed as a single batch. When you rollback(), the last batch is reversed.
Example workflow:
// Initial setup - Batch 1
$migrator->make('create_users_table');
$migrator->make('create_posts_table');
$migrator->run(); // Runs both as Batch 1
// Later additions - Batch 2
$migrator->make('add_fields_to_users');
$migrator->run(); // Runs as Batch 2
// Rollback only the last batch
$migrator->rollback(); // Rolls back Batch 2 only
Migrations Table
The Migrator automatically creates a migrations table to track which migrations have been run:
CREATE TABLE migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
migration VARCHAR(255) NOT NULL,
batch INTEGER NOT NULL
)
This table is created automatically the first time you run migrations. You don’t need to create it manually.
Best Practices
- Never modify existing migrations that have been run in production. Create a new migration instead.
- Always provide a
down() method to reverse your migration
- Test migrations locally before deploying to production
- Use descriptive names for migrations (e.g.,
create_users_table, not just users)
- Keep migrations focused - one logical change per migration
- Order matters - migrations run in alphabetical order by filename
- Use the Schema builder instead of raw SQL when possible for database portability
Common Patterns
Renaming a Table
public function up(): void
{
$pdo = $this->getConnection();
$pdo->exec("ALTER TABLE old_name RENAME TO new_name");
}
public function down(): void
{
$pdo = $this->getConnection();
$pdo->exec("ALTER TABLE new_name RENAME TO old_name");
}
Seeding Data in Migration
public function up(): void
{
$this->getSchema()->create('roles', function ($table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
// Seed initial roles
$pdo = $this->getConnection();
$stmt = $pdo->prepare("INSERT INTO roles (name, created_at) VALUES (?, ?)");
$now = date('Y-m-d H:i:s');
foreach (['admin', 'moderator', 'user'] as $role) {
$stmt->execute([$role, $now]);
}
}
Conditional Migrations
public function up(): void
{
$pdo = $this->getConnection();
// Check if table exists
$result = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name='users'");
if (!$result->fetch()) {
$this->getSchema()->create('users', function ($table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
}