377 words
2 minutes
Auto migration MySQL schema command in Laravel
2024-04-22
No Tags
<?php

namespace App\Console\Commands;

use Doctrine\DBAL\Exception;
use Illuminate\Console\Command;
use Doctrine\DBAL\Schema\Comparator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Connection;

class MigrateAutoCommand extends Command
{
    protected $signature = 'migrate:auto {--f|--fresh} {--s|--seed} {--force} {--pretend}';

    protected Connection $conn;

    public function __construct()
    {
        parent::__construct();

        $configPath = 'database.connections.' . strval(Config::get('database.default'));
        $databaseDriverMapping = [
            'mysql' => 'pdo_mysql',
            'mariadb' => 'pdo_mysql',
            'sqlite' => 'pdo_sqlite',
            'pgsql' => 'pdo_pgsql',
            'sqlsrv' => 'pdo_sqlsrv',
        ];
        $connectionParams = [
            'dbname' => Config::get($configPath . '.database'),
            'user' => Config::get($configPath . '.username'),
            'password' => Config::get($configPath . '.password'),
            'host' => Config::get($configPath . '.host'),
            'driver' => $databaseDriverMapping[strval(Config::get('database.default'))],
        ];

        $this->conn = DriverManager::getConnection($connectionParams);
    }

    /**
     * @throws Exception
     */
    final public function handle(): void
    {
        $this->handleTraditionalMigrations();

        $this->handleAutomaticMigrations();

        if ($this->option('seed')) $this->seed();

        $this->info('Automatic migration completed successfully.');
    }

    private function handleTraditionalMigrations(): void
    {
        $command = 'migrate';

        if ($this->option('fresh')) $command .= ':fresh';
        if ($this->option('force')) $command .= ' --force';

        Artisan::call($command, [], $this->getOutput());
    }

    /**
     * @throws Exception
     */
    private function handleAutomaticMigrations(): void
    {
        $models = collect();

        $modelPaths = [
            'App\\Models' => app_path('Models'),
        ];
        foreach ($modelPaths as $namespace => $path) {
            if (!is_dir($path)) continue;

            foreach ((new Finder)->in($path) as $model) {
                $model = $namespace . str_replace(
                        ['/', '.php'],
                        ['\\', ''],
                        Str::after($model->getRealPath(), realpath($path))
                    );

                if (method_exists($model, 'migration'))
                    $models->push([
                        'object' => $object = app($model),
                        'order' => $object->migrationOrder ?? 0,
                    ]);
            }
        }

        foreach ($models->sortBy('order') as $model)
            $this->migrate($model['object']);
    }

    private function logSql(Model $model, array $queries): void
    {
        $this->line("<info>" . get_class($model) . ":</info> {$queries[0]}");
    }

    /**
     * @throws Exception
     */
    private function migrate(Model $model): void
    {
        $modelTable = $model->getTable();
        $pretend = $this->option('pretend'); // option which will display the SQL that will run
        $createTableQuery = static fn(Blueprint $table) => $model->migration($table);

        // alter existing table
        if (Schema::hasTable($modelTable)) {
            $tempTable = time() . "_doctrine_temp_table_$modelTable";

            Schema::dropIfExists($tempTable);
            Schema::create($tempTable, $createTableQuery);

            $schemaManager = $this->conn->createSchemaManager();
            $platform = $this->conn->getDatabasePlatform();
            $comparator = new Comparator($platform);

            $modelTableDetails = $schemaManager->introspectTable($modelTable);
            $tempTableDetails = $schemaManager->introspectTable($tempTable);

            $tableDiff = $comparator->compareTables($modelTableDetails, $tempTableDetails);

            Schema::drop($tempTable);

            if (!$tableDiff->isEmpty()) {
                $queries = $platform->getAlterTableSQL($tableDiff);

                if ($pretend) {
                    $this->logSql($model, $queries);
                } else {
                    $schemaManager->alterTable($tableDiff);
                    $this->line('<info>Table updated:</info> ' . $modelTable);
                }
            }
        } // create new table
        else {
            $createTableCommand = fn() => Schema::create($modelTable, $createTableQuery);
            $queries = Schema::getConnection()->pretend($createTableCommand);

            if ($pretend) {
                $this->logSql($model, $queries);
            } else {
                $createTableCommand();
                $this->line('<info>Table created:</info> ' . $modelTable);
            }
        }
    }

    private function seed(): void
    {
        $command = 'db:seed';

        if ($this->option('force')) $command .= ' --force';

        Artisan::call($command, [], $this->getOutput());
    }
}