387 lines
14 KiB
PHP
387 lines
14 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* This program is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public License
|
||
|
* as published by the Free Software Foundation; under version 2
|
||
|
* of the License (non-upgradable).
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program; if not, write to the Free Software
|
||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
*
|
||
|
* Copyright (c) 2020 (original work) Open Assessment Technologies SA
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
namespace oat\tao\scripts\tools;
|
||
|
|
||
|
use Doctrine\DBAL\Connection;
|
||
|
use Doctrine\Migrations\Configuration\Configuration;
|
||
|
use Doctrine\Migrations\Configuration\Connection\ExistingConnection;
|
||
|
use Doctrine\Migrations\Configuration\Migration\ExistingConfiguration;
|
||
|
use Doctrine\Migrations\DependencyFactory;
|
||
|
use Doctrine\Migrations\Generator\ClassNameGenerator;
|
||
|
use Doctrine\Migrations\Exception\MigrationException;
|
||
|
use Doctrine\Migrations\Exception\NoMigrationsToExecute;
|
||
|
use Doctrine\Migrations\Tools\Console\Command\MigrateCommand;
|
||
|
use Doctrine\Migrations\Tools\Console\Command\StatusCommand;
|
||
|
use Doctrine\Migrations\Tools\Console\Command\ExecuteCommand;
|
||
|
use Doctrine\Migrations\Tools\Console\Command\VersionCommand;
|
||
|
use Doctrine\Migrations\Tools\Console\Command\SyncMetadataCommand;
|
||
|
use Doctrine\Migrations\Version\Comparator;
|
||
|
use oat\oatbox\service\exception\InvalidServiceManagerException;
|
||
|
use Symfony\Component\Console\Application;
|
||
|
use Symfony\Component\Console\Input\ArrayInput;
|
||
|
use Symfony\Component\Console\Input\InputInterface;
|
||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||
|
use oat\oatbox\extension\script\ScriptAction;
|
||
|
use oat\oatbox\extension\script\ScriptException;
|
||
|
use oat\tao\model\migrations\MigrationsService;
|
||
|
use oat\tao\scripts\tools\migrations\TaoClassNameGenerator;
|
||
|
use oat\tao\scripts\tools\migrations\commands\GenerateCommand;
|
||
|
use oat\tao\scripts\tools\migrations\TaoComparator;
|
||
|
use common_ext_Extension;
|
||
|
use common_report_Report as Report;
|
||
|
use common_ext_ExtensionsManager as ExtensionsManager;
|
||
|
use common_ext_Extension as Extension;
|
||
|
|
||
|
/**
|
||
|
* A script action which is supposed to be used to manage migrations (generate, execute, rollback, etc.) in Tao.
|
||
|
*
|
||
|
* Usage examples:
|
||
|
* ```
|
||
|
* //generate new migration class
|
||
|
* sudo -u www-data php index.php '\oat\tao\scripts\tools\Migrations' -c generate -e taoItems
|
||
|
* //show migrations status
|
||
|
* sudo -u www-data php index.php '\oat\tao\scripts\tools\Migrations' -c status
|
||
|
* //apply all migrations
|
||
|
* sudo -u www-data php index.php '\oat\tao\scripts\tools\Migrations' -c migrate
|
||
|
* //migrate to version
|
||
|
* sudo -u www-data php index.php '\oat\tao\scripts\tools\Migrations' -c migrate -v 'oat\generis\migrations\Version202004220924112348_generis'
|
||
|
* //Add migrations to the migrations table without execution (skip extension migrations)
|
||
|
* sudo -u www-data php index.php '\oat\tao\scripts\tools\Migrations' -c add -e tao
|
||
|
* ```
|
||
|
* @package oat\tao\scripts\tools
|
||
|
*/
|
||
|
class Migrations extends ScriptAction
|
||
|
{
|
||
|
|
||
|
protected const MIGRATIONS_DIR = 'migrations';
|
||
|
protected const COMMAND_GENERATE = 'generate';
|
||
|
protected const COMMAND_STATUS = 'status';
|
||
|
protected const COMMAND_MIGRATE = 'migrate';
|
||
|
protected const COMMAND_EXECUTE = 'execute';
|
||
|
protected const COMMAND_ROLLBACK = 'rollback';
|
||
|
protected const COMMAND_ADD = 'add';
|
||
|
protected const COMMAND_INIT = 'init';
|
||
|
|
||
|
private $commands = [
|
||
|
self::COMMAND_GENERATE => 'migrations:generate',
|
||
|
self::COMMAND_STATUS => 'migrations:status',
|
||
|
self::COMMAND_MIGRATE => 'migrations:migrate',
|
||
|
self::COMMAND_EXECUTE => 'migrations:execute',
|
||
|
self::COMMAND_ROLLBACK => 'migrations:execute',
|
||
|
self::COMMAND_ADD => 'migrations:version',
|
||
|
self::COMMAND_INIT => 'migrations:sync-metadata-storage',
|
||
|
];
|
||
|
|
||
|
protected function provideOptions()
|
||
|
{
|
||
|
return [
|
||
|
'command' => [
|
||
|
'prefix' => 'c',
|
||
|
'longPrefix' => 'command',
|
||
|
'required' => true,
|
||
|
'description' => 'Command to be run'
|
||
|
],
|
||
|
'extension' => [
|
||
|
'prefix' => 'e',
|
||
|
'longPrefix' => 'extension',
|
||
|
'required' => false,
|
||
|
'description' => 'Extension for which migration needs to be generated'
|
||
|
],
|
||
|
'version' => [
|
||
|
'prefix' => 'v',
|
||
|
'longPrefix' => 'version',
|
||
|
'required' => false,
|
||
|
'description' => 'Version number to migrate'
|
||
|
],
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function provideDescription()
|
||
|
{
|
||
|
return 'Tao migrations tool';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return Report
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
public function run()
|
||
|
{
|
||
|
$command = $this->getOption('command');
|
||
|
|
||
|
if (!isset($this->commands[$command])) {
|
||
|
throw new ScriptException(sprintf('Command "%s" is not supported', $command));
|
||
|
}
|
||
|
|
||
|
$output = $this->{$command}();
|
||
|
return new Report(Report::TYPE_INFO, $output->fetch());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return BufferedOutput
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
private function generate()
|
||
|
{
|
||
|
$extension = $this->getExtension();
|
||
|
if (!is_dir($extension->getDir().self::MIGRATIONS_DIR)) {
|
||
|
mkdir($extension->getDir().self::MIGRATIONS_DIR);
|
||
|
}
|
||
|
$input = [
|
||
|
'command' => $this->commands[self::COMMAND_GENERATE],
|
||
|
'--namespace' => $this->getExtensionNamespace($extension)
|
||
|
];
|
||
|
$configuration = $this->getConfiguration();
|
||
|
$taoRoot = $this->getServiceLocator()->get(ExtensionsManager::SERVICE_ID)->getExtensionById('tao')->getDir();
|
||
|
$configuration->setCustomTemplate(
|
||
|
$taoRoot.'scripts'.DIRECTORY_SEPARATOR.'tools'.DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR.'Template.tpl'
|
||
|
);
|
||
|
$dependencyFactory = $this->getDependencyFactory($configuration);
|
||
|
$this->executeMigration($dependencyFactory, new ArrayInput($input), $output = new BufferedOutput());
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return BufferedOutput
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
private function migrate()
|
||
|
{
|
||
|
$input = ['command' => $this->commands[self::COMMAND_MIGRATE], '--no-interaction' => true];
|
||
|
if ($this->hasOption('version')) {
|
||
|
$input['version'] = $this->getOption('version');
|
||
|
}
|
||
|
$dependencyFactory = $this->getDependencyFactory($this->getConfiguration());
|
||
|
$this->executeMigration($dependencyFactory, new ArrayInput($input), $output = new BufferedOutput());
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add versions directly to the migrations table without executing them (skip migration)
|
||
|
* @return BufferedOutput
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
private function add()
|
||
|
{
|
||
|
$input = ['command' => $this->commands[self::COMMAND_ADD], '--add' => true, '--all' => true, '--no-interaction' => true];
|
||
|
$this->executeMigration($this->getDependencyFactory($this->getConfiguration()), new ArrayInput($input), $output = new BufferedOutput());
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return BufferedOutput
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
private function init()
|
||
|
{
|
||
|
$input = ['command' => $this->commands[self::COMMAND_INIT], '--no-interaction' => true];
|
||
|
$this->executeMigration($this->getDependencyFactory($this->getConfiguration()), new ArrayInput($input), $output = new BufferedOutput());
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return BufferedOutput
|
||
|
* @throws MigrationException
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
private function status()
|
||
|
{
|
||
|
$dependencyFactory = $this->getDependencyFactory($this->getConfiguration());
|
||
|
$this->executeMigration($dependencyFactory, new ArrayInput(['command' => $this->commands[self::COMMAND_STATUS]]), $output = new BufferedOutput());
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $rollback
|
||
|
* @return BufferedOutput
|
||
|
* @throws MigrationException
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
private function execute(bool $rollback = false)
|
||
|
{
|
||
|
$input = [
|
||
|
'command' => $this->commands[self::COMMAND_EXECUTE],
|
||
|
'versions' => [$this->getOption('version')],
|
||
|
'--no-interaction' => true
|
||
|
];
|
||
|
if ($rollback) {
|
||
|
$input['--down'] = true;
|
||
|
} else {
|
||
|
$input['--up'] = true;
|
||
|
}
|
||
|
|
||
|
$dependencyFactory = $this->getDependencyFactory($this->getConfiguration());
|
||
|
$this->executeMigration($dependencyFactory, new ArrayInput($input), $output = new BufferedOutput());
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return BufferedOutput
|
||
|
* @throws MigrationException
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
private function rollback()
|
||
|
{
|
||
|
return $this->execute(true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param DependencyFactory $dependencyFactory
|
||
|
* @param InputInterface $input
|
||
|
* @param OutputInterface|null $output
|
||
|
* @throws ScriptException
|
||
|
*/
|
||
|
private function executeMigration(DependencyFactory $dependencyFactory, InputInterface $input, OutputInterface $output = null)
|
||
|
{
|
||
|
$cli = new Application('Doctrine Migrations');
|
||
|
$cli->setCatchExceptions(true);
|
||
|
$cli->setAutoExit(false);
|
||
|
$cli->addCommands(array(
|
||
|
new GenerateCommand($dependencyFactory),
|
||
|
new MigrateCommand($dependencyFactory),
|
||
|
new StatusCommand($dependencyFactory),
|
||
|
new ExecuteCommand($dependencyFactory),
|
||
|
new VersionCommand($dependencyFactory),
|
||
|
new SyncMetadataCommand($dependencyFactory),
|
||
|
));
|
||
|
|
||
|
try {
|
||
|
$cli->run($input, $output);
|
||
|
} catch (NoMigrationsToExecute $e) {
|
||
|
$output->write($e->getMessage());
|
||
|
} catch (\Exception $e) {
|
||
|
$this->logWarning('Migration error: ' . $e->getMessage());
|
||
|
throw new ScriptException('Migration error: ' . $e->getMessage());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param Configuration $configuration
|
||
|
* @return DependencyFactory
|
||
|
* @throws ScriptException
|
||
|
* @throws InvalidServiceManagerException
|
||
|
*/
|
||
|
private function getDependencyFactory($configuration)
|
||
|
{
|
||
|
$connection = $this->getConnection();
|
||
|
$dependencyFactory = DependencyFactory::fromConnection(
|
||
|
new ExistingConfiguration($configuration),
|
||
|
new ExistingConnection($connection)
|
||
|
);
|
||
|
|
||
|
/** @var ExtensionsManager $extManager */
|
||
|
$extManager = $this->getServiceManager()->get(ExtensionsManager::SERVICE_ID);
|
||
|
if ($this->hasOption('extension')) {
|
||
|
$dependencyFactory->setService(ClassNameGenerator::class, new TaoClassNameGenerator($this->getExtension()));
|
||
|
}
|
||
|
$dependencyFactory->setService(Comparator::class, new TaoComparator($extManager, new \helpers_ExtensionHelper()));
|
||
|
|
||
|
return $dependencyFactory;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return Configuration
|
||
|
* @throws ScriptException
|
||
|
* @throws \common_ext_ExtensionException
|
||
|
*/
|
||
|
private function getConfiguration()
|
||
|
{
|
||
|
$configuration = new Configuration();
|
||
|
/** @var ExtensionsManager $extensionManager */
|
||
|
$extensionManager = $this->getServiceLocator()->get(ExtensionsManager::SERVICE_ID);
|
||
|
/** @var Extension $extension */
|
||
|
if ($this->hasOption('extension')) {
|
||
|
$extensions = [$this->getExtension()];
|
||
|
} else {
|
||
|
$extensions = $extensionManager->getInstalledExtensions();
|
||
|
}
|
||
|
foreach ($extensions as $extension) {
|
||
|
if (is_dir($extension->getDir().self::MIGRATIONS_DIR)) {
|
||
|
$configuration->addMigrationsDirectory(
|
||
|
$this->getExtensionNamespace($extension),
|
||
|
$extension->getDir().self::MIGRATIONS_DIR
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
return $configuration;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return Connection
|
||
|
*/
|
||
|
private function getConnection()
|
||
|
{
|
||
|
/** @var MigrationsService $migrationService */
|
||
|
$migrationService = $this->getServiceLocator()->get(MigrationsService::SERVICE_ID);
|
||
|
return $migrationService->getPersistence()->getDriver()->getDbalConnection();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return common_ext_Extension
|
||
|
* @throws ScriptException
|
||
|
*/
|
||
|
private function getExtension()
|
||
|
{
|
||
|
/** @var ExtensionsManager $extensionManager */
|
||
|
$extensionManager = $this->getServiceLocator()->get(ExtensionsManager::SERVICE_ID);
|
||
|
|
||
|
if (!$this->hasOption('extension')) {
|
||
|
throw new ScriptException('Extension parameter missed');
|
||
|
}
|
||
|
|
||
|
$extensionId = $this->getOption('extension');
|
||
|
if (!$extensionManager->isInstalled($extensionId)) {
|
||
|
throw new ScriptException(sprintf('Extension "%s" is not installed', $extensionId));
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
return $extensionManager->getExtensionById($extensionId);
|
||
|
} catch (\common_ext_ExtensionException $e) {
|
||
|
$this->logWarning('Error during extension retrieval: '.$e->getMessage());
|
||
|
throw new ScriptException(sprintf('Cannot retrieve extension "%s"', $extensionId));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This is an assumption
|
||
|
* @param common_ext_Extension $extension
|
||
|
* @return string
|
||
|
*/
|
||
|
private function getExtensionNamespace(common_ext_Extension $extension)
|
||
|
{
|
||
|
return 'oat\\'.$extension->getId().'\\migrations';
|
||
|
}
|
||
|
}
|