io = $io;
$this->ci = $ci;
// Start by testing the DB connexion, just in case
try {
$this->io->writeln("Testing database connection", OutputInterface::VERBOSITY_VERBOSE);
$this->testDB();
$this->io->writeln("Ok", OutputInterface::VERBOSITY_VERBOSE);
} catch (\Exception $e) {
$this->io->error($e->getMessage());
exit(1);
}
// Get schema required to run the table blueprints
$this->schema = Capsule::schema();
// Make sure the setup table exist
$this->setupVersionTable();
}
/**
* Run all the migrations available
*
* @access public
* @param bool $pretend (default: false)
* @return void
*/
public function runUp($pretend = FALSE) {
// Get installed migrations and pluck by class name. We only need this for now
$migrations = Migrations::get();
$this->installed = $migrations->pluck('migration');
$this->io->writeln("\nInstalled migrations:", OutputInterface::VERBOSITY_VERBOSE);
$this->io->writeln($this->installed->toArray(), OutputInterface::VERBOSITY_VERBOSE);
// Get pending migrations
$this->io->section("Fetching available migrations");
$this->pending = $this->getPendingMigrations();
// If there's no pending migration, don't need to go further
if ($this->pending->isEmpty()) {
$this->io->success("Nothing to migrate !");
return;
}
// Resolve the dependencies
$this->resolveDependencies();
// If there are any unfulfillable migration, we can't continue
if (!$this->unfulfillable->isEmpty()) {
$msg = "\nSome migrations dependencies can't be met. Check those migrations for unmet dependencies and try again:";
foreach ($this->unfulfillable as $migration) {
$msg .= "\n{$migration->className} depends on \n - ";
$msg .= implode("\n - ", $migration->dependencies);
$msg .= "\n";
}
$this->io->error($msg);
exit(1);
}
// Ready to run !
$this->io->section("Running migrations");
if ($pretend) {
$this->io->note("Running migration in pretend mode");
}
// We have a list of fulfillable migration, we run them up!
foreach ($this->fulfillable as $migration) {
$this->io->write("\n> Migrating {$migration->className}...");
if ($pretend) {
$this->io->newLine();
$this->pretendToRun($migration, 'up');
} else {
$migration->up();
$migration->seed();
$this->log($migration);
$this->io->writeln(" Done!");
}
}
// If all went well and there's no fatal errors, we are ready to bake
$this->io->success("Migration successful !");
}
/**
* Rollback the last btach of migrations.
*
* @access public
* @param int $step (default: 1). Number of batch we will be going back. -1 revert all migrations
* @param string $sprinkle (default: "") Limit rollback to a specific sprinkle
* @param bool $pretend (default: false)
* @return void
*/
public function runDown($step = 1, $sprinkle = "", $pretend = FALSE) {
// Can't go furhter down than 1 step
if ($step <= 0 && $step != -1) {
throw new \InvalidArgumentException("Step can't be 0 or less");
}
// Get last batch number
$batch = $this->getNextBatchNumber();
// Calculate the number of steps back we need to take
if ($step == -1) {
$stepsBack = 1;
$this->io->warning("Rolling back all migrations");
} else {
$stepsBack = max($batch - $step, 1);
$this->io->note("Rolling back $step steps to batch $stepsBack", OutputInterface::VERBOSITY_VERBOSE);
}
// Get installed migrations
$migrations = Migrations::orderBy("id", "desc")->where('batch', '>=', $stepsBack);
// Add the sprinkle requirement too
if ($sprinkle != "") {
$this->io->note("Rolling back sprinkle `$sprinkle`", OutputInterface::VERBOSITY_VERBOSE);
$migrations->where('sprinkle', $sprinkle);
}
// Run query
$migrations = $migrations->get();
// If there's nothing to rollback, stop here
if ($migrations->isEmpty()) {
$this->io->writeln("Nothing to rollback");
exit(1);
}
// Get pending migrations
$this->io->writeln("Migration to rollback:");
$this->io->listing($migrations->pluck('migration')->toArray());
// Ask confirmation to continue.
if (!$pretend && !$this->io->confirm("Continue?", FALSE)) {
exit(1);
}
if ($pretend) {
$this->io->note("Rolling back in pretend mode");
}
// Loop again to run down each migration
foreach ($migrations as $migration) {
// Check if those migration class are available
if (!class_exists($migration->migration)) {
$this->io->warning("Migration class {$migration->migration} doesn't exist.");
continue;
}
$this->io->write("> Rolling back {$migration->migration}...");
$migrationClass = $migration->migration;
$instance = new $migrationClass($this->schema, $this->io);
if ($pretend) {
$this->io->newLine();
$this->pretendToRun($instance, 'down');
} else {
$instance->down();
$migration->delete();
$this->io->writeln(" Done!");
}
$this->io->newLine();
}
// If all went well and there's no fatal errors, we are ready to bake
$this->io->success("Rollback successful !");
}
/**
* Pretend to run migration class.
*
* @access protected
* @param mixed $migration
* @param string $method up/down
*/
protected function pretendToRun($migration, $method) {
foreach ($this->getQueries($migration, $method) as $query) {
$this->io->writeln($query['query'], OutputInterface::VERBOSITY_VERBOSE);
}
}
/**
* Return all of the queries that would be run for a migration.
*
* @access protected
* @param mixed $migration
* @param string $method up/down
* @return void
*/
protected function getQueries($migration, $method) {
$db = $this->schema->getConnection();
return $db->pretend(function () use ($migration, $method) {
if (method_exists($migration, $method)) {
$migration->{$method}();
}
});
}
/**
* Get pending migrations by looking at all the migration files
* and finding the one not yet runed by compairing with the ran migrations
*
* @access protected
* @return void
*/
protected function getPendingMigrations() {
$pending = collect([]);
// Get the sprinkle list
$sprinkles = $this->ci->sprinkleManager->getSprinkleNames();
// Loop all the sprinkles to find their pending migrations
foreach ($sprinkles as $sprinkle) {
$this->io->writeln("> Fetching from `$sprinkle`");
// We get all the migrations. This will return them as a colleciton of class names
$migrations = $this->getMigrations($sprinkle);
// We filter the available migration by removing the one that have already been run
// This reject the class name found in the installed collection
$migrations = $migrations->reject(function ($value, $key) {
return $this->installed->contains($value);
});
// Load each class
foreach ($migrations as $migrationClass) {
// Make sure the class exist
if (!class_exists($migrationClass)) {
throw new BadClassNameException("Unable to find the migration class '$migrationClass'.");
}
// Load the migration class
$migration = new $migrationClass($this->schema, $this->io);
//Set the sprinkle
$migration->sprinkle = $sprinkle;
// Also set the class name. We could find it using ::class, but this
// will make it easier to manipulate the collection
$migration->className = $migrationClass;
// Add it to the pending list
$pending->push($migration);
}
}
// Get pending migrations
$pendingArray = ($pending->pluck('className')->toArray()) ?: "";
$this->io->writeln("\nPending migrations:", OutputInterface::VERBOSITY_VERBOSE);
$this->io->writeln($pendingArray, OutputInterface::VERBOSITY_VERBOSE);
return $pending;
}
/**
* Get the list of migrations avaiables in the filesystem.
* Return a list of resolved className
*
* @access public
* @param string $sprinkleName
* @return void
*/
public function getMigrations($sprinkle) {
// Find all the migration files
$path = $this->migrationDirectoryPath($sprinkle);
$files = glob($path . "*/*.php");
// Transform the array in a collection
$migrations = collect($files);
// We transform the path into a migration object
$migrations->transform(function ($file) use ($sprinkle, $path) {
// Deconstruct the path
$migration = str_replace($path, "", $file);
$className = basename($file, '.php');
$sprinkleName = Str::studly($sprinkle);
$version = str_replace("/$className.php", "", $migration);
// Reconstruct the classname
$className = "\\UserFrosting\\Sprinkle\\" . $sprinkleName . "\\Database\\Migrations\\" . $version . "\\" . $className;
return $className;
});
return $migrations;
}
/**
* Resolve all the dependencies for all the pending migrations
* This function fills in the `fullfillable` and `unfulfillable` list
*
* @access protected
* @return void
*/
protected function resolveDependencies() {
$this->io->writeln("\nResolving migrations dependencies...", OutputInterface::VERBOSITY_VERBOSE);
// Reset fulfillable/unfulfillable lists
$this->fulfillable = collect([]);
$this->unfulfillable = collect([]);
// Loop pending and check for dependencies
foreach ($this->pending as $migration) {
$this->validateClassDependencies($migration);
}
$fulfillable = ($this->fulfillable->pluck('className')->toArray()) ?: "";
$this->io->writeln("\nFulfillable migrations:", OutputInterface::VERBOSITY_VERBOSE);
$this->io->writeln($fulfillable, OutputInterface::VERBOSITY_VERBOSE);
$unfulfillable = ($this->unfulfillable->pluck('className')->toArray()) ?: "";
$this->io->writeln("\nUnfulfillable migrations:", OutputInterface::VERBOSITY_VERBOSE);
$this->io->writeln($unfulfillable, OutputInterface::VERBOSITY_VERBOSE);
}
/**
* Check if a migration dependencies are met.
* To test if a migration is fulfillable, the class must :
* Already been installed OR exist and have all it's dependencies met
*
* @access protected
* @param $migration
* @return bool true/false if all conditions are met
*/
protected function validateClassDependencies($migration) {
$this->io->writeln("> Checking dependencies for {$migration->className}", OutputInterface::VERBOSITY_VERBOSE);
// If it's already marked as fulfillable, it's fulfillable
// Return true directly (it's already marked)
if ($this->fulfillable->contains($migration)) {
return TRUE;
}
// If it's already marked as unfulfillable, it's unfulfillable
// Return false directly (it's already marked)
if ($this->unfulfillable->contains($migration)) {
return FALSE;
}
// If it's already run, it's fulfillable
// Mark it as such for next time it comes up in this loop
if ($this->installed->contains($migration->className)) {
return $this->markAsFulfillable($migration);
}
// Loop dependencies. If one is not fulfillable, then this one is not either
foreach ($migration->dependencies as $dependencyClass) {
// The dependency might already be installed. Check that first
if ($this->installed->contains($dependencyClass)) {
continue;
}
// Try to find it in the `pending` list. Cant' find it? Then it's not fulfillable
$dependency = $this->pending->where('className', $dependencyClass)->first();
// Check migration dependencies of this one right now
// If ti's not fullfillable, then this one isn't either
if (!$dependency || !$this->validateClassDependencies($dependency)) {
return $this->markAsUnfulfillable($migration);
}
}
// If no dependencies returned false, it's fulfillable
return $this->markAsFulfillable($migration);
}
/**
* Mark a dependency as fulfillable.
* Removes it from the pending list and add it to the fulfillable list
*
* @access protected
* @param $migration
* @return true
*/
protected function markAsFulfillable($migration) {
$this->fulfillable->push($migration);
return TRUE;
}
/**
* Mark a dependency as unfulfillable.
* Removes it from the pending list and add it to the unfulfillable list
*
* @access protected
* @param $migration
* @return false
*/
protected function markAsUnfulfillable($migration) {
$this->unfulfillable->push($migration);
return FALSE;
}
/**
* Log that a migration was run.
*
* @access public
* @param mixed $migration
* @return void
*/
protected function log($migration) {
// Get the next batch number if not defined
if (!$this->batch) {
$this->batch = $this->getNextBatchNumber();
}
$log = new Migrations([
'sprinkle' => $migration->sprinkle,
'migration' => $migration->className,
'batch' => $this->batch
]);
$log->save();
}
/**
* Return the next batch number from the db.
* Batch number is used to group together migration run in the same operation
*
* @access public
* @return int the next batch number
*/
public function getNextBatchNumber() {
$batch = Migrations::max('batch');
return ($batch) ? $batch + 1 : 1;
}
/**
* Create the migration history table if needed.
* Also check if the tables requires migrations
* We run the migration file manually for this one
*
* @access public
* @return void
*/
protected function setupVersionTable() {
// Check if the `migrations` table exist. Create it manually otherwise
if (!$this->schema->hasColumn($this->table, 'id')) {
$this->io->section("Creating the `{$this->table}` table");
$migration = new \UserFrosting\System\Database\Migrations\v410\MigrationTable($this->schema, $this->io);
$migration->up();
$this->io->success("Table `{$this->table}` created");
}
}
/**
* Returns the path of the Migration directory.
*
* @access protected
* @param mixed $sprinkleName
* @return void
*/
protected function migrationDirectoryPath($sprinkleName) {
$path = \UserFrosting\SPRINKLES_DIR .
\UserFrosting\DS .
$sprinkleName .
\UserFrosting\DS .
\UserFrosting\SRC_DIR_NAME .
"/Database/Migrations/";
return $path;
}
}