Migration
Migrations are handled by the roolith/migration package (^2.0) through App\Database\MigrationFactory. Migration files live in database/migrations as *.migration.php and every migration plus seeder row is tracked in the single shared migrations status table (name, file_type, status) so schema never drifts between deploys.
Commands
Run from the project root:
php roolith migrate:create create_users_table
php roolith migrate
php roolith migrate:status
php roolith migrate:status create_users_table
php roolith migrate:rollback create_users_tablemigrate:create <Name>scaffoldsdatabase/migrations/<timestamp>_<rand>_<Slug>.migration.phpwith anup()plusdown()skeleton and registers the row as pending. The directory is created when missing and the name includes a timestamp plus random suffix so concurrent creates never collide.migrate [Name]runs all pending migrations in order, or one named migration when given (full timestamped name or short logical name). A failing migration marks its rowfailed(with the error message) and the rest still run; fix the cause and re-run.migrate:status [Name]lists migration rows with theirpending,completed, orfailedstatus, optionally for one migration (full timestamped name or short logical name).migrate:rollback <Name>runsdown()for one completed migration and flips it back topending. Rollback requires a name; the old internal runner batch rollback no longer exists.- Native
migration:create,migration:run,migration:rollback, andmigration:statuswork too;migrate*names are BC aliases.
Migration files
php roolith migrate:create create_users_table<?php
use Roolith\Migration\Interfaces\MigrationInterface;
use Roolith\Store\Interfaces\DatabaseInterface;
class _Migration_20250908120000_ab12_CreateUsersTable implements MigrationInterface
{
/**
* Apply the migration.
*
* @param DatabaseInterface $db Shared database connection.
* @return void
*/
public function up(DatabaseInterface $db): void
{
}
/**
* Revert the migration.
*
* @param DatabaseInterface $db Shared database connection.
* @return void
*/
public function down(DatabaseInterface $db): void
{
}
}Class names are generated by the package (_Migration_<timestamp>_<rand>_<Name>); App\Database\MigrationInterface remains as a BC alias for files written against the old internal runner. Inside up() and down() you get the database connection, so every driver method from the database page is available on $db.
Complete example
Here is a complete migration file that creates a users table.
<?php
use Roolith\Migration\Interfaces\MigrationInterface;
use Roolith\Store\Interfaces\DatabaseInterface;
class _Migration_20250908120000_ab12_CreateUsersTable implements MigrationInterface
{
/**
* Apply the migration.
*
* @param DatabaseInterface $db Shared database connection.
* @return void
*/
public function up(DatabaseInterface $db): void
{
$db->execute("
CREATE TABLE IF NOT EXISTS `users` (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'user',
last_logged_in DATETIME NULL,
verification_code VARCHAR(100) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
");
}
/**
* Revert the migration.
*
* @param DatabaseInterface $db Shared database connection.
* @return void
*/
public function down(DatabaseInterface $db): void
{
$db->execute("DROP TABLE IF EXISTS `users`;");
}
}Model-to-table contract
One model class maps to one table via protected string $table plus $primaryColumn (default id). The migrator creates those tables; the model reads and writes them. See models for $fillable, $casts, and validate().
Notes
- The shared
migrationsstatus table (id,name,file_type,status,error, timestamps) is created automatically when missing and also tracks seeders; the legacyseedstable from the old internal runner is no longer written (safe to drop after upgrading). - Only
*.migration.phpfiles indatabase/migrationsrun. Commands accept the full timestamped name or the short logical name (_<logical>suffix match); ambiguous short names ask for the full name. - Names allow letters, numbers, underscore, and hyphen; paths,
..traversal, and null bytes are rejected before touching the filesystem. - Failing rows move to
failedwith the error stored; onlypendingrows execute. Missing files warn and skip instead of aborting the batch. - Status writes run as single statements outside user transactions on purpose: MySQL DDL causes implicit commits, so wrapping user DDL plus the status update in one transaction is unsafe.
- Every command returns exit code
0on success and1on failure, somigratecan gate CI.
Seeding data
Migrations own schema; rows belong in seeders (php roolith seed:create, php roolith seed, php roolith seed:status, php roolith seed:run). Migrate first, then seed.