Skip to content

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:

bash
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_table
  • migrate:create <Name> scaffolds database/migrations/<timestamp>_<rand>_<Slug>.migration.php with an up() plus down() 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 row failed (with the error message) and the rest still run; fix the cause and re-run.
  • migrate:status [Name] lists migration rows with their pending, completed, or failed status, optionally for one migration (full timestamped name or short logical name).
  • migrate:rollback <Name> runs down() for one completed migration and flips it back to pending. Rollback requires a name; the old internal runner batch rollback no longer exists.
  • Native migration:create, migration:run, migration:rollback, and migration:status work too; migrate* names are BC aliases.

Migration files ​

bash
php roolith migrate:create create_users_table
php
<?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
<?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 migrations status table (id, name, file_type, status, error, timestamps) is created automatically when missing and also tracks seeders; the legacy seeds table from the old internal runner is no longer written (safe to drop after upgrading).
  • Only *.migration.php files in database/migrations run. 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 failed with the error stored; only pending rows 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 0 on success and 1 on failure, so migrate can 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.

Released under the MIT License.