Migrations
Glueful migrations give you version control over your database schema. Each migration is an isolated, reversible change that is tracked with a checksum, batch number, description, and (optionally) extension source.
Key capabilities:
- Integrity tracking (checksum + description stored in
migrations
table) - Batch-based rollback (step or full)
- Extension discovery (enabled extensions can contribute migrations)
- Fluent schema builder with preview/validate hooks
- Safe reversible contract (
up()
,down()
,getDescription()
)
Always implement all three interface methods:
up
,down
, andgetDescription
. MissinggetDescription()
will prevent proper auditing and status introspection.
Creating a Migration
Generate a new class (use snake_case name):
php glueful migrate:create create_users_table
This creates a numbered file in database/migrations/
(e.g. 001_create_users_table.php
). Implement all interface methods:
<?php
namespace Glueful\Database\Migrations;
use Glueful\Database\Migrations\MigrationInterface;
use Glueful\Database\Schema\Interfaces\SchemaBuilderInterface;
class CreateUsersTable implements MigrationInterface
{
public function getDescription(): string
{
return 'Create users table with auth + lifecycle fields';
}
public function up(SchemaBuilderInterface $schema): void
{
// Using callback auto-executes the create immediately
$schema->createTable('users', function($table) {
$table->id(); // Auto-increment primary key (id)
$table->string('uuid', 12)->unique(); // External identifier
$table->string('name', 100);
$table->string('email')->unique();
$table->string('password');
$table->boolean('is_active')->default(true);
$table->timestamps(); // created_at / updated_at
$table->softDeletes(); // deleted_at
});
}
public function down(SchemaBuilderInterface $schema): void
{
$schema->dropTable('users');
}
}
getDescription()
Matters Why
The MigrationManager
persists your description along with checksum and extension name, supporting audit trails, CLI status clarity, and potential UI tooling.
Executing & Reverting
# Run all pending migrations
php glueful migrate:run
# Rollback last batch (default 1 step)
php glueful migrate:rollback
# Rollback N individual migrations (most recent first)
php glueful migrate:rollback --steps=3
# Reset (drop all tables)
php glueful db:reset
# Fresh start: drop then re-run migrations
php glueful db:reset && php glueful migrate:run
# Show applied vs pending
php glueful migrate:status
Behind the scenes each applied migration is recorded with:
Column | Purpose |
---|---|
migration | Filename of the migration |
batch | Batch number for grouped rollback |
applied_at | Timestamp applied |
checksum | SHA256 hash of file contents |
description | Your getDescription() output |
extension | Source extension (null for core) |
Supported Column Types
Status: Implemented unless marked Pending.
Method | Status | Notes |
---|---|---|
id() | Implemented | Auto-increment primary (defaults to id ) |
string(name, length=255) | Implemented | Variable length text |
text(name) | Implemented | Large text |
integer(name) | Implemented | Standard integer |
bigInteger(name) | Implemented | 64-bit integer |
boolean(name) | Implemented | Boolean flag |
decimal(name, precision=8, scale=2) | Implemented | Fixed precision |
float(name, precision=8, scale=2) | Implemented | Floating point |
double(name, precision=15, scale=8) | Implemented | Double precision |
timestamp(name) | Implemented | Timestamp column |
dateTime(name) | Implemented | Distinct from timestamp |
date(name) | Implemented | Date only |
time(name) | Implemented | Time only |
json(name) | Implemented | JSON storage |
uuid(name) | Implemented | UUID text storage; skeleton often uses string('uuid', 12) |
enum(name, values, default?) | Implemented | Enumerated constraint |
binary(name, length?) | Implemented | Binary blob / fixed length |
foreignId(name) | Implemented | Big integer + FK convenience; pairs with constrained() |
longText(name) | Pending | Use text() for now |
smallInteger(name) | Pending | Use integer() |
tinyInteger(name) | Pending | Use integer() |
jsonb(name) | Pending | Use json() |
Column Modifiers & Fluent Constraints
All column methods return a ColumnBuilderInterface
enabling chained constraints:
$table->string('email')
->unique() // Unique index
->index() // Plain index (optional if unique already)
->nullable(false) // Explicit NOT NULL
->default('') // Static default
->comment('Login email');
$table->decimal('price', 10, 2)
->unsigned()
->default(0)
->check('price >= 0');
$table->timestamp('created_at')
->useCurrent(); // CURRENT_TIMESTAMP
$table->timestamp('updated_at')
->nullable()
->useCurrent()
->useCurrentOnUpdate();
Selected modifier capabilities:
Modifier | Purpose |
---|---|
nullable() / notNull() | Nullability control |
default(value) | Static default |
defaultRaw(expr) | Raw SQL default (e.g. CURRENT_TIMESTAMP) |
useCurrent() | CURRENT_TIMESTAMP default |
useCurrentOnUpdate() | Auto-update timestamp on update |
unique(name?) | Unique index |
index(name?) | Plain index |
primary() | Primary key designation |
autoIncrement() | Auto increment numeric |
unsigned() | Unsigned numeric (where supported) |
check(expr) | CHECK constraint |
comment(text) | Column comment |
charset()/collation() | MySQL column-level overrides |
after()/first() | MySQL positional |
constrained(table?, column='id') | Foreign key helper on foreignId() or manually named column |
cascadeOnDelete()/nullOnDelete()/restrictOnDelete()/noActionOnDelete() | FK delete behaviors |
cascadeOnUpdate()/restrictOnUpdate()/noActionOnUpdate() | FK update behaviors |
Indexes & Keys
// Single
$table->index('email');
// Composite
$table->index(['user_id', 'created_at']);
// Unique
$table->unique('username');
// Named
$table->index('status', 'idx_users_status');
// Primary (composite)
$table->primary(['tenant_id', 'code']);
// Fulltext (where supported)
$table->fulltext('content');
Foreign Keys
Two primary patterns:
- Column then explicit foreign() builder
foreignId()->constrained()
shortcut
$table->foreignId('user_id')
->constrained('users') // References users.id
->cascadeOnDelete();
$table->string('created_by', 12)->nullable();
$table->foreign('created_by')
->references('uuid')
->on('users')
->nullOnDelete();
Available actions: cascadeOnDelete
, cascadeOnUpdate
, nullOnDelete
, restrictOnDelete
, restrictOnUpdate
, noActionOnDelete
, noActionOnUpdate
.
Note on UUID foreign keys:
- If your parent key is a UUID column (e.g.,
users.uuid
), define a matching string column and useforeign()
:$table->string('user_uuid', 12); $table->foreign('user_uuid')->references('uuid')->on('users')->restrictOnDelete();
- Use
foreignId()->constrained()
primarily for numericid
-style foreign keys.
Complete Table Example
$schema->table('products')
->id()
->string('uuid', 12)->unique()
->string('name')
->string('slug')->unique()
->text('description')->nullable()
->decimal('price', 10, 2)->unsigned()->default(0)
->integer('stock')->default(0)
->boolean('is_active')->default(true)
->foreignId('category_id')->constrained('categories')->nullOnDelete()
->json('metadata')->nullable()
->timestamps()
->softDeletes()
->index('is_active')
->index(['category_id', 'is_active'])
->create()
->execute(); // Execute queued SQL
Altering Tables
Use alterTable()
for structural changes; it returns the same fluent builder:
// Add columns
$schema->alterTable('users')
->string('phone')->nullable()
->date('birth_date')->nullable()
->execute();
// Rename & modify
$schema->alterTable('users')
->renameColumn('name', 'full_name')
->modifyColumn('email')->string(320) // Change length
->execute();
// Drop column
$schema->alterTable('users')
->dropColumn('birth_date')
->execute();
// Add index & FK
$schema->alterTable('orders')
->foreignId('user_id')->constrained('users')->cascadeOnDelete()
->index('user_id')
->execute();
The earlier callback form
$schema->table('users', function($table){ ... });
is replaced here by explicit fluent chains for clarity and preview/validate support.
Dropping Tables
$schema->dropTable('users'); // Fails if missing
$schema->dropTableIfExists('archive');
Common Patterns
External UUID + Internal ID
$schema->table('posts')
->id()
->string('uuid', 12)->unique()
->string('title')
->text('content')
->foreignId('user_id')->constrained('users')->cascadeOnDelete()
->timestamps()
->create();
Soft Deletes & Timestamps
$table->timestamps(); // created_at, updated_at
$table->softDeletes(); // deleted_at
Auditing (User Attribution)
$table->foreignId('created_by')->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->constrained('users')->nullOnDelete();
Typical Workflow
- Generate:
php glueful migrate:create create_tasks_table
- Implement
getDescription
,up
,down
- Add schema using fluent builder
- Run:
php glueful migrate:run
- Verify:
php glueful migrate:status
- Rollback if needed:
php glueful migrate:rollback
Preview & Validation
Before executing large or risky structural changes you can preview or validate pending operations (when using builder accumulation patterns):
// Stage operations without executing yet
$schema->table('bulk_demo')
->id()
->string('code', 32)->unique()
->timestamps()
->create();
// Preview and validate staged SQL on the SchemaBuilder
$sql = $schema->preview(); // Array of SQL statements
$result = $schema->validate(); // Validation metadata
// Execute when ready
$schema->execute();
Use
preview()
early when designing complex multi-step migrations to ensure generated SQL matches expectations across different drivers.
Best Practices
down()
1. Always Implement
Reversibility improves confidence & enables continuous delivery rollbacks.
2. One Logical Concern per Migration
Avoid bundling unrelated table creations or modifications.
3. Describe Real Intent
getDescription()
should explain business context, not just restate the class name.
4. Index Strategically
Add indexes for high-cardinality lookups & foreign keys that drive joins.
foreignId()->constrained()
for conventional FKs 5. Prefer
It standardizes naming and reduces mistakes.
6. Validate / Preview Complex Sets
Especially when altering multiple tables in one batch.
7. Keep Migrations Immutable
If you must change a committed migration, create a follow-up corrective migration—checksum tracking will detect edits.
Troubleshooting
Problem: Migration fails with "table already exists".
Cause: Table created manually or migration re-run after manual changes.
Fix: Use db:reset
or drop table manually then migrate:run
.
Problem: Rollback fails.
Cause: down()
incomplete or dependency order issue.
Fix: Ensure reverse operations mirror up()
and drop dependent constraints first.
Problem: Foreign key constraint error. Cause: Order mismatch or missing index. Fix: Create parent table first; ensure column types match; add index if performance-critical.
Problem: Edited old migration now shows checksum mismatch (future feature visibility). Fix: Do not edit historical migrations; append a new corrective migration.
Extension Migrations
Extensions can register additional migration paths; those migrations are tracked with the extension
column. This enables selective auditing & future enable/disable workflows.
Guidelines:
- Avoid naming collisions with core migrations
- Provide clear
getDescription()
context that names the extension - Use foreign keys referencing extension tables only after their creation migration
Next Steps
- Database — Query your migrated tables
- Controllers — Expose resources via API
- Validation — Enforce data integrity at input layer
Accurate as of current TableBuilderInterface
and schema subsystem. If you add new column types or helpers, update this document alongside the implementation.