From 107e3588eebb7ab78033c64e549cefb01146c105 Mon Sep 17 00:00:00 2001 From: Slawomir Dolzycki-Uchto Date: Fri, 29 Mar 2019 08:04:10 -0700 Subject: [PATCH] EZP-26686: Script to migrate legacy XML format to new ezmatrix value (#11) * EZP-26686: Script to migrate legacy XML format to new ezmatrix value * fix: Code Review --- .../Command/MigrateLegacyMatrixCommand.php | 342 ++++++++++++++++++ src/bundle/Resources/config/services.yml | 1 + .../Resources/config/services/fieldtype.yml | 2 - .../Resources/config/services/migration.yml | 7 + .../legacy_ezcontentclass_attribute.xsd | 34 ++ ...d => legacy_ezcontentobject_attribute.xsd} | 0 6 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 src/bundle/Command/MigrateLegacyMatrixCommand.php create mode 100644 src/bundle/Resources/config/services/migration.yml create mode 100644 src/bundle/Resources/schema/legacy_ezcontentclass_attribute.xsd rename src/bundle/Resources/schema/{legacy_matrix.xsd => legacy_ezcontentobject_attribute.xsd} (100%) diff --git a/src/bundle/Command/MigrateLegacyMatrixCommand.php b/src/bundle/Command/MigrateLegacyMatrixCommand.php new file mode 100644 index 0000000..d85cfd0 --- /dev/null +++ b/src/bundle/Command/MigrateLegacyMatrixCommand.php @@ -0,0 +1,342 @@ +connection = $connection; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->addOption( + 'iteration-count', + 'c', + InputOption::VALUE_OPTIONAL, + 'Number of matrix FieldType values fetched into memory and processed at once', + self::DEFAULT_ITERATION_COUNT + ); + } + + /** + * {@inheritdoc} + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + if (!$input->isInteractive()) { + throw new RuntimeException('Command cannot be run in non-interactive mode.'); + } + + $io->caution('Read carefully. This operation is irreversible. Make sure you are using correct database and have backup.'); + $answer = $io->ask('Are you sure you want to start migration? (type "' . self::CONFIRMATION_ANSWER . '" to confirm)'); + + if ($answer !== self::CONFIRMATION_ANSWER) { + $io->comment('Canceled.'); + + exit(); + } + + $io->comment('Migrating legacy ezmatrix fieldtype'); + + $iterationCount = (int)$input->getOption('iteration-count'); + $converter = new MatrixConverter(); + + $contentClassAttributes = $this->getContentClassAttributes(); + + libxml_use_internal_errors(true); + + foreach ($contentClassAttributes as $contentClassAttribute) { + $io->comment(sprintf('Migrate %s:%s attribute.', $contentClassAttribute['contenttype_identifier'], $contentClassAttribute['identifier'])); + + try { + $xml = new SimpleXMLElement((string)$contentClassAttribute['columns']); + + $isValidXml = true; + } + catch (Exception $e) { + $isValidXml = false; + } + + if ($isValidXml) { + $columnList = $xml->xpath('//column-name'); + + $columns = []; + + foreach ($columnList as $column) { + $columns[(int)$column['idx']] = [ + 'identifier' => (string)$column['id'], + 'name' => (string)$column, + ]; + } + + $fieldDefinition = new FieldDefinition(); + $storageFieldDefinition = new StorageFieldDefinition(); + + $fieldDefinition->fieldTypeConstraints->fieldSettings = [ + 'minimum_rows' => $contentClassAttribute['minimum_rows'], + 'columns' => array_values($columns), + ]; + + $converter->toStorageFieldDefinition($fieldDefinition, $storageFieldDefinition); + + $this->updateContentClassAttribute( + (int)$contentClassAttribute['id'], + (int)$storageFieldDefinition->dataInt1, + (string)$storageFieldDefinition->dataText5 + ); + + + $columnsJson = $storageFieldDefinition->dataText5; + } else { + $columnsJson = $contentClassAttribute['columns']; + } + + $contentAttributesCount = $this->getContentObjectAttributesCount( + (int)$contentClassAttribute['id'] + ); + + $columns = json_decode($columnsJson); + + $progressBar = $this->getProgressBar($contentAttributesCount, $output); + $progressBar->start(); + + for ($offset = 0; $offset <= $contentAttributesCount; $offset += $iterationCount) { + gc_disable(); + + $contentObjectAttributes = $this->getContentObjectAttributes( + (int)$contentClassAttribute['id'], + $offset, + $iterationCount + ); + + foreach ($contentObjectAttributes as $contentObjectAttribute) { + try { + $xml = new SimpleXMLElement( + (string)$contentObjectAttribute['data_text'] + ); + } + catch (Exception $e) { + $progressBar->advance(); + + continue; + } + + $storageFieldValue = new StorageFieldValue(); + $fieldValue = new FieldValue([ + 'data' => [ + 'entries' => [], + ] + ]); + + $rows = $this->convertCellsToRows($xml->xpath('c'), $columns); + + $fieldValue->data['entries'] = $rows; + + $converter->toStorageValue($fieldValue, $storageFieldValue); + + $this->updateContentObjectAttribute( + (int)$contentObjectAttribute['id'], + (string)$storageFieldValue->dataText + ); + + $progressBar->advance(); + } + + gc_enable(); + } + + $progressBar->finish(); + + $output->writeln(['', '']); + } + + $io->success('Done.'); + } + + /** + * @param array $cells + * @param array $columns + * + * @return array + */ + private function convertCellsToRows(array $cells, array $columns): array + { + $row = []; + $rows = []; + $columnsCount = count($columns); + + foreach ($cells as $index => $cell) { + $columnIndex = $index % $columnsCount; + $columnIdentifier = $columns[$columnIndex]->identifier; + + $row[$columnIdentifier] = (string)$cell; + + if ($columnIndex === $columnsCount - 1) { + $rows[] = $row; + $row = []; + } + } + + return $rows; + } + + /** + * @return array + */ + private function getContentClassAttributes(): array + { + $query = $this->connection->createQueryBuilder(); + $query + ->select([ + 'attr.id', + 'attr.identifier', + 'attr.data_int1 as minimum_rows', + 'attr.data_text5 as columns', + 'class.identifier as contenttype_identifier', + ]) + ->from('ezcontentclass_attribute', 'attr') + ->join('attr', 'ezcontentclass', 'class', 'class.id = attr.contentclass_id') + ->where('attr.data_type_string = :identifier') + ->setParameter(':identifier', self::EZMATRIX_IDENTIFIER); + + return $query->execute()->fetchAll(); + } + + /** + * @param int $id + * @param int $minimumRows + * @param string $columns + */ + private function updateContentClassAttribute(int $id, int $minimumRows, string $columns): void + { + $query = $this->connection->createQueryBuilder(); + $query + ->update('ezcontentclass_attribute', 'attr') + ->set('attr.data_int1', ':minimum_rows') + ->set('attr.data_text5', ':columns') + ->where('attr.id = :id') + ->setParameter(':id', $id) + ->setParameter(':minimum_rows', $minimumRows) + ->setParameter(':columns', $columns); + + $query->execute(); + } + + /** + * @param int $id + * + * @return int + */ + private function getContentObjectAttributesCount(int $id): int + { + $query = $this->connection->createQueryBuilder(); + $query + ->select('count(1)') + ->from('ezcontentobject_attribute', 'attr') + ->where('attr.contentclassattribute_id = :class_attr_id') + ->setParameter(':class_attr_id', $id); + + return (int)$query->execute()->fetchColumn(0); + } + + /** + * @param int $id + * @param int $offset + * @param int $iterationCount + * + * @return array + */ + private function getContentObjectAttributes(int $id, int $offset, int $iterationCount): array + { + $query = $this->connection->createQueryBuilder(); + $query + ->select(['id', 'data_text']) + ->from('ezcontentobject_attribute', 'attr') + ->where('attr.contentclassattribute_id = :class_attr_id') + ->setParameter(':class_attr_id', $id) + ->setFirstResult($offset) + ->setMaxResults($iterationCount); + + return $query->execute()->fetchAll(); + } + + /** + * @param int $id + * @param string $rows + */ + private function updateContentObjectAttribute(int $id, string $rows): void + { + $query = $this->connection->createQueryBuilder(); + $query + ->update('ezcontentobject_attribute', 'attr') + ->set('attr.data_text', ':rows') + ->where('attr.id = :id') + ->setParameter(':id', $id) + ->setParameter(':rows', $rows); + + $query->execute(); + } + + /** + * @param int $maxSteps + * @param \Symfony\Component\Console\Output\OutputInterface $output + * + * @return \Symfony\Component\Console\Helper\ProgressBar + */ + protected function getProgressBar(int $maxSteps, OutputInterface $output): ProgressBar + { + $progressBar = new ProgressBar($output, $maxSteps); + $progressBar->setFormat( + ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%' + ); + + return $progressBar; + } +} diff --git a/src/bundle/Resources/config/services.yml b/src/bundle/Resources/config/services.yml index 879830c..07f2415 100644 --- a/src/bundle/Resources/config/services.yml +++ b/src/bundle/Resources/config/services.yml @@ -1,2 +1,3 @@ imports: - { resource: services/fieldtype.yml } + - { resource: services/migration.yml } diff --git a/src/bundle/Resources/config/services/fieldtype.yml b/src/bundle/Resources/config/services/fieldtype.yml index 76fe17b..5b94dd7 100644 --- a/src/bundle/Resources/config/services/fieldtype.yml +++ b/src/bundle/Resources/config/services/fieldtype.yml @@ -17,8 +17,6 @@ services: tags: - { name: ezpublish.fieldType, alias: '%ezplatform.fieldtype.matrix.identifier%' } - EzSystems\EzPlatformMatrixFieldtype\FieldType\Converter\StorageValue\StorageValueConverterFactory: ~ - EzSystems\EzPlatformMatrixFieldtype\FieldType\Converter\MatrixConverter: tags: - { name: ezpublish.storageEngine.legacy.converter, alias: '%ezplatform.fieldtype.matrix.identifier%' } diff --git a/src/bundle/Resources/config/services/migration.yml b/src/bundle/Resources/config/services/migration.yml new file mode 100644 index 0000000..4b90927 --- /dev/null +++ b/src/bundle/Resources/config/services/migration.yml @@ -0,0 +1,7 @@ +services: + _defaults: + autoconfigure: true + autowire: true + public: false + + EzSystems\EzPlatformMatrixFieldtypeBundle\Command\MigrateLegacyMatrixCommand: ~ diff --git a/src/bundle/Resources/schema/legacy_ezcontentclass_attribute.xsd b/src/bundle/Resources/schema/legacy_ezcontentclass_attribute.xsd new file mode 100644 index 0000000..5917bc6 --- /dev/null +++ b/src/bundle/Resources/schema/legacy_ezcontentclass_attribute.xsd @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/bundle/Resources/schema/legacy_matrix.xsd b/src/bundle/Resources/schema/legacy_ezcontentobject_attribute.xsd similarity index 100% rename from src/bundle/Resources/schema/legacy_matrix.xsd rename to src/bundle/Resources/schema/legacy_ezcontentobject_attribute.xsd