kaliop-uk /
ezmigrationbundle
| 1 | <?php |
||
| 2 | |||
| 3 | namespace Kaliop\eZMigrationBundle\Command; |
||
| 4 | |||
| 5 | use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface; |
||
| 6 | use Kaliop\eZMigrationBundle\API\MatcherInterface; |
||
| 7 | use Kaliop\eZMigrationBundle\API\EnumerableMatcherInterface; |
||
| 8 | use Kaliop\eZMigrationBundle\API\Event\MigrationGeneratedEvent; |
||
| 9 | use Kaliop\eZMigrationBundle\API\Exception\MigrationBundleException; |
||
| 10 | use Symfony\Component\Console\Input\InputArgument; |
||
| 11 | use Symfony\Component\Console\Input\InputInterface; |
||
| 12 | use Symfony\Component\Console\Input\InputOption; |
||
| 13 | use Symfony\Component\Console\Output\OutputInterface; |
||
| 14 | use Symfony\Component\HttpFoundation\File\Exception\FileException; |
||
| 15 | use Symfony\Component\Yaml\Yaml; |
||
| 16 | |||
| 17 | /** |
||
| 18 | * @todo allow passing in more context options, esp. for content/generate migrations |
||
| 19 | */ |
||
| 20 | class GenerateCommand extends AbstractCommand |
||
| 21 | { |
||
| 22 | const DIR_CREATE_PERMISSIONS = 0755; |
||
| 23 | |||
| 24 | private $availableMigrationFormats = array('yml', 'php', 'sql', 'json'); |
||
| 25 | private $availableModes = array('create', 'update', 'delete'); |
||
| 26 | private $availableTypes = array('content', 'content_type', 'content_type_group', 'language', 'object_state', 'object_state_group', 'role', 'section', 'generic', 'db', 'php', '...'); |
||
| 27 | private $thisBundle = 'EzMigrationBundle'; |
||
| 28 | |||
| 29 | protected $eventName = 'ez_migration.migration_generated'; |
||
| 30 | |||
| 31 | /** |
||
| 32 | * Configure the console command |
||
| 33 | */ |
||
| 34 | 148 | protected function configure() |
|
| 35 | { |
||
| 36 | 148 | $this->setName('kaliop:migration:generate') |
|
| 37 | 148 | ->setDescription('Generate a blank migration definition file.') |
|
| 38 | 148 | ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (' . implode(', ', $this->availableMigrationFormats) . ')', 'yml') |
|
| 39 | 148 | ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (' . implode(', ', $this->availableTypes) . ')', '') |
|
| 40 | 148 | ->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (' . implode(', ', $this->availableModes) . ')', 'create') |
|
| 41 | 148 | ->addOption('match-type', null, InputOption::VALUE_REQUIRED, 'The type of identifier used to find the entity to generate the migration for', null) |
|
| 42 | 148 | ->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) |
|
| 43 | 148 | ->addOption('match-except', null, InputOption::VALUE_NONE, 'Used to match all entities except the ones satisfying the match-value condition', null) |
|
| 44 | 148 | ->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') |
|
| 45 | 148 | ->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, when type=db (mysql, postgresql, ...)', 'mysql') |
|
| 46 | 148 | ->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)") |
|
| 47 | 148 | ->addOption('list-types', null, InputOption::VALUE_NONE, 'Use this option to list all available migration types and their match conditions') |
|
| 48 | 148 | ->addArgument('bundle', InputArgument::OPTIONAL, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle') |
|
| 49 | 148 | ->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null) |
|
| 50 | 148 | ->setHelp(<<<EOT |
|
| 51 | 148 | The <info>kaliop:migration:generate</info> command generates a skeleton migration definition file: |
|
| 52 | |||
| 53 | <info>php bin/console kaliop:migration:generate bundleName</info> |
||
| 54 | |||
| 55 | You can optionally specify the file type to generate with <info>--format</info>, as well a name for the migration: |
||
| 56 | |||
| 57 | <info>php bin/console kaliop:migration:generate --format=json bundleName migrationName</info> |
||
| 58 | |||
| 59 | For SQL type migration you can optionally specify the database server type the migration is for with <info>--dbserver</info>: |
||
| 60 | |||
| 61 | <info>php bin/console kaliop:migration:generate --format=sql bundleName</info> |
||
| 62 | |||
| 63 | For content/content_type/language/object_state/role/section migrations you need to specify the entity that you want to generate the migration for: |
||
| 64 | |||
| 65 | <info>php bin/console kaliop:migration:generate --type=content --match-type=content_id --match-value=10,14 --lang=all bundleName</info> |
||
| 66 | |||
| 67 | For role type migration you will receive a yaml file with the current role definition. You must define ALL the policies |
||
| 68 | you wish for the role. Any not defined will be removed. Example for updating an existing role: |
||
| 69 | |||
| 70 | <info>php bin/console kaliop:migration:generate --type=role --mode=update --match-type=identifier --match-value=Anonymous bundleName</info> |
||
| 71 | |||
| 72 | For freeform php migrations, you will receive a php class definition |
||
| 73 | |||
| 74 | <info>php bin/console kaliop:migration:generate --format=php bundlename classname</info> |
||
| 75 | |||
| 76 | To list all available migration types for generation, as well as the corresponding match-types, run: |
||
| 77 | |||
| 78 | <info>php bin/console ka:mig:gen --list-types</info> |
||
| 79 | |||
| 80 | 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> |
||
| 81 | option when you run the <info>migrate</info> command. |
||
| 82 | EOT |
||
| 83 | ); |
||
| 84 | 148 | } |
|
| 85 | |||
| 86 | /** |
||
| 87 | * Run the command and display the results. |
||
| 88 | * |
||
| 89 | * @param InputInterface $input |
||
| 90 | * @param OutputInterface $output |
||
| 91 | * @return null|int null or 0 if everything went fine, or an error code |
||
| 92 | * @throws \InvalidArgumentException When an unsupported file type is selected |
||
| 93 | * |
||
| 94 | * @todo for type=db, we could fold 'dbserver' option into 'mode' |
||
| 95 | */ |
||
| 96 | 40 | public function execute(InputInterface $input, OutputInterface $output) |
|
| 97 | { |
||
| 98 | 40 | $this->setOutput($output); |
|
| 99 | 40 | $this->setVerbosity($output->getVerbosity()); |
|
| 100 | |||
| 101 | 40 | if ($input->getOption('list-types')) { |
|
| 102 | $this->listAvailableTypes($output); |
||
| 103 | return 0; |
||
| 104 | } |
||
| 105 | |||
| 106 | 40 | $bundleName = $input->getArgument('bundle'); |
|
| 107 | 40 | if ($bundleName === null) { |
|
| 108 | // throw same exception as SF would when declaring 'bundle' as mandatory arg |
||
| 109 | throw new \RuntimeException('Not enough arguments (missing: "bundle").'); |
||
| 110 | } |
||
| 111 | 40 | $name = $input->getArgument('name'); |
|
| 112 | 40 | $fileType = $input->getOption('format'); |
|
| 113 | 40 | $migrationType = $input->getOption('type'); |
|
| 114 | 40 | $matchType = $input->getOption('match-type'); |
|
| 115 | 40 | $matchValue = $input->getOption('match-value'); |
|
| 116 | 40 | $matchExcept = $input->getOption('match-except'); |
|
| 117 | 40 | $mode = $input->getOption('mode'); |
|
| 118 | 40 | $dbServer = $input->getOption('dbserver'); |
|
| 119 | |||
| 120 | 40 | if ($bundleName == $this->thisBundle) { |
|
| 121 | throw new \InvalidArgumentException("It is not allowed to create migrations in bundle '$bundleName'"); |
||
| 122 | } |
||
| 123 | |||
| 124 | // be kind to lazy users |
||
| 125 | 40 | if ($migrationType == '') { |
|
| 126 | 6 | if ($fileType == 'sql') { |
|
| 127 | 2 | $migrationType = 'db'; |
|
| 128 | 4 | } elseif ($fileType == 'php') { |
|
| 129 | 2 | $migrationType = 'php'; |
|
| 130 | } else { |
||
| 131 | 2 | $migrationType = 'generic'; |
|
| 132 | } |
||
| 133 | } |
||
| 134 | |||
| 135 | 40 | if (!in_array($fileType, $this->availableMigrationFormats)) { |
|
| 136 | throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType); |
||
| 137 | } |
||
| 138 | |||
| 139 | 40 | if (!in_array($mode, $this->availableModes)) { |
|
| 140 | throw new \InvalidArgumentException('Unsupported migration mode ' . $mode); |
||
| 141 | } |
||
| 142 | |||
| 143 | 40 | $migrationDirectory = $this->getMigrationDirectory($bundleName); |
|
| 144 | |||
| 145 | 40 | if (!is_dir($migrationDirectory)) { |
|
| 146 | 1 | $output->writeln(sprintf( |
|
| 147 | 1 | "Migrations directory <info>%s</info> does not exist. I will create it now....", |
|
| 148 | $migrationDirectory |
||
| 149 | )); |
||
| 150 | |||
| 151 | 1 | if (mkdir($migrationDirectory, self::DIR_CREATE_PERMISSIONS, true)) { |
|
| 152 | 1 | $output->writeln(sprintf( |
|
| 153 | 1 | "Migrations directory <info>%s</info> has been created", |
|
| 154 | $migrationDirectory |
||
| 155 | )); |
||
| 156 | } else { |
||
| 157 | throw new FileException(sprintf( |
||
| 158 | "Failed to create migrations directory %s.", |
||
| 159 | $migrationDirectory |
||
| 160 | )); |
||
| 161 | } |
||
| 162 | } |
||
| 163 | |||
| 164 | // allow to generate migrations for many entities |
||
| 165 | 40 | if (strpos($matchValue, ',') !== false) { |
|
| 166 | 2 | $matchValue = explode(',', $matchValue); |
|
| 167 | } |
||
| 168 | |||
| 169 | $parameters = array( |
||
| 170 | 40 | 'type' => $migrationType, |
|
| 171 | 40 | 'mode' => $mode, |
|
| 172 | 40 | 'matchType' => $matchType, |
|
| 173 | 40 | 'matchValue' => $matchValue, |
|
| 174 | 40 | 'matchExcept' => $matchExcept, |
|
| 175 | 40 | 'dbserver' => $dbServer, |
|
| 176 | /// @todo move these 2 params out of here, pass the context as template parameter instead |
||
| 177 | 40 | 'lang' => $input->getOption('lang'), |
|
| 178 | 40 | 'adminLogin' => $input->getOption('admin-login') |
|
| 179 | ); |
||
| 180 | |||
| 181 | $date = date('YmdHis'); |
||
| 182 | |||
| 183 | 40 | switch ($fileType) { |
|
| 184 | case 'sql': |
||
| 185 | 40 | /// @todo this logic should come from the DefinitionParser, really |
|
| 186 | 40 | if ($name != '') { |
|
| 187 | $name = '_' . ltrim($name, '_'); |
||
| 188 | 2 | } |
|
| 189 | 1 | $fileName = $date . '_' . $dbServer . $name . '.sql'; |
|
| 190 | break; |
||
| 191 | 2 | ||
| 192 | 2 | case 'php': |
|
| 193 | /// @todo this logic should come from the DefinitionParser, really |
||
| 194 | 38 | $className = ltrim($name, '_'); |
|
| 195 | if ($className == '') { |
||
| 196 | 2 | $className = 'Migration'; |
|
| 197 | 2 | } |
|
| 198 | 1 | // Make sure that php class names are unique, not only migration definition file names |
|
| 199 | $existingMigrations = count(glob($migrationDirectory . '/*_' . $className . '*.php')); |
||
| 200 | if ($existingMigrations) { |
||
| 201 | 2 | $className = $className . sprintf('%03d', $existingMigrations + 1); |
|
| 202 | 2 | } |
|
| 203 | $parameters = array_merge($parameters, array( |
||
| 204 | 'class_name' => $className |
||
| 205 | 2 | )); |
|
| 206 | 2 | $fileName = $date . '_' . $className . '.php'; |
|
| 207 | break; |
||
| 208 | 2 | ||
| 209 | 2 | default: |
|
| 210 | if ($name == '') { |
||
| 211 | $name = 'placeholder'; |
||
| 212 | 36 | } |
|
| 213 | 1 | $fileName = $date . '_' . $name . '.' . $fileType; |
|
| 214 | } |
||
| 215 | 36 | ||
| 216 | $filePath = $migrationDirectory . '/' . $fileName; |
||
| 217 | |||
| 218 | 40 | $warning = $this->generateMigrationFile($migrationType, $mode, $fileType, $filePath, $parameters); |
|
| 219 | |||
| 220 | 40 | $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $filePath)); |
|
| 221 | |||
| 222 | 40 | if ($warning != '') { |
|
| 223 | $output->writeln("<comment>$warning</comment>"); |
||
| 224 | 40 | } |
|
| 225 | 3 | ||
| 226 | return 0; |
||
| 227 | } |
||
| 228 | 40 | ||
| 229 | /** |
||
| 230 | * Generates a migration definition file. |
||
| 231 | * @todo allow non-filesystem storage (delegate saving to a service, just as we do for loading) |
||
| 232 | * |
||
| 233 | * @param string $migrationType The type of migration to generate |
||
| 234 | * @param string $migrationMode |
||
| 235 | * @param string $fileType The type of migration file to generate |
||
| 236 | * @param string $filePath filename to file to generate (full path) |
||
| 237 | * @param array $parameters passed on to twig |
||
| 238 | * @return string A warning message in case file generation was OK but there was something weird |
||
| 239 | * @throws \Exception |
||
| 240 | */ |
||
| 241 | protected function generateMigrationFile($migrationType, $migrationMode, $fileType, $filePath, array $parameters = array()) |
||
| 242 | { |
||
| 243 | 40 | $warning = ''; |
|
| 244 | |||
| 245 | 40 | switch ($migrationType) { |
|
| 246 | case 'db': |
||
| 247 | 40 | case 'generic': |
|
| 248 | 40 | case 'php': |
|
| 249 | 38 | // Generate migration file by template |
|
| 250 | 36 | $template = $migrationType . 'Migration.' . $fileType . '.twig'; |
|
| 251 | $templatePath = $this->getApplication()->getKernel()->getBundle($this->thisBundle)->getPath() . '/Resources/views/MigrationTemplate/'; |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 252 | 6 | if (!is_file($templatePath . $template)) { |
|
| 253 | 6 | throw new \InvalidArgumentException("The combination of migration type '$migrationType' is not supported with format '$fileType'"); |
|
| 254 | 6 | } |
|
| 255 | |||
| 256 | $code = $this->getContainer()->get('twig')->render($this->thisBundle . ':MigrationTemplate:' . $template, $parameters); |
||
| 257 | |||
| 258 | 6 | // allow event handlers to replace data |
|
| 259 | $event = new MigrationGeneratedEvent($migrationType, $migrationMode, $fileType, $code, $filePath); |
||
| 260 | $this->getContainer()->get('event_dispatcher')->dispatch($this->eventName, $event); |
||
| 261 | 6 | $code = $event->getData(); |
|
| 262 | 6 | $filePath = $event->getFile(); |
|
| 263 | 6 | ||
| 264 | 6 | break; |
|
| 265 | |||
| 266 | 6 | default: |
|
| 267 | // Generate migration file by executor |
||
| 268 | $executors = $this->getGeneratingExecutors(); |
||
| 269 | if (!in_array($migrationType, $executors)) { |
||
| 270 | 34 | throw new \InvalidArgumentException("It is not possible to generate a migration of type '$migrationType': executor not found or not a generator"); |
|
| 271 | 34 | } |
|
| 272 | /** @var MigrationGeneratorInterface $executor */ |
||
| 273 | $executor = $this->getMigrationService()->getExecutor($migrationType); |
||
| 274 | |||
| 275 | 34 | $context = $this->migrationContextFromParameters($parameters); |
|
| 276 | |||
| 277 | 34 | $matchCondition = array($parameters['matchType'] => $parameters['matchValue']); |
|
| 278 | if ($parameters['matchExcept']) { |
||
| 279 | 34 | $matchCondition = array(MatcherInterface::MATCH_NOT => $matchCondition); |
|
| 280 | 34 | } |
|
| 281 | 2 | $data = $executor->generateMigration($matchCondition, $migrationMode, $context); |
|
| 282 | |||
| 283 | 34 | // allow event handlers to replace data |
|
| 284 | $event = new MigrationGeneratedEvent($migrationType, $migrationMode, $fileType, $data, $filePath, $matchCondition, $context); |
||
| 285 | $this->getContainer()->get('event_dispatcher')->dispatch($this->eventName, $event); |
||
| 286 | 34 | $data = $event->getData(); |
|
| 287 | 34 | $filePath = $event->getFile(); |
|
| 288 | 34 | ||
| 289 | 34 | if (!is_array($data) || !count($data)) { |
|
| 290 | $warning = 'Note: the generated migration is empty'; |
||
| 291 | 34 | } |
|
| 292 | 3 | ||
| 293 | switch ($fileType) { |
||
| 294 | case 'yml': |
||
| 295 | 34 | case 'yaml': |
|
| 296 | 34 | /// @todo use Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE option if it is supported |
|
| 297 | 12 | $code = Yaml::dump($data, 5); |
|
| 298 | break; |
||
| 299 | 22 | case 'json': |
|
| 300 | 22 | $code = json_encode($data, JSON_PRETTY_PRINT); |
|
| 301 | 12 | break; |
|
| 302 | 12 | default: |
|
| 303 | 12 | throw new \InvalidArgumentException("The combination of migration type '$migrationType' is not supported with format '$fileType'"); |
|
| 304 | } |
||
| 305 | } |
||
| 306 | |||
| 307 | file_put_contents($filePath, $code); |
||
| 308 | |||
| 309 | 40 | return $warning; |
|
| 310 | } |
||
| 311 | 40 | ||
| 312 | protected function listAvailableTypes(OutputInterface $output) |
||
| 313 | { |
||
| 314 | $output->writeln('Specific migration types available for generation (besides sql,php, generic):'); |
||
| 315 | foreach ($this->getGeneratingExecutors() as $executorType) { |
||
| 316 | $output->writeln($executorType); |
||
| 317 | /** @var MigrationGeneratorInterface $executor */ |
||
| 318 | $executor = $this->getMigrationService()->getExecutor($executorType); |
||
| 319 | if ($executor instanceof EnumerableMatcherInterface) { |
||
| 320 | $conditions = $executor->listAllowedConditions(); |
||
| 321 | $conditions = array_diff($conditions, array('and', 'or', 'not')); |
||
| 322 | $output->writeln(" corresponding match types:\n - " . implode("\n - ", $conditions)); |
||
| 323 | } |
||
| 324 | } |
||
| 325 | } |
||
| 326 | |||
| 327 | /** |
||
| 328 | * @param string $bundleName a bundle name or filesystem path to a directory |
||
| 329 | * @return string |
||
| 330 | */ |
||
| 331 | protected function getMigrationDirectory($bundleName) |
||
| 332 | { |
||
| 333 | 40 | // Allow direct usage of a directory path instead of a bundle name |
|
| 334 | if (strpos($bundleName, '/') !== false && is_dir($bundleName)) { |
||
| 335 | return rtrim($bundleName, '/'); |
||
| 336 | 40 | } |
|
| 337 | |||
| 338 | $activeBundles = array(); |
||
| 339 | foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) { |
||
| 340 | 40 | $activeBundles[] = $bundle->getName(); |
|
| 341 | 40 | } |
|
| 342 | 40 | asort($activeBundles); |
|
| 343 | if (!in_array($bundleName, $activeBundles)) { |
||
| 344 | 40 | throw new \InvalidArgumentException("Bundle '$bundleName' does not exist or it is not enabled. Try with one of:\n" . implode(', ', $activeBundles)); |
|
| 345 | 40 | } |
|
| 346 | |||
| 347 | $bundle = $this->getApplication()->getKernel()->getBundle($bundleName); |
||
| 348 | $migrationDirectory = $bundle->getPath() . '/' . $this->getContainer()->get('ez_migration_bundle.helper.config.resolver')->getParameter('kaliop_bundle_migration.version_directory'); |
||
| 349 | 40 | ||
| 350 | 40 | return $migrationDirectory; |
|
| 351 | } |
||
| 352 | 40 | ||
| 353 | /** |
||
| 354 | * @todo move somewhere else. Maybe to the MigrationService itself ? |
||
| 355 | * @return string[] |
||
| 356 | */ |
||
| 357 | protected function getGeneratingExecutors() |
||
| 358 | { |
||
| 359 | 34 | $migrationService = $this->getMigrationService(); |
|
| 360 | $executors = $migrationService->listExecutors(); |
||
| 361 | 34 | foreach ($executors as $key => $name) { |
|
| 362 | 34 | $executor = $migrationService->getExecutor($name); |
|
| 363 | 34 | if (!$executor instanceof MigrationGeneratorInterface) { |
|
| 364 | 34 | unset($executors[$key]); |
|
| 365 | 34 | } |
|
| 366 | 34 | } |
|
| 367 | return $executors; |
||
| 368 | } |
||
| 369 | 34 | ||
| 370 | /** |
||
| 371 | * @see MigrationService::migrationContextFromParameters |
||
| 372 | * @param array $parameters these come directly from cli options |
||
| 373 | * @return array |
||
| 374 | */ |
||
| 375 | protected function migrationContextFromParameters(array $parameters) |
||
| 376 | { |
||
| 377 | 34 | $context = array(); |
|
| 378 | |||
| 379 | 34 | if (isset($parameters['lang']) && $parameters['lang'] != '') { |
|
| 380 | $context['defaultLanguageCode'] = $parameters['lang']; |
||
| 381 | 34 | } |
|
| 382 | if (isset($parameters['adminLogin']) && $parameters['adminLogin'] != '') { |
||
| 383 | $context['adminUserLogin'] = $parameters['adminLogin']; |
||
| 384 | 34 | } |
|
| 385 | |||
| 386 | return $context; |
||
| 387 | 34 | } |
|
| 388 | } |
||
| 389 |