output = function (): void { }; } /** * Run a migration */ public function run( string $path, Direction $direction = Direction::UP, bool $oneStep = false, bool $forceMigration = false ): void { $this->initMigration(); $this->lockTable($forceMigration); $migrations = $this->mergeMigrations( $this->getMigrations($path), $this->getMigrated() ); if ($direction === Direction::DOWN) { $migrations = $migrations->reverse(); } try { foreach ($migrations as $migration) { /** @var array $migration */ $name = $migration['migration']; if ( ($direction === Direction::UP && isset($migration['id'])) || ($direction === Direction::DOWN && !isset($migration['id'])) ) { ($this->output)('Skipping ' . $name); continue; } ($this->output)('Migrating ' . $name . ' (' . $direction->value . ')'); if (isset($migration['path'])) { $this->migrate($migration['path'], $name, $direction); } $this->setMigrated($name, $direction); if ($oneStep) { break; } } } catch (Throwable $e) { $this->unlockTable(); throw $e; } $this->unlockTable(); } /** * Setup migration tables */ public function initMigration(): void { if ($this->schema->hasTable($this->table)) { return; } $this->schema->create($this->table, function (Blueprint $table): void { $table->increments('id'); $table->string('migration'); }); } /** * Merge file migrations with already migrated tables */ protected function mergeMigrations(Collection $migrations, Collection $migrated): Collection { $return = $migrated; $return->transform(function ($migration) use ($migrations) { $migration = (array) $migration; if ($migrations->contains('migration', $migration['migration'])) { $migration += $migrations ->where('migration', $migration['migration']) ->first(); } return $migration; }); $migrations->each(function ($migration) use ($return): void { if ($return->contains('migration', $migration['migration'])) { return; } $return->add($migration); }); return $return; } /** * Get all migrated migrations */ protected function getMigrated(): Collection { return $this->getTableQuery() ->orderBy('id') ->where('migration', '!=', 'lock') ->get(); } /** * Migrate a migration */ protected function migrate(string $file, string $migration, Direction $direction = Direction::UP): void { require_once $file; $className = Str::studly(preg_replace('/\d+_/', '', $migration)); /** @var Migration $class */ $class = $this->app->make('Engelsystem\\Migrations\\' . $className); if (method_exists($class, $direction->value)) { $class->{$direction->value}(); } } /** * Set a migration to migrated */ protected function setMigrated(string $migration, Direction $direction = Direction::UP): void { $table = $this->getTableQuery(); if ($direction === Direction::DOWN) { $table->where(['migration' => $migration])->delete(); return; } $table->insert(['migration' => $migration]); } /** * Lock the migrations table * * * @throws Throwable */ protected function lockTable(bool $forceMigration = false): void { $this->schema->getConnection()->transaction(function () use ($forceMigration): void { $lock = $this->getTableQuery() ->where('migration', 'lock') ->lockForUpdate() ->first(); if ($lock && !$forceMigration) { throw new Exception('Unable to acquire migration table lock'); } $this->getTableQuery() ->insert(['migration' => 'lock']); }); } /** * Unlock a previously locked table */ protected function unlockTable(): void { $this->getTableQuery() ->where('migration', 'lock') ->delete(); } /** * Get a list of migration files */ protected function getMigrations(string $dir): Collection { $files = $this->getMigrationFiles($dir); $migrations = new Collection(); foreach ($files as $dir) { $name = str_replace('.php', '', basename($dir)); $migrations[] = [ 'migration' => $name, 'path' => $dir, ]; } return $migrations->sortBy(function ($value) { return $value['migration']; }); } /** * List all migration files from the given directory */ protected function getMigrationFiles(string $dir): array { return glob($dir . '/*_*.php'); } /** * Init a table query */ protected function getTableQuery(): Builder { return $this->schema->getConnection()->table($this->table); } /** * Set the output function */ public function setOutput(callable $output): void { $this->output = $output; } }