Migrations
Migrations let you version control your database schema, making it easy to share and deploy database changes.
Creating Migrations
Generate a new migration:
php craftsman make:migration create_users_table
This creates a timestamped migration file in the database/migrations directory.
Migration Structure
database/migrations/20240101120000_create_users_table.php
<?php
use ZephyrPHP\Database\Migration;
use Doctrine\DBAL\Schema\Schema;
return new class extends Migration
{
public function up(Schema $schema): void
{
$table = $schema->createTable('users');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'unsigned' => true
]);
$table->addColumn('name', 'string', ['length' => 255]);
$table->addColumn('email', 'string', ['length' => 255]);
$table->addColumn('password', 'string', ['length' => 255]);
$table->addColumn('active', 'boolean', ['default' => true]);
$table->addColumn('created_at', 'datetime');
$table->addColumn('updated_at', 'datetime');
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['email']);
}
public function down(Schema $schema): void
{
$schema->dropTable('users');
}
};
Running Migrations
# Run all pending migrations
php craftsman migrate
# Run a specific number of migrations
php craftsman migrate --step=1
# Check migration status
php craftsman migrate:status
Rolling Back
# Rollback the last migration
php craftsman migrate:rollback
# Rollback multiple migrations
php craftsman migrate:rollback --step=3
# Rollback all migrations
php craftsman migrate:reset
# Rollback and re-run all migrations
php craftsman migrate:fresh
Warning
migrate:fresh drops all tables and re-runs all migrations. Never use this in production!
Column Types
| Method | Description |
|---|---|
integer |
INT column |
bigint |
BIGINT column |
smallint |
SMALLINT column |
string |
VARCHAR column |
text |
TEXT column |
boolean |
BOOLEAN column |
decimal |
DECIMAL column |
float |
FLOAT column |
datetime |
DATETIME column |
date |
DATE column |
time |
TIME column |
json |
JSON column |
blob |
BLOB column |
Column Options
// Nullable column
$table->addColumn('phone', 'string', [
'length' => 20,
'notnull' => false
]);
// Default value
$table->addColumn('status', 'string', [
'length' => 20,
'default' => 'pending'
]);
// Unsigned integer
$table->addColumn('views', 'integer', [
'unsigned' => true,
'default' => 0
]);
// Decimal with precision
$table->addColumn('price', 'decimal', [
'precision' => 10,
'scale' => 2
]);
Indexes
// Primary key
$table->setPrimaryKey(['id']);
// Unique index
$table->addUniqueIndex(['email']);
// Regular index
$table->addIndex(['status']);
// Composite index
$table->addIndex(['user_id', 'created_at']);
// Named index
$table->addIndex(['category_id'], 'idx_category');
Foreign Keys
public function up(Schema $schema): void
{
$table = $schema->createTable('posts');
$table->addColumn('id', 'integer', ['autoincrement' => true]);
$table->addColumn('user_id', 'integer', ['unsigned' => true]);
$table->addColumn('title', 'string', ['length' => 255]);
$table->addColumn('content', 'text');
$table->setPrimaryKey(['id']);
$table->addIndex(['user_id']);
// Add foreign key
$table->addForeignKeyConstraint(
'users', // Reference table
['user_id'], // Local columns
['id'], // Reference columns
[
'onDelete' => 'CASCADE',
'onUpdate' => 'CASCADE'
]
);
}
Modifying Tables
public function up(Schema $schema): void
{
$table = $schema->getTable('users');
// Add column
$table->addColumn('phone', 'string', [
'length' => 20,
'notnull' => false
]);
// Add index
$table->addIndex(['phone']);
}
public function down(Schema $schema): void
{
$table = $schema->getTable('users');
$table->dropIndex('IDX_...');
$table->dropColumn('phone');
}
Renaming Tables/Columns
public function up(Schema $schema): void
{
// Rename table
$schema->renameTable('users', 'members');
// Rename column
$table = $schema->getTable('members');
$table->renameColumn('name', 'full_name');
}
Dropping Tables
public function up(Schema $schema): void
{
// Drop single table
$schema->dropTable('temporary_data');
// Drop if exists
if ($schema->hasTable('old_logs')) {
$schema->dropTable('old_logs');
}
}
Example: Blog Schema
<?php
use ZephyrPHP\Database\Migration;
use Doctrine\DBAL\Schema\Schema;
return new class extends Migration
{
public function up(Schema $schema): void
{
// Categories table
$categories = $schema->createTable('categories');
$categories->addColumn('id', 'integer', ['autoincrement' => true]);
$categories->addColumn('name', 'string', ['length' => 100]);
$categories->addColumn('slug', 'string', ['length' => 100]);
$categories->setPrimaryKey(['id']);
$categories->addUniqueIndex(['slug']);
// Posts table
$posts = $schema->createTable('posts');
$posts->addColumn('id', 'integer', ['autoincrement' => true]);
$posts->addColumn('user_id', 'integer');
$posts->addColumn('category_id', 'integer', ['notnull' => false]);
$posts->addColumn('title', 'string', ['length' => 255]);
$posts->addColumn('slug', 'string', ['length' => 255]);
$posts->addColumn('content', 'text');
$posts->addColumn('published', 'boolean', ['default' => false]);
$posts->addColumn('published_at', 'datetime', ['notnull' => false]);
$posts->addColumn('created_at', 'datetime');
$posts->addColumn('updated_at', 'datetime');
$posts->setPrimaryKey(['id']);
$posts->addUniqueIndex(['slug']);
$posts->addIndex(['user_id']);
$posts->addIndex(['category_id']);
$posts->addIndex(['published', 'published_at']);
$posts->addForeignKeyConstraint('users', ['user_id'], ['id'], [
'onDelete' => 'CASCADE'
]);
$posts->addForeignKeyConstraint('categories', ['category_id'], ['id'], [
'onDelete' => 'SET NULL'
]);
}
public function down(Schema $schema): void
{
$schema->dropTable('posts');
$schema->dropTable('categories');
}
};
Best Practice
Always create a down() method that reverses your up() changes. This makes it easy to rollback if something goes wrong.