diff --git a/.travis.yml b/.travis.yml index 6fb11222..c6dc30a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: php php: - 7.0 - 7.1 - - hhvm before_install: - composer self-update diff --git a/source/Spiral/Migrations/Exceptions/Operations/ReferenceException.php b/source/Spiral/Migrations/Exceptions/Operations/ReferenceException.php new file mode 100644 index 00000000..7e704251 --- /dev/null +++ b/source/Spiral/Migrations/Exceptions/Operations/ReferenceException.php @@ -0,0 +1,16 @@ +config = $config; + $this->dbal = $dbal; + $this->repository = $repository; + } + + /** + * {@inheritdoc} + */ + public function isConfigured(): bool + { + return $this->stateTable()->exists(); + } + + /** + * {@inheritdoc} + */ + public function configure() + { + if ($this->isConfigured()) { + return; + } + + //Migrations table is pretty simple. + $schema = $this->stateTable()->getSchema(); + + /* + * Schema update will automatically sync all needed data + */ + $schema->primary('id'); + $schema->string('migration', 255)->nullable(false); + $schema->datetime('time_executed')->datetime(); + $schema->index(['migration']); + + $schema->save(); + } + + /** + * @return RepositoryInterface + */ + public function getRepository(): RepositoryInterface + { + return $this->repository; + } + + /** + * Get every available migration with valid meta information. + * + * @return MigrationInterface[] + */ + public function getMigrations(): array + { + $result = []; + foreach ($this->repository->getMigrations() as $migration) { + //Populating migration status and execution time (if any) + $result[] = $migration->withState($this->resolveStatus($migration->getState())); + } + + return $result; + } + + /** + * Execute one migration and return it's instance. + * + * @param CapsuleInterface $capsule Default capsule to be used if none given. + * + * @return MigrationInterface|null + */ + public function run(CapsuleInterface $capsule = null) + { + $capsule = $capsule ?? new MigrationCapsule($this->dbal); + + if (!$this->isConfigured()) { + throw new MigrationException("Unable to run migration, Migrator not configured"); + } + + /** + * @var MigrationInterface $migration + */ + foreach ($this->getMigrations() as $migration) { + if ($migration->getState()->getStatus() != State::STATUS_PENDING) { + continue; + } + + //Isolate migration commands in a capsule + $migration = $migration->withCapsule($capsule); + + //Executing migration inside global transaction + $this->execute(function () use ($migration) { + $migration->up(); + }); + + //Registering record in database + $this->stateTable()->insertOne([ + 'migration' => $migration->getState()->getName(), + 'time_executed' => new \DateTime('now') + ]); + + //Update migration state + return $migration->withState( + $this->resolveStatus($migration->getState()) + ); + } + + return null; + } + + /** + * Rollback last migration and return it's instance. + * + * @param CapsuleInterface $capsule Default capsule to be used if none given. + * + * @return MigrationInterface|null + */ + public function rollback(CapsuleInterface $capsule = null) + { + $capsule = $capsule ?? new MigrationCapsule($this->dbal); + + if (!$this->isConfigured()) { + throw new MigrationException("Unable to run migration, Migrator not configured"); + } + + /** + * @var MigrationInterface $migration + */ + foreach (array_reverse($this->getMigrations()) as $migration) { + if ($migration->getState()->getStatus() != State::STATUS_EXECUTED) { + continue; + } + + //Isolate migration commands in a capsule + $migration = $migration->withCapsule($capsule); + + //Executing migration inside global transaction + $this->execute(function () use ($migration) { + $migration->down(); + }); + + //Flushing DB record + $this->stateTable()->delete([ + 'migration' => $migration->getState()->getName() + ])->run(); + + //Update migration state + return $migration->withState( + $this->resolveStatus($migration->getState()) + ); + } + + return null; + } + + /** + * Migration table, all migration information will be stored in it. + * + * @return Table + */ + protected function stateTable(): Table + { + return $this->dbal->database( + $this->config->getDatabase() + )->table( + $this->config->getTable() + ); + } + + /** + * Clarify migration state with valid status and execution time + * + * @param State $initialState + * + * @return State + */ + protected function resolveStatus(State $initialState) + { + //Fetch migration information from database + $state = $this->stateTable() + ->select('id', 'time_executed') + ->where(['migration' => $initialState->getName()]) + ->run() + ->fetch(); + + if (empty($state['time_executed'])) { + return $initialState->withStatus(State::STATUS_PENDING); + } + + return $initialState->withStatus( + State::STATUS_EXECUTED, + new \DateTime( + $state['time_executed'], + $this->stateTable()->getDatabase()->getDriver()->getTimezone() + ) + ); + } + + /** + * Run given code under transaction open for every driver. + * + * @param \Closure $closure + * + * @throws \Throwable + */ + protected function execute(\Closure $closure) + { + $this->beginTransactions(); + try { + call_user_func($closure); + } catch (\Throwable $e) { + $this->rollbackTransactions(); + throw $e; + } + + $this->commitTransactions(); + } + + /** + * Begin transaction for every available driver (we don't know what database migration related + * to). + */ + protected function beginTransactions() + { + foreach ($this->getDrivers() as $driver) { + $driver->beginTransaction(); + } + } + + /** + * Rollback transaction for every available driver. + */ + protected function rollbackTransactions() + { + foreach ($this->getDrivers() as $driver) { + $driver->rollbackTransaction(); + } + } + + /** + * Commit transaction for every available driver. + */ + protected function commitTransactions() + { + foreach ($this->getDrivers() as $driver) { + $driver->commitTransaction(); + } + } + + /** + * Get all available drivers. + * + * @return Driver[] + */ + protected function getDrivers(): array + { + $drivers = []; + foreach ($this->dbal->getDatabases() as $database) { + $driver = $database->getDriver(); + if (!isset($drivers["{$driver->getName()}.{$driver->getSource()}"])) { + $drivers["{$driver->getName()}.{$driver->getSource()}"] = $database->getDriver(); + } + } + + return $drivers; + } +} \ No newline at end of file