File: /home/accemeff/vendor/craftcms/cms/src/db/MigrationManager.php
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/
namespace craft\db;
use Craft;
use craft\errors\MigrationException;
use craft\helpers\App;
use craft\helpers\Db;
use craft\helpers\FileHelper;
use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\db\MigrationInterface;
use yii\di\Instance;
/**
* MigrationManager manages a set of migrations.
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.0
*/
class MigrationManager extends Component
{
/**
* The name of the dummy migration that marks the beginning of the whole migration history.
*/
const BASE_MIGRATION = 'm000000_000000_base';
const TYPE_APP = 'app';
const TYPE_PLUGIN = 'plugin';
const TYPE_CONTENT = 'content';
// Properties
// =========================================================================
/**
* @var string|null The type of migrations we're dealing with here. Can be 'app', 'plugin', or 'content'.
*/
public $type;
/**
* @var int|null The plugin ID, if [[type]] is set to 'plugin'.
*/
public $pluginId;
/**
* @var string|null The namespace that the migration classes are in
*/
public $migrationNamespace;
/**
* @var string|null The path to the migrations directory
*/
public $migrationPath;
/**
* @var Connection|array|string The DB connection object or the application component ID of the DB connection
*/
public $db = 'db';
/**
* @var string The migrations table name
*/
public $migrationTable = Table::MIGRATIONS;
// Public Methods
// =========================================================================
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if ($this->migrationPath === null) {
throw new InvalidConfigException('The migration folder path has not been set.');
}
if (!in_array($this->type, [self::TYPE_APP, self::TYPE_PLUGIN, self::TYPE_CONTENT], true)) {
throw new InvalidConfigException('Invalid migration type: ' . $this->type);
}
if ($this->type == self::TYPE_PLUGIN && $this->pluginId === null) {
throw new InvalidConfigException('The plugin ID has not been set.');
}
if ($this->migrationPath === null) {
throw new InvalidConfigException('The migration path has not been set.');
}
$this->migrationPath = FileHelper::normalizePath(Craft::getAlias($this->migrationPath));
$this->db = Instance::ensure($this->db, Connection::class);
}
/**
* Creates a new migration instance.
*
* @param string $name The migration name
* @return MigrationInterface|\yii\db\Migration The migration instance
* @throws Exception if the migration folder doesn't exist
*/
public function createMigration(string $name)
{
if (!is_dir($this->migrationPath)) {
throw new Exception("Can't instantiate migrations because the migration folder doesn't exist");
}
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php';
$class = $this->migrationNamespace . '\\' . $name;
require_once $file;
return new $class;
}
/**
* Upgrades the application by applying new migrations.
*
* @param int $limit The number of new migrations to be applied. If 0, it means
* applying all available new migrations.
* @throws MigrationException on migrate failure
*/
public function up(int $limit = 0)
{
// This might take a while
App::maxPowerCaptain();
$migrationNames = $this->getNewMigrations();
if (empty($migrationNames)) {
Craft::info('No new migration found. Your system is up-to-date.', __METHOD__);
return;
}
$total = count($migrationNames);
if ($limit !== 0) {
$migrationNames = array_slice($migrationNames, 0, $limit);
}
$n = count($migrationNames);
if ($n === $total) {
$logMessage = "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . ' to be applied:';
} else {
$logMessage = "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . ' to be applied:';
}
foreach ($migrationNames as $migrationName) {
$logMessage .= "\n\t$migrationName";
}
Craft::info($logMessage, __METHOD__);
foreach ($migrationNames as $migrationName) {
try {
$this->migrateUp($migrationName);
} catch (MigrationException $e) {
Craft::error('Migration failed. The rest of the migrations are cancelled.', __METHOD__);
throw $e;
}
}
Craft::info('Migrated up successfully.', __METHOD__);
}
/**
* Downgrades the application by reverting old migrations.
*
* @param int $limit The number of migrations to be reverted. Defaults to 1,
* meaning the last applied migration will be reverted. If set to 0, all migrations will be reverted.
* @throws MigrationException on migrate failure
*/
public function down(int $limit = 1)
{
// This might take a while
App::maxPowerCaptain();
$migrationNames = array_keys($this->getMigrationHistory($limit));
if (empty($migrationNames)) {
Craft::info('No migration has been done before.', __METHOD__);
return;
}
$n = count($migrationNames);
$logMessage = "Total $n " . ($n === 1 ? 'migration' : 'migrations') . ' to be reverted:';
foreach ($migrationNames as $migrationName) {
$logMessage .= "\n\t$migrationName";
}
Craft::info($logMessage, __METHOD__);
foreach ($migrationNames as $migrationName) {
try {
$this->migrateDown($migrationName);
} catch (MigrationException $e) {
Craft::error('Migration failed. The rest of the migrations are cancelled.', __METHOD__);
throw $e;
}
}
Craft::info('Migrated down successfully.', __METHOD__);
}
/**
* Upgrades with the specified migration.
*
* @param string|MigrationInterface|\yii\db\Migration $migration The name of the migration to apply, or the migration itself
* @throws InvalidConfigException if $migration is invalid
* @throws MigrationException on migrate failure
*/
public function migrateUp($migration)
{
list($migrationName, $migration) = $this->_normalizeMigration($migration);
if ($migrationName === self::BASE_MIGRATION) {
return;
}
/** @var \yii\db\Migration $migration */
/** @noinspection CallableParameterUseCaseInTypeContextInspection */
$migration = Instance::ensure($migration, MigrationInterface::class);
Craft::info("Applying $migrationName", __METHOD__);
$isConsoleRequest = Craft::$app->getRequest()->getIsConsoleRequest();
if (!$isConsoleRequest) {
ob_start();
}
$start = microtime(true);
try {
if ($migration instanceof Migration) {
$success = ($migration->up(true) !== false);
} else {
$success = ($migration->up() !== false);
}
} catch (\Throwable $e) {
$success = false;
}
$time = microtime(true) - $start;
$log = ($success ? 'Applied ' : 'Failed to apply ') . $migrationName . ' (time: ' . sprintf('%.3f', $time) . 's).';
if (!$isConsoleRequest) {
$output = ob_get_clean();
$log .= " Output:\n" . $output;
}
if (!$success) {
Craft::error($log, __METHOD__);
throw new MigrationException($migration, $output ?? null, null, 0, $e ?? null);
}
Craft::info($log, __METHOD__);
$this->addMigrationHistory($migrationName);
}
/**
* Downgrades with the specified migration.
*
* @param string|MigrationInterface|\yii\db\Migration $migration The name of the migration to revert, or the migration itself
* @throws InvalidConfigException if $migration is invalid
* @throws MigrationException on migrate failure
*/
public function migrateDown($migration)
{
list($migrationName, $migration) = $this->_normalizeMigration($migration);
if ($migrationName === self::BASE_MIGRATION) {
return;
}
/** @var \yii\db\Migration $migration */
/** @noinspection CallableParameterUseCaseInTypeContextInspection */
$migration = Instance::ensure($migration, MigrationInterface::class);
Craft::info("Reverting $migrationName", __METHOD__);
$isConsoleRequest = Craft::$app->getRequest()->getIsConsoleRequest();
if (!$isConsoleRequest) {
ob_start();
}
$start = microtime(true);
try {
if ($migration instanceof Migration) {
$success = ($migration->down(true) !== false);
} else {
$success = ($migration->down() !== false);
}
} catch (\Throwable $e) {
$success = false;
}
$time = microtime(true) - $start;
$log = ($success ? 'Reverted ' : 'Failed to revert ') . $migrationName . ' (time: ' . sprintf('%.3f', $time) . 's).';
if (!$isConsoleRequest) {
$output = ob_get_clean();
$log .= " Output:\n" . $output;
}
if (!$success) {
Craft::error($log, __METHOD__);
throw new MigrationException($migration, $output ?? null, null, 0, $e ?? null);
}
Craft::info($log, __METHOD__);
$this->removeMigrationHistory($migrationName);
}
/**
* Returns the migration history.
*
* @param int $limit The maximum number of records in the history to be returned. `null` for "no limit".
* @return array The migration history
*/
public function getMigrationHistory(int $limit = 0): array
{
$query = $this->_createMigrationQuery();
if ($limit !== 0) {
$query->limit($limit);
}
$history = $query->pairs($this->db);
unset($history[self::BASE_MIGRATION]);
return $history;
}
/**
* Adds a new migration entry to the history.
*
* @param string $name The migration name
*/
public function addMigrationHistory(string $name)
{
Craft::$app->getDb()->createCommand()
->insert(
$this->migrationTable,
[
'type' => $this->type,
'pluginId' => $this->pluginId,
'name' => $name,
'applyTime' => Db::prepareDateForDb(new \DateTime())
])
->execute();
}
/**
* Removes an existing migration from the history.
*
* @param string $name The migration name
*/
public function removeMigrationHistory(string $name)
{
Craft::$app->getDb()->createCommand()
->delete(
$this->migrationTable,
[
'type' => $this->type,
'pluginId' => $this->pluginId,
'name' => $name
])
->execute();
}
/**
* Truncates the migration history.
*/
public function truncateHistory()
{
Craft::$app->getDb()->createCommand()
->delete(
$this->migrationTable,
[
'type' => $this->type,
'pluginId' => $this->pluginId,
])
->execute();
}
/**
* Returns whether a given migration has been applied.
*
* @param string $name The migration name
* @return bool Whether the migration has been applied
*/
public function hasRun(string $name): bool
{
return $this->_createMigrationQuery()
->andWhere(['name' => $name])
->exists($this->db);
}
/**
* Returns the migrations that are not applied.
*
* @return array The list of new migrations
*/
public function getNewMigrations(): array
{
$migrations = [];
// Ignore if the migrations folder doesn't exist
if (!is_dir($this->migrationPath)) {
return $migrations;
}
$history = $this->getMigrationHistory();
$handle = opendir($this->migrationPath);
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $this->migrationPath . DIRECTORY_SEPARATOR . $file;
if (preg_match('/^(m\d{6}_\d{6}_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($history[$matches[1]])) {
$migrations[] = $matches[1];
}
}
closedir($handle);
sort($migrations);
return $migrations;
}
// Private Methods
// =========================================================================
/**
* Normalizes the $migration argument passed to [[migrateUp()]] and [[migrateDown()]].
*
* @param string|MigrationInterface|\yii\db\Migration $migration The name of the migration to apply, or the migration itself
* @return array
*/
private function _normalizeMigration($migration): array
{
if (is_string($migration)) {
$migrationName = $migration;
$migration = $this->createMigration($migration);
} else {
$classParts = explode('\\', get_class($migration));
$migrationName = array_pop($classParts);
}
return [$migrationName, $migration];
}
/**
* Returns a Query object prepped for retrieving migrations.
*
* @return Query The query
*/
private function _createMigrationQuery(): Query
{
// TODO: Remove after next breakpoint
if (
version_compare(Craft::$app->getInfo()->version, '3.0', '<') &&
Craft::$app->getDb()->columnExists($this->migrationTable, 'version', true)
) {
$query = (new Query())
->select(['version as name', 'applyTime'])
->from([$this->migrationTable])
->orderBy(['name' => SORT_DESC]);
if ($this->type === self::TYPE_PLUGIN) {
$query->where(['pluginId' => $this->pluginId]);
} else {
$query->where(['pluginId' => null]);
}
return $query;
}
$query = (new Query())
->select(['name', 'applyTime'])
->from([$this->migrationTable])
->orderBy(['name' => SORT_DESC])
->where(['type' => $this->type]);
if ($this->type === self::TYPE_PLUGIN) {
$query->andWhere(['pluginId' => $this->pluginId]);
}
return $query;
}
}