0

Built-in commands: make:model, make:migration, migrate

Advanced5 min read·fw-10-004
sql

Concept

Built-in framework commands (make:model, make:migration, migrate) are the commands that come with the framework itself — not application code. They bootstrap developer workflow by generating files and running database migrations.

make:model — code generation: Reads a stub file (template), replaces placeholders, writes the new file. Stub files live in resources/stubs/ or inside the framework package itself. The command gets the model name from an argument, formats it (PascalCase), determines the file path, checks if it already exists, and writes the stub with substitutions.

make:migration — migration generation: Similar to make:model. Generates a timestamped migration file. Timestamp prefix (2024_01_15_123456_) ensures migrations run in creation order. Convention: create_users_table, add_email_to_users_table, drop_sessions_table.

migrate — running migrations: Reads files from database/migrations/, tracks which ones have run (in a migrations table in the DB), runs new ones in order, records them as complete.

Migrations tracking table: CREATE TABLE IF NOT EXISTS migrations (id INT PRIMARY KEY AUTO_INCREMENT, migration VARCHAR(255), batch INT). The batch column groups migrations run together — migrate:rollback rolls back the latest batch.

migrate:rollback: Undoes the last batch. Calls down() on each migration in the batch, removes them from the migrations table.

migrate:fresh: Drops all tables, runs all migrations from scratch. Only for development — DESTRUCTIVE on production.

Code Example

php
<?php
namespace Framework\Console\Commands;

use Framework\Console\Command;

class MakeModelCommand extends Command
{
    protected string $signature   = 'make:model {name : The model class name}';
    protected string $description = 'Create a new Eloquent model class';

    public function handle(): int
    {
        $name = $this->argument('name');
        $path = base_path("app/Models/{$name}.php");

        if (file_exists($path)) {
            $this->error("Model [{$name}] already exists!");
            return self::FAILURE;
        }

        $stub = file_get_contents(__DIR__ . '/stubs/model.stub');
        $code = str_replace(['{{ class }}', '{{ namespace }}'], [$name, 'App\\Models'], $stub);
        file_put_contents($path, $code);

        $this->info("Model [{$name}] created successfully.");
        return self::SUCCESS;
    }
}

// stubs/model.stub:
// <?php
// namespace {{ namespace }};
// use Framework\Orm\Model;
// class {{ class }} extends Model { }

class MigrateCommand extends Command
{
    protected string $signature   = 'migrate {--force : Force run in production}';
    protected string $description = 'Run the database migrations';

    public function handle(): int
    {
        if (env('APP_ENV') === 'production' && !$this->option('force')) {
            if (!$this->confirm('Running in production! Continue?')) {
                return self::SUCCESS;
            }
        }

        $migrator = $this->container->make(Migrator::class);
        $pending  = $migrator->getPending();

        if (empty($pending)) {
            $this->info('Nothing to migrate.');
            return self::SUCCESS;
        }

        foreach ($pending as $file) {
            $this->line("Migrating: {$file}");
            $migrator->run($file);
            $this->info("Migrated:  {$file}");
        }

        return self::SUCCESS;
    }
}

class Migrator
{
    public function __construct(private readonly \Framework\Database\Connection $db) {}

    public function ensureMigrationsTable(): void
    {
        $this->db->statement('CREATE TABLE IF NOT EXISTS migrations (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            migration VARCHAR(255) NOT NULL,
            batch INTEGER NOT NULL
        )');
    }

    public function getPending(): array
    {
        $this->ensureMigrationsTable();
        $ran      = array_column($this->db->select('SELECT migration FROM migrations'), 'migration');
        $allFiles = glob(base_path('database/migrations/*.php'));
        return array_filter($allFiles, fn($f) => !in_array(basename($f), $ran));
    }

    public function run(string $file): void
    {
        $migration = require $file;
        $migration->up();
        $batch = $this->getNextBatch();
        $this->db->insert('INSERT INTO migrations (migration, batch) VALUES (?, ?)', [basename($file), $batch]);
    }

    private function getNextBatch(): int
    {
        return (int) ($this->db->selectOne('SELECT MAX(batch) as b FROM migrations')['b'] ?? 0) + 1;
    }
}