Add 100+ automated unit tests from .expect file specifications Add session system test Add rsx:constants:regenerate command test Add rsx:logrotate command test Add rsx:clean command test Add rsx:manifest:stats command test Add model enum system test Add model mass assignment prevention test Add rsx:check command test Add migrate:status command test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
619 lines
24 KiB
PHP
619 lines
24 KiB
PHP
<?php
|
|
/**
|
|
* CODING CONVENTION:
|
|
* This file follows the coding convention where variable_names and function_names
|
|
* use snake_case (underscore_wherever_possible).
|
|
*/
|
|
|
|
namespace App\RSpade\Commands\Migrate;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\Console\Output\BufferedOutput;
|
|
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
|
use App\Providers\AppServiceProvider;
|
|
use App\RSpade\Core\Database\MigrationValidator;
|
|
use App\RSpade\Core\Database\SqlQueryTransformer;
|
|
|
|
/**
|
|
* Custom migration command that runs standard migrations plus maintenance commands
|
|
*
|
|
* This command extends Laravel's built-in migrate command to also run additional
|
|
* maintenance commands after migrations complete:
|
|
*
|
|
* 1. migrate:normalize_schema - Normalizes database schema (data types, encodings, required columns)
|
|
* 2. migrate:regenerate_constants - Updates model constants and docblocks, exports to JavaScript
|
|
*
|
|
* The command preserves and displays all output from the standard migration command,
|
|
* then runs the maintenance commands.
|
|
*
|
|
* This command is automatically used when running 'php artisan migrate' due to
|
|
* a modification in the artisan script.
|
|
*/
|
|
class Maint_Migrate extends Command
|
|
{
|
|
protected $signature = 'migrate {--force} {--seed} {--step} {--path=*} {--production : Run in production mode, skipping snapshot requirements} {--framework-only : Run only framework migrations (system/database/migrations), skip snapshot, but run normalization}';
|
|
|
|
protected $description = 'Run migrations and maintenance commands';
|
|
|
|
protected $flag_file = '/var/www/html/.migrating';
|
|
|
|
public function handle()
|
|
{
|
|
// Enable SQL query transformation for migrations
|
|
SqlQueryTransformer::enable();
|
|
$this->register_query_transformer();
|
|
|
|
// Enable full query logging to stdout for migrations
|
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_ALL_STDOUT);
|
|
|
|
// Ensure migrations table exists (create it if needed)
|
|
$this->ensure_migrations_table_exists();
|
|
|
|
// Check if we're in production mode (either via flag or environment)
|
|
$is_production = $this->option('production') || app()->environment('production');
|
|
|
|
// Check if we're in framework-only mode
|
|
$is_framework_only = $this->option('framework-only');
|
|
|
|
// Only enforce snapshot protection in development mode without --production or --framework-only flag
|
|
$require_snapshot = !$is_production && !$is_framework_only;
|
|
|
|
// Check for migration mode if we require snapshot
|
|
if ($require_snapshot) {
|
|
if (!file_exists($this->flag_file)) {
|
|
$this->error('[ERROR] Migration mode not active!');
|
|
$this->error('');
|
|
$this->line('In development mode, you must create a database snapshot before running migrations.');
|
|
$this->line('This prevents partial migrations from corrupting your database.');
|
|
$this->info('');
|
|
$this->line('To begin a migration session:');
|
|
$this->line(' php artisan migrate:begin');
|
|
|
|
return 1;
|
|
}
|
|
|
|
$this->info('[OK] Migration mode active - snapshot available for rollback');
|
|
$this->info('');
|
|
} elseif ($is_production) {
|
|
$this->info(' Running in production mode (no snapshot protection)');
|
|
} elseif ($is_framework_only) {
|
|
$this->info(' Running framework-only migrations (no snapshot protection)');
|
|
}
|
|
|
|
$mode_desc = $is_production ? ' (production mode)' : ($is_framework_only ? ' (framework-only)' : ' with maintenance commands');
|
|
$this->info('Running migrations' . $mode_desc . '...');
|
|
|
|
// Get all the options
|
|
$force = $this->option('force');
|
|
$seed = $this->option('seed');
|
|
$step = $this->option('step');
|
|
$paths = $this->option('path');
|
|
|
|
// Check if path option is used and throw exception
|
|
if (!empty($paths)) {
|
|
$this->error('[ERROR] Migration by path is disabled!');
|
|
$this->error('');
|
|
$this->line('This command enforces running all pending migrations in order.');
|
|
$this->line('Please run migrations without the --path option:');
|
|
$this->info(' php artisan migrate');
|
|
$this->error('');
|
|
$this->line('If you need to run specific migrations:');
|
|
$this->line(' 1. Place them in the standard migrations directory');
|
|
$this->line(' 2. Use migration timestamps to control execution order');
|
|
return 1;
|
|
}
|
|
|
|
// Determine which migration paths to use for whitelist check
|
|
$paths_to_check = $is_framework_only
|
|
? [database_path('migrations')]
|
|
: MigrationPaths::get_all_paths();
|
|
|
|
// Check migration whitelist
|
|
if (!$this->checkMigrationWhitelist($paths_to_check)) {
|
|
return 1;
|
|
}
|
|
|
|
// Validate migration files for Schema builder usage (only in non-production)
|
|
if (!$is_production && !$this->_validate_schema_rules()) {
|
|
return 1;
|
|
}
|
|
|
|
// Build command arguments
|
|
$migrateArgs = [];
|
|
if ($force) {
|
|
$migrateArgs['--force'] = true;
|
|
}
|
|
if ($seed) {
|
|
$migrateArgs['--seed'] = true;
|
|
}
|
|
if ($step) {
|
|
$migrateArgs['--step'] = true;
|
|
}
|
|
|
|
// Run normalize_schema BEFORE migrations to fix existing tables
|
|
// Pass --production flag to skip snapshot in both production and framework-only modes
|
|
$requiredColumnsArgs = ($is_production || $is_framework_only) ? ['--production' => true] : [];
|
|
|
|
$this->info("\n Pre-migration normalization (fixing existing tables)...\n");
|
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
|
if ($normalizeExitCode !== 0) {
|
|
$this->error('Pre-migration normalization failed');
|
|
SqlQueryTransformer::disable();
|
|
return $normalizeExitCode;
|
|
}
|
|
|
|
// Use a buffered output to capture migration output
|
|
$bufferedOutput = new BufferedOutput();
|
|
|
|
try {
|
|
// Run the standard migrations and capture the output
|
|
$originalSqlMode = DB::selectOne('SELECT @@sql_mode as sql_mode')->sql_mode;
|
|
DB::statement("SET sql_mode = REPLACE(@@sql_mode, 'NO_ZERO_DATE', '');");
|
|
|
|
// Run migrations directly via migrator to avoid recursion
|
|
$migrator = app('migrator');
|
|
$migrator->setOutput($bufferedOutput);
|
|
|
|
// Use all migration paths (framework + user), or just framework if --framework-only
|
|
$migrationPaths = $is_framework_only
|
|
? [database_path('migrations')]
|
|
: MigrationPaths::get_all_paths();
|
|
|
|
// Run migrations one-by-one with normalization after each
|
|
$this->run_migrations_with_normalization($migrator, $migrationPaths, $step, $requiredColumnsArgs);
|
|
|
|
$exitCode = 0;
|
|
|
|
// Handle seeding if requested
|
|
if ($seed) {
|
|
$this->call('db:seed', ['--force' => $force]);
|
|
}
|
|
|
|
$migrationOutput = $bufferedOutput->fetch();
|
|
|
|
// Show the output from the migration command
|
|
$this->output->write($migrationOutput);
|
|
|
|
DB::statement('SET sql_mode = ?', [$originalSqlMode]);
|
|
|
|
// Check if migration failed
|
|
if ($exitCode !== 0) {
|
|
throw new \Exception("Migration command failed with exit code: $exitCode");
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Restore SQL mode
|
|
try {
|
|
DB::statement('SET sql_mode = ?', [$originalSqlMode]);
|
|
} catch (\Exception $sqlEx) {
|
|
// Ignore SQL mode restore errors
|
|
}
|
|
|
|
$this->error('');
|
|
$this->error('[ERROR] Migration failed!');
|
|
$this->error('Error: ' . $e->getMessage());
|
|
|
|
// If in development mode with snapshot, automatically rollback
|
|
if ($require_snapshot && file_exists($this->flag_file)) {
|
|
$this->error('');
|
|
$this->warn(' Automatically rolling back to snapshot due to migration failure...');
|
|
$this->info('');
|
|
|
|
// Call rollback command with output
|
|
$rollback_result = Artisan::call('migrate:rollback', [], $this->output);
|
|
|
|
if ($rollback_result === 0) {
|
|
$this->info('');
|
|
$this->info('[OK] Database rolled back successfully!');
|
|
$this->info('');
|
|
$this->warn('[WARNING] You are still in migration mode.');
|
|
$this->line('Your options:');
|
|
$this->line(' 1. Fix your migration files');
|
|
$this->line(' 2. Run "php artisan migrate" to try again');
|
|
$this->line(' 3. Run "php artisan migrate:commit" to exit migration mode');
|
|
} else {
|
|
$this->error('[ERROR] Automatic rollback failed!');
|
|
$this->error('Run "php artisan migrate:rollback" manually.');
|
|
$this->warn('[WARNING] The database may be in an inconsistent state!');
|
|
}
|
|
}
|
|
|
|
// Disable query logging before returning
|
|
AppServiceProvider::disable_query_echo();
|
|
|
|
// Disable SQL query transformation
|
|
SqlQueryTransformer::disable();
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Run normalize_schema AFTER migrations to add framework columns to new tables
|
|
$this->info("\n Post-migration normalization (adding framework columns to new tables)...\n");
|
|
|
|
// Switch to destructive-only query logging for normalize_schema
|
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT);
|
|
|
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
|
if ($normalizeExitCode !== 0) {
|
|
$this->error('Post-migration normalization failed');
|
|
return $normalizeExitCode;
|
|
}
|
|
|
|
// Run regenerate constants if not in production mode (framework-only still runs it)
|
|
if (!$is_production) {
|
|
// Disable query logging for regenerate_constants
|
|
AppServiceProvider::disable_query_echo();
|
|
|
|
$maintenanceExitCode = $this->runMaintenanceCommand('rsx:constants:regenerate');
|
|
if ($maintenanceExitCode !== 0) {
|
|
$this->error('Regenerate constants maintenance failed');
|
|
return $maintenanceExitCode;
|
|
}
|
|
} else {
|
|
// In production mode, check manifest consistency with database
|
|
// Disable query logging for consistency check
|
|
AppServiceProvider::disable_query_echo();
|
|
|
|
$this->info("\n");
|
|
$consistency_check_exit = $this->call('rsx:migrate:check_consistency');
|
|
if ($consistency_check_exit !== 0) {
|
|
$exitCode = 1;
|
|
}
|
|
}
|
|
|
|
$this->info("\nAll tasks completed!");
|
|
|
|
// Remind user to commit snapshot in development mode
|
|
if ($require_snapshot) {
|
|
$this->newLine();
|
|
$this->line('Please run <info>php artisan migrate:commit</info> to finish the migration process.');
|
|
}
|
|
|
|
// Disable query logging
|
|
AppServiceProvider::disable_query_echo();
|
|
|
|
// Disable SQL query transformation
|
|
SqlQueryTransformer::disable();
|
|
|
|
return $exitCode;
|
|
}
|
|
|
|
/**
|
|
* Register query transformation listener
|
|
*
|
|
* Intercepts all DB::statement() calls and transforms CREATE/ALTER statements
|
|
* to enforce framework column type conventions.
|
|
*/
|
|
protected function register_query_transformer(): void
|
|
{
|
|
// Store the original statement method
|
|
$original_statement = \Closure::bind(function () {
|
|
return $this->connection->statement(...func_get_args());
|
|
}, DB::getFacadeRoot(), DB::getFacadeRoot());
|
|
|
|
// Override DB::statement to transform queries
|
|
DB::macro('statement', function ($query, $bindings = []) use ($original_statement) {
|
|
// Transform the query before execution
|
|
$transformed = SqlQueryTransformer::transform($query);
|
|
|
|
// Call the original statement method with transformed query
|
|
return DB::connection()->statement($transformed, $bindings);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if all pending migrations are whitelisted
|
|
*
|
|
* @param array $paths Array of migration paths to check
|
|
*/
|
|
protected function checkMigrationWhitelist(array $paths): bool
|
|
{
|
|
// Load whitelists from both locations and merge them
|
|
$whitelistPaths = [
|
|
database_path('migrations/.migration_whitelist'), // Framework migrations
|
|
base_path('rsx/resource/migrations/.migration_whitelist'), // Application migrations
|
|
];
|
|
|
|
$whitelistedMigrations = [];
|
|
$foundAtLeastOne = false;
|
|
|
|
foreach ($whitelistPaths as $whitelistPath) {
|
|
if (file_exists($whitelistPath)) {
|
|
$foundAtLeastOne = true;
|
|
$whitelist = json_decode(file_get_contents($whitelistPath), true);
|
|
$migrations = array_keys($whitelist['migrations'] ?? []);
|
|
$whitelistedMigrations = array_merge($whitelistedMigrations, $migrations);
|
|
}
|
|
}
|
|
|
|
// If no whitelist exists yet, create one with existing migrations
|
|
if (!$foundAtLeastOne) {
|
|
$this->warn('[WARNING] No migration whitelist found. Creating one with existing migrations...');
|
|
$this->createInitialWhitelist();
|
|
return true;
|
|
}
|
|
|
|
// Get all migration files from specified paths
|
|
$migrationFiles = [];
|
|
foreach ($paths as $path) {
|
|
if (is_dir($path)) {
|
|
foreach (glob($path . '/*.php') as $file) {
|
|
$migrationFiles[] = basename($file);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for unauthorized migrations
|
|
$unauthorizedMigrations = array_diff($migrationFiles, $whitelistedMigrations);
|
|
|
|
if (!empty($unauthorizedMigrations)) {
|
|
$this->error('[ERROR] Unauthorized migrations detected!');
|
|
$this->error('');
|
|
$this->line('The following migrations were not created via php artisan make:migration:');
|
|
foreach ($unauthorizedMigrations as $migration) {
|
|
$this->line(' • ' . $migration);
|
|
}
|
|
$this->error('');
|
|
$this->warn('[WARNING] Manually created migrations can cause timestamp conflicts and ordering issues.');
|
|
$this->error('');
|
|
$this->line('To fix this:');
|
|
$this->line('1. Create a new migration using: php artisan make:migration [name]');
|
|
$this->line('2. Copy the migration code from the unauthorized file to the new file');
|
|
$this->line('3. Delete the unauthorized migration file');
|
|
$this->line('4. Run migrations again');
|
|
$this->error('');
|
|
$this->line('This ensures proper timestamp generation and prevents LLM timestamp confusion.');
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create initial whitelist with existing migrations
|
|
* Creates separate whitelist files for framework and application migrations
|
|
*/
|
|
protected function createInitialWhitelist(): void
|
|
{
|
|
$whitelistLocations = [
|
|
database_path('migrations/.migration_whitelist') => database_path('migrations'),
|
|
base_path('rsx/resource/migrations/.migration_whitelist') => base_path('rsx/resource/migrations'),
|
|
];
|
|
|
|
$totalMigrations = 0;
|
|
|
|
foreach ($whitelistLocations as $whitelistPath => $migrationPath) {
|
|
// Skip if migration directory doesn't exist
|
|
if (!is_dir($migrationPath)) {
|
|
continue;
|
|
}
|
|
|
|
$whitelist = [
|
|
'description' => 'This file tracks migrations created via php artisan make:migration',
|
|
'purpose' => 'Prevents manually created migrations from running to avoid timestamp conflicts',
|
|
'migrations' => [],
|
|
];
|
|
|
|
// Add all existing migrations from this specific path
|
|
foreach (glob($migrationPath . '/*.php') as $file) {
|
|
$filename = basename($file);
|
|
$whitelist['migrations'][$filename] = [
|
|
'created_at' => 'pre-whitelist',
|
|
'created_by' => 'system',
|
|
'command' => 'existing migration',
|
|
];
|
|
}
|
|
|
|
// Only create whitelist if there are migrations in this location
|
|
if (!empty($whitelist['migrations'])) {
|
|
file_put_contents($whitelistPath, json_encode($whitelist, JSON_PRETTY_PRINT));
|
|
$count = count($whitelist['migrations']);
|
|
$totalMigrations += $count;
|
|
$location = str_replace(base_path() . '/', '', dirname($whitelistPath));
|
|
$this->info("[OK] Created whitelist in {$location} with {$count} migration(s).");
|
|
}
|
|
}
|
|
|
|
if ($totalMigrations === 0) {
|
|
$this->info('[OK] No existing migrations found. Empty whitelists created.');
|
|
}
|
|
}
|
|
|
|
protected function runMaintenanceCommand($command, $arguments = [])
|
|
{
|
|
$this->info("\nRunning $command...");
|
|
$output = new BufferedOutput();
|
|
$exitCode = Artisan::call($command, $arguments, $output);
|
|
$commandOutput = $output->fetch();
|
|
|
|
// Only show concise output if successful
|
|
if ($exitCode === 0) {
|
|
$this->info("[OK] Command $command completed successfully.");
|
|
} else {
|
|
$this->error("[FAIL] Command $command failed with exit code $exitCode");
|
|
$this->output->write($commandOutput);
|
|
}
|
|
|
|
return $exitCode;
|
|
}
|
|
|
|
/**
|
|
* Validate migration files for Schema builder usage
|
|
* Only enforced in non-production mode
|
|
*/
|
|
protected function _validate_schema_rules(): bool
|
|
{
|
|
$repository = app('migration.repository');
|
|
$pending_migrations = MigrationValidator::get_pending_migrations($repository);
|
|
|
|
if (empty($pending_migrations)) {
|
|
// No pending migrations to validate
|
|
return true;
|
|
}
|
|
|
|
$this->info('Validating migration files for Schema builder usage...');
|
|
|
|
$has_violations = false;
|
|
|
|
foreach ($pending_migrations as $migration_path) {
|
|
try {
|
|
// Validate the migration file for Schema builder usage
|
|
MigrationValidator::validate_migration_file($migration_path);
|
|
$this->info(" [OK] " . basename($migration_path));
|
|
} catch (\RuntimeException $e) {
|
|
// MigrationValidator already printed colored error output
|
|
$has_violations = true;
|
|
break; // Stop at first violation
|
|
}
|
|
}
|
|
|
|
if ($has_violations) {
|
|
$this->error('');
|
|
$this->error('Migration validation failed!');
|
|
$this->info('');
|
|
$this->line('Please update your migration to use raw SQL instead of Schema builder.');
|
|
$this->line('See: php artisan rsx:man migrations');
|
|
return false;
|
|
}
|
|
|
|
// All validations passed - now remove down() methods
|
|
$processed_migrations = [];
|
|
foreach ($pending_migrations as $migration_path) {
|
|
if (MigrationValidator::remove_down_method($migration_path)) {
|
|
$processed_migrations[] = $migration_path;
|
|
}
|
|
}
|
|
|
|
// If we processed any migrations (removed down methods), notify user
|
|
if (!empty($processed_migrations)) {
|
|
$this->info('');
|
|
$this->warn('[WARNING] Modified migration files:');
|
|
foreach ($processed_migrations as $path) {
|
|
$this->line(' • ' . basename($path) . ' - down() method removed');
|
|
}
|
|
$this->info('');
|
|
$this->line('These migrations have been updated to follow framework standards.');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Ensure the migrations table exists in the database
|
|
* Creates it using raw SQL if it doesn't exist
|
|
*/
|
|
protected function ensure_migrations_table_exists(): void
|
|
{
|
|
try {
|
|
// Try to query the migrations table
|
|
$table = config('database.migrations', 'migrations');
|
|
DB::select("SELECT 1 FROM {$table} LIMIT 1");
|
|
} catch (\Exception $e) {
|
|
// Table doesn't exist, create it
|
|
$table = config('database.migrations', 'migrations');
|
|
$this->info('Creating migrations table...');
|
|
|
|
DB::statement("
|
|
CREATE TABLE IF NOT EXISTS {$table} (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
migration VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
batch BIGINT NOT NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
");
|
|
|
|
$this->info('[OK] Migrations table created');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run migrations one-by-one with normalization after each
|
|
*
|
|
* Runs pending migrations individually, normalizing schema after each successful
|
|
* migration. This ensures type consistency before subsequent migrations reference
|
|
* newly-created tables.
|
|
*
|
|
* @param \Illuminate\Database\Migrations\Migrator $migrator
|
|
* @param array $migrationPaths
|
|
* @param bool $step
|
|
* @param array $requiredColumnsArgs
|
|
* @return void
|
|
*/
|
|
protected function run_migrations_with_normalization($migrator, array $migrationPaths, bool $step, array $requiredColumnsArgs): void
|
|
{
|
|
// Get all migration files
|
|
$files = [];
|
|
foreach ($migrationPaths as $path) {
|
|
if (is_dir($path)) {
|
|
$path_files = glob($path . '/*.php');
|
|
if ($path_files) {
|
|
foreach ($path_files as $file) {
|
|
$files[] = $file;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort files chronologically by filename
|
|
sort($files);
|
|
|
|
// Get already-run migrations
|
|
$repository = $migrator->getRepository();
|
|
$ran = $repository->getRan();
|
|
|
|
// Filter to only pending migrations
|
|
$pending = [];
|
|
foreach ($files as $file) {
|
|
$migrationName = $migrator->getMigrationName($file);
|
|
if (!in_array($migrationName, $ran)) {
|
|
$pending[] = $file;
|
|
}
|
|
}
|
|
|
|
if (empty($pending)) {
|
|
$this->info(' INFO Nothing to migrate.');
|
|
return;
|
|
}
|
|
|
|
$totalMigrations = count($pending);
|
|
$currentMigration = 1;
|
|
|
|
// Run each migration individually with normalization after
|
|
foreach ($pending as $file) {
|
|
$migrationName = $migrator->getMigrationName($file);
|
|
|
|
$this->info("\n[$currentMigration/$totalMigrations] Running: $migrationName");
|
|
$this->newLine();
|
|
|
|
// Run this single migration
|
|
$migrator->runPending([$file], [
|
|
'pretend' => false,
|
|
'step' => $step
|
|
]);
|
|
|
|
// Run normalization after this migration (if not the last)
|
|
if ($currentMigration < $totalMigrations) {
|
|
$this->info("\n Normalizing schema after migration...\n");
|
|
|
|
// Switch to destructive-only query logging
|
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT);
|
|
|
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
|
|
|
// Restore full query logging
|
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_ALL_STDOUT);
|
|
|
|
if ($normalizeExitCode !== 0) {
|
|
throw new \Exception("Normalization failed after migration: $migrationName");
|
|
}
|
|
}
|
|
|
|
$currentMigration++;
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info("[OK] All $totalMigrations migration" . ($totalMigrations > 1 ? 's' : '') . " completed successfully");
|
|
}
|
|
}
|