diff --git a/.gitignore b/.gitignore index 00a63032..74ddbe82 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /composer /composer.phar /composer.lock +/Tests/dsl/good/references/test_refs_generated.yml diff --git a/.travis.yml b/.travis.yml index 15f6985d..b3879655 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,29 +11,35 @@ sudo: false # We limit the matrix to one version of eZPublish for each version of PHP matrix: include: - # ezpublish-community 2014.3 corresponds to enterprise 5.3, still supported, installs via composer - - php: 5.4 - env: EZ_PACKAGES='ezsystems/ezpublish-community:~2014.3.2' EZ_VERSION=ezpublish-community EZ_APP_DIR=ezpublish EZ_KERNEL=EzPublishKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=0 + # ezpublish-community 2014.3 corresponds to enterprise 5.3, not supported any more since end of May 2017. + #- php: 5.4 + # env: EZ_PACKAGES='ezsystems/ezpublish-community:~2014.3.2' EZ_VERSION=ezpublish-community EZ_APP_DIR=ezpublish EZ_KERNEL=EzPublishKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=0 #- php: 5.5 # env: EZ_PACKAGES='ezsystems/ezpublish-community:~2014.3.2 netgen/tagsbundle:1.O' EZ_VERSION=ezpublish-community EZ_APP_DIR=ezpublish EZ_KERNEL=EzPublishKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=0 - # the last version of eZPublish Community Project, aka eZPublish 5, corresponds to eZPublish Platform (Enterprise) 5.4 + + # The last version of eZPublish Community Project, aka eZPublish 5, corresponds to eZPublish Platform (Enterprise) 5.4 + # About php versions: + # - php 5.6 can be installed on RHEL/CentOS 6, which is the oldest currently supported + # - current Debian Stable comes with php 5.6 too (Debian 7 has php 5.4 but it is only in LTS support by now) + # We thus only test on php 5.6 #- php: 5.4 # env: EZ_PACKAGES='ezsystems/ezpublish-community:~2014.11.0 ezsystems/behatbundle:~5.4 netgen/tagsbundle:~2.0' EZ_VERSION=ezpublish-community EZ_APP_DIR=ezpublish EZ_KERNEL=EzPublishKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=1 - - php: 5.5 - env: EZ_PACKAGES='ezsystems/ezpublish-community:~2014.11.0 ezsystems/behatbundle:~5.4 netgen/tagsbundle:~2.0' EZ_VERSION=ezpublish-community EZ_APP_DIR=ezpublish EZ_KERNEL=EzPublishKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=1 + #- php: 5.5 + # env: EZ_PACKAGES='ezsystems/ezpublish-community:~2014.11.0 ezsystems/behatbundle:~5.4 netgen/tagsbundle:~2.0' EZ_VERSION=ezpublish-community EZ_APP_DIR=ezpublish EZ_KERNEL=EzPublishKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=1 - php: 5.6 env: EZ_PACKAGES='ezsystems/ezpublish-community:~2014.11.0 ezsystems/behatbundle:~5.4 netgen/tagsbundle:~2.0' EZ_VERSION=ezpublish-community EZ_APP_DIR=ezpublish EZ_KERNEL=EzPublishKernel CODE_COVERAGE=1 INSTALL_TAGSBUNDLE=1 + # latest version currently available of eZPlatform aka eZPublish 6 #- php: 5.6 - # env: EZ_PACKAGES='ezsystems/ezplatform:~1.7.0 ezsystems/behatbundle:^6.3 netgen/tagsbundle:~2.0' EZ_VERSION=ezplatform EZ_APP_DIR=app EZ_KERNEL=AppKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=1 + # env: EZ_PACKAGES='ezsystems/ezplatform:~1.8.0 ezsystems/ezplatform-xmltext-fieldtype:^1.1 ezsystems/behatbundle:^6.3 netgen/tagsbundle:~2.0' EZ_VERSION=ezplatform EZ_APP_DIR=app EZ_KERNEL=AppKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=1 - php: 7.0 env: EZ_PACKAGES='ezsystems/ezplatform:~1.7.0 ezsystems/ezplatform-xmltext-fieldtype:^1.1 ezsystems/behatbundle:^6.3 netgen/tagsbundle:~2.0' EZ_VERSION=ezplatform EZ_APP_DIR=app EZ_KERNEL=AppKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=1 - php: 7.1 env: EZ_PACKAGES='ezsystems/ezplatform:~1.7.0 ezsystems/ezplatform-xmltext-fieldtype:^1.1 ezsystems/behatbundle:^6.3 netgen/tagsbundle:~2.0' EZ_VERSION=ezplatform EZ_APP_DIR=app EZ_KERNEL=AppKernel CODE_COVERAGE=0 INSTALL_TAGSBUNDLE=1 #allow_failures: - # this currently fails because of eZPublish services.yml not quoting usage of @, despite it being tested with Sf 2.8... - #- php: 7.0 + # this currently fails because of the refactoring of location matcher + #- php: 5.4 before_install: # No need for a web server, until we start testing using Selenium diff --git a/API/ComplexFieldInterface.php b/API/ComplexFieldInterface.php deleted file mode 100644 index 18db28e1..00000000 --- a/API/ComplexFieldInterface.php +++ /dev/null @@ -1,19 +0,0 @@ -step = $step; + $this->exception = $exception; + } + + /** + * @return MigrationStep + */ + public function getStep() + { + return $this->step; + } + + /** + * @return MigrationSuspendedException + */ + public function getException() + { + return $this->exception; + } +} diff --git a/API/Exception/MigrationSuspendedException.php b/API/Exception/MigrationSuspendedException.php new file mode 100644 index 00000000..c6db7ae7 --- /dev/null +++ b/API/Exception/MigrationSuspendedException.php @@ -0,0 +1,14 @@ +getMigrationService()->getExecutor($migrationType); - if ($executor instanceof LanguageAwareInterface) { - $executor->setLanguageCode($parameters['lang']); + $context = array(); + if (isset($parameters['lang']) && $parameters['lang'] != '') { + $context['defaultLanguageCode'] = $parameters['lang']; } $matchCondition = array($parameters['matchType'] => $parameters['matchValue']); if ($parameters['matchExcept']) { $matchCondition = array(MatcherInterface::MATCH_NOT => $matchCondition); } - $data = $executor->generateMigration($matchCondition, $parameters['mode']); + $data = $executor->generateMigration($matchCondition, $parameters['mode'], $context); if (!is_array($data) || !count($data)) { $warning = 'Note: the generated migration is empty'; diff --git a/Command/MigrateCommand.php b/Command/MigrateCommand.php index e05d6d3c..7cab0d8c 100644 --- a/Command/MigrateCommand.php +++ b/Command/MigrateCommand.php @@ -38,14 +38,10 @@ protected function configure() ->setName(self::COMMAND_NAME) ->setAliases(array('kaliop:migration:update')) ->setDescription('Execute available migration definitions.') - ->addOption( - 'path', - null, - InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, - "The directory or file to load the migration definitions from" - ) + ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "The directory or file to load the migration definitions from") // nb: when adding options, remember to forward them to sub-commands executed in 'separate-process' mode ->addOption('default-language', 'l', InputOption::VALUE_REQUIRED, "Default language code that will be used if no language is provided in migration steps") + ->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)") ->addOption('ignore-failures', 'i', InputOption::VALUE_NONE, "Keep executing migrations even if one fails") ->addOption('clear-cache', 'c', InputOption::VALUE_NONE, "Clear the cache after the command finishes") ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question") @@ -201,7 +197,8 @@ function($type, $buffer) { $migrationService->executeMigration( $migrationDefinition, !$input->getOption('no-transactions'), - $input->getOption('default-language') + $input->getOption('default-language'), + $input->getOption('admin-login') ); $executed++; } catch (\Exception $e) { diff --git a/Command/MigrationCommand.php b/Command/MigrationCommand.php index 314f8f2a..526cf1ad 100644 --- a/Command/MigrationCommand.php +++ b/Command/MigrationCommand.php @@ -6,6 +6,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; +use Kaliop\eZMigrationBundle\API\Value\Migration; +use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; /** * Command to execute the available migration definitions. @@ -25,12 +27,12 @@ protected function configure() ->setName('kaliop:migration:migration') ->setDescription('Manually delete migrations from the database table.') ->addOption('delete', null, InputOption::VALUE_NONE, "Delete the specified migration.") + ->addOption('info', null, InputOption::VALUE_NONE, "Get info about the specified migration.") ->addOption('add', null, InputOption::VALUE_NONE, "Add the specified migration definition.") ->addOption('skip', null, InputOption::VALUE_NONE, "Mark the specified migration as skipped.") ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question.") - ->addArgument('migration', InputArgument::REQUIRED, 'The migration to add/skip (filename with full path) or delete (plain migration name).', null) - ->setHelp( - <<addArgument('migration', InputArgument::REQUIRED, 'The migration to add/skip (filename with full path) or detail/delete (plain migration name).', null) + ->setHelp(<<kaliop:migration:migration command allows you to manually delete migrations versions from the migration table: ./ezpublish/console kaliop:migration:migration --delete migration_name @@ -51,13 +53,90 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - if (!$input->getOption('add') && !$input->getOption('delete') && !$input->getOption('skip')) { - throw new \InvalidArgumentException('You must specify whether you want to --add, --delete or --skip the specified migration.'); + if (!$input->getOption('add') && !$input->getOption('delete') && !$input->getOption('skip') && !$input->getOption('info')) { + throw new \InvalidArgumentException('You must specify whether you want to --add, --delete, --skip or --info the specified migration.'); } $migrationService = $this->getMigrationService(); $migrationNameOrPath = $input->getArgument('migration'); + if ($input->getOption('info')) { + $output->writeln(''); + + $migration = $migrationService->getMigration($migrationNameOrPath); + if ($migration == null) { + throw new \InvalidArgumentException(sprintf('The migration "%s" does not exist in the migrations table.', $migrationNameOrPath)); + } + + switch ($migration->status) { + case Migration::STATUS_DONE: + $status = 'executed'; + break; + case Migration::STATUS_STARTED: + $status = 'execution started'; + break; + case Migration::STATUS_TODO: + // bold to-migrate! + $status = 'not executed'; + break; + case Migration::STATUS_SKIPPED: + $status = 'skipped'; + break; + case Migration::STATUS_PARTIALLY_DONE: + $status = 'partially executed'; + break; + case Migration::STATUS_SUSPENDED: + $status = 'suspended'; + break; + case Migration::STATUS_FAILED: + $status = 'failed'; + break; + } + + $output->writeln('Migration: ' . $migration->name . ''); + $output->writeln('Status: ' . $status); + $output->writeln('Executed on: ' . ($migration->executionDate != null ? date("Y-m-d H:i:s", $migration->executionDate) : '--'). ''); + $output->writeln('Execution notes: ' . $migration->executionError . ''); + + if ($migration->status == Migration::STATUS_SUSPENDED) { + /// @todo decode the suspension context: date, step, ... + } + + $output->writeln('Definition path: ' . $migration->path . ''); + $output->writeln('Definition md5: ' . $migration->md5 . ''); + + if ($migration->path != '') { + // q: what if we have a loader which does not work with is_file? We could probably remove this check... + if (is_file($migration->path)) { + try { + $migrationDefinitionCollection = $migrationService->getMigrationsDefinitions(array($migration->path)); + if (count($migrationDefinitionCollection)) { + $migrationDefinition = reset($migrationDefinitionCollection); + $migrationDefinition = $migrationService->parseMigrationDefinition($migrationDefinition); + + if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) { + $output->writeln('Definition error: ' . $migrationDefinition->parsingError . ''); + } + + if (md5($migrationDefinition->rawDefinition) != $migration->md5) { + $output->writeln('Notes: The migration definition file has now a different checksum'); + } + } else { + $output->writeln('Definition error: The migration definition file can not be loaded'); + } + } catch (\Exception $e) { + /// @todo one day we should be able to limit the kind of exceptions we have to catch here... + $output->writeln('Definition parsing error: ' . $e->getMessage() . ''); + } + } else { + $output->writeln('Definition error: The migration definition file can not be found any more'); + } + } + + $output->writeln(''); + return; + } + // ask user for confirmation to make changes if ($input->isInteractive() && !$input->getOption('no-interaction')) { $dialog = $this->getHelperSet()->get('dialog'); diff --git a/Command/ResumeCommand.php b/Command/ResumeCommand.php new file mode 100644 index 00000000..19eb9560 --- /dev/null +++ b/Command/ResumeCommand.php @@ -0,0 +1,118 @@ +setName('kaliop:migration:resume') + ->setDescription('Restarts any suspended migrations.') + ->addOption('ignore-failures', 'i', InputOption::VALUE_NONE, "Keep resuming migrations even if one fails") + ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question.") + ->addOption('no-transactions', 'u', InputOption::VALUE_NONE, "Do not use a repository transaction to wrap each migration. Unsafe, but needed for legacy slot handlers") + ->addOption('migration', 'm', InputOption::VALUE_REQUIRED, 'A single migration to resume (plain migration name).', null) + ->setHelp(<<kaliop:migration:resume command allows you to resume any suspended migration +EOT + ); + } + + /** + * Execute the command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return null|int null or 0 if everything went fine, or an error code + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $start = microtime(true); + + $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output); + + $migrationService = $this->getMigrationService(); + + $migrationName = $input->getOption('migration'); + if ($migrationName != null) { + $suspendedMigration = $migrationService->getMigration($migrationName); + if (!$suspendedMigration) { + throw new \Exception("Migration '$migrationName' not found"); + } + if ($suspendedMigration->status != Migration::STATUS_SUSPENDED) { + throw new \Exception("Migration '$migrationName' is not suspended, can not resume it"); + } + + $suspendedMigrations = array($suspendedMigration); + } else { + $suspendedMigrations = $migrationService->getMigrationsByStatus(Migration::STATUS_SUSPENDED); + }; + + $output->writeln('Found ' . count($suspendedMigrations) . ' suspended migrations'); + + if (!count($suspendedMigrations)) { + $output->writeln('Nothing to do'); + return; + } + + // ask user for confirmation to make changes + if ($input->isInteractive() && !$input->getOption('no-interaction')) { + $dialog = $this->getHelperSet()->get('dialog'); + if (!$dialog->askConfirmation( + $output, + 'Careful, the database will be modified. Do you want to continue Y/N ?', + false + ) + ) { + $output->writeln('Migration resuming cancelled!'); + return 0; + } + } + + $executed = 0; + $failed = 0; + + foreach($suspendedMigrations as $suspendedMigration) { + $output->writeln("Resuming {$suspendedMigration->name}"); + + try { + $migrationService->resumeMigration($suspendedMigration, !$input->getOption('no-transactions')); + + $executed++; + } catch (\Exception $e) { + if ($input->getOption('ignore-failures')) { + $output->writeln("\nMigration failed! Reason: " . $e->getMessage() . "\n"); + $failed++; + continue; + } + $output->writeln("\nMigration aborted! Reason: " . $e->getMessage() . ""); + return 1; + } + } + + $time = microtime(true) - $start; + $output->writeln("Resumed $executed migrations, failed $failed"); + $output->writeln("Time taken: ".sprintf('%.2f', $time)." secs, memory: ".sprintf('%.2f', (memory_get_peak_usage(true) / 1000000)). ' MB'); + } +} diff --git a/Command/StatusCommand.php b/Command/StatusCommand.php index e8eda8e8..93926c2d 100644 --- a/Command/StatusCommand.php +++ b/Command/StatusCommand.php @@ -15,16 +15,14 @@ */ class StatusCommand extends AbstractCommand { + const STATUS_INVALID = -1; + protected function configure() { $this->setName('kaliop:migration:status') ->setDescription('View the status of a set of migrations.') - ->addOption( - 'path', - null, - InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, - "The directory or file to load the migration definitions from" - ) + ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "The directory or file to load the migration definitions from") + ->addOption('summary', null, InputOption::VALUE_NONE, "Only print summary information") ->setHelp(<<kaliop:migration:status command displays the status of all available migrations: @@ -61,6 +59,7 @@ public function execute(InputInterface $input, OutputInterface $output) $index[$migration->name] = array('migration' => $migration); // no definition, but a migration is there. Check if the definition sits elsewhere on disk than we expect it to be... + // q: what if we have a loader which does not work with is_file? Could we remove this check? if ($migration->path != '' && is_file($migration->path)) { try { $migrationDefinitionCollection = $migrationsService->getMigrationsDefinitions(array($migration->path)); @@ -75,13 +74,25 @@ public function execute(InputInterface $input, OutputInterface $output) } ksort($index); - if (count($index) > 50000) { - $output->writeln("WARNING: printing the status table might take a while as it contains many rows. Please wait..."); + if (!$input->getOption('summary')) { + if (count($index) > 50000) { + $output->writeln("WARNING: printing the status table might take a while as it contains many rows. Please wait..."); + } + $output->writeln("\n == All Migrations\n"); } - $output->writeln("\n == All Migrations\n"); - + $summary = array( + self::STATUS_INVALID => array('Invalid', 0), + Migration::STATUS_TODO => array('To do', 0), + Migration::STATUS_STARTED => array('Started', 0), + Migration::STATUS_DONE => array('Done', 0), + Migration::STATUS_SUSPENDED => array('Suspended', 0), + Migration::STATUS_FAILED => array('Failed', 0), + Migration::STATUS_SKIPPED => array('Skipped', 0), + Migration::STATUS_PARTIALLY_DONE => array('Partially done', 0), + ); $data = array(); + $i = 1; foreach ($index as $name => $value) { if (!isset($value['migration'])) { @@ -89,6 +100,9 @@ public function execute(InputInterface $input, OutputInterface $output) $notes = ''; if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) { $notes = '' . $migrationDefinition->parsingError . ''; + $summary[self::STATUS_INVALID][1]++; + } else { + $summary[Migration::STATUS_TODO][1]++; } $data[] = array( $i++, @@ -99,6 +113,15 @@ public function execute(InputInterface $input, OutputInterface $output) ); } else { $migration = $value['migration']; + + if (!isset($summary[$migration->status])) { + $summary[$migration->status] = array($migration->status, 0); + } + $summary[$migration->status][1]++; + if ($input->getOption('summary')) { + continue; + } + switch ($migration->status) { case Migration::STATUS_DONE: $status = 'executed'; @@ -116,6 +139,9 @@ public function execute(InputInterface $input, OutputInterface $output) case Migration::STATUS_PARTIALLY_DONE: $status = 'partially executed'; break; + case Migration::STATUS_SUSPENDED: + $status = 'suspended'; + break; case Migration::STATUS_FAILED: $status = 'failed'; break; @@ -146,9 +172,19 @@ public function execute(InputInterface $input, OutputInterface $output) } } + if ($input->getOption('summary')) { + $output->writeln("\n == Migrations Summary\n"); + // do not print info about the not yet supported case + unset($summary[Migration::STATUS_PARTIALLY_DONE]); + $data = $summary; + $headers = array('Status', 'Count'); + } else { + $headers = array('#', 'Migration', 'Status', 'Executed on', 'Notes'); + } + $table = $this->getHelperSet()->get('table'); $table - ->setHeaders(array('#', 'Migration', 'Status', 'Executed on', 'Notes')) + ->setHeaders($headers) ->setRows($data); $table->render($output); } diff --git a/Core/ComplexField/AbstractComplexField.php b/Core/ComplexField/AbstractComplexField.php index 12755b23..e18b0c93 100644 --- a/Core/ComplexField/AbstractComplexField.php +++ b/Core/ComplexField/AbstractComplexField.php @@ -2,22 +2,13 @@ namespace Kaliop\eZMigrationBundle\Core\ComplexField; -use Kaliop\eZMigrationBundle\API\ReferenceResolverInterface; -use Kaliop\eZMigrationBundle\API\ComplexFieldInterface; -abstract class AbstractComplexField implements ComplexFieldInterface -{ - /** @var ReferenceResolverInterface $referenceResolver */ - protected $referenceResolver; - - public function setReferenceResolver(ReferenceResolverInterface $referenceResolver) - { - $this->referenceResolver = $referenceResolver; - } +use Kaliop\eZMigrationBundle\Core\FieldHandler\AbstractFieldHandler; - /// BC - public function createValue($fieldValue, array $context = array()) - { - return $this->hashToFieldValue($fieldValue, $context); - } +/** + * Kept around for BC, will be removed in a future release + * @deprecated + */ +abstract class AbstractComplexField extends AbstractFieldHandler +{ } diff --git a/Core/ComplexField/ComplexFieldManager.php b/Core/ComplexField/ComplexFieldManager.php index 31d40dfc..2569bdc5 100644 --- a/Core/ComplexField/ComplexFieldManager.php +++ b/Core/ComplexField/ComplexFieldManager.php @@ -2,169 +2,12 @@ namespace Kaliop\eZMigrationBundle\Core\ComplexField; -use Kaliop\eZMigrationBundle\API\ComplexFieldInterface; -use Kaliop\eZMigrationBundle\API\FieldValueImporterInterface; -use Kaliop\eZMigrationBundle\API\FieldDefinitionConverterInterface; -use Kaliop\eZMigrationBundle\API\FieldValueConverterInterface; -use eZ\Publish\API\Repository\FieldTypeService; +use Kaliop\eZMigrationBundle\Core\FieldHandlerManager; -class ComplexFieldManager +/** + * Kept around for BC, will be removed in a future release + * @deprecated + */ +class ComplexFieldManager extends FieldHandlerManager { - /** @var FieldValueImporterInterface[][] */ - protected $fieldTypeMap; - protected $fieldTypeService; - - public function __construct(FieldTypeService $fieldTypeService) - { - $this->fieldTypeService = $fieldTypeService; - } - - /** - * @param FieldValueImporterInterface|ComplexFieldInterface $complexField - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @throws \Exception - */ - public function addComplexField($complexField, $fieldTypeIdentifier, $contentTypeIdentifier = null) - { - // This is purely BC; at some point we will typehint to FieldValueImporterInterface - if ((!$complexField instanceof ComplexFieldInterface) && (!$complexField instanceof FieldValueImporterInterface)) { - throw new \Exception("Can not register object of class '" . get_class($complexField) . "' as complex field handler because it does not support the desired interface"); - } - - if ($contentTypeIdentifier == null) { - $contentTypeIdentifier = '*'; - } - $this->fieldTypeMap[$contentTypeIdentifier][$fieldTypeIdentifier] = $complexField; - } - - /** - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @return bool - */ - public function managesField($fieldTypeIdentifier, $contentTypeIdentifier) - { - return (isset($this->fieldTypeMap[$contentTypeIdentifier][$fieldTypeIdentifier]) || - isset($this->fieldTypeMap['*'][$fieldTypeIdentifier])); - } - - /** - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @param mixed $hashValue - * @param array $context - * @return mixed - */ - public function hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $hashValue, array $context = array()) - { - if ($this->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { - $fieldHandler = $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier); - // BC - if (!$fieldHandler instanceof FieldValueImporterInterface) { - return $fieldHandler->createValue($hashValue, $context); - } - return $fieldHandler->hashToFieldValue($hashValue, $context); - } - - $fieldType = $this->fieldTypeService->getFieldType($fieldTypeIdentifier); - return $fieldType->fromHash($hashValue); - } - - /** - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @param \eZ\Publish\SPI\FieldType\Value $value - * @param array $context - * @return mixed - */ - public function fieldValueToHash($fieldTypeIdentifier, $contentTypeIdentifier, $value, array $context = array()) - { - if ($this->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { - $fieldHandler = $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier); - if ($fieldHandler instanceof FieldValueConverterInterface) { - return $fieldHandler->fieldValueToHash($value, $context); - } - } - - $fieldType = $this->fieldTypeService->getFieldType($fieldTypeIdentifier); - return $fieldType->toHash($value); - } - - /** - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @return bool - */ - public function managesFieldDefinition($fieldTypeIdentifier, $contentTypeIdentifier) - { - if (!$this->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { - return false; - } - - $fieldHandler = $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier); - return ($fieldHandler instanceof FieldDefinitionConverterInterface); - } - - /** - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @param mixed $fieldSettingsHash - * @param array $context - * @return mixed - */ - public function hashToFieldSettings($fieldTypeIdentifier, $contentTypeIdentifier, $fieldSettingsHash, array $context = array()) - { - if ($this->managesFieldDefinition($fieldTypeIdentifier, $contentTypeIdentifier)) { - return $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier)->hashToFieldSettings($fieldSettingsHash, $context); - } - - return $fieldSettingsHash; - } - - /** - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @param mixed $fieldSettings - * @param array $context - * @return mixed - */ - public function fieldSettingsToHash($fieldTypeIdentifier, $contentTypeIdentifier, $fieldSettings, array $context = array()) - { - if ($this->managesFieldDefinition($fieldTypeIdentifier, $contentTypeIdentifier)) { - return $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier)->fieldSettingsToHash($fieldSettings, $context); - } - - return $fieldSettings; - } - - /** - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @return FieldValueImporterInterface|ComplexFieldInterface - * @throws \Exception - */ - protected function getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier) { - if (isset($this->fieldTypeMap[$contentTypeIdentifier][$fieldTypeIdentifier])) { - return $this->fieldTypeMap[$contentTypeIdentifier][$fieldTypeIdentifier]; - } else if (isset($this->fieldTypeMap['*'][$fieldTypeIdentifier])) { - return $this->fieldTypeMap['*'][$fieldTypeIdentifier]; - } - - throw new \Exception("No complex field handler registered for field '$fieldTypeIdentifier' in content type '$contentTypeIdentifier'"); - } - - /** - * @param string $fieldTypeIdentifier - * @param string $contentTypeIdentifier - * @param mixed $fieldValue as gotten from a migration definition - * @param array $context - * @return mixed as usable in a Content create/update struct - * - * @deprecated BC - */ - public function getComplexFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $fieldValue, array $context = array()) - { - return $this->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $fieldValue, $context); - } -} +} \ No newline at end of file diff --git a/Core/ContextHandler.php b/Core/ContextHandler.php new file mode 100644 index 00000000..fd17b812 --- /dev/null +++ b/Core/ContextHandler.php @@ -0,0 +1,66 @@ +storageHandler = $storageHandler; + } + + /** + * @param ContextProviderInterface $contextProvider + * @param string $label + */ + public function addProvider(ContextProviderInterface $contextProvider, $label) + { + $this->providers[$label] = $contextProvider; + } + + /** + * @param string $migrationName + */ + public function storeCurrentContext($migrationName) + { + $context = array(); + foreach($this->providers as $label => $provider) { + $context[$label] = $provider->getCurrentContext($migrationName); + } + + $this->storageHandler->storeMigrationContext($migrationName, $context); + } + + /** + * @param string $migrationName + * @throws \Exception + */ + public function restoreCurrentContext($migrationName) + { + $context = $this->storageHandler->loadMigrationContext($migrationName); + if (!is_array($context)) { + throw new \Exception("No execution context found associated with migration '$migrationName'"); + } + foreach($this->providers as $label => $provider) { + if (isset($context[$label])) { + $provider->restoreContext($migrationName, $context[$label]); + } + } + } + + public function deleteContext($migrationName) + { + $this->storageHandler->deleteMigrationContext($migrationName); + } +} diff --git a/Core/EventListener/TracingStepExecutedListener.php b/Core/EventListener/TracingStepExecutedListener.php index 13334d7e..fbbe0aa6 100644 --- a/Core/EventListener/TracingStepExecutedListener.php +++ b/Core/EventListener/TracingStepExecutedListener.php @@ -4,6 +4,7 @@ use Kaliop\eZMigrationBundle\API\Event\StepExecutedEvent; use Kaliop\eZMigrationBundle\API\Event\MigrationAbortedEvent; +use Kaliop\eZMigrationBundle\API\Event\MigrationSuspendedEvent; use \Kaliop\eZMigrationBundle\API\Collection\AbstractCollection; use Symfony\Component\Console\Output\OutputInterface; @@ -92,6 +93,25 @@ public function onMigrationAborted(MigrationAbortedEvent $event) } } + public function onMigrationSuspended(MigrationSuspendedEvent $event) + { + $type = $event->getStep()->type; + $dsl = $event->getStep()->dsl; + if (isset($dsl['mode'])) { + $type .= '/' . $dsl['mode']; + } + + $out = "migration suspended during execution of step '$type'. Message: " . $event->getException()->getMessage(); + + if ($this->output) { + if ($this->output->getVerbosity() >= $this->minVerbosityLevel) { + $this->output->writeln($out); + } + } else { + echo $out . "\n"; + } + } + protected function getObjectIdentifierAsString($objOrCollection) { if ($objOrCollection instanceof AbstractCollection || is_array($objOrCollection)) { diff --git a/Core/Executor/ContentManager.php b/Core/Executor/ContentManager.php index 09038701..8568d192 100644 --- a/Core/Executor/ContentManager.php +++ b/Core/Executor/ContentManager.php @@ -10,7 +10,7 @@ use eZ\Publish\Core\Base\Exceptions\NotFoundException; use Kaliop\eZMigrationBundle\API\Collection\ContentCollection; use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface; -use Kaliop\eZMigrationBundle\Core\ComplexField\ComplexFieldManager; +use Kaliop\eZMigrationBundle\Core\FieldHandlerManager; use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher; use Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher; use Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher; @@ -32,7 +32,7 @@ class ContentManager extends RepositoryExecutor implements MigrationGeneratorInt protected $sectionMatcher; protected $userMatcher; protected $objectStateMatcher; - protected $complexFieldManager; + protected $fieldHandlerManager; protected $locationManager; protected $sortConverter; @@ -41,7 +41,7 @@ public function __construct( SectionMatcher $sectionMatcher, UserMatcher $userMatcher, ObjectStateMatcher $objectStateMatcher, - ComplexFieldManager $complexFieldManager, + FieldHandlerManager $fieldHandlerManager, LocationManager $locationManager, SortConverter $sortConverter ) { @@ -49,7 +49,7 @@ public function __construct( $this->sectionMatcher = $sectionMatcher; $this->userMatcher = $userMatcher; $this->objectStateMatcher = $objectStateMatcher; - $this->complexFieldManager = $complexFieldManager; + $this->fieldHandlerManager = $fieldHandlerManager; $this->locationManager = $locationManager; $this->sortConverter = $sortConverter; } @@ -57,63 +57,63 @@ public function __construct( /** * Handles the content create migration action type */ - protected function create() + protected function create($step) { $contentService = $this->repository->getContentService(); $locationService = $this->repository->getLocationService(); $contentTypeService = $this->repository->getContentTypeService(); - $contentTypeIdentifier = $this->dsl['content_type']; + $contentTypeIdentifier = $step->dsl['content_type']; $contentTypeIdentifier = $this->referenceResolver->resolveReference($contentTypeIdentifier); /// @todo use a contenttypematcher $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier); - $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode()); + $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode($step)); - $this->setFields($contentCreateStruct, $this->dsl['attributes'], $contentType); + $this->setFields($contentCreateStruct, $step->dsl['attributes'], $contentType, $step); - if (isset($this->dsl['always_available'])) { - $contentCreateStruct->alwaysAvailable = $this->dsl['always_available']; + if (isset($step->dsl['always_available'])) { + $contentCreateStruct->alwaysAvailable = $step->dsl['always_available']; } else { // Could be removed when https://github.com/ezsystems/ezpublish-kernel/pull/1874 is merged, // but we strive to support old eZ kernel versions as well... $contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable; } - if (isset($this->dsl['remote_id'])) { - $contentCreateStruct->remoteId = $this->dsl['remote_id']; + if (isset($step->dsl['remote_id'])) { + $contentCreateStruct->remoteId = $step->dsl['remote_id']; } - if (isset($this->dsl['section'])) { - $sectionKey = $this->referenceResolver->resolveReference($this->dsl['section']); + if (isset($step->dsl['section'])) { + $sectionKey = $this->referenceResolver->resolveReference($step->dsl['section']); $section = $this->sectionMatcher->matchOneByKey($sectionKey); $contentCreateStruct->sectionId = $section->id; } - if (isset($this->dsl['owner'])) { - $owner = $this->getUser($this->dsl['owner']); + if (isset($step->dsl['owner'])) { + $owner = $this->getUser($step->dsl['owner']); $contentCreateStruct->ownerId = $owner->id; } // This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version. // We allow it, hoping that nothing gets broken because of it - if (isset($this->dsl['version_creator'])) { + if (isset($step->dsl['version_creator'])) { $realContentOwnerId = $contentCreateStruct->ownerId; if ($realContentOwnerId == null) { $realContentOwnerId = $this->repository->getCurrentUser()->id; } - $versionCreator = $this->getUser($this->dsl['version_creator']); + $versionCreator = $this->getUser($step->dsl['version_creator']); $contentCreateStruct->ownerId = $versionCreator->id; } - if (isset($this->dsl['modification_date'])) { - $contentCreateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']); + if (isset($step->dsl['modification_date'])) { + $contentCreateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']); } // instantiate a location create struct from the parent location: // BC - $locationId = isset($this->dsl['parent_location']) ? $this->dsl['parent_location'] : ( - isset($this->dsl['main_location']) ? $this->dsl['main_location'] : null + $locationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : ( + isset($step->dsl['main_location']) ? $step->dsl['main_location'] : null ); // 1st resolve references $locationId = $this->referenceResolver->resolveReference($locationId); @@ -121,26 +121,26 @@ protected function create() $locationId = $this->locationManager->matchLocationByKey($locationId)->id; $locationCreateStruct = $locationService->newLocationCreateStruct($locationId); - if (isset($this->dsl['location_remote_id'])) { - $locationCreateStruct->remoteId = $this->dsl['location_remote_id']; + if (isset($step->dsl['location_remote_id'])) { + $locationCreateStruct->remoteId = $step->dsl['location_remote_id']; } - if (isset($this->dsl['priority'])) { - $locationCreateStruct->priority = $this->dsl['priority']; + if (isset($step->dsl['priority'])) { + $locationCreateStruct->priority = $step->dsl['priority']; } - if (isset($this->dsl['is_hidden'])) { - $locationCreateStruct->hidden = $this->dsl['is_hidden']; + if (isset($step->dsl['is_hidden'])) { + $locationCreateStruct->hidden = $step->dsl['is_hidden']; } - if (isset($this->dsl['sort_field'])) { - $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($this->dsl['sort_field']); + if (isset($step->dsl['sort_field'])) { + $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($step->dsl['sort_field']); } else { $locationCreateStruct->sortField = $contentType->defaultSortField; } - if (isset($this->dsl['sort_order'])) { - $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($this->dsl['sort_order']); + if (isset($step->dsl['sort_order'])) { + $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($step->dsl['sort_order']); } else { $locationCreateStruct->sortOrder = $contentType->defaultSortOrder; } @@ -148,8 +148,8 @@ protected function create() $locations = array($locationCreateStruct); // BC - $other_locations = isset($this->dsl['other_parent_locations']) ? $this->dsl['other_parent_locations'] : ( - isset($this->dsl['other_locations']) ? $this->dsl['other_locations'] : null + $other_locations = isset($step->dsl['other_parent_locations']) ? $step->dsl['other_parent_locations'] : ( + isset($step->dsl['other_locations']) ? $step->dsl['other_locations'] : null ); if (isset($other_locations)) { foreach ($other_locations as $locationId) { @@ -164,19 +164,19 @@ protected function create() $draft = $contentService->createContent($contentCreateStruct, $locations); $content = $contentService->publishVersion($draft->versionInfo); - if (isset($this->dsl['object_states'])) { - $this->setObjectStates($content, $this->dsl['object_states']); + if (isset($step->dsl['object_states'])) { + $this->setObjectStates($content, $step->dsl['object_states']); } // 2nd part of the hack: re-set the content owner to its intended value - if (isset($this->dsl['version_creator']) || isset($this->dsl['publication_date'])) { + if (isset($step->dsl['version_creator']) || isset($step->dsl['publication_date'])) { $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct(); - if (isset($this->dsl['version_creator'])) { + if (isset($step->dsl['version_creator'])) { $contentMetaDataUpdateStruct->ownerId = $realContentOwnerId; } - if (isset($this->dsl['publication_date'])) { - $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($this->dsl['publication_date']); + if (isset($step->dsl['publication_date'])) { + $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']); } // we have to do this to make sure we preserve the custom modification date if (isset($this->dsl['modification_date'])) { @@ -186,20 +186,21 @@ protected function create() $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct); } - $this->setReferences($content); + $this->setReferences($content, $step); return $content; } - protected function load() + protected function load($step) { - $contentCollection = $this->matchContents('load'); + $contentCollection = $this->matchContents('load', $step); - if (count($contentCollection) > 1 && isset($this->dsl['references'])) { + // This check is already done in setReferences + /*if (count($contentCollection) > 1 && isset($step->dsl['references'])) { throw new \Exception("Can not execute Content load because multiple contents match, and a references section is specified in the dsl. References can be set when only 1 content matches"); - } + }*/ - $this->setReferences($contentCollection); + $this->setReferences($contentCollection, $step); return $contentCollection; } @@ -209,14 +210,14 @@ protected function load() * * @todo handle updating of more metadata fields */ - protected function update() + protected function update($step) { $contentService = $this->repository->getContentService(); $contentTypeService = $this->repository->getContentTypeService(); - $contentCollection = $this->matchContents('update'); + $contentCollection = $this->matchContents('update', $step); - if (count($contentCollection) > 1 && isset($this->dsl['references'])) { + if (count($contentCollection) > 1 && isset($step->dsl['references'])) { throw new \Exception("Can not execute Content update because multiple contents match, and a references section is specified in the dsl. References can be set when only 1 content matches"); } @@ -231,63 +232,63 @@ protected function update() $contentUpdateStruct = $contentService->newContentUpdateStruct(); - if (isset($this->dsl['attributes'])) { - $this->setFields($contentUpdateStruct, $this->dsl['attributes'], $contentType); + if (isset($step->dsl['attributes'])) { + $this->setFields($contentUpdateStruct, $step->dsl['attributes'], $contentType, $step); } $versionCreator = null; - if (isset($this->dsl['version_creator'])) { - $versionCreator = $this->getUser($this->dsl['version_creator']); + if (isset($step->dsl['version_creator'])) { + $versionCreator = $this->getUser($step->dsl['version_creator']); } $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator); $contentService->updateContent($draft->versionInfo, $contentUpdateStruct); $content = $contentService->publishVersion($draft->versionInfo); - if (isset($this->dsl['always_available']) || - isset($this->dsl['new_remote_id']) || - isset($this->dsl['owner']) || - isset($this->dsl['modification_date']) || - isset($this->dsl['publication_date'])) { + if (isset($step->dsl['always_available']) || + isset($step->dsl['new_remote_id']) || + isset($step->dsl['owner']) || + isset($step->dsl['modification_date']) || + isset($step->dsl['publication_date'])) { $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct(); - if (isset($this->dsl['always_available'])) { - $contentMetaDataUpdateStruct->alwaysAvailable = $this->dsl['always_available']; + if (isset($step->dsl['always_available'])) { + $contentMetaDataUpdateStruct->alwaysAvailable = $step->dsl['always_available']; } - if (isset($this->dsl['new_remote_id'])) { - $contentMetaDataUpdateStruct->remoteId = $this->dsl['new_remote_id']; + if (isset($step->dsl['new_remote_id'])) { + $contentMetaDataUpdateStruct->remoteId = $step->dsl['new_remote_id']; } - if (isset($this->dsl['owner'])) { - $owner = $this->getUser($this->dsl['owner']); + if (isset($step->dsl['owner'])) { + $owner = $this->getUser($step->dsl['owner']); $contentMetaDataUpdateStruct->ownerId = $owner->id; } - if (isset($this->dsl['modification_date'])) { - $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']); + if (isset($step->dsl['modification_date'])) { + $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']); } - if (isset($this->dsl['publication_date'])) { - $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($this->dsl['publication_date']); + if (isset($step->dsl['publication_date'])) { + $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']); } $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct); } - if (isset($this->dsl['section'])) { - $this->setSection($content, $this->dsl['section']); + if (isset($step->dsl['section'])) { + $this->setSection($content, $step->dsl['section']); } - if (isset($this->dsl['object_states'])) { - $this->setObjectStates($content, $this->dsl['object_states']); + if (isset($step->dsl['object_states'])) { + $this->setObjectStates($content, $step->dsl['object_states']); } $contentCollection[$key] = $content; } - $this->setReferences($contentCollection); + $this->setReferences($contentCollection, $step); return $contentCollection; } @@ -295,11 +296,11 @@ protected function update() /** * Handles the content delete migration action type */ - protected function delete() + protected function delete($step) { $contentService = $this->repository->getContentService(); - $contentCollection = $this->matchContents('delete'); + $contentCollection = $this->matchContents('delete', $step); foreach ($contentCollection as $content) { try { @@ -318,23 +319,26 @@ protected function delete() * @return ContentCollection * @throws \Exception */ - protected function matchContents($action) + protected function matchContents($action, $step) { - if (!isset($this->dsl['object_id']) && !isset($this->dsl['remote_id']) && !isset($this->dsl['match'])) { + if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) { throw new \Exception("The id or remote id of an object or a match condition is required to $action a location"); } // Backwards compat - if (!isset($this->dsl['match'])) { - if (isset($this->dsl['object_id'])) { - $this->dsl['match'] = array('content_id' => $this->dsl['object_id']); - } elseif (isset($this->dsl['remote_id'])) { - $this->dsl['match'] = array('content_remote_id' => $this->dsl['remote_id']); + + if (isset($step->dsl['match'])) { + $match = $step->dsl['match']; + } else { + if (isset($step->dsl['object_id'])) { + $match = array('content_id' => $step->dsl['object_id']); + } elseif (isset($step->dsl['remote_id'])) { + $match = array('content_remote_id' => $step->dsl['remote_id']); } } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($match); return $this->contentMatcher->match($match); } @@ -346,22 +350,25 @@ protected function matchContents($action) * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute * @return boolean * - * @todo add support for other attributes: contentTypeId, contentTypeIdentifier, section, etc... ? + * @todo add support for other attributes: object_states etc... ? */ - protected function setReferences($content) + protected function setReferences($content, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } if ($content instanceof ContentCollection) { if (count($content) > 1) { - throw new \InvalidArgumentException('Content Manager does not support setting references for creating/updating of multiple contents'); + throw new \InvalidArgumentException('Content Manager does not support setting references for creating/updating/loading of multiple contents'); + } + if (count($content) == 0) { + throw new \InvalidArgumentException('Content Manager does not support setting references for creating/updating/loading of no contents'); } $content = reset($content); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'object_id': @@ -413,6 +420,10 @@ protected function setReferences($content) case 'section_id': $value = $content->contentInfo->sectionId; break; + case 'section_identifier': + $sectionService = $this->repository->getSectionService(); + $value = $sectionService->loadSection($content->contentInfo->sectionId)->identifier; + break; default: // allow to get the value of fields as well as their sub-parts if (strpos($reference['attribute'], 'attributes.') === 0) { @@ -425,7 +436,7 @@ protected function setReferences($content) $fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]); $field = $content->getField($fieldIdentifier); $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier); - $hashValue = $this->complexFieldManager->fieldValueToHash( + $hashValue = $this->fieldHandlerManager->fieldValueToHash( $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value ); if (is_array($hashValue) ) { @@ -445,7 +456,11 @@ protected function setReferences($content) throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; @@ -454,6 +469,7 @@ protected function setReferences($content) /** * @param array $matchCondition * @param string $mode + * @param array $context * @throws \Exception * @return array * @@ -461,9 +477,9 @@ protected function setReferences($content) * @todo add 2ndary locations when in 'update' mode * @todo add dumping of sort_field and sort_order for 2ndary locations */ - public function generateMigration(array $matchCondition, $mode) + public function generateMigration(array $matchCondition, $mode, array $context = array()) { - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context)); $contentCollection = $this->contentMatcher->match($matchCondition); $data = array(); @@ -535,9 +551,9 @@ public function generateMigration(array $matchCondition, $mode) if ($mode != 'delete') { $attributes = array(); - foreach ($content->getFieldsByLanguage($this->getLanguageCode()) as $fieldIdentifier => $field) { + foreach ($content->getFieldsByLanguage($this->getLanguageCodeFromContext($context)) as $fieldIdentifier => $field) { $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier); - $attributes[$field->fieldDefIdentifier] = $this->complexFieldManager->fieldValueToHash( + $attributes[$field->fieldDefIdentifier] = $this->fieldHandlerManager->fieldValueToHash( $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value ); } @@ -545,7 +561,7 @@ public function generateMigration(array $matchCondition, $mode) $contentData = array_merge( $contentData, array( - 'lang' => $this->getLanguageCode(), + 'lang' => $this->getLanguageCodeFromContext($context), 'section' => $content->contentInfo->sectionId, 'owner' => $content->contentInfo->ownerId, 'modification_date' => $content->contentInfo->modificationDate->getTimestamp(), @@ -567,11 +583,12 @@ public function generateMigration(array $matchCondition, $mode) * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings. * * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct - * @param ContentType $contentType * @param array $fields see description of expected format in code below + * @param ContentType $contentType + * @param $step * @throws \Exception */ - protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType) + protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step) { $i = 0; // the 'easy' yml: key = field name, value = value @@ -594,9 +611,9 @@ protected function setFields($createOrUpdateStruct, array $fields, ContentType $ } $fieldDefinition = $contentType->fieldDefinitionsByIdentifier[$fieldIdentifier]; - $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $this->context); + $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context); - $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $this->getLanguageCode()); + $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $this->getLanguageCode($step)); $i++; } @@ -636,11 +653,11 @@ protected function setObjectStates(Content $content, array $stateKeys) protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array()) { $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier; - if (is_array($value) || $this->complexFieldManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { + if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { // inject info about the current content type and field into the context $context['contentTypeIdentifier'] = $contentTypeIdentifier; $context['fieldIdentifier'] = $fieldDefinition->identifier; - return $this->complexFieldManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context); + return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context); } return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context); diff --git a/Core/Executor/ContentTypeGroupManager.php b/Core/Executor/ContentTypeGroupManager.php index 729093d5..c9d13131 100644 --- a/Core/Executor/ContentTypeGroupManager.php +++ b/Core/Executor/ContentTypeGroupManager.php @@ -30,32 +30,32 @@ public function __construct(ContentTypeGroupMatcher $contentTypeGroupMatcher) * @throws \Exception * @todo add support for setting creator id */ - protected function create() + protected function create($step) { - if (!isset($this->dsl['identifier'])) { + if (!isset($step->dsl['identifier'])) { throw new \Exception("The 'identifier' key is required to create a new content type group."); } $contentTypeService = $this->repository->getContentTypeService(); - $createStruct = $contentTypeService->newContentTypeGroupCreateStruct($this->dsl['identifier']); + $createStruct = $contentTypeService->newContentTypeGroupCreateStruct($step->dsl['identifier']); - if (isset($this->dsl['creation_date'])) { - $createStruct->creationDate = $this->toDateTime($this->dsl['creation_date']); + if (isset($step->dsl['creation_date'])) { + $createStruct->creationDate = $this->toDateTime($step->dsl['creation_date']); } $group = $contentTypeService->createContentTypeGroup($createStruct); - $this->setReferences($group); + $this->setReferences($group, $step); return $group; } - protected function update() + protected function update($step) { - $groupsCollection = $this->matchContentTypeGroups('update'); + $groupsCollection = $this->matchContentTypeGroups('update', $step); - if (count($groupsCollection) > 1 && array_key_exists('references', $this->dsl)) { + if (count($groupsCollection) > 1 && array_key_exists('references', $step->dsl)) { throw new \Exception("Can not execute Content Type Group update because multiple types match, and a references section is specified in the dsl. References can be set when only 1 matches"); } @@ -64,11 +64,11 @@ protected function update() foreach ($groupsCollection as $key => $contentTypeGroup) { $updateStruct = $contentTypeService->newContentTypeGroupUpdateStruct(); - if (isset($this->dsl['identifier'])) { - $updateStruct->identifier = $this->dsl['identifier']; + if (isset($step->dsl['identifier'])) { + $updateStruct->identifier = $step->dsl['identifier']; } - if (isset($this->dsl['modification_date'])) { - $updateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']); + if (isset($step->dsl['modification_date'])) { + $updateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']); } $contentTypeService->updateContentTypeGroup($contentTypeGroup, $updateStruct); @@ -77,14 +77,14 @@ protected function update() $groupsCollection[$key] = $group; } - $this->setReferences($groupsCollection); + $this->setReferences($groupsCollection, $step); return $groupsCollection; } - protected function delete() + protected function delete($step) { - $groupsCollection = $this->matchContentTypeGroups('delete'); + $groupsCollection = $this->matchContentTypeGroups('delete', $step); $contentTypeService = $this->repository->getContentTypeService(); @@ -100,14 +100,14 @@ protected function delete() * @return ContentTypeGroupCollection * @throws \Exception */ - protected function matchContentTypeGroups($action) + protected function matchContentTypeGroups($action, $step) { - if (!isset($this->dsl['match'])) { + if (!isset($step->dsl['match'])) { throw new \Exception("A match condition is required to $action an object state group"); } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($step->dsl['match']); return $this->contentTypeGroupMatcher->match($match); } @@ -116,9 +116,9 @@ protected function matchContentTypeGroups($action) * @param ContentTypeGroup|ContentTypeGroupCollection $object * @return bool */ - protected function setReferences($object) + protected function setReferences($object, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } @@ -129,7 +129,7 @@ protected function setReferences($object) $object = reset($object); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'content_type_group_id': @@ -144,7 +144,11 @@ protected function setReferences($object) throw new \InvalidArgumentException('Content Type Group Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; @@ -153,12 +157,13 @@ protected function setReferences($object) /** * @param array $matchCondition * @param string $mode + * @param array $context * @throws \Exception * @return array */ - public function generateMigration(array $matchCondition, $mode) + public function generateMigration(array $matchCondition, $mode, array $context = array()) { - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context)); $contentTypeGroupCollection = $this->contentTypeGroupMatcher->match($matchCondition); $data = array(); diff --git a/Core/Executor/ContentTypeManager.php b/Core/Executor/ContentTypeManager.php index 2e0abfe1..fc9c6125 100644 --- a/Core/Executor/ContentTypeManager.php +++ b/Core/Executor/ContentTypeManager.php @@ -4,80 +4,83 @@ use eZ\Publish\API\Repository\ContentTypeService; use eZ\Publish\API\Repository\Values\ContentType\ContentType; +use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition; use Kaliop\eZMigrationBundle\API\Collection\ContentTypeCollection; use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface; use Kaliop\eZMigrationBundle\API\ReferenceResolverInterface; use Kaliop\eZMigrationBundle\Core\Matcher\ContentTypeMatcher; use Kaliop\eZMigrationBundle\Core\Matcher\ContentTypeGroupMatcher; -use Kaliop\eZMigrationBundle\Core\ComplexField\ComplexFieldManager; +use Kaliop\eZMigrationBundle\Core\FieldHandlerManager; +use JmesPath\Env as JmesPath; /** * Handles content type migrations */ class ContentTypeManager extends RepositoryExecutor implements MigrationGeneratorInterface { + protected $supportedActions = array('create', 'load', 'update', 'delete'); protected $supportedStepTypes = array('content_type'); protected $contentTypeMatcher; protected $contentTypeGroupMatcher; // This resolver is used to resolve references in content-type settings definitions protected $extendedReferenceResolver; - protected $complexFieldManager; + protected $fieldHandlerManager; public function __construct(ContentTypeMatcher $matcher, ContentTypeGroupMatcher $contentTypeGroupMatcher, - ReferenceResolverInterface $extendedReferenceResolver, ComplexFieldManager $complexFieldManager) + ReferenceResolverInterface $extendedReferenceResolver, FieldHandlerManager $fieldHandlerManager) { $this->contentTypeMatcher = $matcher; $this->contentTypeGroupMatcher = $contentTypeGroupMatcher; $this->extendedReferenceResolver = $extendedReferenceResolver; - $this->complexFieldManager = $complexFieldManager; + $this->fieldHandlerManager = $fieldHandlerManager; } /** * Method to handle the create operation of the migration instructions */ - protected function create() + protected function create($step) { foreach (array('identifier', 'content_type_group', 'name_pattern', 'name', 'attributes') as $key) { - if (!isset($this->dsl[$key])) { + if (!isset($step->dsl[$key])) { throw new \Exception("The '$key' key is missing in a content type creation definition"); } } $contentTypeService = $this->repository->getContentTypeService(); - $contentTypeGroupId = $this->dsl['content_type_group']; + $contentTypeGroupId = $step->dsl['content_type_group']; $contentTypeGroupId = $this->referenceResolver->resolveReference($contentTypeGroupId); $contentTypeGroup = $this->contentTypeGroupMatcher->matchOneByKey($contentTypeGroupId); - $contentTypeCreateStruct = $contentTypeService->newContentTypeCreateStruct($this->dsl['identifier']); - $contentTypeCreateStruct->mainLanguageCode = $this->getLanguageCode(); + $contentTypeCreateStruct = $contentTypeService->newContentTypeCreateStruct($step->dsl['identifier']); + $contentTypeCreateStruct->mainLanguageCode = $this->getLanguageCode($step); // Object Name pattern - $contentTypeCreateStruct->nameSchema = $this->dsl['name_pattern']; + $contentTypeCreateStruct->nameSchema = $step->dsl['name_pattern']; // set names for the content type $contentTypeCreateStruct->names = array( - $this->getLanguageCode() => $this->dsl['name'], + $this->getLanguageCode($step) => $step->dsl['name'], ); - if (isset($this->dsl['description'])) { + if (isset($step->dsl['description'])) { // set description for the content type $contentTypeCreateStruct->descriptions = array( - $this->getLanguageCode() => $this->dsl['description'], + $this->getLanguageCode($step) => $step->dsl['description'], ); } - if (isset($this->dsl['url_name_pattern'])) { - $contentTypeCreateStruct->urlAliasSchema = $this->dsl['url_name_pattern']; + if (isset($step->dsl['url_name_pattern'])) { + $contentTypeCreateStruct->urlAliasSchema = $step->dsl['url_name_pattern']; } - if (isset($this->dsl['is_container'])) { - $contentTypeCreateStruct->isContainer = $this->dsl['is_container']; + if (isset($step->dsl['is_container'])) { + $contentTypeCreateStruct->isContainer = $step->dsl['is_container']; } - if (isset($this->dsl['default_always_available'])) { - $contentTypeCreateStruct->defaultAlwaysAvailable = $this->dsl['default_always_available']; + if (isset($step->dsl['default_always_available'])) { + $contentTypeCreateStruct->defaultAlwaysAvailable = $step->dsl['default_always_available']; } // Add attributes @@ -85,8 +88,8 @@ protected function create() // We go out of our way to avoid collisions and preserve an order: fields without position go *last* $maxFieldDefinitionPos = 0; $fieldDefinitions = array(); - foreach ($this->dsl['attributes'] as $position => $attribute) { - $fieldDefinition = $this->createFieldDefinition($contentTypeService, $attribute, $this->dsl['identifier']); + foreach ($step->dsl['attributes'] as $position => $attribute) { + $fieldDefinition = $this->createFieldDefinition($contentTypeService, $attribute, $step->dsl['identifier'], $this->getLanguageCode($step)); $maxFieldDefinitionPos = $fieldDefinition->position > $maxFieldDefinitionPos ? $fieldDefinition->position : $maxFieldDefinitionPos; $fieldDefinitions[] = $fieldDefinition; } @@ -103,23 +106,37 @@ protected function create() // Set references $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeDraft->identifier); - $this->setReferences($contentType); + $this->setReferences($contentType, $step); return $contentType; } + protected function load($step) + { + $contentTypeCollection = $this->matchContentTypes('load', $step); + + // This check is already done in setReferences + /*if (count($contentTypeCollection) > 1 && isset($step->dsl['references'])) { + throw new \Exception("Can not execute Content Type load because multiple contents match, and a references section is specified in the dsl. References can be set when only 1 content matches"); + }*/ + + $this->setReferences($contentTypeCollection, $step); + + return $contentTypeCollection; + } + /** * Method to handle the update operation of the migration instructions */ - protected function update() + protected function update($step) { - $contentTypeCollection = $this->matchContentTypes('update'); + $contentTypeCollection = $this->matchContentTypes('update', $step); - if (count($contentTypeCollection) > 1 && array_key_exists('references', $this->dsl)) { + if (count($contentTypeCollection) > 1 && array_key_exists('references', $step->dsl)) { throw new \Exception("Can not execute Content Type update because multiple types match, and a references section is specified in the dsl. References can be set when only 1 type matches"); } - if (count($contentTypeCollection) > 1 && array_key_exists('new_identifier', $this->dsl)) { + if (count($contentTypeCollection) > 1 && array_key_exists('new_identifier', $step->dsl)) { throw new \Exception("Can not execute Content Type update because multiple roles match, and a new_identifier is specified in the dsl."); } @@ -129,48 +146,48 @@ protected function update() $contentTypeDraft = $contentTypeService->createContentTypeDraft($contentType); $contentTypeUpdateStruct = $contentTypeService->newContentTypeUpdateStruct(); - $contentTypeUpdateStruct->mainLanguageCode = $this->getLanguageCode(); + $contentTypeUpdateStruct->mainLanguageCode = $this->getLanguageCode($step); - if (isset($this->dsl['new_identifier'])) { - $contentTypeUpdateStruct->identifier = $this->dsl['new_identifier']; + if (isset($step->dsl['new_identifier'])) { + $contentTypeUpdateStruct->identifier = $step->dsl['new_identifier']; } - if (isset($this->dsl['name'])) { - $contentTypeUpdateStruct->names = array($this->getLanguageCode() => $this->dsl['name']); + if (isset($step->dsl['name'])) { + $contentTypeUpdateStruct->names = array($this->getLanguageCode($step) => $step->dsl['name']); } - if (isset($this->dsl['description'])) { + if (isset($step->dsl['description'])) { $contentTypeUpdateStruct->descriptions = array( - $this->getLanguageCode() => $this->dsl['description'], + $this->getLanguageCode($step) => $step->dsl['description'], ); } - if (isset($this->dsl['name_pattern'])) { - $contentTypeUpdateStruct->nameSchema = $this->dsl['name_pattern']; + if (isset($step->dsl['name_pattern'])) { + $contentTypeUpdateStruct->nameSchema = $step->dsl['name_pattern']; } - if (isset($this->dsl['url_name_pattern'])) { - $contentTypeUpdateStruct->urlAliasSchema = $this->dsl['url_name_pattern']; + if (isset($step->dsl['url_name_pattern'])) { + $contentTypeUpdateStruct->urlAliasSchema = $step->dsl['url_name_pattern']; } - if (isset($this->dsl['is_container'])) { - $contentTypeUpdateStruct->isContainer = $this->dsl['is_container']; + if (isset($step->dsl['is_container'])) { + $contentTypeUpdateStruct->isContainer = $step->dsl['is_container']; } // Add/edit attributes - if (isset($this->dsl['attributes'])) { + if (isset($step->dsl['attributes'])) { // NB: seems like eZ gets mixed up if we pass some attributes with a position and some without... // We go out of our way to avoid collisions and preserve order $maxFieldDefinitionPos = count($contentType->fieldDefinitions); $newFieldDefinitions = array(); - foreach ($this->dsl['attributes'] as $attribute) { + foreach ($step->dsl['attributes'] as $attribute) { $existingFieldDefinition = $this->contentTypeHasFieldDefinition($contentType, $attribute['identifier']); if ($existingFieldDefinition) { // Edit existing attribute $fieldDefinitionUpdateStruct = $this->updateFieldDefinition( - $contentTypeService, $attribute, $attribute['identifier'], $contentType->identifier + $contentTypeService, $attribute, $attribute['identifier'], $contentType->identifier, $this->getLanguageCode($step) ); $contentTypeService->updateFieldDefinition( $contentTypeDraft, @@ -185,7 +202,7 @@ protected function update() } else { // Create new attributes, keep them in temp array - $newFieldDefinition = $this->createFieldDefinition($contentTypeService, $attribute, $contentType->identifier); + $newFieldDefinition = $this->createFieldDefinition($contentTypeService, $attribute, $contentType->identifier, $this->getLanguageCode($step)); $maxFieldDefinitionPos = $newFieldDefinition->position > $maxFieldDefinitionPos ? $newFieldDefinition->position : $maxFieldDefinitionPos; $newFieldDefinitions[] = $newFieldDefinition; } @@ -201,8 +218,8 @@ protected function update() } // Remove attributes - if (isset($this->dsl['remove_attributes'])) { - foreach ($this->dsl['remove_attributes'] as $attribute) { + if (isset($step->dsl['remove_attributes'])) { + foreach ($step->dsl['remove_attributes'] as $attribute) { $existingFieldDefinition = $this->contentTypeHasFieldDefinition($contentType, $attribute); if ($existingFieldDefinition) { $contentTypeService->removeFieldDefinition($contentTypeDraft, $existingFieldDefinition); @@ -214,8 +231,8 @@ protected function update() $contentTypeService->publishContentTypeDraft($contentTypeDraft); // Set references - if (isset($this->dsl['new_identifier'])) { - $contentType = $contentTypeService->loadContentTypeByIdentifier($this->dsl['new_identifier']); + if (isset($step->dsl['new_identifier'])) { + $contentType = $contentTypeService->loadContentTypeByIdentifier($step->dsl['new_identifier']); } else { $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeDraft->identifier); } @@ -223,7 +240,7 @@ protected function update() $contentTypeCollection[$key] = $contentType; } - $this->setReferences($contentTypeCollection); + $this->setReferences($contentTypeCollection, $step); return $contentTypeCollection; } @@ -231,9 +248,9 @@ protected function update() /** * Method to handle the delete operation of the migration instructions */ - protected function delete() + protected function delete($step) { - $contentTypeCollection = $this->matchContentTypes('delete'); + $contentTypeCollection = $this->matchContentTypes('delete', $step); $contentTypeService = $this->repository->getContentTypeService(); @@ -249,19 +266,21 @@ protected function delete() * @return ContentTypeCollection * @throws \Exception */ - protected function matchContentTypes($action) + protected function matchContentTypes($action, $step) { - if (!isset($this->dsl['identifier']) && !isset($this->dsl['match'])) { + if (!isset($step->dsl['identifier']) && !isset($step->dsl['match'])) { throw new \Exception("The identifier of a content type or a match condition is required to $action it"); } // Backwards compat - if (!isset($this->dsl['match'])) { - $this->dsl['match'] = array('identifier' => $this->dsl['identifier']); + if (isset($step->dsl['match'])) { + $match = $step->dsl['match']; + } else { + $match = array('identifier' => $step->dsl['identifier']); } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($match); return $this->contentTypeMatcher->match($match); } @@ -276,20 +295,23 @@ protected function matchContentTypes($action) * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|ContentTypeCollection $contentType * @return bool */ - protected function setReferences($contentType) + protected function setReferences($contentType, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } if ($contentType instanceof ContentTypeCollection) { if (count($contentType) > 1) { - throw new \InvalidArgumentException('Content Type Manager does not support setting references for creating/updating of multiple content types'); + throw new \InvalidArgumentException('Content Type Manager does not support setting references for creating/updating/loading of multiple content types'); + } + if (count($contentType) == 0) { + throw new \InvalidArgumentException('Content Type Manager does not support setting references for creating/updating/loading of no content types'); } $contentType = reset($contentType); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'content_type_id': case 'id': @@ -318,10 +340,29 @@ protected function setReferences($contentType) $value = $contentType->urlAliasSchema; break; default: + // allow to get the value of fields as well as their sub-parts + if (strpos($reference['attribute'], 'attributes.') === 0) { + $parts = explode('.', $reference['attribute']); + // totally not sure if this list of special chars is correct for what could follow a jmespath identifier... + // also what about quoted strings? + $fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]); + $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier); + $hashValue = $this->fieldDefinitionToHash($contentType, $fieldDefinition, $step->context); + if (count($parts) == 2 && $fieldIdentifier === $parts[1]) { + throw new \InvalidArgumentException('Content Type Manager does not support setting references for attribute ' . $reference['attribute'] . ': please specify an attribute definition sub element'); + } + $value = JmesPath::search(implode('.', array_slice($parts, 1)), array($fieldIdentifier => $hashValue)); + break; + } + throw new \InvalidArgumentException('Content Type Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; @@ -331,12 +372,13 @@ protected function setReferences($contentType) /** * @param array $matchCondition * @param string $mode + * @param array $context * @throws \Exception * @return array */ - public function generateMigration(array $matchCondition, $mode) + public function generateMigration(array $matchCondition, $mode, array $context = array()) { - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context)); $contentTypeCollection = $this->contentTypeMatcher->match($matchCondition); $data = array(); @@ -387,53 +429,20 @@ public function generateMigration(array $matchCondition, $mode) if ($mode != 'delete') { - $fieldTypeService = $this->repository->getFieldTypeService(); - $attributes = array(); foreach ($contentType->getFieldDefinitions() as $i => $fieldDefinition) { - $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier; - $attribute = array( - 'identifier' => $fieldDefinition->identifier, - 'type' => $fieldTypeIdentifier, - 'name' => $fieldDefinition->getName($this->getLanguageCode()), - 'description' => (string)$fieldDefinition->getDescription($this->getLanguageCode()), - 'required' => $fieldDefinition->isRequired, - 'searchable' => $fieldDefinition->isSearchable, - 'info-collector' => $fieldDefinition->isInfoCollector, - 'disable-translation' => !$fieldDefinition->isTranslatable, - 'category' => $fieldDefinition->fieldGroup, - // Should we cheat and do like the eZ4 Admin Interface and used sequential numbering 1,2,3... ? - // But what if the end user then edits the 'update' migration and only leaves in it a single - // field position update? He/she might be surprised when executing it... - 'position' => $fieldDefinition->position - ); - - $fieldType = $fieldTypeService->getFieldType($fieldTypeIdentifier); - $nullValue = $fieldType->getEmptyValue(); - if ($fieldDefinition->defaultValue != $nullValue) { - $attribute['default-value'] = $this->complexFieldManager->fieldValueToHash( - $fieldTypeIdentifier, $contentType->identifier, $fieldDefinition->defaultValue - ); - } - - $attribute['field-settings'] = $this->complexFieldManager->fieldSettingsToHash( - $fieldTypeIdentifier, $contentType->identifier, $fieldDefinition->fieldSettings - ); - - $attribute['validator-configuration'] = $fieldDefinition->validatorConfiguration; - - $attributes[] = $attribute; + $attributes[] = $this->fieldDefinitionToHash($contentType, $fieldDefinition, $context); } $contentTypeData = array_merge( $contentTypeData, array( - 'name' => $contentType->getName($this->getLanguageCode()), - 'description' => $contentType->getDescription($this->getLanguageCode()), + 'name' => $contentType->getName($this->getLanguageCodeFromContext($context)), + 'description' => $contentType->getDescription($this->getLanguageCodeFromContext($context)), 'name_pattern' => $contentType->nameSchema, 'url_name_pattern' => $contentType->urlAliasSchema, 'is_container' => $contentType->isContainer, - 'lang' => $this->getLanguageCode(), + 'lang' => $this->getLanguageCodeFromContext($context), 'attributes' => $attributes ) ); @@ -446,6 +455,50 @@ public function generateMigration(array $matchCondition, $mode) return $data; } + /** + * @param ContentType $contentType + * @param FieldDefinition $fieldDefinition + * @param array $context + * @return array + */ + protected function fieldDefinitionToHash(ContentType $contentType, FieldDefinition $fieldDefinition, $context) + { + $fieldTypeService = $this->repository->getFieldTypeService(); + $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier; + + $attribute = array( + 'identifier' => $fieldDefinition->identifier, + 'type' => $fieldTypeIdentifier, + 'name' => $fieldDefinition->getName($this->getLanguageCodeFromContext($context)), + 'description' => (string)$fieldDefinition->getDescription($this->getLanguageCodeFromContext($context)), + 'required' => $fieldDefinition->isRequired, + 'searchable' => $fieldDefinition->isSearchable, + 'info-collector' => $fieldDefinition->isInfoCollector, + 'disable-translation' => !$fieldDefinition->isTranslatable, + 'category' => $fieldDefinition->fieldGroup, + // Should we cheat and do like the eZ4 Admin Interface and used sequential numbering 1,2,3... ? + // But what if the end user then edits the 'update' migration and only leaves in it a single + // field position update? He/she might be surprised when executing it... + 'position' => $fieldDefinition->position + ); + + $fieldType = $fieldTypeService->getFieldType($fieldTypeIdentifier); + $nullValue = $fieldType->getEmptyValue(); + if ($fieldDefinition->defaultValue != $nullValue) { + $attribute['default-value'] = $this->fieldHandlerManager->fieldValueToHash( + $fieldTypeIdentifier, $contentType->identifier, $fieldDefinition->defaultValue + ); + } + + $attribute['field-settings'] = $this->fieldHandlerManager->fieldSettingsToHash( + $fieldTypeIdentifier, $contentType->identifier, $fieldDefinition->fieldSettings + ); + + $attribute['validator-configuration'] = $fieldDefinition->validatorConfiguration; + + return $attribute; + } + /** * Helper function to create field definitions to be added to a new/existing content type. * @@ -453,10 +506,11 @@ public function generateMigration(array $matchCondition, $mode) * @param ContentTypeService $contentTypeService * @param array $attribute * @param string $contentTypeIdentifier + * @param string $lang * @return \eZ\Publish\API\Repository\Values\ContentType\FieldDefinitionCreateStruct * @throws \Exception */ - private function createFieldDefinition(ContentTypeService $contentTypeService, array $attribute, $contentTypeIdentifier) + private function createFieldDefinition(ContentTypeService $contentTypeService, array $attribute, $contentTypeIdentifier, $lang) { if (!isset($attribute['identifier']) || !isset($attribute['type'])) { throw new \Exception("Keys 'type' and 'identifier' are mandatory to define a new field in a field type"); @@ -470,10 +524,10 @@ private function createFieldDefinition(ContentTypeService $contentTypeService, a foreach ($attribute as $key => $value) { switch ($key) { case 'name': - $fieldDefinition->names = array($this->getLanguageCode() => $value); + $fieldDefinition->names = array($lang => $value); break; case 'description': - $fieldDefinition->descriptions = array($this->getLanguageCode() => $value); + $fieldDefinition->descriptions = array($lang => $value); break; case 'required': $fieldDefinition->isRequired = $value; @@ -492,7 +546,7 @@ private function createFieldDefinition(ContentTypeService $contentTypeService, a break; case 'default-value': /// @todo check that this works for all field types. Maybe we should use fromHash() on the field type, - /// or, better, use the complexFieldManager? + /// or, better, use the FieldHandlerManager? $fieldDefinition->defaultValue = $value; break; case 'field-settings': @@ -518,10 +572,11 @@ private function createFieldDefinition(ContentTypeService $contentTypeService, a * @param array $attribute * @param string $fieldTypeIdentifier * @param string $contentTypeIdentifier + * @param string $lang * @return \eZ\Publish\API\Repository\Values\ContentType\FieldDefinitionUpdateStruct * @throws \Exception */ - private function updateFieldDefinition(ContentTypeService $contentTypeService, array $attribute, $fieldTypeIdentifier, $contentTypeIdentifier) + private function updateFieldDefinition(ContentTypeService $contentTypeService, array $attribute, $fieldTypeIdentifier, $contentTypeIdentifier, $lang) { if (!isset($attribute['identifier'])) { throw new \Exception("The 'identifier' of an attribute is missing in the content type update definition."); @@ -535,10 +590,10 @@ private function updateFieldDefinition(ContentTypeService $contentTypeService, a $fieldDefinitionUpdateStruct->identifier = $value; break; case 'name': - $fieldDefinitionUpdateStruct->names = array($this->getLanguageCode() => $value); + $fieldDefinitionUpdateStruct->names = array($lang => $value); break; case 'description': - $fieldDefinitionUpdateStruct->descriptions = array($this->getLanguageCode() => $value); + $fieldDefinitionUpdateStruct->descriptions = array($lang => $value); break; case 'required': $fieldDefinitionUpdateStruct->isRequired = $value; @@ -593,8 +648,8 @@ private function getFieldSettings($value, $fieldTypeIdentifier, $contentTypeIden } // then handle the conversion of the settings from Hash to Repo representation - if ($this->complexFieldManager->managesFieldDefinition($fieldTypeIdentifier, $contentTypeIdentifier)) { - $ret = $this->complexFieldManager->hashToFieldSettings($fieldTypeIdentifier, $contentTypeIdentifier, $value); + if ($this->fieldHandlerManager->managesFieldDefinition($fieldTypeIdentifier, $contentTypeIdentifier)) { + $ret = $this->fieldHandlerManager->hashToFieldSettings($fieldTypeIdentifier, $contentTypeIdentifier, $value); } return $ret; diff --git a/Core/Executor/LanguageManager.php b/Core/Executor/LanguageManager.php index 31bddc7a..b1688734 100644 --- a/Core/Executor/LanguageManager.php +++ b/Core/Executor/LanguageManager.php @@ -15,25 +15,25 @@ class LanguageManager extends RepositoryExecutor /** * Handles the language create migration action */ - protected function create() + protected function create($step) { $languageService = $this->repository->getContentLanguageService(); - if (!isset($this->dsl['lang'])) { + if (!isset($step->dsl['lang'])) { throw new \Exception("The 'lang' key is required to create a new language."); } $languageCreateStruct = $languageService->newLanguageCreateStruct(); - $languageCreateStruct->languageCode = $this->dsl['lang']; - if (isset($this->dsl['name'])) { - $languageCreateStruct->name = $this->dsl['name']; + $languageCreateStruct->languageCode = $step->dsl['lang']; + if (isset($step->dsl['name'])) { + $languageCreateStruct->name = $step->dsl['name']; } - if (isset($this->dsl['enabled'])) { - $languageCreateStruct->enabled = (bool)$this->dsl['enabled']; + if (isset($step->dsl['enabled'])) { + $languageCreateStruct->enabled = (bool)$step->dsl['enabled']; } $language = $languageService->createLanguage($languageCreateStruct); - $this->setReferences($language); + $this->setReferences($language, $step); return $language; } @@ -43,30 +43,30 @@ protected function create() * * @todo use a matcher for flexible matching? */ - protected function update() + protected function update($step) { throw new \Exception('Language update is not implemented yet'); /*$languageService = $this->repository->getContentLanguageService(); - if (!isset($this->dsl['lang'])) { + if (!isset($step->dsl['lang'])) { throw new \Exception("The 'lang' key is required to update a language."); } - $this->setReferences($language);*/ + $this->setReferences($language, $step);*/ } /** * Handles the language delete migration action */ - protected function delete() + protected function delete($step) { - if (!isset($this->dsl['lang'])) { + if (!isset($step->dsl['lang'])) { throw new \Exception("The 'lang' key is required to delete a language."); } $languageService = $this->repository->getContentLanguageService(); - $language = $languageService->loadLanguage($this->dsl['lang']); + $language = $languageService->loadLanguage($step->dsl['lang']); $languageService->deleteLanguage($language); @@ -80,9 +80,9 @@ protected function delete() * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute * @return boolean */ - protected function setReferences($language) + protected function setReferences($language, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } @@ -93,7 +93,7 @@ protected function setReferences($language) $language = reset($language); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'language_id': @@ -114,7 +114,11 @@ protected function setReferences($language) throw new \InvalidArgumentException('Language Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; diff --git a/Core/Executor/LocationManager.php b/Core/Executor/LocationManager.php index ab558465..deb7324e 100644 --- a/Core/Executor/LocationManager.php +++ b/Core/Executor/LocationManager.php @@ -31,19 +31,19 @@ public function __construct(ContentMatcher $contentMatcher, LocationMatcher $loc /** * Method to handle the create operation of the migration instructions */ - protected function create() + protected function create($step) { $locationService = $this->repository->getLocationService(); - if (!isset($this->dsl['parent_location']) && !isset($this->dsl['parent_location_id'])) { + if (!isset($step->dsl['parent_location']) && !isset($step->dsl['parent_location_id'])) { throw new \Exception('Missing parent location id. This is required to create the new location.'); } // support legacy tag: parent_location_id - if (!isset($this->dsl['parent_location']) && isset($this->dsl['parent_location_id'])) { - $parentLocationIds = $this->dsl['parent_location_id']; + if (!isset($step->dsl['parent_location']) && isset($step->dsl['parent_location_id'])) { + $parentLocationIds = $step->dsl['parent_location_id']; } else { - $parentLocationIds = $this->dsl['parent_location']; + $parentLocationIds = $step->dsl['parent_location']; } if (!is_array($parentLocationIds)) { @@ -56,7 +56,7 @@ protected function create() $parentLocationIds[$id] = $this->matchLocationByKey($parentLocationId)->id; } - $contentCollection = $this->matchContents('create'); + $contentCollection = $this->matchContents('create', $step); $locations = null; foreach ($contentCollection as $content) { @@ -65,20 +65,20 @@ protected function create() foreach ($parentLocationIds as $parentLocationId) { $locationCreateStruct = $locationService->newLocationCreateStruct($parentLocationId); - if (isset($this->dsl['is_hidden'])) { - $locationCreateStruct->hidden = $this->dsl['is_hidden']; + if (isset($step->dsl['is_hidden'])) { + $locationCreateStruct->hidden = $step->dsl['is_hidden']; } - if (isset($this->dsl['priority'])) { - $locationCreateStruct->priority = $this->dsl['priority']; + if (isset($step->dsl['priority'])) { + $locationCreateStruct->priority = $step->dsl['priority']; } - if (isset($this->dsl['sort_order'])) { - $locationCreateStruct->sortOrder = $this->getSortOrder($this->dsl['sort_order']); + if (isset($step->dsl['sort_order'])) { + $locationCreateStruct->sortOrder = $this->getSortOrder($step->dsl['sort_order']); } - if (isset($this->dsl['sort_field'])) { - $locationCreateStruct->sortField = $this->getSortField($this->dsl['sort_field']); + if (isset($step->dsl['sort_field'])) { + $locationCreateStruct->sortField = $this->getSortField($step->dsl['sort_field']); } $locations[] = $locationService->createLocation($contentInfo, $locationCreateStruct); @@ -87,20 +87,21 @@ protected function create() $locationCollection = new LocationCollection($locations); - $this->setReferences($locationCollection); + $this->setReferences($locationCollection, $step); return $locationCollection; } - protected function load() + protected function load($step) { - $locationCollection = $this->matchLocations('load'); + $locationCollection = $this->matchLocations('load', $step); - if (count($locationCollection) > 1 && isset($this->dsl['references'])) { + // This check is already done in setReferences + /*if (count($locationCollection) > 1 && isset($step->dsl['references'])) { throw new \Exception("Can not execute Location load because multiple locations match, and a references section is specified in the dsl. References can be set when only 1 location matches"); - } + }*/ - $this->setReferences($locationCollection); + $this->setReferences($locationCollection, $step); return $locationCollection; } @@ -112,56 +113,56 @@ protected function load() * * @todo add support for flexible matchers */ - protected function update() + protected function update($step) { $locationService = $this->repository->getLocationService(); - $locationCollection = $this->matchLocations('update'); + $locationCollection = $this->matchLocations('update', $step); - if (count($locationCollection) > 1 && isset($this->dsl['references'])) { + if (count($locationCollection) > 1 && isset($step->dsl['references'])) { throw new \Exception("Can not execute Location update because multiple locations match, and a references section is specified in the dsl. References can be set when only 1 location matches"); } - if (count($locationCollection) > 1 && isset($this->dsl['swap_with_location'])) { + if (count($locationCollection) > 1 && isset($step->dsl['swap_with_location'])) { throw new \Exception("Can not execute Location update because multiple locations match, and a swap_with_location is specified in the dsl."); } // support legacy tag: parent_location_id - if (isset($this->dsl['swap_with_location']) && (isset($this->dsl['parent_location']) || isset($this->dsl['parent_location_id']))) { + if (isset($step->dsl['swap_with_location']) && (isset($step->dsl['parent_location']) || isset($step->dsl['parent_location_id']))) { throw new \Exception('Cannot move location to a new parent and swap location with another location at the same time.'); } foreach ($locationCollection as $key => $location) { - if (isset($this->dsl['priority']) - || isset($this->dsl['sort_field']) - || isset($this->dsl['sort_order']) - || isset($this->dsl['remote_id']) + if (isset($step->dsl['priority']) + || isset($step->dsl['sort_field']) + || isset($step->dsl['sort_order']) + || isset($step->dsl['remote_id']) ) { $locationUpdateStruct = $locationService->newLocationUpdateStruct(); - if (isset($this->dsl['priority'])) { - $locationUpdateStruct->priority = $this->dsl['priority']; + if (isset($step->dsl['priority'])) { + $locationUpdateStruct->priority = $step->dsl['priority']; } - if (isset($this->dsl['sort_field'])) { - $locationUpdateStruct->sortField = $this->getSortField($this->dsl['sort_field'], $location->sortField); + if (isset($step->dsl['sort_field'])) { + $locationUpdateStruct->sortField = $this->getSortField($step->dsl['sort_field'], $location->sortField); } - if (isset($this->dsl['sort_order'])) { - $locationUpdateStruct->sortOrder = $this->getSortOrder($this->dsl['sort_order'], $location->sortOrder); + if (isset($step->dsl['sort_order'])) { + $locationUpdateStruct->sortOrder = $this->getSortOrder($step->dsl['sort_order'], $location->sortOrder); } - if (isset($this->dsl['remote_id'])) { - $locationUpdateStruct->remoteId = $this->dsl['remote_id']; + if (isset($step->dsl['remote_id'])) { + $locationUpdateStruct->remoteId = $step->dsl['remote_id']; } $location = $locationService->updateLocation($location, $locationUpdateStruct); } // Check if visibility needs to be updated - if (isset($this->dsl['is_hidden'])) { - if ($this->dsl['is_hidden']) { + if (isset($step->dsl['is_hidden'])) { + if ($step->dsl['is_hidden']) { $location = $locationService->hideLocation($location); } else { $location = $locationService->unhideLocation($location); @@ -169,17 +170,17 @@ protected function update() } // Move or swap location - if (isset($this->dsl['parent_location']) || isset($this->dsl['parent_location_id'])) { + if (isset($step->dsl['parent_location']) || isset($step->dsl['parent_location_id'])) { // Move the location and all its children to a new parent - $parentLocationId = isset($this->dsl['parent_location']) ? $this->dsl['parent_location'] : $this->dsl['parent_location_id']; + $parentLocationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : $step->dsl['parent_location_id']; $parentLocationId = $this->referenceResolver->resolveReference($parentLocationId); $newParentLocation = $locationService->loadLocation($parentLocationId); $locationService->moveSubtree($location, $newParentLocation); - } elseif (isset($this->dsl['swap_with_location'])) { + } elseif (isset($step->dsl['swap_with_location'])) { // Swap locations - $swapLocationId = $this->dsl['swap_with_location']; + $swapLocationId = $step->dsl['swap_with_location']; $swapLocationId = $this->referenceResolver->resolveReference($swapLocationId); $locationToSwap = $this->matchLocationByKey($swapLocationId); @@ -190,7 +191,7 @@ protected function update() $locationCollection[$key] = $location; } - $this->setReferences($locationCollection); + $this->setReferences($locationCollection, $step); return $locationCollection; } @@ -200,11 +201,11 @@ protected function update() * * @todo add support for flexible matchers */ - protected function delete() + protected function delete($step) { $locationService = $this->repository->getLocationService(); - $locationCollection = $this->matchLocations('delete'); + $locationCollection = $this->matchLocations('delete', $step); foreach ($locationCollection as $location) { $locationService->deleteLocation($location); @@ -218,19 +219,21 @@ protected function delete() * @return LocationCollection * @throws \Exception */ - protected function matchLocations($action) + protected function matchLocations($action, $step) { - if (!isset($this->dsl['location_id']) && !isset($this->dsl['match'])) { + if (!isset($step->dsl['location_id']) && !isset($step->dsl['match'])) { throw new \Exception("The id or a match condition is required to $action a location"); } // Backwards compat - if (!isset($this->dsl['match'])) { - $this->dsl['match'] = array('location_id' => $this->dsl['location_id']); + if (isset($step->dsl['match'])) { + $match = $step->dsl['match']; + } else { + $match = array('location_id' => $step->dsl['location_id']); } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($match); return $this->locationMatcher->match($match); } @@ -244,20 +247,24 @@ protected function matchLocations($action) * @param \eZ\Publish\API\Repository\Values\Content\Location|LocationCollection $location * @return boolean */ - protected function setReferences($location) + protected function setReferences($location, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } if ($location instanceof LocationCollection) { if (count($location) > 1) { - throw new \InvalidArgumentException('Location Manager does not support setting references for creating/updating of multiple locations'); + throw new \InvalidArgumentException('Location Manager does not support setting references for creating/updating/loading of multiple locations'); } + if (count($location) == 0) { + throw new \InvalidArgumentException('Location Manager does not support setting references for creating/updating/loading of no locations'); + } + $location = reset($location); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'location_id': case 'id': @@ -311,9 +318,6 @@ protected function setReferences($location) case 'path': $value = $location->pathString; break; - case 'position': - $value = $location->position; - break; case 'priority': $value = $location->priority; break; @@ -323,6 +327,10 @@ protected function setReferences($location) case 'section_id': $value = $location->contentInfo->sectionId; break; + case 'section_identifier': + $sectionService = $this->repository->getSectionService(); + $value = $sectionService->loadSection($location->contentInfo->sectionId)->identifier; + break; case 'sort_field': $value = $this->sortConverter->sortField2Hash($location->sortField); break; @@ -333,7 +341,11 @@ protected function setReferences($location) throw new \InvalidArgumentException('Location Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; @@ -355,22 +367,22 @@ public function matchLocationByKey($locationKey) * @return ContentCollection * @throws \Exception */ - protected function matchContents($action) + protected function matchContents($action, $step) { - if (!isset($this->dsl['object_id']) && !isset($this->dsl['remote_id']) && !isset($this->dsl['match'])) { + if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) { throw new \Exception("The ID or remote ID of an object or a Match Condition is required to $action a new location."); } // Backwards compat - if (!isset($this->dsl['match'])) { - if (isset($this->dsl['object_id'])) { - $this->dsl['match'] = array('content_id' => $this->dsl['object_id']); - } elseif (isset($this->dsl['remote_id'])) { - $this->dsl['match'] = array('content_remote_id' => $this->dsl['remote_id']); + if (!isset($step->dsl['match'])) { + if (isset($step->dsl['object_id'])) { + $step->dsl['match'] = array('content_id' => $step->dsl['object_id']); + } elseif (isset($step->dsl['remote_id'])) { + $step->dsl['match'] = array('content_remote_id' => $step->dsl['remote_id']); } } - $match = $this->dsl['match']; + $match = $step->dsl['match']; // convert the references passed in the match foreach ($match as $condition => $values) { diff --git a/Core/Executor/MigrationDefinitionExecutor.php b/Core/Executor/MigrationDefinitionExecutor.php index 9a8f32f3..dd9630f0 100644 --- a/Core/Executor/MigrationDefinitionExecutor.php +++ b/Core/Executor/MigrationDefinitionExecutor.php @@ -3,7 +3,6 @@ namespace Kaliop\eZMigrationBundle\Core\Executor; use Kaliop\eZMigrationBundle\API\Value\MigrationStep; -use Kaliop\eZMigrationBundle\API\LanguageAwareInterface; use Kaliop\eZMigrationBundle\API\MatcherInterface; use Kaliop\eZMigrationBundle\API\ReferenceBagInterface; use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface; @@ -54,7 +53,8 @@ public function execute(MigrationStep $step) * @return array * @throws \Exception */ - protected function generate($dsl, $context) { + protected function generate($dsl, $context) + { if (!isset($dsl['migration_type'])) { throw new \Exception("Invalid step definition: miss 'migration_type'"); } @@ -74,15 +74,16 @@ protected function generate($dsl, $context) { } $executor = $this->migrationService->getExecutor($migrationType); - if (isset($dsl['lang']) && $executor instanceof LanguageAwareInterface) { - $executor->setLanguageCode($dsl['lang']); + $context = array(); + if (isset($dsl['lang']) && $dsl['lang'] != '') { + $context['defaultLanguageCode'] = $dsl['lang']; } $matchCondition = array($match['type'] => $match['value']); if (isset($match['except']) && $match['except']) { $matchCondition = array(MatcherInterface::MATCH_NOT => $matchCondition); } - $result = $executor->generateMigration($matchCondition, $migrationMode); + $result = $executor->generateMigration($matchCondition, $migrationMode, $context); $this->setReferences($result, $dsl); @@ -100,13 +101,17 @@ protected function setReferences($result, $dsl) throw new \InvalidArgumentException('MigrationDefinition Executor does not support setting references if not using a json_path expression'); } + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } $value = JmesPath::search($reference['json_path'], $result); - $this->referenceResolver->addReference($reference['identifier'], $value); + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } } /** - * @todo cache this for faster acccess + * @todo cache this for faster access * @return array */ protected function getGeneratingExecutors() diff --git a/Core/Executor/MigrationExecutor.php b/Core/Executor/MigrationExecutor.php new file mode 100644 index 00000000..a9757f3e --- /dev/null +++ b/Core/Executor/MigrationExecutor.php @@ -0,0 +1,142 @@ +referenceMatcher = $referenceMatcher; + $this->contentManager = $contentManager; + $this->locationManager = $locationManager; + $this->contentTypeManager = $contentTypeManager; + } + + /** + * @param MigrationStep $step + * @return mixed + * @throws \Exception + */ + public function execute(MigrationStep $step) + { + parent::execute($step); + + if (!isset($step->dsl['mode'])) { + throw new \Exception("Invalid step definition: missing 'mode'"); + } + + $action = $step->dsl['mode']; + + if (!in_array($action, $this->supportedActions)) { + throw new \Exception("Invalid step definition: value '$action' is not allowed for 'mode'"); + } + + return $this->$action($step->dsl, $step->context); + } + + /** + * @param array $dsl + * @param array $context + * @return true + * @throws \Exception + */ + protected function cancel($dsl, $context) + { + $message = isset($dsl['message']) ? $dsl['message'] : ''; + + if (isset($dsl['if'])) { + if (!$this->matchConditions($dsl['if'])) { + // q: return timestamp, matched condition or ... ? + return true; + } + } + + throw new MigrationAbortedException($message); + } + + /** + * @param array $dsl + * @param array $context + * @return true + * @throws \Exception + */ + protected function suspend($dsl, $context) + { + $message = isset($dsl['message']) ? $dsl['message'] : ''; + + if (!isset($dsl['until'])) { + throw new \Exception("An until condition is required to suspend a migration"); + } + + if (isset($dsl['load'])) { + $this->loadEntity($dsl['load'], $context); + } + + if ($this->matchConditions($dsl['until'])) { + // the time has come to resume! + // q: return timestamp, matched condition or ... ? + return true; + } + + throw new MigrationSuspendedException($message); + } + + protected function loadEntity($dsl, $context) { + if (!isset($dsl['type']) || !isset($dsl['match'])) { + throw new \Exception("A 'type' and a 'match' are required to load entities when suspending a migration"); + } + + $dsl['mode'] = 'load'; + // be kind to users and allow them not to specify this explicitly + if (isset($dsl['references'])) { + foreach($dsl['references'] as &$refDef) { + $refDef['overwrite'] = true; + } + } + $step = new MigrationStep($dsl['type'], $dsl, $context); + + switch($dsl['type']) { + case 'content': + return $this->contentManager->execute($step); + case 'location': + return $this->locationManager->execute($step); + case 'content_type': + return $this->contentTypeManager->execute($step); + } + } + + protected function matchConditions($conditions) + { + foreach ($conditions as $key => $values) { + + /*if (!is_array($values)) { + $values = array($values); + }*/ + + switch ($key) { + case 'date': + return time() >= $values; + + case 'match': + $match = $this->referenceMatcher->match($values); + return reset($match); + + default: + throw new \Exception("Unknown until condition: '$key' when suspending a migration"); + } + } + } +} diff --git a/Core/Executor/ObjectStateGroupManager.php b/Core/Executor/ObjectStateGroupManager.php index c44ee38a..bb6c2f76 100644 --- a/Core/Executor/ObjectStateGroupManager.php +++ b/Core/Executor/ObjectStateGroupManager.php @@ -34,31 +34,31 @@ public function __construct(ObjectStateGroupMatcher $objectStateGroupMatcher) * * @todo add support for flexible defaultLanguageCode */ - protected function create() + protected function create($step) { foreach (array('names', 'identifier') as $key) { - if (!isset($this->dsl[$key])) { + if (!isset($step->dsl[$key])) { throw new \Exception("The '$key' key is missing in a object state group creation definition"); } } $objectStateService = $this->repository->getObjectStateService(); - $objectStateGroupCreateStruct = $objectStateService->newObjectStateGroupCreateStruct($this->dsl['identifier']); + $objectStateGroupCreateStruct = $objectStateService->newObjectStateGroupCreateStruct($step->dsl['identifier']); $objectStateGroupCreateStruct->defaultLanguageCode = self::DEFAULT_LANGUAGE_CODE; - foreach ($this->dsl['names'] as $languageCode => $name) { + foreach ($step->dsl['names'] as $languageCode => $name) { $objectStateGroupCreateStruct->names[$languageCode] = $name; } - if (isset($this->dsl['descriptions'])) { - foreach ($this->dsl['descriptions'] as $languageCode => $description) { + if (isset($step->dsl['descriptions'])) { + foreach ($step->dsl['descriptions'] as $languageCode => $description) { $objectStateGroupCreateStruct->descriptions[$languageCode] = $description; } } $objectStateGroup = $objectStateService->createObjectStateGroup($objectStateGroupCreateStruct); - $this->setReferences($objectStateGroup); + $this->setReferences($objectStateGroup, $step); return $objectStateGroup; } @@ -68,39 +68,39 @@ protected function create() * * @todo add support for defaultLanguageCode */ - protected function update() + protected function update($step) { $objectStateService = $this->repository->getObjectStateService(); - $groupsCollection = $this->matchObjectStateGroups('update'); + $groupsCollection = $this->matchObjectStateGroups('update', $step); - if (count($groupsCollection) > 1 && isset($this->dsl['references'])) { + if (count($groupsCollection) > 1 && isset($step->dsl['references'])) { throw new \Exception("Can not execute Object State Group update because multiple groups match, and a references section is specified in the dsl. References can be set when only 1 state group matches"); } - if (count($groupsCollection) > 1 && isset($this->dsl['identifier'])) { + if (count($groupsCollection) > 1 && isset($step->dsl['identifier'])) { throw new \Exception("Can not execute Object State Group update because multiple groups match, and an identifier is specified in the dsl."); } foreach ($groupsCollection as $objectStateGroup) { $objectStateGroupUpdateStruct = $objectStateService->newObjectStateGroupUpdateStruct(); - if (isset($this->dsl['identifier'])) { - $objectStateGroupUpdateStruct->identifier = $this->dsl['identifier']; + if (isset($step->dsl['identifier'])) { + $objectStateGroupUpdateStruct->identifier = $step->dsl['identifier']; } - if (isset($this->dsl['names'])) { - foreach ($this->dsl['names'] as $languageCode => $name) { + if (isset($step->dsl['names'])) { + foreach ($step->dsl['names'] as $languageCode => $name) { $objectStateGroupUpdateStruct->names[$languageCode] = $name; } } - if (isset($this->dsl['descriptions'])) { - foreach ($this->dsl['descriptions'] as $languageCode => $description) { + if (isset($step->dsl['descriptions'])) { + foreach ($step->dsl['descriptions'] as $languageCode => $description) { $objectStateGroupUpdateStruct->descriptions[$languageCode] = $description; } } $objectStateGroup = $objectStateService->updateObjectStateGroup($objectStateGroup, $objectStateGroupUpdateStruct); - $this->setReferences($objectStateGroup); + $this->setReferences($objectStateGroup, $step); } return $groupsCollection; @@ -109,9 +109,9 @@ protected function update() /** * Handles the delete step of object state group migrations */ - protected function delete() + protected function delete($step) { - $groupsCollection = $this->matchObjectStateGroups('delete'); + $groupsCollection = $this->matchObjectStateGroups('delete', $step); $objectStateService = $this->repository->getObjectStateService(); @@ -127,14 +127,14 @@ protected function delete() * @return ObjectStateGroupCollection * @throws \Exception */ - protected function matchObjectStateGroups($action) + protected function matchObjectStateGroups($action, $step) { - if (!isset($this->dsl['match'])) { + if (!isset($step->dsl['match'])) { throw new \Exception("A match condition is required to $action an object state group"); } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($step->dsl['match']); return $this->objectStateGroupMatcher->match($match); } @@ -143,13 +143,13 @@ protected function matchObjectStateGroups($action) * {@inheritdoc} * @param \eZ\Publish\API\Repository\Values\ObjectState\ObjectStateGroup $objectStateGroup */ - protected function setReferences($objectStateGroup) + protected function setReferences($objectStateGroup, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'object_state_group_id': case 'id': @@ -163,7 +163,11 @@ protected function setReferences($objectStateGroup) throw new \InvalidArgumentException('Object State Group Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; @@ -172,12 +176,13 @@ protected function setReferences($objectStateGroup) /** * @param array $matchCondition * @param string $mode + * @param array $context * @throws \Exception * @return array */ - public function generateMigration(array $matchCondition, $mode) + public function generateMigration(array $matchCondition, $mode, array $context = array()) { - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context)); $objectStateGroupCollection = $this->objectStateGroupMatcher->match($matchCondition); $data = array(); diff --git a/Core/Executor/ObjectStateManager.php b/Core/Executor/ObjectStateManager.php index 3f36e765..9fe1b4c3 100644 --- a/Core/Executor/ObjectStateManager.php +++ b/Core/Executor/ObjectStateManager.php @@ -42,39 +42,39 @@ public function __construct(ObjectStateMatcher $objectStateMatcher, ObjectStateG * * @throws \Exception */ - protected function create() + protected function create($step) { foreach (array('object_state_group', 'names', 'identifier') as $key) { - if (!isset($this->dsl[$key])) { + if (!isset($step->dsl[$key])) { throw new \Exception("The '$key' key is missing in a object state creation definition"); } } - if (!count($this->dsl['names'])) { + if (!count($step->dsl['names'])) { throw new \Exception('No object state names have been defined. Need to specify at least one to create the state.'); } $objectStateService = $this->repository->getObjectStateService(); - $objectStateGroupId = $this->dsl['object_state_group']; + $objectStateGroupId = $step->dsl['object_state_group']; $objectStateGroupId = $this->referenceResolver->resolveReference($objectStateGroupId); $objectStateGroup = $this->objectStateGroupMatcher->matchOneByKey($objectStateGroupId); - $objectStateCreateStruct = $objectStateService->newObjectStateCreateStruct($this->dsl['identifier']); + $objectStateCreateStruct = $objectStateService->newObjectStateCreateStruct($step->dsl['identifier']); $objectStateCreateStruct->defaultLanguageCode = self::DEFAULT_LANGUAGE_CODE; - foreach ($this->dsl['names'] as $languageCode => $name) { + foreach ($step->dsl['names'] as $languageCode => $name) { $objectStateCreateStruct->names[$languageCode] = $name; } - if (isset($this->dsl['descriptions'])) { - foreach ($this->dsl['descriptions'] as $languageCode => $description) { + if (isset($step->dsl['descriptions'])) { + foreach ($step->dsl['descriptions'] as $languageCode => $description) { $objectStateCreateStruct->descriptions[$languageCode] = $description; } } $objectState = $objectStateService->createObjectState($objectStateGroup, $objectStateCreateStruct); - $this->setReferences($objectState); + $this->setReferences($objectState, $step); return $objectState; } @@ -84,15 +84,15 @@ protected function create() * * @throws \Exception */ - protected function update() + protected function update($step) { - $stateCollection = $this->matchObjectStates('update'); + $stateCollection = $this->matchObjectStates('update', $step); - if (count($stateCollection) > 1 && array_key_exists('references', $this->dsl)) { + if (count($stateCollection) > 1 && array_key_exists('references', $step->dsl)) { throw new \Exception("Can not execute Object State update because multiple states match, and a references section is specified in the dsl. References can be set when only 1 state matches"); } - if (count($stateCollection) > 1 && isset($this->dsl['identifier'])) { + if (count($stateCollection) > 1 && isset($step->dsl['identifier'])) { throw new \Exception("Can not execute Object State update because multiple states match, and an identifier is specified in the dsl."); } @@ -101,22 +101,22 @@ protected function update() foreach ($stateCollection as $state) { $objectStateUpdateStruct = $objectStateService->newObjectStateUpdateStruct(); - if (isset($this->dsl['identifier'])) { - $objectStateUpdateStruct->identifier = $this->dsl['identifier']; + if (isset($step->dsl['identifier'])) { + $objectStateUpdateStruct->identifier = $step->dsl['identifier']; } - if (isset($this->dsl['names'])) { - foreach ($this->dsl['names'] as $name) { + if (isset($step->dsl['names'])) { + foreach ($step->dsl['names'] as $name) { $objectStateUpdateStruct->names[$name['languageCode']] = $name['name']; } } - if (isset($this->dsl['descriptions'])) { - foreach ($this->dsl['descriptions'] as $languageCode => $description) { + if (isset($step->dsl['descriptions'])) { + foreach ($step->dsl['descriptions'] as $languageCode => $description) { $objectStateUpdateStruct->descriptions[$languageCode] = $description; } } $state = $objectStateService->updateObjectState($state, $objectStateUpdateStruct); - $this->setReferences($state); + $this->setReferences($state, $step); } return $stateCollection; @@ -125,9 +125,9 @@ protected function update() /** * Handles the deletion step of object state migrations. */ - protected function delete() + protected function delete($step) { - $stateCollection = $this->matchObjectStates('delete'); + $stateCollection = $this->matchObjectStates('delete', $step); $objectStateService = $this->repository->getObjectStateService(); @@ -143,14 +143,14 @@ protected function delete() * @return ObjectStateCollection * @throws \Exception */ - protected function matchObjectStates($action) + protected function matchObjectStates($action, $step) { - if (!isset($this->dsl['match'])) { + if (!isset($step->dsl['match'])) { throw new \Exception("A match condition is required to $action an object state"); } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($step->dsl['match']); return $this->objectStateMatcher->match($match); } @@ -159,13 +159,13 @@ protected function matchObjectStates($action) * {@inheritdoc} * @param \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $objectState */ - protected function setReferences($objectState) + protected function setReferences($objectState, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'object_state_id': case 'id': @@ -178,7 +178,11 @@ protected function setReferences($objectState) throw new \InvalidArgumentException('Object State Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; @@ -187,12 +191,13 @@ protected function setReferences($objectState) /** * @param array $matchCondition * @param string $mode + * @param array $context * @throws \Exception * @return array */ - public function generateMigration(array $matchCondition, $mode) + public function generateMigration(array $matchCondition, $mode, array $context = array()) { - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context)); $objectStateCollection = $this->objectStateMatcher->match($matchCondition); $data = array(); diff --git a/Core/Executor/PHPExecutor.php b/Core/Executor/PHPExecutor.php index 09f37a37..7ffb043d 100644 --- a/Core/Executor/PHPExecutor.php +++ b/Core/Executor/PHPExecutor.php @@ -18,7 +18,7 @@ public function __construct(ContainerInterface $container) /** * @param MigrationStep $step - * @return void + * @return mixed * @throws \Exception */ public function execute(MigrationStep $step) diff --git a/Core/Executor/ReferenceExecutor.php b/Core/Executor/ReferenceExecutor.php index e8491b7e..d81e2e84 100644 --- a/Core/Executor/ReferenceExecutor.php +++ b/Core/Executor/ReferenceExecutor.php @@ -7,18 +7,19 @@ use Symfony\Component\Yaml\Yaml; use Symfony\Component\VarDumper\VarDumper; use Kaliop\eZMigrationBundle\API\Value\MigrationStep; -use Kaliop\eZMigrationBundle\API\ReferenceBagInterface; +use Kaliop\eZMigrationBundle\API\ReferenceResolverBagInterface; +use Kaliop\eZMigrationBundle\API\EnumerableReferenceResolverInterface; class ReferenceExecutor extends AbstractExecutor { protected $supportedStepTypes = array('reference'); - protected $supportedActions = array('set', 'load', 'dump'); + protected $supportedActions = array('set', 'load', 'save', 'dump'); protected $container; - /** @var ReferenceBagInterface $referenceResolver */ + /** @var ReferenceResolverBagInterface $referenceResolver */ protected $referenceResolver; - public function __construct(ContainerInterface $container, ReferenceBagInterface $referenceResolver) + public function __construct(ContainerInterface $container, ReferenceResolverBagInterface $referenceResolver) { $this->container = $container; $this->referenceResolver = $referenceResolver; @@ -46,7 +47,8 @@ public function execute(MigrationStep $step) return $this->$action($step->dsl, $step->context); } - protected function set($dsl, $context) { + protected function set($dsl, $context) + { if (!isset($dsl['identifier'])) { throw new \Exception("Invalid step definition: miss 'identifier' for setting reference"); } @@ -104,13 +106,59 @@ protected function load($dsl, $context) if (preg_match('/%.+%$/', $value)) { $value = $this->container->getParameter(trim($value, '%')); } - $this->referenceResolver->addReference($refName, $value, $overwrite); + + if (!$this->referenceResolver->addReference($refName, $value, $overwrite)) { + throw new \Exception("Failed adding to Reference Resolver the reference: $refName"); + } } return $data; } - protected function dump($dsl, $context) { + /** + * @todo find a smart way to allow saving the references file next to the current migration + */ + protected function save($dsl, $context) + { + if (!isset($dsl['file'])) { + throw new \Exception("Invalid step definition: miss 'file' for saving references"); + } + $fileName = $dsl['file']; + + $overwrite = isset($dsl['overwrite']) ? $overwrite = $dsl['overwrite'] : false; + + $fileName = str_replace('{ENV}', $this->container->get('kernel')->getEnvironment(), $fileName); + + if (is_file($fileName) && !$overwrite) { + throw new \Exception("Invalid step definition: file '$fileName' for saving references already exists"); + } + + if (! $this->referenceResolver instanceof EnumerableReferenceResolverInterface) { + throw new \Exception("Can not save references as resolver is not enumerable"); + } + + $data = $this->referenceResolver->listReferences(); + + $ext = pathinfo($fileName, PATHINFO_EXTENSION); + switch ($ext) { + case 'json': + $data = json_encode($data, JSON_PRETTY_PRINT); + break; + case 'yml': + case 'yaml': + $data = Yaml::dump($data); + break; + default: + throw new \Exception("Invalid step definition: unsupported file extension for saving references to"); + } + + file_put_contents($fileName, $data); + + return $data; + } + + protected function dump($dsl, $context) + { if (!isset($dsl['identifier'])) { throw new \Exception("Invalid step definition: miss 'identifier' for dumping reference"); } diff --git a/Core/Executor/RepositoryExecutor.php b/Core/Executor/RepositoryExecutor.php index b17c3a93..98a0d1d7 100644 --- a/Core/Executor/RepositoryExecutor.php +++ b/Core/Executor/RepositoryExecutor.php @@ -3,39 +3,37 @@ namespace Kaliop\eZMigrationBundle\Core\Executor; use eZ\Publish\API\Repository\Repository; -use Kaliop\eZMigrationBundle\API\LanguageAwareInterface; -use Kaliop\eZMigrationBundle\API\ReferenceResolverInterface; +use Kaliop\eZMigrationBundle\API\ReferenceResolverBagInterface; use Kaliop\eZMigrationBundle\API\Value\MigrationStep; use Kaliop\eZMigrationBundle\Core\RepositoryUserSetterTrait; /** * The core manager class that all migration action managers inherit from. */ -abstract class RepositoryExecutor extends AbstractExecutor implements LanguageAwareInterface +abstract class RepositoryExecutor extends AbstractExecutor { use RepositoryUserSetterTrait; /** - * Constant defining the default language code + * Constant defining the default language code (used if not specified by the migration or on the command line) */ const DEFAULT_LANGUAGE_CODE = 'eng-GB'; /** - * Constant defining the default Admin user ID. - * @todo inject via config parameter + * The default Admin user Id, used when no Admin user is specified */ const ADMIN_USER_ID = 14; - /** @todo inject via config parameter */ + /** Used if not specified by the migration */ const USER_CONTENT_TYPE = 'user'; /** * @var array $dsl The parsed DSL instruction array */ - protected $dsl; + //protected $dsl; /** @var array $context The context (configuration) for the execution of the current step */ - protected $context; + //protected $context; /** * The eZ Publish 5 API repository. @@ -49,14 +47,14 @@ abstract class RepositoryExecutor extends AbstractExecutor implements LanguageAw * * @var string */ - private $languageCode; + //private $languageCode; /** * @var string */ - private $defaultLanguageCode; + //private $defaultLanguageCode; - /** @var ReferenceResolverInterface $referenceResolver */ + /** @var ReferenceResolverBagInterface $referenceResolver */ protected $referenceResolver; // to redefine in subclasses if they don't support all methods, or if they support more... @@ -69,7 +67,7 @@ public function setRepository(Repository $repository) $this->repository = $repository; } - public function setReferenceResolver(ReferenceResolverInterface $referenceResolver) + public function setReferenceResolver(ReferenceResolverBagInterface $referenceResolver) { $this->referenceResolver = $referenceResolver; } @@ -89,17 +87,11 @@ public function execute(MigrationStep $step) throw new \Exception("Invalid step definition: value '$action' is not allowed for 'mode'"); } - $this->dsl = $step->dsl; - $this->context = $step->context; - if (isset($this->dsl['lang'])) { - $this->setLanguageCode($this->dsl['lang']); - } - if (method_exists($this, $action)) { - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($step->context)); try { - $output = $this->$action(); + $output = $this->$action($step); } catch (\Exception $e) { $this->loginUser($previousUserId); throw $e; @@ -123,26 +115,47 @@ public function execute(MigrationStep $step) * @param $object * @return boolean */ - abstract protected function setReferences($object); + abstract protected function setReferences($object, $step); - public function setLanguageCode($languageCode) + /** + * @param MigrationStep $step + * @return string + */ + protected function getLanguageCode($step) { - $this->languageCode = $languageCode; + return isset($step->dsl['lang']) ? $step->dsl['lang'] : $this->getLanguageCodeFromContext($step->context); } - public function getLanguageCode() + /** + * @param array $context + * @return string + */ + protected function getLanguageCodeFromContext($context) { - return $this->languageCode ?: $this->getDefaultLanguageCode(); + return isset($context['defaultLanguageCode']) ? $context['defaultLanguageCode'] : self::DEFAULT_LANGUAGE_CODE; } - public function setDefaultLanguageCode($languageCode) + /** + * @param MigrationStep $step + * @return string + */ + protected function getUserContentType($step) + { + return isset($step->dsl['user_content_type']) ? $step->dsl['user_content_type'] : $this->getUserContentTypeFromContext($step->context); + } + + protected function getUserContentTypeFromContext($context) { - $this->defaultLanguageCode = $languageCode; + return isset($context['userContentType']) ? $context['userContentType'] : self::USER_CONTENT_TYPE; } - public function getDefaultLanguageCode() + protected function getAdminUserIdentifierFromContext($context) { - return $this->defaultLanguageCode ?: self::DEFAULT_LANGUAGE_CODE; + if (isset($context['adminUserLogin'])) { + return $context['adminUserLogin']; + } + + return self::ADMIN_USER_ID; } /** diff --git a/Core/Executor/RoleManager.php b/Core/Executor/RoleManager.php index bcb2d907..922dc595 100644 --- a/Core/Executor/RoleManager.php +++ b/Core/Executor/RoleManager.php @@ -29,12 +29,12 @@ public function __construct(RoleMatcher $roleMatcher, LimitationConverter $limit /** * Method to handle the create operation of the migration instructions */ - protected function create() + protected function create($step) { $roleService = $this->repository->getRoleService(); $userService = $this->repository->getUserService(); - $roleCreateStruct = $roleService->newRoleCreateStruct($this->dsl['name']); + $roleCreateStruct = $roleService->newRoleCreateStruct($step->dsl['name']); // Publish new role $role = $roleService->createRole($roleCreateStruct); @@ -42,17 +42,17 @@ protected function create() $roleService->publishRoleDraft($role); } - if (isset($this->dsl['policies'])) { - foreach ($this->dsl['policies'] as $key => $ymlPolicy) { + if (isset($step->dsl['policies'])) { + foreach ($step->dsl['policies'] as $key => $ymlPolicy) { $this->addPolicy($role, $roleService, $ymlPolicy); } } - if (isset($this->dsl['assign'])) { - $this->assignRole($role, $roleService, $userService, $this->dsl['assign']); + if (isset($step->dsl['assign'])) { + $this->assignRole($role, $roleService, $userService, $step->dsl['assign']); } - $this->setReferences($role); + $this->setReferences($role, $step); return $role; } @@ -60,15 +60,15 @@ protected function create() /** * Method to handle the update operation of the migration instructions */ - protected function update() + protected function update($step) { - $roleCollection = $this->matchRoles('update'); + $roleCollection = $this->matchRoles('update', $step); - if (count($roleCollection) > 1 && isset($this->dsl['references'])) { + if (count($roleCollection) > 1 && isset($step->dsl['references'])) { throw new \Exception("Can not execute Role update because multiple roles match, and a references section is specified in the dsl. References can be set when only 1 role matches"); } - if (count($roleCollection) > 1 && isset($this->dsl['new_name'])) { + if (count($roleCollection) > 1 && isset($step->dsl['new_name'])) { throw new \Exception("Can not execute Role update because multiple roles match, and a new_name is specified in the dsl."); } @@ -79,14 +79,14 @@ protected function update() foreach ($roleCollection as $key => $role) { // Updating role name - if (isset($this->dsl['new_name'])) { + if (isset($step->dsl['new_name'])) { $update = $roleService->newRoleUpdateStruct(); - $update->identifier = $this->dsl['new_name']; + $update->identifier = $step->dsl['new_name']; $role = $roleService->updateRole($role, $update); } - if (isset($this->dsl['policies'])) { - $ymlPolicies = $this->dsl['policies']; + if (isset($step->dsl['policies'])) { + $ymlPolicies = $step->dsl['policies']; // Removing all policies so we can add them back. // TODO: Check and update policies instead of remove and add. @@ -100,14 +100,14 @@ protected function update() } } - if (isset($this->dsl['assign'])) { - $this->assignRole($role, $roleService, $userService, $this->dsl['assign']); + if (isset($step->dsl['assign'])) { + $this->assignRole($role, $roleService, $userService, $step->dsl['assign']); } $roleCollection[$key] = $role; } - $this->setReferences($roleCollection); + $this->setReferences($roleCollection, $step); return $roleCollection; } @@ -115,9 +115,9 @@ protected function update() /** * Method to handle the delete operation of the migration instructions */ - protected function delete() + protected function delete($step) { - $roleCollection = $this->matchRoles('delete'); + $roleCollection = $this->matchRoles('delete', $step); $roleService = $this->repository->getRoleService(); @@ -133,19 +133,21 @@ protected function delete() * @return RoleCollection * @throws \Exception */ - protected function matchRoles($action) + protected function matchRoles($action, $step) { - if (!isset($this->dsl['name']) && !isset($this->dsl['match'])) { + if (!isset($step->dsl['name']) && !isset($step->dsl['match'])) { throw new \Exception("The name of a role or a match condition is required to $action it"); } // Backwards compat - if (!isset($this->dsl['match'])) { - $this->dsl['match'] = array('identifier' => $this->dsl['name']); + if (isset($step->dsl['match'])) { + $match = $step->dsl['match']; + } else { + $match = array('identifier' => $step->dsl['name']); } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($match); return $this->roleMatcher->match($match); } @@ -159,9 +161,9 @@ protected function matchRoles($action) * @throws \InvalidArgumentException When trying to assign a reference to an unsupported attribute * @return boolean */ - protected function setReferences($role) + protected function setReferences($role, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } @@ -172,7 +174,7 @@ protected function setReferences($role) $role = reset($role); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'role_id': case 'id': @@ -186,7 +188,11 @@ protected function setReferences($role) throw new \InvalidArgumentException('Role Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; @@ -195,12 +201,13 @@ protected function setReferences($role) /** * @param array $matchCondition * @param string $mode + * @param array $context * @throws \Exception * @return array */ - public function generateMigration(array $matchCondition, $mode) + public function generateMigration(array $matchCondition, $mode, array $context = array()) { - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context)); $roleCollection = $this->roleMatcher->match($matchCondition); $data = array(); diff --git a/Core/Executor/SQLExecutor.php b/Core/Executor/SQLExecutor.php index 0c6f4d99..a2612462 100644 --- a/Core/Executor/SQLExecutor.php +++ b/Core/Executor/SQLExecutor.php @@ -72,7 +72,11 @@ protected function setReferences($result, $dsl) throw new \InvalidArgumentException('Sql Executor does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } } } diff --git a/Core/Executor/SectionManager.php b/Core/Executor/SectionManager.php index b0a7e98f..75593c12 100644 --- a/Core/Executor/SectionManager.php +++ b/Core/Executor/SectionManager.php @@ -27,18 +27,18 @@ public function __construct(SectionMatcher $sectionMatcher) /** * Handles the section create migration action */ - protected function create() + protected function create($step) { $sectionService = $this->repository->getSectionService(); $sectionCreateStruct = $sectionService->newSectionCreateStruct(); - $sectionCreateStruct->identifier = $this->dsl['identifier']; - $sectionCreateStruct->name = $this->dsl['name']; + $sectionCreateStruct->identifier = $step->dsl['identifier']; + $sectionCreateStruct->name = $step->dsl['name']; $section = $sectionService->createSection($sectionCreateStruct); - $this->setReferences($section); + $this->setReferences($section, $step); return $section; } @@ -46,11 +46,11 @@ protected function create() /** * Handles the section update migration action */ - protected function update() + protected function update($step) { - $sectionCollection = $this->matchSections('update'); + $sectionCollection = $this->matchSections('update', $step); - if (count($sectionCollection) > 1 && array_key_exists('references', $this->dsl)) { + if (count($sectionCollection) > 1 && array_key_exists('references', $step->dsl)) { throw new \Exception("Can not execute Section update because multiple types match, and a references section is specified in the dsl. References can be set when only 1 section matches"); } @@ -58,11 +58,11 @@ protected function update() foreach ($sectionCollection as $key => $section) { $sectionUpdateStruct = $sectionService->newSectionUpdateStruct(); - if (isset($this->dsl['identifier'])) { - $sectionUpdateStruct->identifier = $this->dsl['identifier']; + if (isset($step->dsl['identifier'])) { + $sectionUpdateStruct->identifier = $step->dsl['identifier']; } - if (isset($this->dsl['name'])) { - $sectionUpdateStruct->name = $this->dsl['name']; + if (isset($step->dsl['name'])) { + $sectionUpdateStruct->name = $step->dsl['name']; } $section = $sectionService->updateSection($section, $sectionUpdateStruct); @@ -70,7 +70,7 @@ protected function update() $sectionCollection[$key] = $section; } - $this->setReferences($sectionCollection); + $this->setReferences($sectionCollection, $step); return $sectionCollection; } @@ -78,9 +78,9 @@ protected function update() /** * Handles the section delete migration action */ - protected function delete() + protected function delete($step) { - $sectionCollection = $this->matchSections('delete'); + $sectionCollection = $this->matchSections('delete', $step); $sectionService = $this->repository->getSectionService(); @@ -96,16 +96,16 @@ protected function delete() * @return SectionCollection * @throws \Exception */ - protected function matchSections($action) + protected function matchSections($action, $step) { - if (!isset($this->dsl['match'])) { + if (!isset($step->dsl['match'])) { throw new \Exception("A match condition is required to $action a section"); } - $match = $this->dsl['match']; + $match = $step->dsl['match']; // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($step->dsl['match']); return $this->sectionMatcher->match($match); } @@ -117,9 +117,9 @@ protected function matchSections($action) * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute * @return boolean */ - protected function setReferences($section) + protected function setReferences($section, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } @@ -130,7 +130,7 @@ protected function setReferences($section) $section = reset($section); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'section_id': @@ -149,7 +149,11 @@ protected function setReferences($section) throw new \InvalidArgumentException('Section Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; @@ -158,12 +162,13 @@ protected function setReferences($section) /** * @param array $matchCondition * @param string $mode + * @param array $context * @throws \Exception * @return array */ - public function generateMigration(array $matchCondition, $mode) + public function generateMigration(array $matchCondition, $mode, array $context = array()) { - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context)); $sectionCollection = $this->sectionMatcher->match($matchCondition); $data = array(); diff --git a/Core/Executor/TagManager.php b/Core/Executor/TagManager.php index 5de00751..8aad6ebf 100644 --- a/Core/Executor/TagManager.php +++ b/Core/Executor/TagManager.php @@ -33,23 +33,23 @@ public function __construct(TagMatcher $tagMatcher, $tagService = null) * @return mixed * @throws \Exception */ - protected function create() + protected function create($step) { $this->checkTagsBundleInstall(); - $alwaysAvail = isset($this->dsl['always_available']) ? $this->dsl['always_available'] : true; + $alwaysAvail = isset($step->dsl['always_available']) ? $step->dsl['always_available'] : true; $parentTagId = 0; - if (isset($this->dsl['parent_tag_id'])) { - $parentTagId = $this->dsl['parent_tag_id']; + if (isset($step->dsl['parent_tag_id'])) { + $parentTagId = $step->dsl['parent_tag_id']; $parentTagId = $this->referenceResolver->resolveReference($parentTagId); } - $remoteId = isset($this->dsl['remote_id']) ? $this->dsl['remote_id'] : null; + $remoteId = isset($step->dsl['remote_id']) ? $step->dsl['remote_id'] : null; - if (isset($this->dsl['lang'])) { - $lang = $this->dsl['lang']; - } elseif (isset($this->dsl['main_language_code'])) { + if (isset($step->dsl['lang'])) { + $lang = $step->dsl['lang']; + } elseif (isset($step->dsl['main_language_code'])) { // deprecated tag - $lang = $this->dsl['main_language_code']; + $lang = $step->dsl['main_language_code']; } else { throw new \Exception("The 'lang' key is required to create a tag."); } @@ -62,28 +62,28 @@ protected function create() ); $tagCreateStruct = new \Netgen\TagsBundle\API\Repository\Values\Tags\TagCreateStruct($tagCreateArray); - foreach ($this->dsl['keywords'] as $langCode => $keyword) + foreach ($step->dsl['keywords'] as $langCode => $keyword) { $tagCreateStruct->setKeyword($keyword, $langCode); } $tag = $this->tagService->createTag($tagCreateStruct); - $this->setReferences($tag); + $this->setReferences($tag, $step); return $tag; } - protected function update() + protected function update($step) { $this->checkTagsBundleInstall(); throw new \Exception('Tag update is not implemented yet'); } - protected function delete() + protected function delete($step) { $this->checkTagsBundleInstall(); - $tagsCollection = $this->matchTags('delete'); + $tagsCollection = $this->matchTags('delete', $step); foreach ($tagsCollection as $tag) { $this->tagService->deleteTag($tag); @@ -97,14 +97,14 @@ protected function delete() * @return TagCollection * @throws \Exception */ - protected function matchTags($action) + protected function matchTags($action, $step) { - if (!isset($this->dsl['match'])) { + if (!isset($step->dsl['match'])) { throw new \Exception("A match condition is required to $action a Tag"); } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($step->dsl['match']); return $this->tagMatcher->match($match); } @@ -113,13 +113,13 @@ protected function matchTags($action) * @param $object * @return bool */ - protected function setReferences($object) + protected function setReferences($object, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'id': $value = $object->id; @@ -128,7 +128,11 @@ protected function setReferences($object) throw new \InvalidArgumentException('Tag Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; diff --git a/Core/Executor/UserGroupManager.php b/Core/Executor/UserGroupManager.php index 1ab13006..5a8adeb1 100644 --- a/Core/Executor/UserGroupManager.php +++ b/Core/Executor/UserGroupManager.php @@ -29,46 +29,46 @@ public function __construct(UserGroupMatcher $userGroupMatcher, RoleMatcher $rol /** * Method to handle the create operation of the migration instructions */ - protected function create() + protected function create($step) { $userService = $this->repository->getUserService(); - $parentGroupId = $this->dsl['parent_group_id']; + $parentGroupId = $step->dsl['parent_group_id']; $parentGroupId = $this->referenceResolver->resolveReference($parentGroupId); $parentGroup = $this->userGroupMatcher->matchOneByKey($parentGroupId); $contentType = $this->repository->getContentTypeService()->loadContentTypeByIdentifier("user_group"); - $userGroupCreateStruct = $userService->newUserGroupCreateStruct($this->getLanguageCode(), $contentType); - $userGroupCreateStruct->setField('name', $this->dsl['name']); + $userGroupCreateStruct = $userService->newUserGroupCreateStruct($this->getLanguageCode($step), $contentType); + $userGroupCreateStruct->setField('name', $step->dsl['name']); - if (isset($this->dsl['remote_id'])) { - $userGroupCreateStruct->remoteId = $this->dsl['remote_id']; + if (isset($step->dsl['remote_id'])) { + $userGroupCreateStruct->remoteId = $step->dsl['remote_id']; } - if (isset($this->dsl['description'])) { - $userGroupCreateStruct->setField('description', $this->dsl['description']); + if (isset($step->dsl['description'])) { + $userGroupCreateStruct->setField('description', $step->dsl['description']); } - if (isset($this->dsl['section'])) { - $sectionKey = $this->referenceResolver->resolveReference($this->dsl['section']); + if (isset($step->dsl['section'])) { + $sectionKey = $this->referenceResolver->resolveReference($step->dsl['section']); $section = $this->sectionMatcher->matchOneByKey($sectionKey); $userGroupCreateStruct->sectionId = $section->id; } $userGroup = $userService->createUserGroup($userGroupCreateStruct, $parentGroup); - if (isset($this->dsl['roles'])) { + if (isset($step->dsl['roles'])) { $roleService = $this->repository->getRoleService(); // we support both Ids and Identifiers - foreach ($this->dsl['roles'] as $roleId) { + foreach ($step->dsl['roles'] as $roleId) { $roleId = $this->referenceResolver->resolveReference($roleId); $role = $this->roleMatcher->matchOneByKey($roleId); $roleService->assignRoleToUserGroup($role, $userGroup); } } - $this->setReferences($userGroup); + $this->setReferences($userGroup, $step); return $userGroup; } @@ -78,11 +78,11 @@ protected function create() * * @throws \Exception When the ID of the user group is missing from the migration definition. */ - protected function update() + protected function update($step) { - $userGroupCollection = $this->matchUserGroups('update'); + $userGroupCollection = $this->matchUserGroups('update', $step); - if (count($userGroupCollection) > 1 && isset($this->dsl['references'])) { + if (count($userGroupCollection) > 1 && isset($step->dsl['references'])) { throw new \Exception("Can not execute Group update because multiple groups match, and a references section is specified in the dsl. References can be set when only 1 group matches"); } @@ -97,24 +97,24 @@ protected function update() /** @var $contentUpdateStruct \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct */ $contentUpdateStruct = $contentService->newContentUpdateStruct(); - if (isset($this->dsl['name'])) { - $contentUpdateStruct->setField('name', $this->dsl['name']); + if (isset($step->dsl['name'])) { + $contentUpdateStruct->setField('name', $step->dsl['name']); } - if (isset($this->dsl['remote_id'])) { - $contentUpdateStruct->remoteId = $this->dsl['remote_id']; + if (isset($step->dsl['remote_id'])) { + $contentUpdateStruct->remoteId = $step->dsl['remote_id']; } - if (isset($this->dsl['description'])) { - $contentUpdateStruct->setField('description', $this->dsl['description']); + if (isset($step->dsl['description'])) { + $contentUpdateStruct->setField('description', $step->dsl['description']); } $updateStruct->contentUpdateStruct = $contentUpdateStruct; $userGroup = $userService->updateUserGroup($userGroup, $updateStruct); - if (isset($this->dsl['parent_group_id'])) { - $parentGroupId = $this->dsl['parent_group_id']; + if (isset($step->dsl['parent_group_id'])) { + $parentGroupId = $step->dsl['parent_group_id']; $parentGroupId = $this->referenceResolver->resolveReference($parentGroupId); $newParentGroup = $this->userGroupMatcher->matchOneByKey($parentGroupId); @@ -122,14 +122,14 @@ protected function update() $userService->moveUserGroup($userGroup, $newParentGroup); } - if (isset($this->dsl['section'])) { - $this->setSection($userGroup, $this->dsl['section']); + if (isset($step->dsl['section'])) { + $this->setSection($userGroup, $step->dsl['section']); } $userGroupCollection[$key] = $userGroup; } - $this->setReferences($userGroupCollection); + $this->setReferences($userGroupCollection, $step); return $userGroupCollection; } @@ -139,9 +139,9 @@ protected function update() * * @throws \Exception When there are no groups specified for deletion. */ - protected function delete() + protected function delete($step) { - $userGroupCollection = $this->matchUserGroups('delete'); + $userGroupCollection = $this->matchUserGroups('delete', $step); $userService = $this->repository->getUserService(); @@ -157,24 +157,26 @@ protected function delete() * @return UserGroupCollection * @throws \Exception */ - protected function matchUserGroups($action) + protected function matchUserGroups($action, $step) { - if (!isset($this->dsl['id']) && !isset($this->dsl['group']) && !isset($this->dsl['match'])) { + if (!isset($step->dsl['id']) && !isset($step->dsl['group']) && !isset($step->dsl['match'])) { throw new \Exception("The id of a user group or a match condition is required to $action it"); } // Backwards compat - if (!isset($this->dsl['match'])) { - if (isset($this->dsl['id'])) { - $this->dsl['match']['id'] = $this->dsl['id']; + if (isset($step->dsl['match'])) { + $match = $step->dsl['match']; + } else { + if (isset($step->dsl['id'])) { + $match = array('id' => $step->dsl['id']); } - if (isset($this->dsl['group'])) { - $this->dsl['match']['id'] = $this->dsl['group']; + if (isset($step->dsl['group'])) { + $match = array('id' => $step->dsl['group']); } } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($match); return $this->userGroupMatcher->match($match); } @@ -186,9 +188,9 @@ protected function matchUserGroups($action) * @param \eZ\Publish\API\Repository\Values\User\UserGroup|UserGroupCollection $userGroup * @return boolean */ - protected function setReferences($userGroup) + protected function setReferences($userGroup, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } @@ -199,7 +201,7 @@ protected function setReferences($userGroup) $userGroup = reset($userGroup); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'object_id': @@ -212,7 +214,11 @@ protected function setReferences($userGroup) throw new \InvalidArgumentException('User Group Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; diff --git a/Core/Executor/UserManager.php b/Core/Executor/UserManager.php index 407a8958..7ab31975 100644 --- a/Core/Executor/UserManager.php +++ b/Core/Executor/UserManager.php @@ -28,21 +28,21 @@ public function __construct(UserMatcher $userMatcher, UserGroupMatcher $userGrou * * @todo allow setting extra profile attributes! */ - protected function create() + protected function create($step) { - if (!isset($this->dsl['groups'])) { + if (!isset($step->dsl['groups'])) { throw new \Exception('No user groups set to create user in.'); } - if (!is_array($this->dsl['groups'])) { - $this->dsl['groups'] = array($this->dsl['groups']); + if (!is_array($step->dsl['groups'])) { + $step->dsl['groups'] = array($step->dsl['groups']); } $userService = $this->repository->getUserService(); $contentTypeService = $this->repository->getContentTypeService(); $userGroups = array(); - foreach ($this->dsl['groups'] as $groupId) { + foreach ($step->dsl['groups'] as $groupId) { $groupId = $this->referenceResolver->resolveReference($groupId); $userGroup = $this->userGroupMatcher->matchOneByKey($groupId); @@ -53,22 +53,22 @@ protected function create() } // FIXME: Hard coding content type to user for now - $userContentType = $contentTypeService->loadContentTypeByIdentifier(self::USER_CONTENT_TYPE); + $userContentType = $contentTypeService->loadContentTypeByIdentifier($this->getUserContentType($step)); $userCreateStruct = $userService->newUserCreateStruct( - $this->dsl['username'], - $this->dsl['email'], - $this->dsl['password'], - $this->getLanguageCode(), + $step->dsl['username'], + $step->dsl['email'], + $step->dsl['password'], + $this->getLanguageCode($step), $userContentType ); - $userCreateStruct->setField('first_name', $this->dsl['first_name']); - $userCreateStruct->setField('last_name', $this->dsl['last_name']); + $userCreateStruct->setField('first_name', $step->dsl['first_name']); + $userCreateStruct->setField('last_name', $step->dsl['last_name']); // Create the user $user = $userService->createUser($userCreateStruct, $userGroups); - $this->setReferences($user); + $this->setReferences($user, $step); return $user; } @@ -78,15 +78,15 @@ protected function create() * * @todo allow setting extra profile attributes! */ - protected function update() + protected function update($step) { - $userCollection = $this->matchUsers('user'); + $userCollection = $this->matchUsers('user', $step); - if (count($userCollection) > 1 && isset($this->dsl['references'])) { + if (count($userCollection) > 1 && isset($step->dsl['references'])) { throw new \Exception("Can not execute User update because multiple user match, and a references section is specified in the dsl. References can be set when only 1 user matches"); } - if (count($userCollection) > 1 && isset($this->dsl['email'])) { + if (count($userCollection) > 1 && isset($step->dsl['email'])) { throw new \Exception("Can not execute User update because multiple user match, and an email section is specified in the dsl."); } @@ -96,28 +96,30 @@ protected function update() $userUpdateStruct = $userService->newUserUpdateStruct(); - if (isset($this->dsl['email'])) { - $userUpdateStruct->email = $this->dsl['email']; + if (isset($step->dsl['email'])) { + $userUpdateStruct->email = $step->dsl['email']; } - if (isset($this->dsl['password'])) { - $userUpdateStruct->password = (string)$this->dsl['password']; + if (isset($step->dsl['password'])) { + $userUpdateStruct->password = (string)$step->dsl['password']; } - if (isset($this->dsl['enabled'])) { - $userUpdateStruct->enabled = $this->dsl['enabled']; + if (isset($step->dsl['enabled'])) { + $userUpdateStruct->enabled = $step->dsl['enabled']; } $user = $userService->updateUser($user, $userUpdateStruct); - if (isset($this->dsl['groups'])) { - if (!is_array($this->dsl['groups'])) { - $this->dsl['groups'] = array($this->dsl['groups']); + if (isset($step->dsl['groups'])) { + $groups = $step->dsl['groups']; + + if (!is_array($groups)) { + $groups = array($groups); } $assignedGroups = $userService->loadUserGroupsOfUser($user); $targetGroupIds = []; // Assigning new groups to the user - foreach ($this->dsl['groups'] as $groupToAssignId) { + foreach ($groups as $groupToAssignId) { $groupId = $this->referenceResolver->resolveReference($groupToAssignId); $groupToAssign = $this->userGroupMatcher->matchOneByKey($groupId); $targetGroupIds[] = $groupToAssign->id; @@ -146,7 +148,7 @@ protected function update() $userCollection[$key] = $user; } - $this->setReferences($userCollection); + $this->setReferences($userCollection, $step); return $userCollection; } @@ -154,9 +156,9 @@ protected function update() /** * Method to handle the delete operation of the migration instructions */ - protected function delete() + protected function delete($step) { - $userCollection = $this->matchUsers('delete'); + $userCollection = $this->matchUsers('delete', $step); $userService = $this->repository->getUserService(); @@ -172,32 +174,34 @@ protected function delete() * @return UserCollection * @throws \Exception */ - protected function matchUsers($action) + protected function matchUsers($action, $step) { - if (!isset($this->dsl['id']) && !isset($this->dsl['user_id']) && !isset($this->dsl['email']) && !isset($this->dsl['username']) && !isset($this->dsl['match'])) { + if (!isset($step->dsl['id']) && !isset($step->dsl['user_id']) && !isset($step->dsl['email']) && !isset($step->dsl['username']) && !isset($step->dsl['match'])) { throw new \Exception("The id, email or username of a user or a match condition is required to $action it"); } // Backwards compat - if (!isset($this->dsl['match'])) { + if (isset($step->dsl['match'])) { + $match = $step->dsl['match']; + } else { $conds = array(); - if (isset($this->dsl['id'])) { - $conds['id'] = $this->dsl['id']; + if (isset($step->dsl['id'])) { + $conds['id'] = $step->dsl['id']; } - if (isset($this->dsl['user_id'])) { - $conds['id'] = $this->dsl['user_id']; + if (isset($step->dsl['user_id'])) { + $conds['id'] = $step->dsl['user_id']; } - if (isset($this->dsl['email'])) { - $conds['email'] = $this->dsl['email']; + if (isset($step->dsl['email'])) { + $conds['email'] = $step->dsl['email']; } - if (isset($this->dsl['username'])) { - $conds['login'] = $this->dsl['username']; + if (isset($step->dsl['username'])) { + $conds['login'] = $step->dsl['username']; } - $this->dsl['match'] = $conds; + $match = $conds; } // convert the references passed in the match - $match = $this->resolveReferencesRecursively($this->dsl['match']); + $match = $this->resolveReferencesRecursively($match); return $this->userMatcher->match($match); } @@ -211,9 +215,9 @@ protected function matchUsers($action) * @throws \InvalidArgumentException when trying to set references to unsupported attributes * @return boolean */ - protected function setReferences($user) + protected function setReferences($user, $step) { - if (!array_key_exists('references', $this->dsl)) { + if (!array_key_exists('references', $step->dsl)) { return false; } @@ -224,7 +228,7 @@ protected function setReferences($user) $user = reset($user); } - foreach ($this->dsl['references'] as $reference) { + foreach ($step->dsl['references'] as $reference) { switch ($reference['attribute']) { case 'user_id': case 'id': @@ -243,7 +247,11 @@ protected function setReferences($user) throw new \InvalidArgumentException('User Manager does not support setting references for attribute ' . $reference['attribute']); } - $this->referenceResolver->addReference($reference['identifier'], $value); + $overwrite = false; + if (isset($reference['overwrite'])) { + $overwrite = $reference['overwrite']; + } + $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite); } return true; diff --git a/Core/FieldHandler/AbstractFieldHandler.php b/Core/FieldHandler/AbstractFieldHandler.php new file mode 100644 index 00000000..83a04117 --- /dev/null +++ b/Core/FieldHandler/AbstractFieldHandler.php @@ -0,0 +1,16 @@ +referenceResolver = $referenceResolver; + } +} diff --git a/Core/ComplexField/EzAuthor.php b/Core/FieldHandler/EzAuthor.php similarity index 90% rename from Core/ComplexField/EzAuthor.php rename to Core/FieldHandler/EzAuthor.php index 04727a17..6f65fbb9 100644 --- a/Core/ComplexField/EzAuthor.php +++ b/Core/FieldHandler/EzAuthor.php @@ -1,6 +1,6 @@ resolver and $this->referenceResolver -class EzRichText extends AbstractComplexField implements FieldValueImporterInterface +class EzRichText extends AbstractFieldHandler implements FieldValueImporterInterface { protected $resolver; diff --git a/Core/ComplexField/EzSelection.php b/Core/FieldHandler/EzSelection.php similarity index 95% rename from Core/ComplexField/EzSelection.php rename to Core/FieldHandler/EzSelection.php index 092de8e8..a0678910 100644 --- a/Core/ComplexField/EzSelection.php +++ b/Core/FieldHandler/EzSelection.php @@ -1,12 +1,12 @@ resolver and $this->referenceResolver -class EzXmlText extends AbstractComplexField implements FieldValueImporterInterface, FieldDefinitionConverterInterface +class EzXmlText extends AbstractFieldHandler implements FieldValueImporterInterface, FieldDefinitionConverterInterface { protected $resolver; diff --git a/Core/ComplexField/FileField.php b/Core/FieldHandler/FileFieldHandler.php similarity index 72% rename from Core/ComplexField/FileField.php rename to Core/FieldHandler/FileFieldHandler.php index 2ce9dd3a..e1de7f46 100644 --- a/Core/ComplexField/FileField.php +++ b/Core/FieldHandler/FileFieldHandler.php @@ -1,10 +1,10 @@ fieldTypeService = $fieldTypeService; + } + + /** + * @param FieldValueImporterInterface $fieldHandler + * @param string $fieldTypeIdentifier + * @param string $contentTypeIdentifier + * @throws \Exception + */ + public function addFieldHandler($fieldHandler, $fieldTypeIdentifier, $contentTypeIdentifier = null) + { + // This is purely BC; at some point we will typehint to FieldValueImporterInterface + if (!$fieldHandler instanceof FieldValueImporterInterface) { + throw new \Exception("Can not register object of class '" . get_class($fieldHandler) . "' as field handler because it does not support the desired interface"); + } + + if ($contentTypeIdentifier == null) { + $contentTypeIdentifier = '*'; + } + $this->fieldTypeMap[$contentTypeIdentifier][$fieldTypeIdentifier] = $fieldHandler; + } + + /** + * @param string $fieldTypeIdentifier + * @param string $contentTypeIdentifier + * @return bool + */ + public function managesField($fieldTypeIdentifier, $contentTypeIdentifier) + { + return (isset($this->fieldTypeMap[$contentTypeIdentifier][$fieldTypeIdentifier]) || + isset($this->fieldTypeMap['*'][$fieldTypeIdentifier])); + } + + /** + * @param string $fieldTypeIdentifier + * @param string $contentTypeIdentifier + * @param mixed $hashValue + * @param array $context + * @return mixed + */ + public function hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $hashValue, array $context = array()) + { + if ($this->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { + $fieldHandler = $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier); + // BC + if (!$fieldHandler instanceof FieldValueImporterInterface) { + return $fieldHandler->createValue($hashValue, $context); + } + return $fieldHandler->hashToFieldValue($hashValue, $context); + } + + $fieldType = $this->fieldTypeService->getFieldType($fieldTypeIdentifier); + return $fieldType->fromHash($hashValue); + } + + /** + * @param string $fieldTypeIdentifier + * @param string $contentTypeIdentifier + * @param \eZ\Publish\SPI\FieldType\Value $value + * @param array $context + * @return mixed + */ + public function fieldValueToHash($fieldTypeIdentifier, $contentTypeIdentifier, $value, array $context = array()) + { + if ($this->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { + $fieldHandler = $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier); + if ($fieldHandler instanceof FieldValueConverterInterface) { + return $fieldHandler->fieldValueToHash($value, $context); + } + } + + $fieldType = $this->fieldTypeService->getFieldType($fieldTypeIdentifier); + return $fieldType->toHash($value); + } + + /** + * @param string $fieldTypeIdentifier + * @param string $contentTypeIdentifier + * @return bool + */ + public function managesFieldDefinition($fieldTypeIdentifier, $contentTypeIdentifier) + { + if (!$this->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) { + return false; + } + + $fieldHandler = $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier); + return ($fieldHandler instanceof FieldDefinitionConverterInterface); + } + + /** + * @param string $fieldTypeIdentifier + * @param string $contentTypeIdentifier + * @param mixed $fieldSettingsHash + * @param array $context + * @return mixed + */ + public function hashToFieldSettings($fieldTypeIdentifier, $contentTypeIdentifier, $fieldSettingsHash, array $context = array()) + { + if ($this->managesFieldDefinition($fieldTypeIdentifier, $contentTypeIdentifier)) { + return $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier)->hashToFieldSettings($fieldSettingsHash, $context); + } + + return $fieldSettingsHash; + } + + /** + * @param string $fieldTypeIdentifier + * @param string $contentTypeIdentifier + * @param mixed $fieldSettings + * @param array $context + * @return mixed + */ + public function fieldSettingsToHash($fieldTypeIdentifier, $contentTypeIdentifier, $fieldSettings, array $context = array()) + { + if ($this->managesFieldDefinition($fieldTypeIdentifier, $contentTypeIdentifier)) { + return $this->getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier)->fieldSettingsToHash($fieldSettings, $context); + } + + return $fieldSettings; + } + + /** + * @param string $fieldTypeIdentifier + * @param string $contentTypeIdentifier + * @return FieldValueImporterInterface + * @throws \Exception + */ + protected function getFieldHandler($fieldTypeIdentifier, $contentTypeIdentifier) { + if (isset($this->fieldTypeMap[$contentTypeIdentifier][$fieldTypeIdentifier])) { + return $this->fieldTypeMap[$contentTypeIdentifier][$fieldTypeIdentifier]; + } else if (isset($this->fieldTypeMap['*'][$fieldTypeIdentifier])) { + return $this->fieldTypeMap['*'][$fieldTypeIdentifier]; + } + + throw new \Exception("No complex field handler registered for field '$fieldTypeIdentifier' in content type '$contentTypeIdentifier'"); + } +} diff --git a/Core/Matcher/AbstractMatcher.php b/Core/Matcher/AbstractMatcher.php index 8403afc4..4f70eadd 100644 --- a/Core/Matcher/AbstractMatcher.php +++ b/Core/Matcher/AbstractMatcher.php @@ -19,7 +19,7 @@ protected function validateConditions(array $conditions) throw new \Exception($this->returns . ' can not be matched because the matching conditions are empty'); } - if (count($conditions) > $this->maxConditions) { + if ($this->maxConditions > 0 && count($conditions) > $this->maxConditions) { throw new \Exception($this->returns . " can not be matched because multiple matching conditions are specified. Only {$this->maxConditions} condition(s) are supported"); } @@ -33,6 +33,7 @@ protected function validateConditions(array $conditions) protected function matchAnd($conditionsArray) { + /// @todo introduce proper re-validation of all child conditions if (!is_array($conditionsArray) || !count($conditionsArray)) { throw new \Exception($this->returns . " can not be matched because no matching conditions found for 'and' clause."); } @@ -60,6 +61,7 @@ protected function matchAnd($conditionsArray) protected function matchOr(array $conditionsArray) { + /// @todo introduce proper re-validation of all child conditions if (!is_array($conditionsArray) || !count($conditionsArray)) { throw new \Exception($this->returns . " can not be matched because no matching conditions found for 'or' clause."); } @@ -92,5 +94,9 @@ public function matchOne(array $conditions) return reset($results); } + /** + * @param array $conditions + * @return array|\ArrayObject the keys must be a unique identifier of the matched entities + */ abstract public function match(array $conditions); } diff --git a/Core/Matcher/ContentMatcher.php b/Core/Matcher/ContentMatcher.php index 8b322c25..1c80b5bd 100644 --- a/Core/Matcher/ContentMatcher.php +++ b/Core/Matcher/ContentMatcher.php @@ -6,27 +6,17 @@ use eZ\Publish\API\Repository\Values\Content\Query; use Kaliop\eZMigrationBundle\API\Collection\ContentCollection; -/** - * @todo extend to allow matching by visibility, subtree, depth, object state, section, creation/modification date... - * @todo optimize the matches on multiple conditions (and, or) by compiling them in a single query - */ -class ContentMatcher extends RepositoryMatcher +class ContentMatcher extends QueryBasedMatcher { use FlexibleKeyMatcherTrait; - const MATCH_CONTENT_ID = 'content_id'; - const MATCH_LOCATION_ID = 'location_id'; - const MATCH_CONTENT_REMOTE_ID = 'content_remote_id'; - const MATCH_LOCATION_REMOTE_ID = 'location_remote_id'; - const MATCH_PARENT_LOCATION_ID = 'parent_location_id'; - const MATCH_PARENT_LOCATION_REMOTE_ID = 'parent_location_remote_id'; - const MATCH_CONTENT_TYPE_ID = 'contenttype_id'; - const MATCH_CONTENT_TYPE_IDENTIFIER = 'contenttype_identifier'; - protected $allowedConditions = array( - self::MATCH_AND, self::MATCH_OR, + self::MATCH_AND, self::MATCH_OR, self::MATCH_NOT, self::MATCH_CONTENT_ID, self::MATCH_LOCATION_ID, self::MATCH_CONTENT_REMOTE_ID, self::MATCH_LOCATION_REMOTE_ID, - self::MATCH_PARENT_LOCATION_ID, self::MATCH_PARENT_LOCATION_REMOTE_ID, self::MATCH_CONTENT_TYPE_IDENTIFIER, + self::MATCH_ATTRIBUTE, self::MATCH_CONTENT_TYPE_ID, self::MATCH_CONTENT_TYPE_IDENTIFIER, self::MATCH_GROUP, + self::MATCH_CREATION_DATE, self::MATCH_MODIFICATION_DATE, self::MATCH_OBJECT_STATE, self::MATCH_OWNER, + self::MATCH_PARENT_LOCATION_ID, self::MATCH_PARENT_LOCATION_REMOTE_ID, self::MATCH_SECTION, self::MATCH_SUBTREE, + self::MATCH_VISIBILITY, // aliases 'content_type', 'content_type_id', 'content_type_identifier' ); @@ -51,10 +41,6 @@ public function matchContent(array $conditions) foreach ($conditions as $key => $values) { - if (!is_array($values)) { - $values = array($values); - } - // BC support if ($key == 'content_type') { if (is_int($values[0]) || ctype_digit($values[0])) { @@ -64,39 +50,29 @@ public function matchContent(array $conditions) } } + $query = new Query(); + $query->limit = PHP_INT_MAX; + $query->filter = $this->getQueryCriterion($key, $values); switch ($key) { - case self::MATCH_CONTENT_ID: - return new ContentCollection($this->findContentsByContentIds($values)); - - case self::MATCH_LOCATION_ID: - return new ContentCollection($this->findContentsByLocationIds($values)); - - case self::MATCH_CONTENT_REMOTE_ID: - return new ContentCollection($this->findContentsByContentRemoteIds($values)); - - case self::MATCH_LOCATION_REMOTE_ID: - return new ContentCollection($this->findContentsByLocationRemoteIds($values)); - - case self::MATCH_PARENT_LOCATION_ID: - return new ContentCollection($this->findContentsByParentLocationIds($values)); - - case self::MATCH_PARENT_LOCATION_REMOTE_ID: - return new ContentCollection($this->findContentsByParentLocationRemoteIds($values)); - case 'content_type_id': case self::MATCH_CONTENT_TYPE_ID: - return new ContentCollection($this->findContentsByContentTypeIds($values)); - case 'content_type_identifier': case self::MATCH_CONTENT_TYPE_IDENTIFIER: - return new ContentCollection($this->findContentsByContentTypeIdentifiers($values)); - - case self::MATCH_AND: - return $this->matchAnd($values); + // sort objects by depth, lower to higher, so that deleting them has less chances of failure + // NB: we only do this in eZP versions that allow depth sorting on content queries + if (class_exists('eZ\Publish\API\Repository\Values\Content\Query\SortClause\LocationDepth')) { + $query->sortClauses = array(new Query\SortClause\LocationDepth(Query::SORT_DESC)); + } + } + $results = $this->repository->getSearchService()->findContent($query); - case self::MATCH_OR: - return $this->matchOr($values); + $contents = []; + foreach ($results->searchHits as $result) { + // make sure we return every object only once + $contents[$result->valueObject->contentInfo->id] = $result->valueObject; } + + return new ContentCollection($contents); } } @@ -116,6 +92,7 @@ protected function getConditionsFromKey($key) /** * @param int[] $contentIds * @return Content[] + * @deprecated */ protected function findContentsByContentIds(array $contentIds) { @@ -133,6 +110,7 @@ protected function findContentsByContentIds(array $contentIds) /** * @param string[] $remoteContentIds * @return Content[] + * @deprecated */ protected function findContentsByContentRemoteIds(array $remoteContentIds) { @@ -150,6 +128,7 @@ protected function findContentsByContentRemoteIds(array $remoteContentIds) /** * @param int[] $locationIds * @return Content[] + * @deprecated */ protected function findContentsByLocationIds(array $locationIds) { @@ -167,6 +146,7 @@ protected function findContentsByLocationIds(array $locationIds) /** * @param string[] $remoteLocationIds * @return Content[] + * @deprecated */ protected function findContentsByLocationRemoteIds($remoteLocationIds) { @@ -184,6 +164,7 @@ protected function findContentsByLocationRemoteIds($remoteLocationIds) /** * @param int[] $parentLocationIds * @return Content[] + * @deprecated */ protected function findContentsByParentLocationIds($parentLocationIds) { @@ -204,6 +185,7 @@ protected function findContentsByParentLocationIds($parentLocationIds) /** * @param string[] $remoteParentLocationIds * @return Content[] + * @deprecated */ protected function findContentsByParentLocationRemoteIds($remoteParentLocationIds) { @@ -221,6 +203,7 @@ protected function findContentsByParentLocationRemoteIds($remoteParentLocationId /** * @param string[] $contentTypeIdentifiers * @return Content[] + * @deprecated */ protected function findContentsByContentTypeIdentifiers(array $contentTypeIdentifiers) { @@ -247,6 +230,7 @@ protected function findContentsByContentTypeIdentifiers(array $contentTypeIdenti /** * @param int[] $contentTypeIds * @return Content[] + * @deprecated */ protected function findContentsByContentTypeIds(array $contentTypeIds) { @@ -268,4 +252,5 @@ protected function findContentsByContentTypeIds(array $contentTypeIds) return $contents; } + } diff --git a/Core/Matcher/LocationMatcher.php b/Core/Matcher/LocationMatcher.php index 04da46c1..ed466a94 100644 --- a/Core/Matcher/LocationMatcher.php +++ b/Core/Matcher/LocationMatcher.php @@ -5,26 +5,27 @@ use eZ\Publish\API\Repository\Values\Content\Query; use Kaliop\eZMigrationBundle\API\Collection\LocationCollection; use eZ\Publish\API\Repository\Values\Content\LocationQuery; +use eZ\Publish\API\Repository\Values\Content\Location; -/** - * @todo extend to allow matching by visibility, subtree, depth, object state, section, creation/modification date... - * @todo optimize the matches on multiple conditions (and, or) by compiling them in a single query - */ -class LocationMatcher extends RepositoryMatcher +class LocationMatcher extends QueryBasedMatcher { use FlexibleKeyMatcherTrait; - const MATCH_CONTENT_ID = 'content_id'; - const MATCH_LOCATION_ID = 'location_id'; - const MATCH_CONTENT_REMOTE_ID = 'content_remote_id'; - const MATCH_LOCATION_REMOTE_ID = 'location_remote_id'; - const MATCH_PARENT_LOCATION_ID = 'parent_location_id'; - const MATCH_PARENT_LOCATION_REMOTE_ID = 'parent_location_remote_id'; + const MATCH_DEPTH = 'depth'; + const MATCH_IS_MAIN_LOCATION = 'is_main_location'; + const MATCH_PRIORITY = 'priority'; protected $allowedConditions = array( - self::MATCH_AND, self::MATCH_OR, + self::MATCH_AND, self::MATCH_OR, self::MATCH_NOT, self::MATCH_CONTENT_ID, self::MATCH_LOCATION_ID, self::MATCH_CONTENT_REMOTE_ID, self::MATCH_LOCATION_REMOTE_ID, - self::MATCH_PARENT_LOCATION_ID, self::MATCH_PARENT_LOCATION_REMOTE_ID + self::MATCH_ATTRIBUTE, self::MATCH_CONTENT_TYPE_ID, self::MATCH_CONTENT_TYPE_IDENTIFIER, self::MATCH_GROUP, + self::MATCH_CREATION_DATE, self::MATCH_MODIFICATION_DATE, self::MATCH_OBJECT_STATE, self::MATCH_OWNER, + self::MATCH_PARENT_LOCATION_ID, self::MATCH_PARENT_LOCATION_REMOTE_ID, self::MATCH_SECTION, self::MATCH_SUBTREE, + self::MATCH_VISIBILITY, + // aliases + 'content_type', 'content_type_id', 'content_type_identifier', + // location-only + self::MATCH_DEPTH, self::MATCH_IS_MAIN_LOCATION, self::MATCH_PRIORITY, ); protected $returns = 'Location'; @@ -47,35 +48,17 @@ public function matchLocation(array $conditions) foreach ($conditions as $key => $values) { - if (!is_array($values)) { - $values = array($values); - } - - switch ($key) { - case self::MATCH_CONTENT_ID: - return new LocationCollection($this->findLocationsByContentIds($values)); - - case self::MATCH_LOCATION_ID: - return new LocationCollection($this->findLocationsByLocationIds($values)); - - case self::MATCH_CONTENT_REMOTE_ID: - return new LocationCollection($this->findLocationsByContentRemoteIds($values)); - - case self::MATCH_LOCATION_REMOTE_ID: - return new LocationCollection($this->findLocationsByLocationRemoteIds($values)); - - case self::MATCH_PARENT_LOCATION_ID: - return new LocationCollection($this->findLocationsByParentLocationIds($values)); - - case self::MATCH_PARENT_LOCATION_REMOTE_ID: - return new LocationCollection($this->findLocationsByParentLocationRemoteIds($values)); - - case self::MATCH_AND: - return $this->matchAnd($values); + $query = new LocationQuery(); + $query->limit = PHP_INT_MAX; + $query->filter = $this->getQueryCriterion($key, $values); + $results = $this->repository->getSearchService()->findLocations($query); - case self::MATCH_OR: - return $this->matchOr($values); + $locations = []; + foreach ($results->searchHits as $result) { + $locations[$result->valueObject->id] = $result->valueObject; } + + return new LocationCollection($locations); } } @@ -92,11 +75,48 @@ protected function getConditionsFromKey($key) return array(self::MATCH_LOCATION_REMOTE_ID => $key); } + protected function getQueryCriterion($key, $values) + { + if (!is_array($values)) { + $values = array($values); + } + + switch ($key) { + case self::MATCH_DEPTH: + $match = reset($values); + $operator = key($values); + if (!isset(self::$operatorsMap[$operator])) { + throw new \Exception("Can not use '$operator' as comparison operator for depth"); + } + return new Query\Criterion\Location\Depth(self::$operatorsMap[$operator], $match); + + case self::MATCH_IS_MAIN_LOCATION: + /// @todo error/warning if there is more than 1 value... + $value = reset($values); + if ($value) { + return new Query\Criterion\Location\IsMainLocation(Query\Criterion\Location\IsMainLocation::MAIN); + } else { + return new Query\Criterion\Location\IsMainLocation(Query\Criterion\Location\IsMainLocation::NOT_MAIN); + } + + case self::MATCH_PRIORITY: + $match = reset($values); + $operator = key($values); + if (!isset(self::$operatorsMap[$operator])) { + throw new \Exception("Can not use '$operator' as comparison operator for depth"); + } + return new Query\Criterion\Location\Priority(self::$operatorsMap[$operator], $match); + } + + return parent::getQueryCriterion($key, $values); + } + /** * Returns all locations of a set of objects * * @param int[] $contentIds * @return Location[] + * @deprecated */ protected function findLocationsByContentIds(array $contentIds) { @@ -117,6 +137,7 @@ protected function findLocationsByContentIds(array $contentIds) * * @param int[] $remoteContentIds * @return Location[] + * @deprecated */ protected function findLocationsByContentRemoteIds(array $remoteContentIds) { @@ -135,6 +156,7 @@ protected function findLocationsByContentRemoteIds(array $remoteContentIds) /** * @param int[] $locationIds * @return Location[] + * @deprecated */ protected function findLocationsByLocationIds(array $locationIds) { @@ -150,6 +172,7 @@ protected function findLocationsByLocationIds(array $locationIds) /** * @param int[] $locationRemoteIds * @return Location[] + * @deprecated */ protected function findLocationsByLocationRemoteIds($locationRemoteIds) { @@ -166,6 +189,7 @@ protected function findLocationsByLocationRemoteIds($locationRemoteIds) /** * @param int[] $parentLocationIds * @return Location[] + * @deprecated */ protected function findLocationsByParentLocationIds($parentLocationIds) { @@ -187,6 +211,7 @@ protected function findLocationsByParentLocationIds($parentLocationIds) /** * @param int[] $parentLocationRemoteIds * @return Location[] + * @deprecated */ protected function findLocationsByParentLocationRemoteIds($parentLocationRemoteIds) { diff --git a/Core/Matcher/QueryBasedMatcher.php b/Core/Matcher/QueryBasedMatcher.php new file mode 100644 index 00000000..ea2b72f0 --- /dev/null +++ b/Core/Matcher/QueryBasedMatcher.php @@ -0,0 +1,221 @@ + Operator::EQ, + 'gt' => Operator::GT, + 'gte' => Operator::GTE, + 'lt' => Operator::LT, + 'lte' => Operator::LTE, + 'in' => Operator::IN, + 'between' => Operator::BETWEEN, + 'like' => Operator::LIKE, + 'contains' => Operator::CONTAINS, + Operator::EQ => Operator::EQ, + Operator::GT => Operator::GT, + Operator::GTE => Operator::GTE, + Operator::LT => Operator::LT, + Operator::LTE => Operator::LTE, + ); + + /** @var KeyMatcherInterface $groupMatcher */ + protected $groupMatcher; + /** @var KeyMatcherInterface $sectionMatcher */ + protected $sectionMatcher; + /** @var KeyMatcherInterface $stateMatcher */ + protected $stateMatcher; + /** @var KeyMatcherInterface $userMatcher */ + protected $userMatcher; + + /** + * @param Repository $repository + * @param KeyMatcherInterface $groupMatcher + * @param KeyMatcherInterface $sectionMatcher + * @param KeyMatcherInterface $stateMatcher + * @param KeyMatcherInterface $userMatcher + * @todo inject the services needed, not the whole repository + */ + public function __construct(Repository $repository, KeyMatcherInterface $groupMatcher = null, + KeyMatcherInterface $sectionMatcher = null, KeyMatcherInterface $stateMatcher = null, + KeyMatcherInterface $userMatcher = null) + { + parent::__construct($repository); + $this->userMatcher = $userMatcher; + $this->sectionMatcher = $sectionMatcher; + $this->stateMatcher = $stateMatcher; + $this->userMatcher = $userMatcher; + } + + /** + * @param $key + * @param $values + * @return mixed should it be \eZ\Publish\API\Repository\Values\Content\Query\CriterionInterface ? + * @throws \Exception for unsupported keys + */ + protected function getQueryCriterion($key, $values) + { + if (!is_array($values)) { + $values = array($values); + } + + switch ($key) { + case self::MATCH_CONTENT_ID: + return new Query\Criterion\ContentId($values); + + case self::MATCH_LOCATION_ID: + // NB: seems to cause problems with EZP 2014.3 + return new Query\Criterion\LocationId(reset($values)); + + case self::MATCH_CONTENT_REMOTE_ID: + return new Query\Criterion\RemoteId($values); + + case self::MATCH_LOCATION_REMOTE_ID: + return new Query\Criterion\LocationRemoteId($values); + + case self::MATCH_ATTRIBUTE: + $spec = reset($values); + $attribute = key($values); + $match = reset($spec); + $operator = key($spec); + if (!isset(self::$operatorsMap[$operator])) { + throw new \Exception("Can not use '$operator' as comparison operator for attributes"); + } + return new Query\Criterion\Field($attribute, self::$operatorsMap[$operator], $match); + + case 'content_type_id': + case self::MATCH_CONTENT_TYPE_ID: + return new Query\Criterion\ContentTypeId($values); + + case 'content_type_identifier': + case self::MATCH_CONTENT_TYPE_IDENTIFIER: + return new Query\Criterion\ContentTypeIdentifier($values); + + case self::MATCH_CREATION_DATE: + $match = reset($values); + $operator = key($values); + if (!isset(self::$operatorsMap[$operator])) { + throw new \Exception("Can not use '$operator' as comparison operator for dates"); + } + return new Query\Criterion\DateMetadata(Query\Criterion\DateMetadata::CREATED, self::$operatorsMap[$operator], $match); + + case self::MATCH_GROUP: + foreach($values as &$value) { + if (!ctype_digit($value)) { + $value = $this->groupMatcher->matchOneByKey($value)->id; + } + } + return new Query\Criterion\UserMetadata(Query\Criterion\UserMetadata::GROUP, Operator::IN, $values); + + case self::MATCH_MODIFICATION_DATE: + $match = reset($values); + $operator = key($values); + if (!isset(self::$operatorsMap[$operator])) { + throw new \Exception("Can not use '$operator' as comparison operator for dates"); + } + return new Query\Criterion\DateMetadata(Query\Criterion\DateMetadata::MODIFIED, self::$operatorsMap[$operator], $match); + + case self::MATCH_OBJECT_STATE: + foreach($values as &$value) { + if (!ctype_digit($value)) { + $value = $this->stateMatcher->matchOneByKey($value)->id; + } + } + return new Query\Criterion\ObjectStateId($values); + + case self::MATCH_OWNER: + foreach($values as &$value) { + if (!ctype_digit($value)) { + $value = $this->userMatcher->matchOneByKey($value)->id; + } + } + return new Query\Criterion\UserMetadata(Query\Criterion\UserMetadata::OWNER, Operator::IN, $values); + + case self::MATCH_PARENT_LOCATION_ID: + return new Query\Criterion\ParentLocationId($values); + + case self::MATCH_PARENT_LOCATION_REMOTE_ID: + $locationIds = []; + foreach ($values as $remoteParentLocationId) { + $location = $this->repository->getLocationService()->loadLocationByRemoteId($remoteParentLocationId); + // unique locations + $locationIds[$location->id] = $location->id; + } + return new Query\Criterion\ParentLocationId($locationIds); + + case self::MATCH_SECTION: + foreach($values as &$value) { + if (!ctype_digit($value)) { + $value = $this->sectionMatcher->matchOneByKey($value)->id; + } + } + return new Query\Criterion\SectionId($values); + + case self::MATCH_SUBTREE: + return new Query\Criterion\Subtree($values); + + case self::MATCH_VISIBILITY: + /// @todo error/warning if there is more than 1 value... + $value = reset($values); + if ($value) { + return new Query\Criterion\Visibility(Query\Criterion\Visibility::VISIBLE); + } else { + return new Query\Criterion\Visibility(Query\Criterion\Visibility::HIDDEN); + } + + case self::MATCH_AND: + $subCriteria = array(); + foreach($values as $subCriterion) { + $value = reset($subCriterion); + $subCriteria[] = $this->getQueryCriterion(key($subCriterion), $value); + } + return new Query\Criterion\LogicalAnd($subCriteria); + + case self::MATCH_OR: + $subCriteria = array(); + foreach($values as $subCriterion) { + $value = reset($subCriterion); + $subCriteria[] = $this->getQueryCriterion(key($subCriterion), $value); + } + return new Query\Criterion\LogicalOr($subCriteria); + + case self::MATCH_NOT: + /// @todo throw if more than one sub-criteria found + $value = reset($values); + $subCriterion = $this->getQueryCriterion(key($values), $value); + return new Query\Criterion\LogicalNot($subCriterion); + + default: + throw new \Exception($this->returns . " can not be matched because matching condition '$key' is not supported. Supported conditions are: " . + implode(', ', $this->allowedConditions)); + } + } +} diff --git a/Core/Matcher/ReferenceMatcher.php b/Core/Matcher/ReferenceMatcher.php new file mode 100644 index 00000000..549e02cf --- /dev/null +++ b/Core/Matcher/ReferenceMatcher.php @@ -0,0 +1,139 @@ + '\Symfony\Component\Validator\Constraints\EqualTo', + 'gt' => '\Symfony\Component\Validator\Constraints\GreaterThan', + 'gte' => '\Symfony\Component\Validator\Constraints\GreaterThanOrEqual', + 'lt' => '\Symfony\Component\Validator\Constraints\LessThan', + 'lte' => '\Symfony\Component\Validator\Constraints\LessThanOrEqual', + 'ne' => '\Symfony\Component\Validator\Constraints\NotEqualTo', + + 'count' => '\Symfony\Component\Validator\Constraints\Count', + 'length' => '\Symfony\Component\Validator\Constraints\Length', + 'regex' => '\Symfony\Component\Validator\Constraints\Regex', + //'in' => Operator::IN, + //'between' => Operator::BETWEEN, => use count/length with min & max sub-members + //'like' => Operator::LIKE, => use regex + //'contains' => Operator::CONTAINS, + Operator::EQ => '\Symfony\Component\Validator\Constraints\EqualTo', + Operator::GT => '\Symfony\Component\Validator\Constraints\GreaterThan', + Operator::GTE => '\Symfony\Component\Validator\Constraints\GreaterThanOrEqual', + Operator::LT => '\Symfony\Component\Validator\Constraints\LessThan', + Operator::LTE => '\Symfony\Component\Validator\Constraints\LessThanOrEqual', + '!=' => '\Symfony\Component\Validator\Constraints\NotEqualTo', + '<>' => '\Symfony\Component\Validator\Constraints\NotEqualTo', + ); + + public function __construct(ReferenceResolverInterface $referenceResolver, ValidatorInterface $validator) + { + $this->referenceResolver = $referenceResolver; + $this->validator = $validator; + } + + protected function validateConditions(array $conditions) + { + foreach ($conditions as $key => $val) { + if ($this->referenceResolver->isReference($key)) { + return true; + } + } + + return parent::validateConditions($conditions); + } + + /** + * @param array $conditions key: condition, value: int / string / int[] / string[] + * @return array 1 element with the value true/false + */ + public function match(array $conditions) + { + $this->validateConditions($conditions); + + foreach ($conditions as $key => $values) { + + switch ($key) { + case self::MATCH_AND: + foreach($values as $subCriterion) { + $value = $this->match($subCriterion); + if (!reset($value)) { + return $value; + } + } + return array(true); + + case self::MATCH_OR: + foreach($values as $subCriterion) { + $value = $this->match($subCriterion); + if (reset($value)) { + return $value; + } + } + return array(false); + + case self::MATCH_NOT: + return array(!reset($this->match($values))); + + default: + // we assume that all are refs because of the call to validate() + $currentValue = $this->referenceResolver->resolveReference($key); + $targetValue = reset($values); + $constraint = key($values); + $errorList = $this->validator->validate($currentValue, $this->getConstraint($constraint, $targetValue)); + if (0 === count($errorList)) { + return array(true); + } + return array(false); + } + } + } + + /** + * @param string $constraint + * @param $targetValue + * @return mixed + * @throws \Exception for unsupported keys + */ + protected function getConstraint($constraint, $targetValue) + { + /*if (!is_array($values)) { + $values = array($values); + }*/ + + if (!isset(self::$operatorsMap[$constraint])) { + throw new \Exception("Matching condition '$constraint' is not supported. Supported conditions are: " . + implode(', ', array_keys(self::$operatorsMap)) + ); + } + + $class = self::$operatorsMap[$constraint]; + return new $class($targetValue); + } +} diff --git a/Core/MigrationService.php b/Core/MigrationService.php index f2bc30e5..cd32d51c 100644 --- a/Core/MigrationService.php +++ b/Core/MigrationService.php @@ -2,29 +2,31 @@ namespace Kaliop\eZMigrationBundle\Core; +use Kaliop\eZMigrationBundle\API\Value\MigrationStep; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use eZ\Publish\API\Repository\Repository; use Kaliop\eZMigrationBundle\API\Collection\MigrationDefinitionCollection; -use Kaliop\eZMigrationBundle\API\LanguageAwareInterface; use Kaliop\eZMigrationBundle\API\StorageHandlerInterface; use Kaliop\eZMigrationBundle\API\LoaderInterface; use Kaliop\eZMigrationBundle\API\DefinitionParserInterface; use Kaliop\eZMigrationBundle\API\ExecutorInterface; +use Kaliop\eZMigrationBundle\API\ContextProviderInterface; use Kaliop\eZMigrationBundle\API\Value\Migration; use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; use Kaliop\eZMigrationBundle\API\Exception\MigrationStepExecutionException; use Kaliop\eZMigrationBundle\API\Exception\MigrationAbortedException; +use Kaliop\eZMigrationBundle\API\Exception\MigrationSuspendedException; use Kaliop\eZMigrationBundle\API\Event\BeforeStepExecutionEvent; use Kaliop\eZMigrationBundle\API\Event\StepExecutedEvent; use Kaliop\eZMigrationBundle\API\Event\MigrationAbortedEvent; +use Kaliop\eZMigrationBundle\API\Event\MigrationSuspendedEvent; -class MigrationService +class MigrationService implements ContextProviderInterface { use RepositoryUserSetterTrait; /** - * Constant defining the default Admin user ID. - * @todo inject via config parameter + * The default Admin user Id, used when no Admin user is specified */ const ADMIN_USER_ID = 14; @@ -47,14 +49,23 @@ class MigrationService protected $dispatcher; + /** + * @var ContextHandler $contextHandler + */ + protected $contextHandler; + protected $eventPrefix = 'ez_migration.'; - public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository, EventDispatcherInterface $eventDispatcher) + protected $migrationContext = array(); + + public function __construct(LoaderInterface $loader, StorageHandlerInterface $storageHandler, Repository $repository, + EventDispatcherInterface $eventDispatcher, $contextHandler) { $this->loader = $loader; $this->storageHandler = $storageHandler; $this->repository = $repository; $this->dispatcher = $eventDispatcher; + $this->contextHandler = $contextHandler; } public function addDefinitionParser(DefinitionParserInterface $DefinitionParser) @@ -120,11 +131,26 @@ public function getMigrationsDefinitions(array $paths = array()) /** * Returns the list of all the migrations which where executed or attempted so far * + * @param int $limit 0 or below will be treated as 'no limit' + * @param int $offset + * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection + */ + public function getMigrations($limit = null, $offset = null) + { + return $this->storageHandler->loadMigrations($limit, $offset); + } + + /** + * Returns the list of all the migrations in a given status which where executed or attempted so far + * + * @param int $status + * @param int $limit 0 or below will be treated as 'no limit' + * @param int $offset * @return \Kaliop\eZMigrationBundle\API\Collection\MigrationCollection */ - public function getMigrations() + public function getMigrationsByStatus($status, $limit = null, $offset = null) { - return $this->storageHandler->loadMigrations(); + return $this->storageHandler->loadMigrationsByStatus($status, $limit, $offset); } /** @@ -212,11 +238,11 @@ public function parseMigrationDefinition(MigrationDefinition $migrationDefinitio * @param MigrationDefinition $migrationDefinition * @param bool $useTransaction when set to false, no repo transaction will be used to wrap the migration * @param string $defaultLanguageCode + * @param string $adminLogin * @throws \Exception - * - * @todo add support for skipped migrations, partially executed migrations */ - public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true, $defaultLanguageCode = null) + public function executeMigration(MigrationDefinition $migrationDefinition, $useTransaction = true, + $defaultLanguageCode = null, $adminLogin = null) { if ($migrationDefinition->status == MigrationDefinition::STATUS_TO_PARSE) { $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition); @@ -226,38 +252,48 @@ public function executeMigration(MigrationDefinition $migrationDefinition, $useT throw new \Exception("Can not execute migration '{$migrationDefinition->name}': {$migrationDefinition->parsingError}"); } - // Inject default language code in executors that support it. - if ($defaultLanguageCode) { - foreach ($this->executors as $executor) { - if ($executor instanceof LanguageAwareInterface) { - $executor->setDefaultLanguageCode($defaultLanguageCode); - } - } - } + /// @todo add support for setting in $migrationContext a userContentType ? + $migrationContext = $this->migrationContextFromParameters($defaultLanguageCode, $adminLogin); // set migration as begun - has to be in own db transaction $migration = $this->storageHandler->startMigration($migrationDefinition); + $this->executeMigrationInner($migration, $migrationDefinition, $migrationContext, 0, $useTransaction, $adminLogin); + } + + /** + * @param Migration $migration + * @param MigrationDefinition $migrationDefinition + * @param array $migrationContext + * @param int $stepOffset + * @param bool $useTransaction when set to false, no repo transaction will be used to wrap the migration + * @param string $adminLogin + * @throws \Exception + */ + protected function executeMigrationInner(Migration $migration, MigrationDefinition $migrationDefinition, + $migrationContext, $stepOffset = 0, $useTransaction = true, $adminLogin = null) + { if ($useTransaction) { $this->repository->beginTransaction(); } $previousUserId = null; + $steps = array_slice($migrationDefinition->steps->getArrayCopy(), $stepOffset); try { - $i = 1; + $i = $stepOffset+1; $finalStatus = Migration::STATUS_DONE; $finalMessage = null; try { - foreach ($migrationDefinition->steps as $step) { + foreach ($steps as $step) { + + $step = $this->injectContextIntoStep($step, $migrationContext); + // we validated the fact that we have a good executor at parsing time $executor = $this->executors[$step->type]; - if ($executor instanceof LanguageAwareInterface) { - $executor->setLanguageCode(null); - } $beforeStepExecutionEvent = new BeforeStepExecutionEvent($step, $executor); $this->dispatcher->dispatch($this->eventPrefix . 'before_execution', $beforeStepExecutionEvent); @@ -279,6 +315,18 @@ public function executeMigration(MigrationDefinition $migrationDefinition, $useT $finalStatus = $e->getCode(); $finalMessage = "Abort in execution of step $i: " . $e->getMessage(); + } catch (MigrationSuspendedException $e) { + // allow a migration step (or events) to suspend the migration via a specific exception + + $this->dispatcher->dispatch($this->eventPrefix . 'migration_suspended', new MigrationSuspendedEvent($step, $e)); + + // prepare data for the context handler + $this->migrationContext[$migrationDefinition->name] = array('step' => $i, 'context' => $migrationContext); + // let the context handler store our data, along context data from any other (tagged) service which has some + $this->contextHandler->storeCurrentContext($migrationDefinition->name); + + $finalStatus = Migration::STATUS_SUSPENDED; + $finalMessage = "Suspended in execution of step $i: " . $e->getMessage(); } // set migration as done @@ -293,7 +341,7 @@ public function executeMigration(MigrationDefinition $migrationDefinition, $useT if ($useTransaction) { // there might be workflows or other actions happening at commit time that fail if we are not admin - $previousUserId = $this->loginUser(self::ADMIN_USER_ID); + $previousUserId = $this->loginUser($this->getAdminUserIdentifier($adminLogin)); $this->repository->commit(); $this->loginUser($previousUserId); } @@ -348,6 +396,91 @@ public function executeMigration(MigrationDefinition $migrationDefinition, $useT } } + /** + * @param Migration $migration + * @param bool $useTransaction + * @throws \Exception + * + * @todo add support for adminLogin ? + */ + public function resumeMigration(Migration $migration, $useTransaction = true) + { + if ($migration->status != Migration::STATUS_SUSPENDED) { + throw new \Exception("Can not resume migration '{$migration->name}': it is not in suspended status"); + } + + $migrationDefinitions = $this->getMigrationsDefinitions(array($migration->path)); + if (!count($migrationDefinitions)) { + throw new \Exception("Can not resume migration '{$migration->name}': its definition is missing"); + } + + $defs = $migrationDefinitions->getArrayCopy(); + $migrationDefinition = reset($defs); + + $migrationDefinition = $this->parseMigrationDefinition($migrationDefinition); + if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) { + throw new \Exception("Can not resume migration '{$migration->name}': {$migrationDefinition->parsingError}"); + } + + // restore context + $this->contextHandler->restoreCurrentContext($migration->name); + $restoredContext = $this->migrationContext[$migration->name]; + + /// @todo check that restored context is valid + + // update migration status + $migration = $this->storageHandler->resumeMigration($migration); + + // clean up restored context - ideally it should be in the same db transaction as the line above + $this->contextHandler->deleteContext($migration->name); + + // and go + // note: we store the current step counting starting at 1, but use offset staring at 0, hence the -1 here + $this->executeMigrationInner($migration, $migrationDefinition, $restoredContext['context'], + $restoredContext['step'] - 1, $useTransaction); + } + + /** + * @param string $defaultLanguageCode + * @param string $adminLogin + * @return array + */ + protected function migrationContextFromParameters($defaultLanguageCode = null, $adminLogin = null) + { + $properties = array(); + + if ($defaultLanguageCode != null) { + $properties['defaultLanguageCode'] = $defaultLanguageCode; + } + if ($adminLogin != null) { + $properties['adminUserLogin'] = $adminLogin; + } + + return $properties; + } + + protected function injectContextIntoStep(MigrationStep $step, array $context) + { + return new MigrationStep( + $step->type, + $step->dsl, + array_merge($step->context, $context) + ); + } + + /** + * @param string $adminLogin + * @return int|string + */ + protected function getAdminUserIdentifier($adminLogin) + { + if ($adminLogin != null) { + return $adminLogin; + } + + return self::ADMIN_USER_ID; + } + /** * Turns eZPublish cryptic exceptions into something more palatable for random devs * @todo should this be moved to a lower layer ? @@ -402,4 +535,22 @@ protected function getFullExceptionMessage(\Exception $e) return $message; } + + /** + * @param string $migrationName + * @return array + */ + public function getCurrentContext($migrationName) + { + return isset($this->migrationContext[$migrationName]) ? $this->migrationContext[$migrationName] : null; + } + + /** + * @param string $migrationName + * @param array $context + */ + public function restoreContext($migrationName, array $context) + { + $this->migrationContext[$migrationName] = $context; + } } diff --git a/Core/ReferenceResolver/ChainResolver.php b/Core/ReferenceResolver/ChainResolver.php index 4b45b343..eb560738 100644 --- a/Core/ReferenceResolver/ChainResolver.php +++ b/Core/ReferenceResolver/ChainResolver.php @@ -4,8 +4,10 @@ use Kaliop\eZMigrationBundle\API\ReferenceResolverInterface; use Kaliop\eZMigrationBundle\API\ReferenceBagInterface; +use Kaliop\eZMigrationBundle\API\ReferenceResolverBagInterface; +use Kaliop\eZMigrationBundle\API\EnumerableReferenceResolverInterface; -class ChainResolver implements ReferenceResolverInterface, ReferenceBagInterface +class ChainResolver implements ReferenceResolverBagInterface, EnumerableReferenceResolverInterface { /** @var ReferenceResolverInterface[] $resolvers */ protected $resolvers = array(); @@ -41,6 +43,7 @@ public function isReference($stringIdentifier) /** * @param string $stringIdentifier * @return mixed + * @throws \Exception */ public function getReferenceValue($stringIdentifier) { @@ -49,6 +52,7 @@ public function getReferenceValue($stringIdentifier) foreach ($this->resolvers as $resolver) { if ($resolver->isReference($stringIdentifier)) { $stringIdentifier = $resolver->getReferenceValue($stringIdentifier); + // In case of many resolvers resolving the same ref, the last one wins. Should we default to the 1st winning ? $resolvedOnce = true; } } @@ -88,4 +92,20 @@ public function addReference($identifier, $value, $overwrite = false) return false; } + + public function listReferences() + { + $refs = array(); + + foreach ($this->resolvers as $resolver) { + if (! $resolver instanceof EnumerableReferenceResolverInterface) { + throw new \Exception("Could not enumerate references because of chained resolver of type: " . get_class($resolver)); + } + + // later resolvers are stronger (see getReferenceValue) + $refs = array_merge($refs, $resolver->listReferences()); + } + + return $refs; + } } diff --git a/Core/ReferenceResolver/CustomReferenceResolver.php b/Core/ReferenceResolver/CustomReferenceResolver.php index f478a8e0..5e791a84 100644 --- a/Core/ReferenceResolver/CustomReferenceResolver.php +++ b/Core/ReferenceResolver/CustomReferenceResolver.php @@ -2,12 +2,15 @@ namespace Kaliop\eZMigrationBundle\Core\ReferenceResolver; -use Kaliop\eZMigrationBundle\API\ReferenceBagInterface; +use Kaliop\eZMigrationBundle\API\ReferenceResolverBagInterface; +use Kaliop\eZMigrationBundle\API\EnumerableReferenceResolverInterface; +use Kaliop\eZMigrationBundle\API\ContextProviderInterface; /** * Handle 'any' references by letting the developer store them and retrieve them afterwards */ -class CustomReferenceResolver extends AbstractResolver implements ReferenceBagInterface +class CustomReferenceResolver extends AbstractResolver implements ReferenceResolverBagInterface, + EnumerableReferenceResolverInterface, ContextProviderInterface { /** * Defines the prefix for all reference identifier strings in definitions @@ -57,4 +60,33 @@ public function addReference($identifier, $value, $overwrite = false) return true; } + + /** + * List all existing references + * @return array + */ + public function listReferences() + { + return $this->references; + } + + /** + * The custom reference resolver has only 'global' references, regardless of the current migration + * @param string $migrationName + * @return array|null + */ + public function getCurrentContext($migrationName) + { + return $this->references; + } + + /** + * The custom reference resolver has only 'global' references, regardless of the current migration + * @param string $migrationName + * @param array $context + */ + public function restoreContext($migrationName, array $context) + { + $this->references = $context; + } } diff --git a/Core/RepositoryUserSetterTrait.php b/Core/RepositoryUserSetterTrait.php index 254ec31d..25fd4118 100644 --- a/Core/RepositoryUserSetterTrait.php +++ b/Core/RepositoryUserSetterTrait.php @@ -2,6 +2,8 @@ namespace Kaliop\eZMigrationBundle\Core; +use \eZ\Publish\API\Repository\Values\User\User; + /** * NB: needs a class member 'repository' */ @@ -9,17 +11,27 @@ trait RepositoryUserSetterTrait { /** * Helper method to log in a user that can make changes to the system. - * @param int $userId + * @param int|string $userLoginOrId a user login or user-id * @return int id of the previously logged in user */ - protected function loginUser($userId) + protected function loginUser($userLoginOrId) { $previousUser = $this->repository->getCurrentUser(); - if ($userId != $previousUser->id) { - $this->repository->setCurrentUser($this->repository->getUserService()->loadUser($userId)); + if (is_int($userLoginOrId)) { + if ($userLoginOrId == $previousUser->id) { + return $previousUser->id; + } + $newUser = $this->repository->getUserService()->loadUser($userLoginOrId); + } else { + if ($userLoginOrId == $previousUser->login) { + return $previousUser->id; + } + $newUser = $this->repository->getUserService()->loadUserByLogin($userLoginOrId); } + $this->repository->setCurrentUser($newUser); + return $previousUser->id; } } diff --git a/Core/StorageHandler/Database.php b/Core/StorageHandler/Database.php index 59d0d6e9..2b21788a 100644 --- a/Core/StorageHandler/Database.php +++ b/Core/StorageHandler/Database.php @@ -2,421 +2,11 @@ namespace Kaliop\eZMigrationBundle\Core\StorageHandler; -use Kaliop\eZMigrationBundle\API\StorageHandlerInterface; -use Kaliop\eZMigrationBundle\API\Collection\MigrationCollection; -use eZ\Publish\Core\Persistence\Database\DatabaseHandler; -use Doctrine\DBAL\Schema\Schema; -use eZ\Publish\Core\Persistence\Database\SelectQuery; -use Kaliop\eZMigrationBundle\API\Value\Migration; -use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Kaliop\eZMigrationBundle\Core\StorageHandler\Database\Migration; /** - * Database-backed storage for info on executed migrations - * - * @todo replace all usage of the ezcdb api with the doctrine dbal one, so that we only depend on one + * @deprecated Left in for backwards compatibility only */ -class Database implements StorageHandlerInterface +class Database extends Migration { - /** - * Flag to indicate that the migration table has been created - * - * @var boolean - */ - private $migrationsTableExists = false; - - /** - * Name of the database table where installed migration versions are tracked. - * @var string - * - * @todo add setter/getter, as we need to clear versionTableExists when switching this - */ - protected $migrationsTableName; - - /** - * @var DatabaseHandler $connection - */ - protected $dbHandler; - - protected $fieldList = 'migration, md5, path, execution_date, status, execution_error'; - /** - * @param DatabaseHandler $dbHandler - * @param string $migrationsTableName - */ - public function __construct(DatabaseHandler $dbHandler, $migrationsTableName = 'kaliop_migrations') - { - $this->dbHandler = $dbHandler; - $this->migrationsTableName = $migrationsTableName; - } - - /** - * @return MigrationCollection - * @todo add support offset, limit - */ - public function loadMigrations() - { - $this->createMigrationsTableIfNeeded(); - - /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ - $q = $this->dbHandler->createSelectQuery(); - $q->select($this->fieldList) - ->from($this->migrationsTableName) - ->orderBy('migration', SelectQuery::ASC); - $stmt = $q->prepare(); - $stmt->execute(); - $results = $stmt->fetchAll(); - - $migrations = array(); - foreach ($results as $result) { - $migrations[$result['migration']] = $this->arrayToMigration($result); - } - - return new MigrationCollection($migrations); - } - - /** - * @param string $migrationName - * @return Migration|null - */ - public function loadMigration($migrationName) - { - $this->createMigrationsTableIfNeeded(); - - /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ - $q = $this->dbHandler->createSelectQuery(); - $q->select($this->fieldList) - ->from($this->migrationsTableName) - ->where($q->expr->eq('migration', $q->bindValue($migrationName))); - $stmt = $q->prepare(); - $stmt->execute(); - $result = $stmt->fetch(\PDO::FETCH_ASSOC); - - if (is_array($result) && !empty($result)) { - return $this->arrayToMigration($result); - } - - return null; - } - - /** - * Creates and stores a new migration (leaving it in TODO status) - * @param MigrationDefinition $migrationDefinition - * @return Migration - * @throws \Exception If the migration exists already (we rely on the PK for that) - */ - public function addMigration(MigrationDefinition $migrationDefinition) - { - $this->createMigrationsTableIfNeeded(); - - $conn = $this->dbHandler->getConnection(); - - $migration = new Migration( - $migrationDefinition->name, - md5($migrationDefinition->rawDefinition), - $migrationDefinition->path, - null, - Migration::STATUS_TODO - ); - try { - $conn->insert($this->migrationsTableName, $this->migrationToArray($migration)); - } catch (UniqueConstraintViolationException $e) { - throw new \Exception("Migration '{$migrationDefinition->name}' already exists"); - } - - return $migration; - } - - /** - * Starts a migration, given its definition: stores its status in the db, returns the Migration object - * - * @param MigrationDefinition $migrationDefinition - * @return Migration - * @throws \Exception if migration was already executing or already done - * @todo add a parameter to allow re-execution of already-done migrations - */ - public function startMigration(MigrationDefinition $migrationDefinition) - { - return $this->createMigration($migrationDefinition, Migration::STATUS_STARTED, 'started'); - } - - /** - * Stops a migration by storing it in the db. Migration status can not be 'started' - * - * NB: if this call happens within another DB transaction which has already been flagged for rollback, the result - * will be that a RuntimeException is thrown, as Doctrine does not allow to call commit() after rollback(). - * One way to fix the problem would be not to use a transaction and select-for-update here, but since that is the - * best way to insure atomic updates, I am loath to remove it. - * A known workaround is to call the Doctrine Connection method setNestTransactionsWithSavepoints(true); this can - * be achieved as simply as setting the parameter 'use_savepoints' in the doctrine connection configuration. - * - * @param Migration $migration - * @param bool $force When true, the migration will be updated even if it was not in 'started' status - * @throws \Exception If the migration was not started (unless $force=true) - */ - public function endMigration(Migration $migration, $force = false) - { - if ($migration->status == Migration::STATUS_STARTED) { - throw new \Exception("Migration '{$migration->name}' can not be ended as its status is 'started'..."); - } - - $this->createMigrationsTableIfNeeded(); - - // select for update - - // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... - // at least the doctrine one allows us to still use parameter binding when we add our sql particle - $conn = $this->dbHandler->getConnection(); - - $qb = $conn->createQueryBuilder(); - $qb->select('*') - ->from($this->migrationsTableName, 'm') - ->where('migration = ?'); - $sql = $qb->getSQL() . ' FOR UPDATE'; - - $conn->beginTransaction(); - - $stmt = $conn->executeQuery($sql, array($migration->name)); - $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); - - // fail if it was not executing - - if (!is_array($existingMigrationData)) { - // commit to release the lock - $conn->commit(); - throw new \Exception("Migration '{$migration->name}' can not be ended as it is not found"); - } - - if (($existingMigrationData['status'] != Migration::STATUS_STARTED) && !$force) { - // commit to release the lock - $conn->commit(); - throw new \Exception("Migration '{$migration->name}' can not be ended as it is not executing"); - } - - $conn->update( - $this->migrationsTableName, - array( - 'status' => $migration->status, - /// @todo use mb_substr (if all dbs we support count col length not in bytes but in chars...) - 'execution_error' => substr($migration->executionError, 0, 4000), - 'execution_date' => $migration->executionDate - ), - array('migration' => $migration->name) - ); - - $conn->commit(); - } - - /** - * Removes a Migration from the table - regardless of its state! - * - * @param Migration $migration - */ - public function deleteMigration(Migration $migration) - { - $this->createMigrationsTableIfNeeded(); - $conn = $this->dbHandler->getConnection(); - $conn->delete($this->migrationsTableName, array('migration' => $migration->name)); - } - - /** - * Skips a migration by storing it in the db. Migration status can not be 'started' - * - * @param MigrationDefinition $migrationDefinition - * @return Migration - * @throws \Exception If the migration was already executed or executing - */ - public function skipMigration(MigrationDefinition $migrationDefinition) - { - return $this->createMigration($migrationDefinition, Migration::STATUS_SKIPPED, 'skipped'); - } - - /** - * @param MigrationDefinition $migrationDefinition - * @param int $status - * @param string $action - * @return Migration - * @throws \Exception - */ - protected function createMigration(MigrationDefinition $migrationDefinition, $status, $action) - { - $this->createMigrationsTableIfNeeded(); - - // select for update - - // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... - // at least the doctrine one allows us to still use parameter binding when we add our sql particle - $conn = $this->dbHandler->getConnection(); - - $qb = $conn->createQueryBuilder(); - $qb->select('*') - ->from($this->migrationsTableName, 'm') - ->where('migration = ?'); - $sql = $qb->getSQL() . ' FOR UPDATE'; - - $conn->beginTransaction(); - - $stmt = $conn->executeQuery($sql, array($migrationDefinition->name)); - $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); - - if (is_array($existingMigrationData)) { - // migration exists - - // fail if it was already executing or already done - if ($existingMigrationData['status'] == Migration::STATUS_STARTED) { - // commit to release the lock - $conn->commit(); - throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it is already executing"); - } - if ($existingMigrationData['status'] == Migration::STATUS_DONE) { - // commit to release the lock - $conn->commit(); - throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already executed"); - } - if ($existingMigrationData['status'] == Migration::STATUS_SKIPPED) { - // commit to release the lock - $conn->commit(); - throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already skipped"); - } - - // do not set migration start date if we are skipping it - $migration = new Migration( - $migrationDefinition->name, - md5($migrationDefinition->rawDefinition), - $migrationDefinition->path, - ($status == Migration::STATUS_SKIPPED ? null : time()), - $status - ); - $conn->update( - $this->migrationsTableName, - array( - 'execution_date' => $migration->executionDate, - 'status' => $status, - 'execution_error' => null - ), - array('migration' => $migrationDefinition->name) - ); - $conn->commit(); - - } else { - // migration did not exist. Create it! - - // commit immediately, to release the lock and avoid deadlocks - $conn->commit(); - - $migration = new Migration( - $migrationDefinition->name, - md5($migrationDefinition->rawDefinition), - $migrationDefinition->path, - ($status == Migration::STATUS_SKIPPED ? null : time()), - $status - ); - $conn->insert($this->migrationsTableName, $this->migrationToArray($migration)); - } - - return $migration; - } - - /** - * Removes all migration from storage (regardless of their status) - */ - public function deleteMigrations() - { - if ($this->tableExist($this->migrationsTableName)) { - $this->dbHandler->exec('DROP TABLE ' . $this->migrationsTableName); - } - } - - /** - * Check if the version db table exists and create it if not. - * - * @return bool true if table has been created, false if it was already there - * - * @todo add a 'force' flag to force table re-creation - * @todo manage changes to table definition - */ - public function createMigrationsTableIfNeeded() - { - if ($this->migrationsTableExists) { - return false; - } - - if ($this->tableExist($this->migrationsTableName)) { - $this->migrationsTableExists = true; - return false; - } - - $this->createMigrationsTable(); - - $this->migrationsTableExists = true; - return true; - } - - public function createMigrationsTable() - { - /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */ - $sm = $this->dbHandler->getConnection()->getSchemaManager(); - $dbPlatform = $sm->getDatabasePlatform(); - - $schema = new Schema(); - - $t = $schema->createTable($this->migrationsTableName); - $t->addColumn('migration', 'string', array('length' => 255)); - $t->addColumn('path', 'string', array('length' => 4000)); - $t->addColumn('md5', 'string', array('length' => 32)); - $t->addColumn('execution_date', 'integer', array('notnull' => false)); - $t->addColumn('status', 'integer', array('default ' => Migration::STATUS_TODO)); - $t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false)); - $t->setPrimaryKey(array('migration')); - // in case users want to look up migrations by their full path - // NB: disabled for the moment, as it causes problems on some versions of mysql which limit index length to 767 bytes, - // and 767 bytes can be either 255 chars or 191 chars depending on charset utf8 or utf8mb4... - //$t->addIndex(array('path')); - - foreach ($schema->toSql($dbPlatform) as $sql) { - $this->dbHandler->exec($sql); - } - } - - /** - * Check if a table exists in the database - * - * @param string $tableName - * @return bool - */ - protected function tableExist($tableName) - { - /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */ - $sm = $this->dbHandler->getConnection()->getSchemaManager(); - foreach ($sm->listTables() as $table) { - if ($table->getName() == $tableName) { - return true; - } - } - - return false; - } - - protected function migrationToArray(Migration $migration) - { - return array( - 'migration' => $migration->name, - 'md5' => $migration->md5, - 'path' => $migration->path, - 'execution_date' => $migration->executionDate, - 'status' => $migration->status, - 'execution_error' => $migration->executionError - ); - } - - protected function arrayToMigration(array $data) - { - return new Migration( - $data['migration'], - $data['md5'], - $data['path'], - $data['execution_date'], - $data['status'], - $data['execution_error'] - ); - } -} +} \ No newline at end of file diff --git a/Core/StorageHandler/Database/Context.php b/Core/StorageHandler/Database/Context.php new file mode 100644 index 00000000..797a5670 --- /dev/null +++ b/Core/StorageHandler/Database/Context.php @@ -0,0 +1,134 @@ +createTableIfNeeded(); + + /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ + $q = $this->dbHandler->createSelectQuery(); + $q->select($this->fieldList) + ->from($this->tableName) + ->where($q->expr->eq('migration', $q->bindValue($migrationName))); + $stmt = $q->prepare(); + $stmt->execute(); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (is_array($result) && !empty($result)) { + return $this->stringToContext($result['context']); + } + + return null; + } + + /** + * Stores a migration context + * + * @param string $migrationName + * @param array $context + */ + public function storeMigrationContext($migrationName, array $context) + { + $this->createTableIfNeeded(); + + // select for update + + // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... + // at least the doctrine one allows us to still use parameter binding when we add our sql particle + $conn = $this->getConnection(); + + $qb = $conn->createQueryBuilder(); + $qb->select('*') + ->from($this->tableName, 'm') + ->where('migration = ?'); + $sql = $qb->getSQL() . ' FOR UPDATE'; + + $conn->beginTransaction(); + + $stmt = $conn->executeQuery($sql, array($migrationName)); + $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (is_array($existingMigrationData)) { + // context exists + + $conn->update( + $this->tableName, + array( + 'context' => $this->contextToString($context), + 'insertion_date' => time(), + ), + array('migration' => $migrationName) + ); + $conn->commit(); + + } else { + // context did not exist. Create it! + + // commit immediately, to release the lock and avoid deadlocks + $conn->commit(); + + $conn->insert($this->tableName, array( + 'migration' => $migrationName, + 'context' => $this->contextToString($context), + 'insertion_date' => time(), + )); + } + } + + /** + * Removes a migration context from storage + * + * @param string $migrationName + */ + public function deleteMigrationContext($migrationName) + { + $this->createTableIfNeeded(); + $conn = $this->getConnection(); + $conn->delete($this->tableName, array('migration' => $migrationName)); + } + + /** + * Removes all migration contexts from storage (regardless of the migration status/existence) + */ + public function deleteMigrationContexts() + { + $this->truncate(); + } + + public function createTable() + { + /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */ + $sm = $this->dbHandler->getConnection()->getSchemaManager(); + $dbPlatform = $sm->getDatabasePlatform(); + + $schema = new Schema(); + + $t = $schema->createTable($this->tableName); + $t->addColumn('migration', 'string', array('length' => 255)); + $t->addColumn('context', 'text'); + $t->addColumn('insertion_date', 'integer'); + $t->setPrimaryKey(array('migration')); + + foreach ($schema->toSql($dbPlatform) as $sql) { + $this->dbHandler->exec($sql); + } + } + + protected function stringToContext($data) + { + return json_decode($data, true); + } + + protected function contextToString(array $context) + { + return json_encode($context); + } +} diff --git a/Core/StorageHandler/Database/Migration.php b/Core/StorageHandler/Database/Migration.php new file mode 100644 index 00000000..8e3127d4 --- /dev/null +++ b/Core/StorageHandler/Database/Migration.php @@ -0,0 +1,449 @@ +loadMigrationsInner(null, $limit, $offset); + } + + /** + * @param int $status + * @param int $limit + * @param int $offset + * @return MigrationCollection + */ + public function loadMigrationsByStatus($status, $limit = null, $offset = null) + { + return $this->loadMigrationsInner($status, $limit, $offset); + } + + /** + * @param int $status + * @param int $limit + * @param int $offset + * @return MigrationCollection + */ + protected function loadMigrationsInner($status = null, $limit = null, $offset = null) + { + $this->createTableIfNeeded(); + + /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ + $q = $this->dbHandler->createSelectQuery(); + $q->select($this->fieldList) + ->from($this->tableName) + ->orderBy('migration', SelectQuery::ASC); + if ($status !== null) { + $q->where($q->expr->eq('status', $q->bindValue($status))); + } + if ($limit > 0 || $offset > 0) { + if ($limit <= 0) { + $limit = null; + } + if ($offset == 0) { + $offset = null; + } + $q->limit($limit, $offset); + } + $stmt = $q->prepare(); + $stmt->execute(); + $results = $stmt->fetchAll(); + + $migrations = array(); + foreach ($results as $result) { + $migrations[$result['migration']] = $this->arrayToMigration($result); + } + + return new MigrationCollection($migrations); + } + + /** + * @param string $migrationName + * @return APIMigration|null + */ + public function loadMigration($migrationName) + { + $this->createTableIfNeeded(); + + /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ + $q = $this->dbHandler->createSelectQuery(); + $q->select($this->fieldList) + ->from($this->tableName) + ->where($q->expr->eq('migration', $q->bindValue($migrationName))); + $stmt = $q->prepare(); + $stmt->execute(); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (is_array($result) && !empty($result)) { + return $this->arrayToMigration($result); + } + + return null; + } + + /** + * Creates and stores a new migration (leaving it in TODO status) + * @param MigrationDefinition $migrationDefinition + * @return APIMigration + * @throws \Exception If the migration exists already (we rely on the PK for that) + */ + public function addMigration(MigrationDefinition $migrationDefinition) + { + $this->createTableIfNeeded(); + + $conn = $this->getConnection(); + + $migration = new APIMigration( + $migrationDefinition->name, + md5($migrationDefinition->rawDefinition), + $migrationDefinition->path, + null, + APIMigration::STATUS_TODO + ); + try { + $conn->insert($this->tableName, $this->migrationToArray($migration)); + } catch (UniqueConstraintViolationException $e) { + throw new \Exception("Migration '{$migrationDefinition->name}' already exists"); + } + + return $migration; + } + + /** + * Starts a migration, given its definition: stores its status in the db, returns the Migration object + * + * @param MigrationDefinition $migrationDefinition + * @return APIMigration + * @throws \Exception if migration was already executing or already done + * @todo add a parameter to allow re-execution of already-done migrations + */ + public function startMigration(MigrationDefinition $migrationDefinition) + { + return $this->createMigration($migrationDefinition, APIMigration::STATUS_STARTED, 'started'); + } + + /** + * Stops a migration by storing it in the db. Migration status can not be 'started' + * + * NB: if this call happens within another DB transaction which has already been flagged for rollback, the result + * will be that a RuntimeException is thrown, as Doctrine does not allow to call commit() after rollback(). + * One way to fix the problem would be not to use a transaction and select-for-update here, but since that is the + * best way to insure atomic updates, I am loath to remove it. + * A known workaround is to call the Doctrine Connection method setNestTransactionsWithSavepoints(true); this can + * be achieved as simply as setting the parameter 'use_savepoints' in the doctrine connection configuration. + * + * @param APIMigration $migration + * @param bool $force When true, the migration will be updated even if it was not in 'started' status + * @throws \Exception If the migration was not started (unless $force=true) + */ + public function endMigration(APIMigration $migration, $force = false) + { + if ($migration->status == APIMigration::STATUS_STARTED) { + throw new \Exception("Migration '{$migration->name}' can not be ended as its status is 'started'..."); + } + + $this->createTableIfNeeded(); + + // select for update + + // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... + // at least the doctrine one allows us to still use parameter binding when we add our sql particle + $conn = $this->getConnection(); + + $qb = $conn->createQueryBuilder(); + $qb->select('*') + ->from($this->tableName, 'm') + ->where('migration = ?'); + $sql = $qb->getSQL() . ' FOR UPDATE'; + + $conn->beginTransaction(); + + $stmt = $conn->executeQuery($sql, array($migration->name)); + $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); + + // fail if it was not executing + + if (!is_array($existingMigrationData)) { + // commit to release the lock + $conn->commit(); + throw new \Exception("Migration '{$migration->name}' can not be ended as it is not found"); + } + + if (($existingMigrationData['status'] != APIMigration::STATUS_STARTED) && !$force) { + // commit to release the lock + $conn->commit(); + throw new \Exception("Migration '{$migration->name}' can not be ended as it is not executing"); + } + + $conn->update( + $this->tableName, + array( + 'status' => $migration->status, + /// @todo use mb_substr (if all dbs we support count col length not in bytes but in chars...) + 'execution_error' => substr($migration->executionError, 0, 4000), + 'execution_date' => $migration->executionDate + ), + array('migration' => $migration->name) + ); + + $conn->commit(); + } + + /** + * Removes a Migration from the table - regardless of its state! + * + * @param APIMigration $migration + */ + public function deleteMigration(APIMigration $migration) + { + $this->createTableIfNeeded(); + $conn = $this->getConnection(); + $conn->delete($this->tableName, array('migration' => $migration->name)); + } + + /** + * Skips a migration by storing it in the db. Migration status can not be 'started' + * + * @param MigrationDefinition $migrationDefinition + * @return APIMigration + * @throws \Exception If the migration was already executed or executing + */ + public function skipMigration(MigrationDefinition $migrationDefinition) + { + return $this->createMigration($migrationDefinition, APIMigration::STATUS_SKIPPED, 'skipped'); + } + + /** + * @param MigrationDefinition $migrationDefinition + * @param int $status + * @param string $action + * @return APIMigration + * @throws \Exception + */ + protected function createMigration(MigrationDefinition $migrationDefinition, $status, $action) + { + $this->createTableIfNeeded(); + + // select for update + + // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... + // at least the doctrine one allows us to still use parameter binding when we add our sql particle + $conn = $this->getConnection(); + + $qb = $conn->createQueryBuilder(); + $qb->select('*') + ->from($this->tableName, 'm') + ->where('migration = ?'); + $sql = $qb->getSQL() . ' FOR UPDATE'; + + $conn->beginTransaction(); + + $stmt = $conn->executeQuery($sql, array($migrationDefinition->name)); + $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (is_array($existingMigrationData)) { + // migration exists + + // fail if it was already executing or already done + if ($existingMigrationData['status'] == APIMigration::STATUS_STARTED) { + // commit to release the lock + $conn->commit(); + throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it is already executing"); + } + if ($existingMigrationData['status'] == APIMigration::STATUS_DONE) { + // commit to release the lock + $conn->commit(); + throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already executed"); + } + if ($existingMigrationData['status'] == APIMigration::STATUS_SKIPPED) { + // commit to release the lock + $conn->commit(); + throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already skipped"); + } + + // do not set migration start date if we are skipping it + $migration = new APIMigration( + $migrationDefinition->name, + md5($migrationDefinition->rawDefinition), + $migrationDefinition->path, + ($status == APIMigration::STATUS_SKIPPED ? null : time()), + $status + ); + $conn->update( + $this->tableName, + array( + 'execution_date' => $migration->executionDate, + 'status' => $status, + 'execution_error' => null + ), + array('migration' => $migrationDefinition->name) + ); + $conn->commit(); + + } else { + // migration did not exist. Create it! + + // commit immediately, to release the lock and avoid deadlocks + $conn->commit(); + + $migration = new APIMigration( + $migrationDefinition->name, + md5($migrationDefinition->rawDefinition), + $migrationDefinition->path, + ($status == APIMigration::STATUS_SKIPPED ? null : time()), + $status + ); + $conn->insert($this->tableName, $this->migrationToArray($migration)); + } + + return $migration; + } + + public function resumeMigration(APIMigration $migration) + { + $this->createTableIfNeeded(); + + // select for update + + // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... + // at least the doctrine one allows us to still use parameter binding when we add our sql particle + $conn = $this->getConnection(); + + $qb = $conn->createQueryBuilder(); + $qb->select('*') + ->from($this->tableName, 'm') + ->where('migration = ?'); + $sql = $qb->getSQL() . ' FOR UPDATE'; + + $conn->beginTransaction(); + + $stmt = $conn->executeQuery($sql, array($migration->name)); + $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!is_array($existingMigrationData)) { + // commit immediately, to release the lock and avoid deadlocks + $conn->commit(); + throw new \Exception("Migration '{$migration->name}' can not be resumed as it is not found"); + } + + // migration exists + + // fail if it was not suspended + if ($existingMigrationData['status'] != APIMigration::STATUS_SUSPENDED) { + // commit to release the lock + $conn->commit(); + throw new \Exception("Migration '{$migration->name}' can not be resumed as it is not suspended"); + } + + $migration = new APIMigration( + $migration->name, + $migration->md5, + $migration->path, + time(), + APIMigration::STATUS_STARTED + ); + + $conn->update( + $this->tableName, + array( + 'execution_date' => $migration->executionDate, + 'status' => APIMigration::STATUS_STARTED, + 'execution_error' => null + ), + array('migration' => $migration->name) + ); + $conn->commit(); + + return $migration; + } + + /** + * Removes all migration from storage (regardless of their status) + */ + public function deleteMigrations() + { + $this->drop(); + } + + public function createTable() + { + /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */ + $sm = $this->getConnection()->getSchemaManager(); + $dbPlatform = $sm->getDatabasePlatform(); + + $schema = new Schema(); + + $t = $schema->createTable($this->tableName); + $t->addColumn('migration', 'string', array('length' => 255)); + $t->addColumn('path', 'string', array('length' => 4000)); + $t->addColumn('md5', 'string', array('length' => 32)); + $t->addColumn('execution_date', 'integer', array('notnull' => false)); + $t->addColumn('status', 'integer', array('default ' => APIMigration::STATUS_TODO)); + $t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false)); + $t->setPrimaryKey(array('migration')); + // in case users want to look up migrations by their full path + // NB: disabled for the moment, as it causes problems on some versions of mysql which limit index length to 767 bytes, + // and 767 bytes can be either 255 chars or 191 chars depending on charset utf8 or utf8mb4... + //$t->addIndex(array('path')); + + foreach ($schema->toSql($dbPlatform) as $sql) { + $this->dbHandler->exec($sql); + } + } + + protected function migrationToArray(APIMigration $migration) + { + return array( + 'migration' => $migration->name, + 'md5' => $migration->md5, + 'path' => $migration->path, + 'execution_date' => $migration->executionDate, + 'status' => $migration->status, + 'execution_error' => $migration->executionError + ); + } + + protected function arrayToMigration(array $data) + { + return new APIMigration( + $data['migration'], + $data['md5'], + $data['path'], + $data['execution_date'], + $data['status'], + $data['execution_error'] + ); + } +} diff --git a/Core/StorageHandler/Database/TableStorage.php b/Core/StorageHandler/Database/TableStorage.php new file mode 100644 index 00000000..d09129d0 --- /dev/null +++ b/Core/StorageHandler/Database/TableStorage.php @@ -0,0 +1,112 @@ +dbHandler = $dbHandler; + $this->tableName = $tableName; + } + + abstract function createTable(); + + /** + * @return mixed + */ + protected function getConnection() + { + return $this->dbHandler->getConnection(); + } + + /** + * Check if a table exists in the database + * + * @param string $tableName + * @return bool + */ + protected function tableExist($tableName) + { + /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */ + $sm = $this->dbHandler->getConnection()->getSchemaManager(); + foreach ($sm->listTables() as $table) { + if ($table->getName() == $tableName) { + return true; + } + } + + return false; + } + + /** + * Check if the version db table exists and create it if not. + * + * @return bool true if table has been created, false if it was already there + * + * @todo add a 'force' flag to force table re-creation + * @todo manage changes to table definition + */ + protected function createTableIfNeeded() + { + if ($this->tableExists) { + return false; + } + + if ($this->tableExist($this->tableName)) { + $this->tableExists = true; + return false; + } + + $this->createTable(); + + $this->tableExists = true; + return true; + } + + /** + * Removes all data from storage as well as removing the tables itself + */ + protected function drop() + { + if ($this->tableExist($this->tableName)) { + $this->dbHandler->exec('DROP TABLE ' . $this->tableName); + } + } + + /** + * Removes all data from storage + */ + public function truncate() + { + if ($this->tableExist($this->tableName)) { + $this->dbHandler->exec('TRUNCATE ' . $this->tableName); + } + } +} \ No newline at end of file diff --git a/DependencyInjection/CompilerPass/TaggedServicesCompilerPass.php b/DependencyInjection/CompilerPass/TaggedServicesCompilerPass.php index 8f27e2ed..c142360f 100644 --- a/DependencyInjection/CompilerPass/TaggedServicesCompilerPass.php +++ b/DependencyInjection/CompilerPass/TaggedServicesCompilerPass.php @@ -54,7 +54,7 @@ public function process(ContainerBuilder $container) asort($priorities); foreach ($priorities as $id => $priority) { - $migrationService->addMethodCall('addComplexField', $handlers[$id]); + $migrationService->addMethodCall('addFieldHandler', $handlers[$id]); } } @@ -68,5 +68,16 @@ public function process(ContainerBuilder $container) )); } } + + if ($container->has('ez_migration_bundle.context_handler')) { + $contextHandlerService = $container->findDefinition('ez_migration_bundle.context_handler'); + $ContextProviders = $container->findTaggedServiceIds('ez_migration_bundle.context_provider'); + + foreach ($ContextProviders as $id => $tags) { + foreach ($tags as $attributes) { + $contextHandlerService->addMethodCall('addProvider', array(new Reference($id), $attributes['label'])); + } + } + } } } diff --git a/README.md b/README.md index 8d2a1f06..a81554b1 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ You can think of it as the grandson of the legacy [ezxmlinstaller](https://githu ## Requirements -* PHP 5.4 or later. +* PHP 5.6 or later. -* eZPublish Enterprise 5.3 or Community 2014.3 or later. +* eZPublish Enterprise 5.4 or Community 2014.11 or later. ## Installation @@ -285,10 +285,6 @@ and the corresponding php class: take a look at the following issue for a possible explanation and ideas for workarounds: https://jira.ez.no/browse/EZP-24691 -* the bundle does not at the moment support creation of user accounts using a custom contentType - -* the bundle at the moment does not support creating entities with a creator other than user id 14 ('admin') - * if you are using eZPublish versions prior to 2015.9, you will not be able to create/update Role definitions that contain policies with limitations for custom modules/functions. The known workaround is to take over the RoleService and alter its constructor to inject into it the desired limitations @@ -304,7 +300,7 @@ and the corresponding php class: type: ezstring name: Topbar-hover-color - identifier: topbar-hover-color + identifier: topbar-hover-color ## Frequently asked questions @@ -365,7 +361,7 @@ It is recommended to run the tests suite using a dedicated eZPublish installatio #### Setting up a dedicated test environment for the bundle -A safer choice to run the tests of the bundle is to set up a dedicated environment, as done when the testsuite is run on +A safer choice to run the tests of the bundle is to set up a dedicated environment, as done when the test suite is run on Travis. The advantages are multiple: one one hand you can start with any version of eZPublish you want; on the other you will be more confident that the tests wll still pass on Travis. diff --git a/Resources/config/services.yml b/Resources/config/services.yml index f39d67bb..3d54c040 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -2,8 +2,9 @@ parameters: # The directory (inside each bundle) where to look for migrations definitions # NB: this pasrasmeter will be renamed in v3 kaliop_bundle_migration.version_directory: 'MigrationVersions' - # The dtabase table used to store migration information. Will be created automatically + # The database tables used to store migration information. Will be created automatically ez_migration_bundle.table_name: 'kaliop_migrations' + ez_migration_bundle.context_table_name: 'kaliop_migrations_contexts' # Used to discriminate valid migration definitions ez_migration_bundle.valid_databases: # names have to correspond to doctrine 'platform', short of version numbers and lowercase @@ -28,8 +29,12 @@ parameters: ez_migration_bundle.definition_parser.sql.class: Kaliop\eZMigrationBundle\Core\DefinitionParser\SQLDefinitionParser ez_migration_bundle.definition_parser.php.class: Kaliop\eZMigrationBundle\Core\DefinitionParser\PHPDefinitionParser - ez_migration_bundle.storage_handler.database.class: Kaliop\eZMigrationBundle\Core\StorageHandler\Database + ez_migration_bundle.context_handler.class: Kaliop\eZMigrationBundle\Core\ContextHandler + ez_migration_bundle.storage_handler.database.class: Kaliop\eZMigrationBundle\Core\StorageHandler\Database\Migration + ez_migration_bundle.context_storage_handler.database.class: Kaliop\eZMigrationBundle\Core\StorageHandler\Database\Context + + ez_migration_bundle.executor.migration.class: Kaliop\eZMigrationBundle\Core\Executor\MigrationExecutor ez_migration_bundle.executor.migration_definition.class: Kaliop\eZMigrationBundle\Core\Executor\MigrationDefinitionExecutor ez_migration_bundle.executor.php.class: Kaliop\eZMigrationBundle\Core\Executor\PHPExecutor ez_migration_bundle.executor.sql.class: Kaliop\eZMigrationBundle\Core\Executor\SQLExecutor @@ -55,27 +60,28 @@ parameters: ez_migration_bundle.location_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\LocationMatcher ez_migration_bundle.object_state_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateMatcher ez_migration_bundle.object_state_group_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateGroupMatcher + ez_migration_bundle.reference_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\ReferenceMatcher ez_migration_bundle.role_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\RoleMatcher ez_migration_bundle.section_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher ez_migration_bundle.tag_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\TagMatcher ez_migration_bundle.user_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher ez_migration_bundle.user_group_matcher.class: Kaliop\eZMigrationBundle\Core\Matcher\UserGroupMatcher - ez_migration_bundle.complex_field_manager.class: Kaliop\eZMigrationBundle\Core\ComplexField\ComplexFieldManager - ez_migration_bundle.complex_field.ezauthor.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzAuthor - ez_migration_bundle.complex_field.ezbinaryfile.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzBinaryFile - ez_migration_bundle.complex_field.ezboolean.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzBoolean - ez_migration_bundle.complex_field.ezdate.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzDate - ez_migration_bundle.complex_field.ezdatetime.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzDateAndTime - ez_migration_bundle.complex_field.ezimage.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzImage - ez_migration_bundle.complex_field.ezmedia.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzMedia - ez_migration_bundle.complex_field.ezpage.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzPage - ez_migration_bundle.complex_field.ezrelation.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzRelation - ez_migration_bundle.complex_field.ezrelationlist.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzRelationList - ez_migration_bundle.complex_field.ezrichtext.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzRichText - ez_migration_bundle.complex_field.ezselection.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzSelection - ez_migration_bundle.complex_field.ezxmltext.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzXmlText - ez_migration_bundle.complex_field.eztags.class: Kaliop\eZMigrationBundle\Core\ComplexField\EzTags + ez_migration_bundle.complex_field_manager.class: Kaliop\eZMigrationBundle\Core\FieldHandlerManager + ez_migration_bundle.complex_field.ezauthor.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzAuthor + ez_migration_bundle.complex_field.ezbinaryfile.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzBinaryFile + ez_migration_bundle.complex_field.ezboolean.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzBoolean + ez_migration_bundle.complex_field.ezdate.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzDate + ez_migration_bundle.complex_field.ezdatetime.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzDateAndTime + ez_migration_bundle.complex_field.ezimage.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzImage + ez_migration_bundle.complex_field.ezmedia.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzMedia + ez_migration_bundle.complex_field.ezpage.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzPage + ez_migration_bundle.complex_field.ezrelation.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzRelation + ez_migration_bundle.complex_field.ezrelationlist.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzRelationList + ez_migration_bundle.complex_field.ezrichtext.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzRichText + ez_migration_bundle.complex_field.ezselection.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzSelection + ez_migration_bundle.complex_field.ezxmltext.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzXmlText + ez_migration_bundle.complex_field.eztags.class: Kaliop\eZMigrationBundle\Core\FieldHandler\EzTags ez_migration_bundle.reference_resolver.chain.class: Kaliop\eZMigrationBundle\Core\ReferenceResolver\ChainResolver ez_migration_bundle.reference_resolver.chain_prefix.class: Kaliop\eZMigrationBundle\Core\ReferenceResolver\ChainPrefixResolver @@ -99,6 +105,9 @@ services: - '@ez_migration_bundle.storage_handler' - '@ezpublish.api.repository' - '@event_dispatcher' + - '@ez_migration_bundle.context_handler' + tags: + - { name: ez_migration_bundle.context_provider, label: migration_service } ### loaders @@ -149,8 +158,27 @@ services: - '@ezpublish.connection' - '%ez_migration_bundle.table_name%' + ez_migration_bundle.context_storage_handler: + alias: ez_migration_bundle.context_storage_handler.database + + ez_migration_bundle.context_storage_handler.database: + class: '%ez_migration_bundle.context_storage_handler.database.class%' + arguments: + - '@ezpublish.connection' + - '%ez_migration_bundle.context_table_name%' + ### executors + ez_migration_bundle.executor.migration: + class: '%ez_migration_bundle.executor.migration.class%' + arguments: + - '@ez_migration_bundle.reference_matcher' + - '@ez_migration_bundle.executor.content_manager' + - '@ez_migration_bundle.executor.location_manager' + - '@ez_migration_bundle.executor.content_type_manager' + tags: + - { name: ez_migration_bundle.executor } + ez_migration_bundle.executor.migration_definition: class: '%ez_migration_bundle.executor.migration_definition.class%' arguments: @@ -169,18 +197,18 @@ services: tags: - { name: ez_migration_bundle.executor } - ez_migration_bundle.executor.sql: - class: '%ez_migration_bundle.executor.sql.class%' + ez_migration_bundle.executor.reference: + class: '%ez_migration_bundle.executor.reference.class%' arguments: - - '@ezpublish.connection' + - '@service_container' - '@ez_migration_bundle.reference_resolver.customreference' tags: - { name: ez_migration_bundle.executor } - ez_migration_bundle.executor.reference: - class: '%ez_migration_bundle.executor.reference.class%' + ez_migration_bundle.executor.sql: + class: '%ez_migration_bundle.executor.sql.class%' arguments: - - '@service_container' + - '@ezpublish.connection' - '@ez_migration_bundle.reference_resolver.customreference' tags: - { name: ez_migration_bundle.executor } @@ -309,49 +337,53 @@ services: class: '%ez_migration_bundle.content_matcher.class%' arguments: - '@ezpublish.api.repository' + - '@ez_migration_bundle.user_group_matcher' + - '@ez_migration_bundle.section_matcher' + - '@ez_migration_bundle.object_state_matcher' + - '@ez_migration_bundle.user_matcher' - ez_migration_bundle.location_matcher: - class: '%ez_migration_bundle.location_matcher.class%' - arguments: - - '@ezpublish.api.repository' - - ez_migration_bundle.user_matcher: - class: '%ez_migration_bundle.user_matcher.class%' + ez_migration_bundle.content_type_matcher: + class: '%ez_migration_bundle.content_type_matcher.class%' arguments: - '@ezpublish.api.repository' - ez_migration_bundle.user_group_matcher: - class: '%ez_migration_bundle.user_group_matcher.class%' + ez_migration_bundle.content_type_group_matcher: + class: '%ez_migration_bundle.content_type_group_matcher.class%' arguments: - '@ezpublish.api.repository' - ez_migration_bundle.role_matcher: - class: '%ez_migration_bundle.role_matcher.class%' + ez_migration_bundle.location_matcher: + class: '%ez_migration_bundle.location_matcher.class%' arguments: - '@ezpublish.api.repository' + - '@ez_migration_bundle.user_group_matcher' + - '@ez_migration_bundle.section_matcher' + - '@ez_migration_bundle.object_state_matcher' + - '@ez_migration_bundle.user_matcher' - ez_migration_bundle.content_type_matcher: - class: '%ez_migration_bundle.content_type_matcher.class%' + ez_migration_bundle.object_state_matcher: + class: '%ez_migration_bundle.object_state_matcher.class%' arguments: - '@ezpublish.api.repository' - ez_migration_bundle.section_matcher: - class: '%ez_migration_bundle.section_matcher.class%' + ez_migration_bundle.object_state_group_matcher: + class: '%ez_migration_bundle.object_state_group_matcher.class%' arguments: - '@ezpublish.api.repository' - ez_migration_bundle.object_state_matcher: - class: '%ez_migration_bundle.object_state_matcher.class%' + ez_migration_bundle.reference_matcher: + class: '%ez_migration_bundle.reference_matcher.class%' arguments: - - '@ezpublish.api.repository' + - '@ez_migration_bundle.reference_resolver.customreference' + - '@validator' - ez_migration_bundle.object_state_group_matcher: - class: '%ez_migration_bundle.object_state_group_matcher.class%' + ez_migration_bundle.role_matcher: + class: '%ez_migration_bundle.role_matcher.class%' arguments: - '@ezpublish.api.repository' - ez_migration_bundle.content_type_group_matcher: - class: '%ez_migration_bundle.content_type_group_matcher.class%' + ez_migration_bundle.section_matcher: + class: '%ez_migration_bundle.section_matcher.class%' arguments: - '@ezpublish.api.repository' @@ -361,6 +393,16 @@ services: - '@ezpublish.translation_helper' - '@?ezpublish.api.service.tags' + ez_migration_bundle.user_matcher: + class: '%ez_migration_bundle.user_matcher.class%' + arguments: + - '@ezpublish.api.repository' + + ez_migration_bundle.user_group_matcher: + class: '%ez_migration_bundle.user_group_matcher.class%' + arguments: + - '@ezpublish.api.repository' + ### field handlers ez_migration_bundle.complex_field_manager: @@ -528,6 +570,8 @@ services: ez_migration_bundle.reference_resolver.customreference.base: class: '%ez_migration_bundle.reference_resolver.customreference.class%' arguments: [] + tags: + - { name: ez_migration_bundle.context_provider, label: reference_resolver_customreference } # NB: This service will see added to the chain any service tagged as 'ez_migration_bundle.reference_resolver.customreference' ez_migration_bundle.reference_resolver.customreference.flexible: @@ -537,12 +581,19 @@ services: ### misc + ez_migration_bundle.context_handler: + class: '%ez_migration_bundle.context_handler.class%' + arguments: + - '@ez_migration_bundle.context_storage_handler' + lazy: true + # This service is used for the verbose mode when executing migrations on the command line ez_migration_bundle.step_executed_listener.tracing: class: '%ez_migration_bundle.step_executed_listener.tracing.class%' tags: - { name: kernel.event_listener, event: ez_migration.step_executed, method: onStepExecuted } - { name: kernel.event_listener, event: ez_migration.migration_aborted, method: onMigrationAborted } + - { name: kernel.event_listener, event: ez_migration.migration_suspended, method: onMigrationSuspended } ez_migration_bundle.helper.limitation_converter: class: '%ez_migration_bundle.helper.limitation_converter.class%' diff --git a/Resources/doc/DSL/Assertions.yml b/Resources/doc/DSL/Assertions.yml index 1d772b2a..5d1d8df4 100644 --- a/Resources/doc/DSL/Assertions.yml +++ b/Resources/doc/DSL/Assertions.yml @@ -2,8 +2,8 @@ - type: assert - target: reference # for the moment the only targets available for assertions are references - identifier: reference:abcdef # a reference identifier + target: reference # for the moment the only targets available for assertions are references, so this is a fixed value + identifier: reference:abcdef # a reference identifier. NB: must include the 'reference:' part test: # one and only one of the following conditions: # (the conditions correspond to phpunit assertXXX methods from class PHPUnit_Framework_Assert) diff --git a/Resources/doc/DSL/ManageContent.yml b/Resources/doc/DSL/ManageContent.yml index d2b3998e..5aa7ffba 100644 --- a/Resources/doc/DSL/ManageContent.yml +++ b/Resources/doc/DSL/ManageContent.yml @@ -3,20 +3,6 @@ mode: create content_type: xxx # Content type identifier parent_location: x # The id of the parent location. When a non numeric string is used, it is assumed to be a location remote id - priority: 0 # Optional. Set the priority of the main location - is_hidden: true|false # Optional - sort_field: x # Optional. See ManageLocation.yml for details - sort_order: ASC|DESC # Optional - location_remote_id: xxx # Optional, remote id of the new location which will be created - other_parent_locations: [x, y, z] # Optional, ids of extra parent locations. When a non numeric string is used as element, it is assumed to be a location remote id - remote_id: custom_remote_id # Optional, will set a custom remote id - lang: xxx-YY # Optional, will fallback to default language if not provided (--default-language option or 'eng-GB' by default) - section: x # string|int Optional section id or identifier - owner: xxx # user id, login or email if unique; Optional, set the content owner - version_creator: yyy # user id, login or email if unique; Optional, set the creator of the 1st version - modification_date: zzz # Optional, set content modification date (if integer: timestamp is assumed. if string, see: http://php.net/manual/en/datetime.formats.php) - publication_date: zzz # Optional, set content publication date (if integer: timstamp is assumed. if string, see: http://php.net/manual/en/datetime.formats.php) - always_available: true|false # Optional, will default to content type defaultAlwaysAvailable attributes: attribute1: value1 attribute2: value2 @@ -81,10 +67,24 @@ attribute15: [ 1, "option 2" ] # array of selection options ids or textual values # ezselection, single value attribute16: "option 3" # integer index or string + always_available: true|false # Optional, will default to content type defaultAlwaysAvailable + is_hidden: true|false # Optional + lang: xxx-YY # Optional, will fallback to default language if not provided (--default-language option or 'eng-GB' by default) + location_remote_id: xxx # Optional, remote id of the new location which will be created + modification_date: zzz # Optional, set content modification date (if integer: timestamp is assumed. if string, see: http://php.net/manual/en/datetime.formats.php) # Optionally assign object states to the content object_states: - xxx # int|string an object state id or identifier. If the identifier is not unique, use obj-state-gorup-identifier/obj-state-identifier - yyy + other_parent_locations: [x, y, z] # Optional, ids of extra parent locations. When a non numeric string is used as element, it is assumed to be a location remote id + owner: xxx # user id, login or email if unique; Optional, set the content owner + priority: 0 # Optional. Set the priority of the main location + publication_date: zzz # Optional, set content publication date (if integer: timstamp is assumed. if string, see: http://php.net/manual/en/datetime.formats.php) + remote_id: custom_remote_id # Optional, will set a custom remote id + section: x # string|int Optional section id or identifier + sort_field: x # Optional. See ManageLocation.yml for details + sort_order: ASC|DESC # Optional + version_creator: yyy # user id, login or email if unique; Optional, set the creator of the 1st version # The list in references tells the manager to store specific values for later use by other steps in the current migration. # NB: these are NEW VARIABLES THAT YOU ARE CREATING. They are not used in the current migration step! references: # Optional @@ -108,10 +108,24 @@ content_remote_id: xxx # string|string[] location_id: x # int|int[] location_remote_id: xxx # string|string[] + attribute: + _attr_name_: + _operator_: value # _operator_: eq, gt, gte, lt, lte, in, between, like; value: depending on the attribute type + content_type_id: yyy # int|int[] a content type id + content_type_identifier: yyy # string|string[] a content type identifier + creation_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + group: xxx # user id, login or email if unique + modification_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + object_state: zzz # int|int[]|string|string[] object state(s) either as id or using 'group/identifier' + owner: xxx # user id, login or email if unique parent_location_id: x # int|int[] parent_location_remote_id: xxx # string|string[] - content_type: yyy # string|string[] a content type identifier - or: # match any of the conditions below. *NB:* less efficient that using the array notation for a single condition + section: xx # int|int[]|string|string[] section id(s) or identifier(s) + subtree: /1/2/2345 # string|string[] + visibility: true # bool + or: # match any of the conditions below - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - @@ -120,21 +134,23 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' - lang: xxx-YY # Optional, will fallback to default language if not provided (--default-language option or 'eng-GB' by default) - section: x # string|int Optional section id or identifier - owner: xxx # user id, login or email if unique; Optional, set the content owner (original creator) - version_creator: yyy # user id, login or email if unique; Optional, set the creator of the new version - modification_date: zzz # Optional, set content modification date (for formats, see: http://php.net/manual/en/datetime.formats.php) - publication_date: zzz # Optional, set content publication date (for formats, see: http://php.net/manual/en/datetime.formats.php) - new_remote_id: xxx # string Optional set a new RemoteId - always_available: true|false # Optional + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' + not: # matches elements NOT satisfying the wrapped condition + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' attributes: # the list of attribute identifier value pairs. For the format to use, see above attribute1: value1 attribute2: value2 + always_available: true|false # Optional + lang: xxx-YY # Optional, will fallback to default language if not provided (--default-language option or 'eng-GB' by default) + modification_date: zzz # Optional, set content modification date (for formats, see: http://php.net/manual/en/datetime.formats.php) + new_remote_id: xxx # string Optional set a new RemoteId # Optionally assign object states to the content object_states: - xxx # int|string an object state id or identifier. For the format to use, see above + owner: xxx # user id, login or email if unique; Optional, set the content owner (original creator) + publication_date: zzz # Optional, set content publication date (for formats, see: http://php.net/manual/en/datetime.formats.php) + section: x # string|int Optional section id or identifier + version_creator: yyy # user id, login or email if unique; Optional, set the creator of the new version # The list in references tells the manager to store specific values for later use by other steps in the current migration. # NB: these are NEW VARIABLES THAT YOU ARE CREATING. They are not used in the current migration step! references: # Optional @@ -158,10 +174,24 @@ content_remote_id: xxx # string|string[] location_id: x # int|int[] location_remote_id: xxx # string|string[] + attribute: + _attr_name_: + _operator_: value # _operator_: eq, gt, gte, lt, lte, in, between, like; value: depending on the attribute type + content_type_id: yyy # int|int[] a content type id + content_type_identifier: yyy # string|string[] a content type identifier + creation_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + modification_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + group: xxx # user id, login or email if unique + object_state: zzz # int|int[]|string|string[] object state(s) either as id or using 'group/identifier' + owner: xxx # user id, login or email if unique parent_location_id: x # int|int[] parent_location_remote_id: xxx # string|string[] - content_type: yyy # string|string[] a content type identifier - or: # match any of the conditions below. *NB:* less efficient that using the array notation for a single condition + section: xx # int|int[]|string|string[] section id(s) or identifier(s) + subtree: /1/2/2345 # string|string[] + visibility: true # bool + or: # match any of the conditions below - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - @@ -170,20 +200,36 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' + not: # matches elements NOT satisfying the wrapped condition + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - type: content mode: load - match: # Specify ONLY ONE of the following, to match a content or a set of contents to update + match: # Specify ONLY ONE of the following, to match a content or a set of contents to load content_id: x # int|int[] content_remote_id: xxx # string|string[] location_id: x # int|int[] location_remote_id: xxx # string|string[] + attribute: + _attr_name_: + _operator_: value # _operator_: eq, gt, gte, lt, lte, in, between, like; value: depending on the attribute type + content_type_id: yyy # int|int[] a content type id + content_type_identifier: yyy # string|string[] a content type identifier + creation_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + group: xxx # user id, login or email if unique + modification_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + object_state: zzz # int|int[]|string|string[] object state(s) either as id or using 'group/identifier' + owner: xxx # user id, login or email if unique parent_location_id: x # int|int[] parent_location_remote_id: xxx # string|string[] - content_type: yyy # string|string[] a content type identifier - or: # match any of the conditions below. *NB:* less efficient that using the array notation for a single condition + section: xx # int|int[]|string|string[] section id(s) or identifier(s) + subtree: /1/2/2345 # string|string[] + visibility: true # bool + or: # match any of the conditions below - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - @@ -192,7 +238,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' + not: # matches elements NOT satisfying the wrapped condition + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' # The list in references tells the manager to store specific values for later use by other steps in the current migration. # NB: these are NEW VARIABLES THAT YOU ARE CREATING. They are not used in the current migration step! references: # Optional diff --git a/Resources/doc/DSL/ManageContentType.yml b/Resources/doc/DSL/ManageContentType.yml index 4be76b5d..4861ee4d 100644 --- a/Resources/doc/DSL/ManageContentType.yml +++ b/Resources/doc/DSL/ManageContentType.yml @@ -81,11 +81,6 @@ identifier: xyz # Class identifier name: xyz # Name of new class name_pattern: pattern # Pattern to use for the name of the contents (ex: ) - description: xyz # Optional - url_name_pattern: pattern # Optional - is_container: true|false # Optional, defaults to false - lang: xxx-YY # Optional, will fallback to default language if not provided (--default-language option or 'eng-GB' by default) - default_always_available: true|false # Optional, defaults to true attributes: - type: xyz # Attribute type (See list https://doc.ez.no/display/EZP/FieldTypes+reference) @@ -105,15 +100,25 @@ position: # Optional, will set the attribute to this position. # NB: unlike in the eZ4 Admin Interface, fields without specified positions will be counted 1,2,3... # NB: mixing attributes with a position field and some without it might give unexpected results + default_always_available: true|false # Optional, defaults to true + description: xyz # Optional + is_container: true|false # Optional, defaults to false + lang: xxx-YY # Optional, will fallback to default language if not provided (--default-language option or 'eng-GB' by default) + url_name_pattern: pattern # Optional # The list in references tells the manager to store specific values for later use by other steps in the current # migration. references: # Optional - identifier: referenceId # A string used to identify the reference attribute: attributeId # An attribute to get the value of for the reference. - # Supports: content_type_id, content_type_identifier, creation_date, modification_date, name_pattern, remote_id, status and url_name_pattern + # Supports: content_type_id, content_type_identifier, creation_date, modification_date, + # name_pattern, remote_id, status and url_name_pattern # The shorthand 'id' can be used instead of 'content_type_id' # The shorthand 'identifier' can be used instead of 'content_type_identifier' + # It also supports attributes.$identifier.$field and complex expressions, such as f.e. + # attributes.title.required (boolean value) + # attributes.ezobjectrelationlist."field-settings".selectionContentTypes.length(@) (nr. of cotent-types allowed in an object-relations field) + # (for the full syntax supported, see jmespath) - type: content_type @@ -132,16 +137,16 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' - new_identifier: xyz # Optional, will update the identifier if set - name: xyz # Optional, will be updated if set + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' description: xyz # Optional, will be updated if set - name_pattern: xyz # Optional, will be updated if set - url_name_pattern: xyz # Optional, will be updated if set is_container: true|false # Optional, will be updated if set lang: xxx-YY # Optional, will fallback to default language if not provided (--default-language option or 'eng-GB' by default) + name: xyz # Optional, will be updated if set + name_pattern: xyz # Optional, will be updated if set + new_identifier: xyz # Optional, will update the identifier if set + url_name_pattern: xyz # Optional, will be updated if set attributes: # Optional, if set will update existing ones or add new ones. - identifier: xyz # Identifier of the attribute to update or identifier of the new attribute to add @@ -190,9 +195,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' # Attribute examples. diff --git a/Resources/doc/DSL/ManageContentTypeGroup.yml b/Resources/doc/DSL/ManageContentTypeGroup.yml index a0d939ca..4483ae3b 100644 --- a/Resources/doc/DSL/ManageContentTypeGroup.yml +++ b/Resources/doc/DSL/ManageContentTypeGroup.yml @@ -26,9 +26,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' identifier: xyz # Identifier/name of new content type group modification_date: 123 # Optional, custom modification date for the content type group (timestamp) references: #Optional @@ -54,6 +54,6 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' diff --git a/Resources/doc/DSL/ManageLocation.yml b/Resources/doc/DSL/ManageLocation.yml index b3191397..11181b44 100644 --- a/Resources/doc/DSL/ManageLocation.yml +++ b/Resources/doc/DSL/ManageLocation.yml @@ -11,8 +11,8 @@ parent_location_id: # the location id(s) of the parent(s) of contents we want to add a location to parent_location_remote_id: # the remote location id(s) of the parent(s) of contents we want to add a location to parent_location: y # The Location ID of the parent where the new location should be placed. When a non numeric string is used, it is assumed to be a location remote id - priority: x # Optional is_hidden: true|false # Optional + priority: x # Optional sort_field: x # Optional. Possible values for sort_field are: # - published # - priority @@ -36,7 +36,7 @@ # Supports: location_id, location_remote_id, # always_available, content_id, content_type_id, content_type_identifier, current_version_no, # depth, is_hidden, main_language_code, main_location_id, main_language_code, - # modification_date, name, owner_id, parent_location_id, path, position, priority, + # modification_date, name, owner_id, parent_location_id, path, priority, # publication_date, section_id, sort_field, sort_order) - @@ -44,8 +44,8 @@ mode: create match: ... # See above parent_location: [x, y, z] # Multiple locations can be added in a single step using an array - priority: x # Optional is_hidden: ... # See above + priority: x # Optional sort_field: ... # See above sort_order: ... # See above references: ... # See above @@ -55,13 +55,33 @@ mode: update match: # The locations to update # Possible values for matching. only one of them is allowed at a time. All of them can be single or array - content_id: # the content id(s) of the content we want to add a location to - content_remote_id: # the remote content id(s) of the content we want to add a location to - location_id: # the location id(s) of the content we want to add a location to - location_remote_id: # the location remote id(s) of the content we want to add a location to - parent_location_id: # the location id(s) of the parent(s) of contents we want to add a location to - parent_location_remote_id: # the remote location id(s) of the parent(s) of contents we want to add a location to - or: # match any of the conditions below. *NB:* less efficient that using the array notation for a single condition + content_id: # the content id(s) of the locations we want to update + content_remote_id: # the remote content id(s) of the the locations we want to update + location_id: # the location id(s) of the the locations we want to update + location_remote_id: # the location remote id(s) of the the locations we want to update + attribute: + _attr_name_: + _operator_: value # _operator_: eq, gt, gte, lt, lte, in, between, like; value: depending on the attribute type + content_type_identifier: yyy # string|string[] a content type identifier + content_type_id: yyy # int|int[] a content type id + creation_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + depth: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: int + is_main_location: true # bool + group: xxx # user id, login or email if unique + modification_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + object_state: zzz # int|int[]|string|string[] object state(s) either as id or using 'group/identifier' + owner: xxx # user id, login or email if unique + parent_location_id: # the location id(s) of the parent(s) of the locations we want to update + parent_location_remote_id: # the remote location id(s) of the parent(s) of the locations we want to update + priority: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: int + section: xx # int|int[]|string|string[] section id(s) or identifier(s) + subtree: /1/2/2345 # string|string[] + visibility: true # bool + or: # match any of the conditions below - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - @@ -70,15 +90,17 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' - swap_with_location: y # Optional, The ID of the location to swap the location with. Cannot be set at the same time than parent_location - # When a non numeric string is used, it is assumed to be a location remote id + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' + not: # matches elements NOT satisfying the wrapped condition + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' parent_location: x # Optional The parent location to move the subtree to. Cannot be set at the same time than swap_with_location # When a non numeric string is used, it is assumed to be a location remote id - priority: x # Optional, will be updated if set is_hidden: true|false # Optional, Set the visibility of the location + priority: x # Optional, will be updated if set sort_field: x # Optional sort_order: ASC|DESC # Optional + swap_with_location: y # Optional, The ID of the location to swap the location with. Cannot be set at the same time than parent_location + # When a non numeric string is used, it is assumed to be a location remote id remote_id: # Optional, Set the remote_id of the location # The list in references tells the manager to store specific values for later use by other steps in the current migration. # NB: these are NEW VARIABLES THAT YOU ARE CREATING. They are not used in the current migration step! @@ -89,21 +111,41 @@ # Supports: location_id, location_remote_id, # always_available, content_id, content_type_id, content_type_identifier, current_version_no, # depth, is_hidden, main_language_code, main_location_id, main_language_code, - # modification_date, name, owner_id, parent_location_id, path, position, priority, + # modification_date, name, owner_id, parent_location_id, path, priority, # publication_date, section_id, sort_field, sort_order) - type: location mode: delete - match: # The locations to update + match: # The locations to delete # Possible values for matching. only one of them is allowed at a time. All of them can be single or array - content_id: # the content id(s) of the content we want to add a location to - content_remote_id: # the remote content id(s) of the content we want to add a location to - location_id: # the location id(s) of the content we want to add a location to - location_remote_id: # the location remote id(s) of the content we want to add a location to - parent_location_id: # the location id(s) of the parent(s) of contents we want to add a location to - parent_location_remote_id: # the remote location id(s) of the parent(s) of contents we want to add a location to - or: # match any of the conditions below. *NB:* less efficient that using the array notation for a single condition + content_id: # the content id(s) of the content we want to delete + content_remote_id: # the remote content id(s) of the content we want to delete + location_id: # the location id(s) we want to delete + location_remote_id: # the location remote id(s) we want to delete + attribute: + _attr_name_: + _operator_: value # _operator_: eq, gt, gte, lt, lte, in, between, like; value: depending on the attribute type + content_type_identifier: yyy # string|string[] a content type identifier + content_type_id: yyy # int|int[] a content type id + creation_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + depth: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: int + group: xxx # user id, login or email if unique + is_main_location: true # bool + modification_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + object_state: zzz # int|int[]|string|string[] object state(s) either as id or using 'group/identifier' + owner: xxx # user id, login or email if unique + parent_location_id: # the location id(s) of the parent(s) of locations we want to delete + parent_location_remote_id: # the remote location id(s) of the parent(s) of locations we want to delete + priority: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: int + section: xx # int|int[]|string|string[] section id(s) or identifier(s) + subtree: /1/2/2345 # string|string[] + visibility: true # bool + or: # match any of the conditions below - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - @@ -112,20 +154,41 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' - + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' + not: # matches elements NOT satisfying the wrapped condition + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - type: location mode: load match: # The locations to load # Possible values for matching. only one of them is allowed at a time. All of them can be single or array - content_id: # the content id(s) of the content we want to add a location to - content_remote_id: # the remote content id(s) of the content we want to add a location to - location_id: # the location id(s) of the content we want to add a location to - location_remote_id: # the location remote id(s) of the content we want to add a location to - parent_location_id: # the location id(s) of the parent(s) of contents we want to add a location to - parent_location_remote_id: # the remote location id(s) of the parent(s) of contents we want to add a location to - or: # match any of the conditions below. *NB:* less efficient that using the array notation for a single condition + content_id: # the content id(s) of the locations we want to load + content_remote_id: # the remote content id(s) of the locations we want to load + attribute: + _attr_name_: + _operator_: value # _operator_: eq, gt, gte, lt, lte, in, between, like; value: depending on the attribute type + location_id: # the location id(s) of the locations we want to load + location_remote_id: # the location remote id(s) of the location we want to load + content_type_identifier: yyy # string|string[] a content type identifier + content_type_id: yyy # int|int[] a content type id + creation_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + depth: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: int + group: xxx # user id, login or email if unique + is_main_location: true # bool + modification_date: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: unix timestamp + object_state: zzz # int|int[]|string|string[] object state(s) either as id or using 'group/identifier' + owner: xxx # user id, login or email if unique + parent_location_id: # the location id(s) of the parent(s) of locations we want to load + parent_location_remote_id: # the remote location id(s) of the parent(s) of locations we want to load + priority: + _operator_: value # _operator_: eq, gt, gte, lt, lte, value: int + section: xx # int|int[]|string|string[] section id(s) or identifier(s) + subtree: /1/2/2345 # string|string[] + visibility: true # bool + or: # match any of the conditions below - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - @@ -134,7 +197,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' + not: # matches elements NOT satisfying the wrapped condition + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' # The list in references tells the manager to store specific values for later use by other steps in the current migration. # NB: these are NEW VARIABLES THAT YOU ARE CREATING. They are not used in the current migration step! references: # Optional @@ -143,5 +208,5 @@ attribute: attributeId # The attribute to get the value of for the reference (supports: location_id, location_remote_id, # always_available, content_id, content_type_id, content_type_identifier, current_version_no, # depth, is_hidden, main_language_code, main_location_id, main_language_code, - # modification_date, name, owner_id, parent_location_id, path, position, priority, + # modification_date, name, owner_id, parent_location_id, path, priority, # publication_date, section_id, sort_field, sort_order) diff --git a/Resources/doc/DSL/ManageObjectStatesAndGoups.yml b/Resources/doc/DSL/ManageObjectStatesAndGoups.yml index 26cbfe8a..e179f8a0 100644 --- a/Resources/doc/DSL/ManageObjectStatesAndGoups.yml +++ b/Resources/doc/DSL/ManageObjectStatesAndGoups.yml @@ -31,9 +31,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' identifier: x # Optional String: new identifier of the object state group names: # Optional: array of names keyed by language code. languageCodeA: name @@ -67,9 +67,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - @@ -108,9 +108,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' identifier: x # Optional String: new identifier of the object state names: # Optional: array of names keyed by language code. languageCodeA: name @@ -143,6 +143,6 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' diff --git a/Resources/doc/DSL/ManageReferences.yml b/Resources/doc/DSL/ManageReferences.yml index c0885122..ee874518 100644 --- a/Resources/doc/DSL/ManageReferences.yml +++ b/Resources/doc/DSL/ManageReferences.yml @@ -19,6 +19,19 @@ # The reference-value can be a Symfony configuration parameter by using the syntax '%name%' overwrite: true|false # Optional, default false. If not set, and any reference already exists, an exception is thrown +- + # Save the complete set of references currently defined to a json or yaml file + type: reference + mode: save + file: xxx # path to the file to load. It must have an extension of .yml or .json. + # It can be an absolute path. + # NB: if found, the token '{ENV}' will be replaced with the current Symfony environment, eg: + # ezpublish/config/migrations_{ENV}.yml will become ezpublish/config/migrations_dev.yml + # + # The format for the contents of this file is: an array of reference-key => reference-value + # The reference-value can be a Symfony configuration parameter by using the syntax '%name%' + overwrite: true|false # Optional, default false. If not set, and the file already exists, an exception is thrown + - # Dumps a reference to STDOUT. For debugging purposes type: reference diff --git a/Resources/doc/DSL/ManageRolesAndPolicies.yml b/Resources/doc/DSL/ManageRolesAndPolicies.yml index 7109795e..e9d47633 100644 --- a/Resources/doc/DSL/ManageRolesAndPolicies.yml +++ b/Resources/doc/DSL/ManageRolesAndPolicies.yml @@ -103,9 +103,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' assign: # Optional - type: # Must be user or group @@ -153,6 +153,6 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' diff --git a/Resources/doc/DSL/ManageSection.yml b/Resources/doc/DSL/ManageSection.yml index be72f752..8289983a 100644 --- a/Resources/doc/DSL/ManageSection.yml +++ b/Resources/doc/DSL/ManageSection.yml @@ -28,9 +28,9 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' name: xyz identifier: xyz # The list in references tells the manager to store specific values for later use by other steps in the current migration. @@ -58,6 +58,6 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' not: # matches elements NOT satisfying the wrapped condition - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' diff --git a/Resources/doc/DSL/ManageTags.yml b/Resources/doc/DSL/ManageTags.yml index aa380f4b..7d47afc5 100644 --- a/Resources/doc/DSL/ManageTags.yml +++ b/Resources/doc/DSL/ManageTags.yml @@ -25,4 +25,4 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' diff --git a/Resources/doc/DSL/ManageUsersAndGroups.yml b/Resources/doc/DSL/ManageUsersAndGroups.yml index 3ecefa9f..1599c3f2 100644 --- a/Resources/doc/DSL/ManageUsersAndGroups.yml +++ b/Resources/doc/DSL/ManageUsersAndGroups.yml @@ -6,6 +6,7 @@ username: xyz email: xyz password: xyz + user_content_type: xxx # Optional, Content Type identifier (defaults to 'user') lang: xxx-YY # Optional, will fallback to default language if not provided (--default-language option or 'eng-GB' by default) groups: [x, y, z] # The user group ID or IDs (Content IDs or its/their remote_ids as string) to put the user into # The list in references tells the manager to store specific values for later use @@ -33,7 +34,7 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' email: xyz # Optional. NB: can only be set if the match definition latches a single user password: xyz # Optional enabled: true|false # Optional @@ -65,7 +66,7 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - type: user_group @@ -100,7 +101,7 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' name: xyz # Optional. Can only be used when the group to be updated is single description: xyz # Optional parent_group_id: x # Optional, the new parent user group ID or group's remote_id @@ -132,4 +133,4 @@ - _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' - - _condition_: value # where _condition_ can any of ones specified above, including 'and' and 'or' + _condition_: value # where _condition_ can be any of ones specified above, including 'and' and 'or' diff --git a/Resources/doc/DSL/Migrations.yml b/Resources/doc/DSL/Migrations.yml new file mode 100644 index 00000000..a7851f42 --- /dev/null +++ b/Resources/doc/DSL/Migrations.yml @@ -0,0 +1,34 @@ +- + # Cancel execution of a migration: all steps following the current one will not be executed. + # The migrations will show up as 'executed' when checking ist status + type: migration + mode: cancel + message: string # Optional. Will be part of the migration status stored in the database + if: # Optional. If set, the migration will be halted when the condition is matched + "reference:_ref_name": # name of a reference to be used for the test + _operator_: value # allowed operators: eq, gt, gte, lt, lte, ne, count, length, regexp + + +- + # Suspend execution of a migration: 'freeze" execution at the current step. + # The migration will be resumed when running the `command ka:mi:resume`, and the 'until' condition will be checked again, + # at which point the migration will either keep executing or be re-suspended. + # Note that the values of all references existing at the time of migration suspension will be preserved and restored + # upon resuming. + type: migration + mode: suspend + message: string # Optional. Will be part of the migration status stored in the database + load: + type: content/location/content_type # use this to re-load an entity and recalculate the references when the migration + # is resumed. NB: ALL other references are fixed on the 1st execution and + # will keep the same value upon resuming. + match: # same as when loading content/location/content_type + references: # same as when loading content/location/content_type + until: # Optional. If set, migration will be suspended until the given condition is satisfied. + # NB: needs a cronjob to be set up to later resume. + # Use only one of either 'date' or 'match' + date: timestamp # unix timestamp + match: # Optional. If set, the migration will be suspended until the condition is matched + "reference:_ref_name": # Name of a reference to be used for the test. + # NB: it should have been set using the 'load' tag in the same migration step + _operator_: value # allowed operators: eq, gt, gte, lt, lte, ne, count, length, regexp diff --git a/Resources/doc/DSL/README.md b/Resources/doc/DSL/README.md index e3f80a16..6a6f2c3a 100644 --- a/Resources/doc/DSL/README.md +++ b/Resources/doc/DSL/README.md @@ -107,7 +107,7 @@ In the update role action we retrieve the value of the reference by using the `r To tell the system that you want to use a previously stored reference you need to prefix the reference name with the string `reference:`. This instructs the system to look in the list of stored references and replace the current tag with the value -associated to the reference found. +associated to the reference found. Eg: > **Important:** Please note that the reference **must be a quoted string**, as `reference:<reference_name>` uses > YAML reserved characters. @@ -115,6 +115,17 @@ associated to the reference found. > **Bad:** `some_key: reference:foo`<br> > **Good:** `some_key: 'reference:foo'` +Note that, unlike variables in programming laguages, you can not change the value of an existing references by default. +This is done to prevent accidental overwrites of an existing reference with another one, as the most common use case +for reference is set once, use multiple times. +If you want to be able to change the value of a reference after having created it, use the `overwrite` tag: + + references: + - + identifier: section_page_class + attribute: content_type_id + overwrite: true + *NB:* references are stored *in memory* only and will not propagate across different migrations, unless you execute the migrations in a single command (and without the 'separate processes' switch). diff --git a/Tests/dsl/exceptions/UnitTestOK020_cancel.yml b/Tests/dsl/exceptions/UnitTestOK020_cancel.yml new file mode 100644 index 00000000..6a54e758 --- /dev/null +++ b/Tests/dsl/exceptions/UnitTestOK020_cancel.yml @@ -0,0 +1,19 @@ +- + type: reference + mode: set + identifier: kmb_test_20 + value: hello + +- + type: migration + mode: cancel + until: + match: + "reference:kmb_test_21": + eq: hello + +- + type: reference + mode: set + identifier: kmb_test_20b + value: unreachable diff --git a/Tests/dsl/exceptions/UnitTestOK021_suspend.yml b/Tests/dsl/exceptions/UnitTestOK021_suspend.yml new file mode 100644 index 00000000..c4203b74 --- /dev/null +++ b/Tests/dsl/exceptions/UnitTestOK021_suspend.yml @@ -0,0 +1,19 @@ +- + type: reference + mode: set + identifier: kmb_test_21 + value: hello + +- + type: migration + mode: suspend + until: + match: + "reference:kmb_test_21": + eq: world + +- + type: reference + mode: set + identifier: kmb_test_21b + value: unreachable diff --git a/Tests/dsl/eztags/UnitTestOK013_eztags.yml b/Tests/dsl/eztags/UnitTestOK019_eztags.yml similarity index 100% rename from Tests/dsl/eztags/UnitTestOK013_eztags.yml rename to Tests/dsl/eztags/UnitTestOK019_eztags.yml diff --git a/Tests/dsl/good/UnitTestOK003_contentType.yml b/Tests/dsl/good/UnitTestOK003_contentType.yml index e0d376ad..16c62e80 100644 --- a/Tests/dsl/good/UnitTestOK003_contentType.yml +++ b/Tests/dsl/good/UnitTestOK003_contentType.yml @@ -221,6 +221,33 @@ identifier: kmb_test_1_1 attribute: identifier +- + type: content_type + mode: load + match: + contenttype_identifier: kmb_test_1_1 + references: + - + identifier: dear + attribute: 'attributes.title.required' + - + identifier: world + attribute: 'attributes.ezobjectrelationlist."field-settings".selectionContentTypes.length(@)' + +- + type: assert + target: reference + identifier: reference:dear + test: + equals: false + +- + type: assert + target: reference + identifier: reference:world + test: + equals: 1 + - type: content_type mode: delete diff --git a/Tests/dsl/good/UnitTestOK004_content.yml b/Tests/dsl/good/UnitTestOK004_content.yml index 86100eb0..eaccefc9 100644 --- a/Tests/dsl/good/UnitTestOK004_content.yml +++ b/Tests/dsl/good/UnitTestOK004_content.yml @@ -119,6 +119,9 @@ - identifier: kmb_test_2 attribute: identifier + - + identifier: kmb_test_2_0 + attribute: id - type: content mode: create @@ -140,6 +143,9 @@ - identifier: kmb_test_2_1_path attribute: path + - + identifier: kmb_test_2_1_rid + attribute: content_remote_id - type: content_type mode: update @@ -279,15 +285,49 @@ new_remote_id: is_this_a_very_unlikely_remoteid modification_date: "2008:07:06 18:11:31" publication_date: "2007:08:09 18:11:31" + +- + type: content + mode: load + match: + and: + - content_id: reference:kmb_test_2_1 + - location_id: reference:kmb_test_2_1_loc + - content_remote_id: is_this_a_very_unlikely_remoteid + - content_type_id: reference:kmb_test_2_0 + - content_type_identifier: reference:kmb_test_2 + - section: 2 + - creation_date: + gt: 1234567 + - attribute: + ezstring: + eq: 'hello world 1' + - owner: anonymous + - not: + visibility: false + references: + - + identifier: kmb_test_2_4 + attribute: object_id + +- + type: assert + target: reference + identifier: reference:kmb_test_2_1 + test: + equals: reference:kmb_test_2_4 + - type: content mode: delete object_id: [ 'reference:kmb_test_2_2', 'reference:kmb_test_2_3' ] + - type: content mode: delete match: content_type: reference:kmb_test_2 + - type: content_type mode: delete diff --git a/Tests/dsl/good/UnitTestOK005_location.yml b/Tests/dsl/good/UnitTestOK005_location.yml index 9f0bf038..2fdcfa5d 100644 --- a/Tests/dsl/good/UnitTestOK005_location.yml +++ b/Tests/dsl/good/UnitTestOK005_location.yml @@ -17,6 +17,10 @@ - identifier: kmb_test_3 attribute: identifier + - + identifier: kmb_test_3_0 + attribute: id + - type: content mode: create @@ -139,6 +143,40 @@ # location_id: reference:kmb_test_3_3_loc_3 # swap_with_location: reference:kmb_test_3_2_loc +- + type: location + mode: load + match: + and: + - content_id: reference:kmb_test_3_3 + - location_id: reference:kmb_test_3_3_loc_3 + - location_remote_id: reference:kmb_test_3_3_loc_3_rid + - content_type_id: reference:kmb_test_3_0 + - content_type_identifier: reference:kmb_test_3 + - section: 1 + - depth: + gt: 1 + - or: + - + priority: + lt: 2 + - + priority: + gte: 2 + - not: + visibility: false + references: + - + identifier: kmb_test_3_5 + attribute: location_id + +- + type: assert + target: reference + identifier: reference:kmb_test_3_5 + test: + equals: reference:kmb_test_3_3_loc_3 + - type: content mode: delete diff --git a/Tests/dsl/good/UnitTestOK016_references.yml b/Tests/dsl/good/UnitTestOK016_references.yml index 652902ce..9bfaf694 100644 --- a/Tests/dsl/good/UnitTestOK016_references.yml +++ b/Tests/dsl/good/UnitTestOK016_references.yml @@ -41,3 +41,9 @@ identifier: reference:hello test: equals: world + +- + type: reference + mode: save + file: Tests/dsl/good/references/test_refs_generated.yml + overwrite: true diff --git a/Tests/helper/AssertExecutor.php b/Tests/helper/AssertExecutor.php index 686219d4..832e78a5 100644 --- a/Tests/helper/AssertExecutor.php +++ b/Tests/helper/AssertExecutor.php @@ -4,18 +4,17 @@ use Kaliop\eZMigrationBundle\Core\Executor\AbstractExecutor; use Kaliop\eZMigrationBundle\API\Value\MigrationStep; -use PHPUnit_Framework_Assert; -use Kaliop\eZMigrationBundle\API\ReferenceBagInterface; +use Kaliop\eZMigrationBundle\API\ReferenceResolverInterface; class AssertExecutor extends AbstractExecutor { protected $supportedStepTypes = array('assert'); protected $supportedActions = array('reference'/*, 'generated'*/); - /** @var ReferenceBagInterface $referenceResolver */ + /** @var ReferenceResolverInterface $referenceResolver */ protected $referenceResolver; - public function __construct(ReferenceBagInterface $referenceResolver) + public function __construct(ReferenceResolverInterface $referenceResolver) { $this->referenceResolver = $referenceResolver; } @@ -59,11 +58,19 @@ protected function assertReference($dsl, $context) { }*/ + /** + * @todo !important switch to using symfony/validator for uniformity with the rest of the codebase ? + * This would allow us to move the 'assert' executor outside of test code... + * @param mixed $value + * @param array $condition + * @throws \Exception + */ protected function validate($value, array $condition) { + // we do resolve references as well in the value to check against $targetValue = reset($condition); - $flip = array_flip($condition); - $testCondition = reset($flip); + $targetValue = $this->referenceResolver->resolveReference($targetValue); + $testCondition = key($condition); $testMethod = 'assert' . ucfirst($testCondition); if (! is_callable(array('PHPUnit_Framework_Assert', $testMethod))) { throw new \Exception("Invalid step definition: invalid test condition '$testCondition'"); diff --git a/Tests/helper/CustomReferenceResolver.php b/Tests/helper/CustomReferenceResolver.php index ec901d6b..0e74e954 100644 --- a/Tests/helper/CustomReferenceResolver.php +++ b/Tests/helper/CustomReferenceResolver.php @@ -3,11 +3,12 @@ namespace Kaliop\eZMigrationBundle\Tests\helper; use Kaliop\eZMigrationBundle\Core\ReferenceResolver\PrefixBasedResolverInterface; +use Kaliop\eZMigrationBundle\API\EnumerableReferenceResolverInterface; /** * Does nothing for the moment, except making sure that it can be injected correctly via a tagged service */ -class CustomReferenceResolver implements PrefixBasedResolverInterface +class CustomReferenceResolver implements PrefixBasedResolverInterface, EnumerableReferenceResolverInterface { public function isReference($stringIdentifier) { @@ -34,4 +35,9 @@ public function getRegexp() { return ''; } + + public function listReferences() + { + return array(); + } } diff --git a/Tests/helper/StepExecutedListener.php b/Tests/helper/StepExecutedListener.php index 0d129a89..b8bc9d30 100644 --- a/Tests/helper/StepExecutedListener.php +++ b/Tests/helper/StepExecutedListener.php @@ -13,7 +13,7 @@ public function onStepExecuted(StepExecutedEvent $event) { $result = $event->getResult(); - if ($event->getResult() === null && $event->getStep()->type !== 'assert') { + if ($event->getResult() === null && $event->getStep()->type !== 'assert' && $event->getStep()->type !== 'void') { throw new \Exception('Received null as step execution event result'); } diff --git a/Tests/phpunit/2_MigrateTest.php b/Tests/phpunit/2_MigrateTest.php index ba62d70e..9352c87a 100644 --- a/Tests/phpunit/2_MigrateTest.php +++ b/Tests/phpunit/2_MigrateTest.php @@ -97,7 +97,7 @@ public function testExecuteBadDSL($filePath = '') } /** - * Test the --default-language option for the migrate command. + * Tests the --default-language option for the migrate command. */ public function testDefaultLanguage() { @@ -120,7 +120,12 @@ public function testDefaultLanguage() $repository = $this->getRepository(); $contentService = $repository->getContentService(); - // check if the content was created with the default language + // check that the 1st content was created with the yml-specified language + $content = $contentService->loadContentByRemoteId('kmb_test_18_content_1', null, null, false); + $this->assertInstanceOf('eZ\Publish\API\Repository\Values\Content\Content', $content); + $this->assertSame('eng-GB', $content->contentInfo->mainLanguageCode); + + // check that the 2nd content was created with the default language from cli $content = $contentService->loadContentByRemoteId('kmb_test_18_content_2', [$defaultLanguage], null, false); $this->assertInstanceOf('eZ\Publish\API\Repository\Values\Content\Content', $content); $this->assertSame($defaultLanguage, $content->contentInfo->mainLanguageCode); diff --git a/Tests/phpunit/3_TagsTest.php b/Tests/phpunit/3_TagsTest.php index 65391eb7..cb4e33bb 100644 --- a/Tests/phpunit/3_TagsTest.php +++ b/Tests/phpunit/3_TagsTest.php @@ -71,5 +71,4 @@ public function goodDSLProvider() } return $out; } - } diff --git a/Tests/phpunit/4_ExceptionTest.php b/Tests/phpunit/4_ExceptionTest.php index b95b067b..f25e28cd 100644 --- a/Tests/phpunit/4_ExceptionTest.php +++ b/Tests/phpunit/4_ExceptionTest.php @@ -2,11 +2,14 @@ include_once(__DIR__.'/CommandTest.php'); +use Symfony\Component\Console\Input\ArrayInput; use Kaliop\eZMigrationBundle\API\ExecutorInterface; use Kaliop\eZMigrationBundle\API\Exception\MigrationAbortedException; use Kaliop\eZMigrationBundle\API\Value\MigrationStep; use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; use Kaliop\eZMigrationBundle\API\Value\Migration; +use Kaliop\eZMigrationBundle\Tests\helper\BeforeStepExecutionListener; +use Kaliop\eZMigrationBundle\Tests\helper\StepExecutedListener; /** * Tests the MigrationAbortedException, as well as direct manipulation of the migration service @@ -29,6 +32,77 @@ public function testMigrationAbortedException() $this->assertContains('Oh yeah', $m->executionError, 'Migration aborted but its exception message lost'); } + /** + * @param string $filePath + * @dataProvider goodDSLProvider + */ + public function testExecuteGoodDSL($filePath = '') + { + $bundles = $this->container->getParameter('kernel.bundles'); + $ms = $this->container->get('ez_migration_bundle.migration_service'); + + if ($filePath == '') { + $this->markTestSkipped(); + return; + } + + // Make sure migration is not in the db: delete it, ignoring errors + $input = new ArrayInput(array('command' => 'kaliop:migration:migration', 'migration' => basename($filePath), '--delete' => true, '-n' => true)); + $this->app->run($input, $this->output); + $this->fetchOutput(); + + $input = new ArrayInput(array('command' => 'kaliop:migration:migration', 'migration' => $filePath, '--add' => true, '-n' => true)); + $exitCode = $this->app->run($input, $this->output); + $output = $this->fetchOutput(); + $this->assertSame(0, $exitCode, 'CLI Command failed. Output: ' . $output); + $this->assertRegexp('?Added migration?', $output); + + $count1 = BeforeStepExecutionListener::getExecutions(); + $count2 = StepExecutedListener::getExecutions(); + + $input = new ArrayInput(array('command' => 'kaliop:migration:migrate', '--path' => array($filePath), '-n' => true, '-u' => true)); + $exitCode = $this->app->run($input, $this->output); + $output = $this->fetchOutput(); + $this->assertSame(0, $exitCode, 'CLI Command failed. Output: ' . $output); + + $count3 = BeforeStepExecutionListener::getExecutions(); + $count4 = StepExecutedListener::getExecutions(); + $this->assertEquals($count1 + 2, $count3, "Migration not suspended/canceled: executed incorrect number of steps"); + $this->assertEquals($count2 + 1, $count4, "Migration not suspended/canceled: executed incorrect number of steps"); + + $m = $ms->getMigration(basename($filePath)); + $this->assertThat( + $m->status, + $this->logicalOr( + $this->equalTo(Migration::STATUS_SUSPENDED), + $this->equalTo(Migration::STATUS_DONE) + ), + 'Migration supposed to be aborted/suspended but in unexpected state' + ); + + $input = new ArrayInput(array('command' => 'kaliop:migration:migration', 'migration' => basename($filePath), '--delete' => true, '-n' => true)); + $exitCode = $this->app->run($input, $this->output); + $output = $this->fetchOutput(); + $this->assertSame(0, $exitCode, 'CLI Command failed. Output: ' . $output); + } + + public function goodDSLProvider() + { + $dslDir = $this->dslDir.'/exceptions'; + if (!is_dir($dslDir)) { + return array(); + } + + $out = array(); + foreach (scandir($dslDir) as $fileName) { + $filePath = $dslDir . '/' . $fileName; + if (is_file($filePath)) { + $out[] = array($filePath); + } + } + return $out; + } + public function supportedTypes() { return array('abort'); diff --git a/Tests/phpunit/5_ServiceTest.php b/Tests/phpunit/5_ServiceTest.php new file mode 100644 index 00000000..bad7d95a --- /dev/null +++ b/Tests/phpunit/5_ServiceTest.php @@ -0,0 +1,77 @@ +<?php + +include_once(__DIR__.'/CommandTest.php'); + +use Kaliop\eZMigrationBundle\API\ExecutorInterface; +use Kaliop\eZMigrationBundle\API\Value\MigrationStep; +use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; +use Kaliop\eZMigrationBundle\API\Value\Migration; + +class ServiceTest extends CommandTest implements ExecutorInterface +{ + public function testMigrationFetching() + { + $ms = $this->container->get('ez_migration_bundle.migration_service'); + $ms->addExecutor($this); + $md = new MigrationDefinition( + 'storage_test1.json', + '/dev/null', + json_encode(array(array('type' => 'void'))) + ); + $ms->executeMigration($md); + + $m = $ms->getMigration('exception_test.json'); + $this->assertEquals(Migration::STATUS_DONE, $m->status, 'Migration supposed to be aborted but in unexpected state'); + + $migrations = $ms->getMigrationsByStatus(Migration::STATUS_DONE); + $this->assertGreaterThanOrEqual(1, $migrations->count()); + foreach($migrations as $migration) { + $this->assertEquals(Migration::STATUS_DONE, $migration->status, 'Fetched migration has unexpected status'); + } + + $md = new MigrationDefinition( + 'storage_test2.json', + '/dev/null', + json_encode(array(array('type' => 'void'))) + ); + $ms->addMigration($md); + $md = new MigrationDefinition( + 'storage_test3.json', + '/dev/null', + json_encode(array(array('type' => 'void'))) + ); + $ms->addMigration($md); + + $migrations = $ms->getMigrationsByStatus(Migration::STATUS_TODO); + $this->assertGreaterThanOrEqual(2, $migrations->count()); + foreach($migrations as $migration) { + $this->assertEquals(Migration::STATUS_TODO, $migration->status, 'Fetched migration has unexpected status'); + } + $migrations = $ms->getMigrationsByStatus(Migration::STATUS_TODO, 1); + $this->assertEquals(1, $migrations->count()); + + $migrations = $ms->getMigrationsByStatus(Migration::STATUS_TODO, 1, 1); + $this->assertEquals(1, $migrations->count()); + + $migrations = $ms->getMigrationsByStatus(Migration::STATUS_TODO, 1, 999); + $this->assertEquals(0, $migrations->count()); + + $migrations = $ms->getMigrations(1); + $this->assertEquals(1, $migrations->count()); + + $migrations = $ms->getMigrations(1, 1); + $this->assertEquals(1, $migrations->count()); + + $migrations = $ms->getMigrations(1, 999); + $this->assertEquals(0, $migrations->count()); + } + + public function supportedTypes() + { + return array('void'); + } + + public function execute(MigrationStep $step) + { + } +} diff --git a/WHATSNEW.md b/WHATSNEW.md index f9dd54fb..6a5092de 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,21 +1,75 @@ -Version 3.6.3 -============= +Version 4.0 RC-1 +================ -New: the `migrate` command by default will print out the number of executed, failed and skipped migrations, as well as +* New: the `migrate` command by default will print out the number of executed, failed and skipped migrations, as well as time and memory taken +* New: the `ka:mi:migration` command learned a new `--info` action to give detailed information on a single migration + migration at a time -Version 3.6.2 -============= +* New: the `ka:mi:status` command learned a new `--summary` option to print only the number of migrations per status + +* New: migrations can now be cancelled by using a custom migration step. Ex: + + - + type: migration + mode: cancel + if: ... + + More details in [Resources/doc/DSL/Migrations.yml](Resources/doc/DSL/Migrations.yml) + +* New: migrations can now be suspended and resumed: + + - + type: migration + mode: suspended + until: ... + + More details in [Resources/doc/DSL/Migrations.yml](Resources/doc/DSL/Migrations.yml) + +* New: it is possible to use `overwrite: true` to change the value of an existing reference + +* New: it is now possible to save the current references to a file + +* New: it is now possible to specify a custom Content Type for users created via `user/create` migrations + +* New: it is now possible to specify a custom Admin account used to carry out migrations instead of the user 14 + +* New: it is possible to use a 'not', 'attribute', 'creation_date', 'group', 'modification_date', 'object_state', 'owner', + 'section', 'subtree' and 'visibility' condition when matching Contents. + Matching when using 'and' and 'or' is also more efficient + +* New: it is possible to use a 'not', 'attribute', 'content_type_id', 'content_type_identifier', 'creation_date', 'depth', + 'group', 'modification_date', 'object_state', 'owner', 'priority', 'section', 'subtree' and 'visibility' condition + when matching Locations. + Matching when using 'and' and 'or' is also more efficient + +* New: it is now possible to set references to the values of Content Type field definitions. The syntax to use is similar + to the one available for Content fields, described in the notes for release 3.6 a few lines below + +* New: it is now possible to set references to 'section_identifier' when creating/updating/loading Contents and Locations + +* Fixed: removed from the list of possible references which can be set for Locations the non-working 'position' + +* New: the Executor services have been made reentrant + +* BC changes: + + - eZPublish 5.3 and eZPublish Community 2014.3 are not supported any more (eZPublish 5.3 ended support in May 2017) + + - the code will start targeting php 5.6 as minimum version starting with this release + + - the following interfaces have been modified: MigrationGeneratorInterface, StorageHandlerInterface, + + - the following deprecated interfaces have been removed: ComplexFieldInterface -Fix: when using an AND condition to match locations, the results where closer to what an OR condition would produce; too - many nodes would be matched + - lots of refactoring in the Core (non API) classes. If you have extended them, be prepared for some porting work Version 3.6.1 ============= -Fix: when setting both content creation and modification time upon content creation, modification time was lost +* Fixed: when setting both content creation and modification time upon content creation, modification time was lost Version 3.6 diff --git a/composer.json b/composer.json index 8d14664e..73710f20 100644 --- a/composer.json +++ b/composer.json @@ -22,12 +22,13 @@ } ], "require": { - "php": ">=5.4", - "ezsystems/ezpublish-kernel": ">=5.3|>=2014.03", + "php": ">=5.6", + "ezsystems/ezpublish-kernel": ">=5.4|>=2014.11", "ext-pdo": "*", "nikic/php-parser": "2.*", "symfony/process": "*", "symfony/var-dumper": "*", + "symfony/validator": "*", "mtdowling/jmespath.php": "2.*" }, "require-dev": {