mirror of
https://github.com/code-rhapsodie/ezmigrationbundle2.git
synced 2026-03-24 06:42:19 +01:00
412 lines
18 KiB
PHP
412 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Kaliop\eZMigrationBundle\Command;
|
|
|
|
use Kaliop\eZMigrationBundle\API\ConfigResolverInterface;
|
|
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
|
|
use Kaliop\eZMigrationBundle\API\MatcherInterface;
|
|
use Kaliop\eZMigrationBundle\API\EnumerableMatcherInterface;
|
|
use Kaliop\eZMigrationBundle\API\Event\MigrationGeneratedEvent;
|
|
use Kaliop\eZMigrationBundle\Core\MigrationService;
|
|
use Symfony\Component\Console\Input\InputArgument;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
|
use Symfony\Component\HttpKernel\Kernel;
|
|
use Symfony\Component\Yaml\Yaml;
|
|
use Twig\Environment;
|
|
|
|
/**
|
|
* @todo allow passing in more context options, esp. for content/generate migrations
|
|
*/
|
|
class GenerateCommand extends AbstractCommand
|
|
{
|
|
const DIR_CREATE_PERMISSIONS = 0755;
|
|
|
|
protected static $defaultName = 'kaliop:migration:generate';
|
|
|
|
private $availableMigrationFormats = array('yml', 'php', 'sql', 'json');
|
|
private $availableModes = array('create', 'update', 'delete');
|
|
private $availableTypes = array('content', 'content_type', 'content_type_group', 'language', 'object_state', 'object_state_group', 'role', 'section', 'generic', 'db', 'php', '...');
|
|
private $thisBundle = 'eZMigrationBundle';
|
|
|
|
protected $eventName = 'ez_migration.migration_generated';
|
|
protected $eventDispatcher;
|
|
protected $twig;
|
|
protected $configResolver;
|
|
|
|
public function __construct(MigrationService $migrationService, EventDispatcherInterface $eventDispatcher,
|
|
Environment $twig, ConfigResolverInterface $configResolver)
|
|
{
|
|
parent::__construct($migrationService);
|
|
$this->eventDispatcher = $eventDispatcher;
|
|
$this->twig = $twig;
|
|
$this->configResolver = $configResolver;
|
|
}
|
|
|
|
/**
|
|
* Configure the console command
|
|
*/
|
|
protected function configure()
|
|
{
|
|
$this
|
|
->setDescription('Generate a blank migration definition file.')
|
|
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (' . implode(', ', $this->availableMigrationFormats) . ')', 'yml')
|
|
->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (' . implode(', ', $this->availableTypes) . ')', '')
|
|
->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (' . implode(', ', $this->availableModes) . ')', 'create')
|
|
->addOption('match-type', null, InputOption::VALUE_REQUIRED, 'The type of identifier used to find the entity to generate the migration for', null)
|
|
->addOption('match-value', null, InputOption::VALUE_REQUIRED, 'The identifier value used to find the entity to generate the migration for. Can have many values separated by commas', null)
|
|
->addOption('match-except', null, InputOption::VALUE_NONE, 'Used to match all entities except the ones satisfying the match-value condition', null)
|
|
->addOption('lang', 'l', InputOption::VALUE_REQUIRED, 'The language of the migration (eng-GB, ger-DE, ...). If null, the default language of the current siteaccess is used')
|
|
->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, when type=db (mysql, postgresql, ...)', 'mysql')
|
|
->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)")
|
|
->addOption('list-types', null, InputOption::VALUE_NONE, 'Use this option to list all available migration types and their match conditions')
|
|
->addArgument('bundle', InputArgument::OPTIONAL, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle')
|
|
->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null)
|
|
->setHelp(<<<EOT
|
|
The <info>kaliop:migration:generate</info> command generates a skeleton migration definition file:
|
|
|
|
<info>php ezpublish/console kaliop:migration:generate</info>
|
|
|
|
You can optionally specify the file type to generate with <info>--format</info>, bundle name where the migration definition should be created as well a name for the migration:
|
|
|
|
<info>php ezpublish/console kaliop:migration:generate --format=json bundleName migrationName</info>
|
|
|
|
For SQL type migration you can optionally specify the database server type the migration is for with <info>--dbserver</info>:
|
|
|
|
<info>php ezpublish/console kaliop:migration:generate --format=sql</info>
|
|
|
|
For content/content_type/language/object_state/role/section migrations you need to specify the entity that you want to generate the migration for:
|
|
|
|
<info>php ezpublish/console kaliop:migration:generate --type=content --match-type=content_id --match-value=10,14 --lang=all</info>
|
|
|
|
For role type migration you will receive a yaml file with the current role definition. You must define ALL the policies
|
|
you wish for the role. Any not defined will be removed. Example for updating an existing role:
|
|
|
|
<info>php ezpublish/console kaliop:migration:generate --type=role --mode=update --match-type=identifier --match-value=Anonymous</info>
|
|
|
|
For freeform php migrations, you will receive a php class definition
|
|
|
|
<info>php ezpublish/console kaliop:migration:generate --format=php classname</info>
|
|
|
|
To list all available migration types for generation, as well as the corresponding match-types, run:
|
|
|
|
<info>php ezpublish/console ka:mig:gen --list-types</info>
|
|
|
|
Note that you can pass in a custom directory path instead of a bundle name, but, if you do, you will have to use the <info>--path</info>
|
|
option when you run the <info>migrate</info> command.
|
|
EOT
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Run the command and display the results.
|
|
*
|
|
* @param InputInterface $input
|
|
* @param OutputInterface $output
|
|
* @return int 0 if everything went fine, or an error code
|
|
* @throws \InvalidArgumentException When an unsupported file type is selected
|
|
*
|
|
* @todo for type=db, we could fold 'dbserver' option into 'mode'
|
|
*/
|
|
public function execute(InputInterface $input, OutputInterface $output)
|
|
{
|
|
$this->setOutput($output);
|
|
$this->setVerbosity($output->getVerbosity());
|
|
|
|
if ($input->getOption('list-types')) {
|
|
$this->listAvailableTypes($output);
|
|
return 0;
|
|
}
|
|
|
|
$bundleName = $input->getArgument('bundle');
|
|
$name = $input->getArgument('name');
|
|
$fileType = $input->getOption('format');
|
|
$migrationType = $input->getOption('type');
|
|
$matchType = $input->getOption('match-type');
|
|
$matchValue = $input->getOption('match-value');
|
|
$matchExcept = $input->getOption('match-except');
|
|
$mode = $input->getOption('mode');
|
|
$dbServer = $input->getOption('dbserver');
|
|
|
|
if ($bundleName == $this->thisBundle) {
|
|
throw new \InvalidArgumentException("It is not allowed to create migrations in bundle '$bundleName'");
|
|
}
|
|
|
|
// be kind to lazy users
|
|
if ($migrationType == '') {
|
|
if ($fileType == 'sql') {
|
|
$migrationType = 'db';
|
|
} elseif ($fileType == 'php') {
|
|
$migrationType = 'php';
|
|
} else {
|
|
$migrationType = 'generic';
|
|
}
|
|
}
|
|
|
|
if (!in_array($fileType, $this->availableMigrationFormats)) {
|
|
throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType);
|
|
}
|
|
|
|
if (!in_array($mode, $this->availableModes)) {
|
|
throw new \InvalidArgumentException('Unsupported migration mode ' . $mode);
|
|
}
|
|
|
|
$migrationDirectory = $this->getMigrationDirectory($bundleName);
|
|
|
|
if (!is_dir($migrationDirectory)) {
|
|
$output->writeln(sprintf(
|
|
"Migrations directory <info>%s</info> does not exist. I will create it now....",
|
|
$migrationDirectory
|
|
));
|
|
|
|
if (mkdir($migrationDirectory, self::DIR_CREATE_PERMISSIONS, true)) {
|
|
$output->writeln(sprintf(
|
|
"Migrations directory <info>%s</info> has been created",
|
|
$migrationDirectory
|
|
));
|
|
} else {
|
|
throw new FileException(sprintf(
|
|
"Failed to create migrations directory %s.",
|
|
$migrationDirectory
|
|
));
|
|
}
|
|
}
|
|
|
|
// allow to generate migrations for many entities
|
|
if (strpos($matchValue, ',') !== false ) {
|
|
$matchValue = explode(',', $matchValue);
|
|
}
|
|
|
|
$parameters = array(
|
|
'type' => $migrationType,
|
|
'mode' => $mode,
|
|
'matchType' => $matchType,
|
|
'matchValue' => $matchValue,
|
|
'matchExcept' => $matchExcept,
|
|
'dbserver' => $dbServer,
|
|
/// @todo move these 2 params out of here, pass the context as template parameter instead
|
|
'lang' => $input->getOption('lang'),
|
|
'adminLogin' => $input->getOption('admin-login')
|
|
/// @todo should we allow users to specify this ?
|
|
//'forceSigchildEnabled' => null
|
|
);
|
|
|
|
$date = date('YmdHis');
|
|
|
|
switch ($fileType) {
|
|
case 'sql':
|
|
/// @todo this logic should come from the DefinitionParser, really
|
|
if ($name != '') {
|
|
$name = '_' . ltrim($name, '_');
|
|
}
|
|
$fileName = $date . '_' . $dbServer . $name . '.sql';
|
|
break;
|
|
|
|
case 'php':
|
|
/// @todo this logic should come from the DefinitionParser, really
|
|
$className = ltrim($name, '_');
|
|
if ($className == '') {
|
|
$className = 'Migration';
|
|
}
|
|
// Make sure that php class names are unique, not only migration definition file names
|
|
$existingMigrations = count(glob($migrationDirectory . '/*_' . $className . '*.php'));
|
|
if ($existingMigrations) {
|
|
$className = $className . sprintf('%03d', $existingMigrations + 1);
|
|
}
|
|
$parameters = array_merge($parameters, array(
|
|
'class_name' => $className
|
|
));
|
|
$fileName = $date . '_' . $className . '.php';
|
|
break;
|
|
|
|
default:
|
|
if ($name == '') {
|
|
$name = 'placeholder';
|
|
}
|
|
$fileName = $date . '_' . $name . '.' . $fileType;
|
|
}
|
|
|
|
$filePath = $migrationDirectory . '/' . $fileName;
|
|
|
|
$warning = $this->generateMigrationFile($migrationType, $mode, $fileType, $filePath, $parameters);
|
|
|
|
$output->writeln(sprintf("Generated new migration file: <info>%s</info>", $filePath));
|
|
|
|
if ($warning != '') {
|
|
$output->writeln("<comment>$warning</comment>");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Generates a migration definition file.
|
|
* @todo allow non-filesystem storage (delegate saving to a service, just as we do for loading)
|
|
*
|
|
* @param string $migrationType The type of migration to generate
|
|
* @param string $migrationMode
|
|
* @param string $fileType The type of migration file to generate
|
|
* @param string $filePath filename to file to generate (full path)
|
|
* @param array $parameters passed on to twig
|
|
* @return string A warning message in case file generation was OK but there was something weird
|
|
* @throws \Exception
|
|
*/
|
|
protected function generateMigrationFile($migrationType, $migrationMode, $fileType, $filePath, array $parameters = array())
|
|
{
|
|
$warning = '';
|
|
|
|
switch ($migrationType) {
|
|
case 'db':
|
|
case 'generic':
|
|
case 'php':
|
|
// Generate migration file by template
|
|
$template = $migrationType . 'Migration.' . $fileType . '.twig';
|
|
$templatePath = $this->getApplication()->getKernel()->getBundle($this->thisBundle)->getPath() . '/Resources/views/MigrationTemplate/';
|
|
if (!is_file($templatePath . $template)) {
|
|
throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
|
|
}
|
|
$code = $this->twig->render('@' . preg_replace('/Bundle$/', '', $this->thisBundle) . '/MigrationTemplate/' . $template, $parameters);
|
|
|
|
// allow event handlers to replace data
|
|
$event = new MigrationGeneratedEvent($migrationType, $migrationMode, $fileType, $code, $filePath);
|
|
$this->eventDispatcher->dispatch($event, $this->eventName);
|
|
$code = $event->getData();
|
|
$filePath = $event->getFile();
|
|
|
|
break;
|
|
|
|
default:
|
|
// Generate migration file by executor
|
|
$executors = $this->getGeneratingExecutors();
|
|
if (!in_array($migrationType, $executors)) {
|
|
throw new \Exception("It is not possible to generate a migration of type '$migrationType': executor not found or not a generator");
|
|
}
|
|
/** @var MigrationGeneratorInterface $executor */
|
|
$executor = $this->getMigrationService()->getExecutor($migrationType);
|
|
|
|
$context = $this->migrationContextFromParameters($parameters);
|
|
|
|
$matchCondition = array($parameters['matchType'] => $parameters['matchValue']);
|
|
if ($parameters['matchExcept']) {
|
|
$matchCondition = array(MatcherInterface::MATCH_NOT => $matchCondition);
|
|
}
|
|
$data = $executor->generateMigration($matchCondition, $migrationMode, $context);
|
|
|
|
// allow event handlers to replace data
|
|
$event = new MigrationGeneratedEvent($migrationType, $migrationMode, $fileType, $data, $filePath, $matchCondition, $context);
|
|
$this->eventDispatcher->dispatch($event, $this->eventName);
|
|
$data = $event->getData();
|
|
$filePath = $event->getFile();
|
|
|
|
if (!is_array($data) || !count($data)) {
|
|
$warning = 'Note: the generated migration is empty';
|
|
}
|
|
|
|
switch ($fileType) {
|
|
case 'yml':
|
|
case 'yaml':
|
|
/// @todo use Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE option if it is supported
|
|
$code = Yaml::dump($data, 5);
|
|
break;
|
|
case 'json':
|
|
$code = json_encode($data, JSON_PRETTY_PRINT);
|
|
break;
|
|
default:
|
|
throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
|
|
}
|
|
}
|
|
|
|
file_put_contents($filePath, $code);
|
|
|
|
return $warning;
|
|
}
|
|
|
|
protected function listAvailableTypes(OutputInterface $output)
|
|
{
|
|
$output->writeln('Specific migration types available for generation (besides sql,php, generic):');
|
|
foreach ($this->getGeneratingExecutors() as $executorType) {
|
|
$output->writeln($executorType);
|
|
/** @var MigrationGeneratorInterface $executor */
|
|
$executor = $this->getMigrationService()->getExecutor($executorType);
|
|
if ($executor instanceof EnumerableMatcherInterface) {
|
|
$conditions = $executor->listAllowedConditions();
|
|
$conditions = array_diff($conditions, array('and', 'or', 'not'));
|
|
$output->writeln(" corresponding match types:\n - " . implode("\n - ", $conditions));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string|null $bundleName a bundle name or filesystem path to a directory
|
|
* @return string
|
|
*/
|
|
protected function getMigrationDirectory($bundleName)
|
|
{
|
|
if (!$bundleName) {
|
|
return $this->getApplication()->getKernel()->getProjectDir() . '/src/' . $this->configResolver->getParameter('ez_migration_bundle.version_directory');
|
|
}
|
|
|
|
// Allow direct usage of a directory path instead of a bundle name
|
|
if (strpos($bundleName, '/') !== false && is_dir($bundleName)) {
|
|
return rtrim($bundleName, '/');
|
|
}
|
|
|
|
$activeBundles = array();
|
|
foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) {
|
|
$activeBundles[] = $bundle->getName();
|
|
}
|
|
asort($activeBundles);
|
|
if (!in_array($bundleName, $activeBundles)) {
|
|
throw new \InvalidArgumentException("Bundle '$bundleName' does not exist or it is not enabled. Try with one of:\n" . implode(', ', $activeBundles));
|
|
}
|
|
|
|
$bundle = $this->getApplication()->getKernel()->getBundle($bundleName);
|
|
$migrationDirectory = $bundle->getPath() . '/' . $this->configResolver->getParameter('ez_migration_bundle.version_directory');
|
|
|
|
return $migrationDirectory;
|
|
}
|
|
|
|
/**
|
|
* @todo move somewhere else. Maybe to the MigrationService itself ?
|
|
* @return string[]
|
|
*/
|
|
protected function getGeneratingExecutors()
|
|
{
|
|
$migrationService = $this->getMigrationService();
|
|
$executors = $migrationService->listExecutors();
|
|
foreach($executors as $key => $name) {
|
|
$executor = $migrationService->getExecutor($name);
|
|
if (!$executor instanceof MigrationGeneratorInterface) {
|
|
unset($executors[$key]);
|
|
}
|
|
}
|
|
return $executors;
|
|
}
|
|
|
|
/**
|
|
* @see MigrationService::migrationContextFromParameters
|
|
* @param array $parameters these come directly from cli options
|
|
* @return array
|
|
*/
|
|
protected function migrationContextFromParameters(array $parameters)
|
|
{
|
|
$context = array();
|
|
|
|
if (isset($parameters['lang']) && $parameters['lang'] != '') {
|
|
$context['defaultLanguageCode'] = $parameters['lang'];
|
|
}
|
|
if (isset($parameters['adminLogin']) && $parameters['adminLogin'] != '') {
|
|
$context['adminUserLogin'] = $parameters['adminLogin'];
|
|
}
|
|
if (isset($parameters['forceSigchildEnabled']) && $parameters['forceSigchildEnabled'] !== null)
|
|
{
|
|
$context['forceSigchildEnabled'] = $parameters['forceSigchildEnabled'];
|
|
}
|
|
|
|
return $context;
|
|
}
|
|
}
|