From 42ce4ee2ccfc90893d957041d797fff37ab9803e Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Mon, 21 Jul 2025 23:21:23 +0200 Subject: [PATCH 01/25] [With or Without IDs][Export] export without IDs (id, parentsId); optionally IDs included --- .../export/assets/converters_populators.yaml | 6 ++-- .../documents/converters_populators.yaml | 5 +-- .../elements/converters_populators.yaml | 11 ++++-- .../export/objects/converters_populators.yaml | 5 +-- phpunit.xml.dist | 1 + public/js/exportAsset.js | 4 ++- public/js/exportDataObjects.js | 4 ++- public/js/exportDocument.js | 4 ++- .../Base/AbstractExportBaseCommand.php | 6 ++++ src/Command/ExportAssetsCommand.php | 2 +- src/Command/ExportDataObjectsCommand.php | 2 +- src/Command/ExportDocumentsCommand.php | 2 +- .../Admin/ExportAssetsController.php | 22 +++++++++--- .../Admin/ExportDataObjectsController.php | 24 ++++++++++--- .../Admin/ExportDocumentsController.php | 24 ++++++++++--- src/Export/Exporter.php | 10 ++++-- src/Model/Element.php | 4 +-- src/Populator/IdsPopulator.php | 21 +++++++++++ tests/DummyKernelTest.php | 0 tests/Integration/Export/ExporterTest.php | 36 +++++++++++++++++++ .../Export/ImportExportYamlDriver.php | 0 ...st__test_simple_saved_pages_export__1.yaml | 4 --- ...__test_simple_unsaved_pages_export__1.yaml | 4 --- ...rterTest__test_single_image_export__1.yaml | 2 -- ...orterTest__test_single_page_export__1.yaml | 2 -- ...Test__test_single_page_export_json__1.json | 2 -- ...porterTest__test_tree_pages_export__1.yaml | 6 ---- tests/Integration/Import/ImporterTest.php | 0 .../Import/ParentRelationResolverTest.php | 0 .../Admin/ExportDocumentsControllerTest.php | 6 ++-- .../Converter/TypeStrategyConverterTest.php | 0 .../Fixture/TestDataObject.php | 0 .../PropertyBasedMappingPopulatorTest.php | 0 tests/app/.env | 0 tests/app/.gitignore | 0 tests/app/TestKernel.php | 0 tests/app/config/.gitkeep | 0 tests/app/config/services.yaml | 0 tests/bootstrap.php | 0 translations/admin.de.yaml | 1 + translations/admin.en.yaml | 1 + 41 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 src/Populator/IdsPopulator.php mode change 100644 => 100755 tests/DummyKernelTest.php mode change 100644 => 100755 tests/Integration/Export/ExporterTest.php mode change 100644 => 100755 tests/Integration/Export/ImportExportYamlDriver.php mode change 100644 => 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml mode change 100644 => 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml mode change 100644 => 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml mode change 100644 => 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml mode change 100644 => 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json mode change 100644 => 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml mode change 100644 => 100755 tests/Integration/Import/ImporterTest.php mode change 100644 => 100755 tests/Integration/Import/ParentRelationResolverTest.php mode change 100644 => 100755 tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php mode change 100644 => 100755 tests/Unit/Converter/TypeStrategyConverterTest.php mode change 100644 => 100755 tests/Unit/PimcoreConverter/Fixture/TestDataObject.php mode change 100644 => 100755 tests/Unit/PimcoreConverter/Populator/PropertyBasedMappingPopulatorTest.php mode change 100644 => 100755 tests/app/.env mode change 100644 => 100755 tests/app/.gitignore mode change 100644 => 100755 tests/app/TestKernel.php mode change 100644 => 100755 tests/app/config/.gitkeep mode change 100644 => 100755 tests/app/config/services.yaml mode change 100644 => 100755 tests/bootstrap.php diff --git a/config/pimcore/export/assets/converters_populators.yaml b/config/pimcore/export/assets/converters_populators.yaml index 09ca22a..68680f6 100644 --- a/config/pimcore/export/assets/converters_populators.yaml +++ b/config/pimcore/export/assets/converters_populators.yaml @@ -8,12 +8,12 @@ neusta_converter: ########################################################### neusta_pimcore_import_export.export_asset_folder: target: Neusta\Pimcore\ImportExportBundle\Model\Asset\Asset + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator properties: - id: ~ key: ~ type: ~ path: ~ - parentId: ~ services: _defaults: @@ -87,6 +87,8 @@ services: ########################################################### # Export Populator (Pimcore Asset -> Asset) ########################################################### + Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator: ~ + neusta_pimcore_import_export.asset.filename.populator: class: Neusta\ConverterBundle\Populator\PropertyMappingPopulator arguments: diff --git a/config/pimcore/export/documents/converters_populators.yaml b/config/pimcore/export/documents/converters_populators.yaml index 1a7192f..cd5333e 100644 --- a/config/pimcore/export/documents/converters_populators.yaml +++ b/config/pimcore/export/documents/converters_populators.yaml @@ -12,13 +12,12 @@ neusta_converter: - neusta_pimcore_import_export.page.property.language.populator - neusta_pimcore_import_export.page.property.navigation_title.populator - neusta_pimcore_import_export.page.property.navigation_name.populator + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator properties: - id: ~ key: ~ type: ~ published: ~ path: ~ - parentId: ~ controller: ~ neusta_pimcore_import_export.editable_converter: @@ -75,6 +74,8 @@ services: ########################################################### # Export Populator (Page -> Page) ########################################################### + Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator: ~ + neusta_pimcore_import_export.page.title.populator: class: Neusta\ConverterBundle\Populator\PropertyMappingPopulator arguments: diff --git a/config/pimcore/export/elements/converters_populators.yaml b/config/pimcore/export/elements/converters_populators.yaml index e19f2db..00d9c38 100644 --- a/config/pimcore/export/elements/converters_populators.yaml +++ b/config/pimcore/export/elements/converters_populators.yaml @@ -8,9 +8,16 @@ neusta_converter: ########################################################### neusta_pimcore_import_export.export_element: target: Neusta\Pimcore\ImportExportBundle\Model\Element + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator properties: - id: ~ key: ~ type: ~ path: ~ - parentId: ~ + +services: + _defaults: + autowire: true + autoconfigure: true + + Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator: ~ diff --git a/config/pimcore/export/objects/converters_populators.yaml b/config/pimcore/export/objects/converters_populators.yaml index 4672aa4..04067d5 100644 --- a/config/pimcore/export/objects/converters_populators.yaml +++ b/config/pimcore/export/objects/converters_populators.yaml @@ -9,16 +9,15 @@ neusta_converter: neusta_pimcore_import_export.export_object: target: Neusta\Pimcore\ImportExportBundle\Model\Object\DataObject populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator - neusta_pimcore_import_export.export_object.fields.populator - neusta_pimcore_import_export.export_object.relations.populator properties: className: ~ - id: ~ key: ~ type: ~ published: ~ path: ~ - parentId: ~ neusta_pimcore_import_export.export_object.without.relations: target: Neusta\Pimcore\ImportExportBundle\Model\Object\DataObject @@ -47,6 +46,8 @@ services: $typeToConverterMap: Pimcore\Model\DataObject\Concrete: '@neusta_pimcore_import_export.export_object' + Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator: ~ + neusta_pimcore_import_export.export_object.fields.populator: class: Neusta\Pimcore\ImportExportBundle\Populator\DataObjectExportFieldsPopulator diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 148fef9..7ab7d2c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,7 @@ + diff --git a/public/js/exportAsset.js b/public/js/exportAsset.js index 459941a..444a67c 100644 --- a/public/js/exportAsset.js +++ b/public/js/exportAsset.js @@ -21,9 +21,11 @@ neusta_pimcore_import_export.plugin.asset.export = Class.create({ iconCls: "pimcore_icon_asset pimcore_icon_overlay_download", handler: function () { let defaultFilename = asset.data.key + '.yaml'; + let includeIds = !confirm(t('neusta_pimcore_import_export_exclude_ids_question')); // Yes = false, No = true + let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); if (filename) { - pimcore.helpers.download(Routing.generate(route, {asset_id: asset.data.id, filename: filename, format: 'yaml'})); + pimcore.helpers.download(Routing.generate(route, {asset_id: asset.data.id, filename: filename, format: 'yaml', ids_included: includeIds})); } } })); diff --git a/public/js/exportDataObjects.js b/public/js/exportDataObjects.js index 722173a..2ca6fd9 100644 --- a/public/js/exportDataObjects.js +++ b/public/js/exportDataObjects.js @@ -21,9 +21,11 @@ neusta_pimcore_import_export.plugin.object.export = Class.create({ iconCls: icon, handler: function () { let defaultFilename = object.data.key + '.yaml'; + let includeIds = !confirm(t('neusta_pimcore_import_export_exclude_ids_question')); // Yes = false, No = true + let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); if (filename) { - pimcore.helpers.download(Routing.generate(route, {object_id: object.data.id, filename: filename, format: 'yaml'})); + pimcore.helpers.download(Routing.generate(route, {object_id: object.data.id, filename: filename, format: 'yaml', ids_included: includeIds})); } } })); diff --git a/public/js/exportDocument.js b/public/js/exportDocument.js index 1c0bbe8..352996a 100755 --- a/public/js/exportDocument.js +++ b/public/js/exportDocument.js @@ -21,9 +21,11 @@ neusta_pimcore_import_export.plugin.document.export = Class.create({ iconCls: icon, handler: function () { let defaultFilename = document.data.key + '.yaml'; + let includeIds = !confirm(t('neusta_pimcore_import_export_exclude_ids_question')); // Yes = false, No = true + let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); if (filename) { - pimcore.helpers.download(Routing.generate(route, {doc_id: document.data.id, filename: filename, format: 'yaml'})); + pimcore.helpers.download(Routing.generate(route, {doc_id: document.data.id, filename: filename, format: 'yaml', ids_included: includeIds})); } } })); diff --git a/src/Command/Base/AbstractExportBaseCommand.php b/src/Command/Base/AbstractExportBaseCommand.php index 1f0d5ed..15bcddc 100644 --- a/src/Command/Base/AbstractExportBaseCommand.php +++ b/src/Command/Base/AbstractExportBaseCommand.php @@ -37,6 +37,12 @@ public function __construct( protected function configure(): void { $this + ->addOption( + 'includeIds', + null, + InputOption::VALUE_NONE, + 'If set, the export will include asset/document/object IDs and ParentIDs - be aware with re-importing' + ) ->addOption( 'output', 'o', diff --git a/src/Command/ExportAssetsCommand.php b/src/Command/ExportAssetsCommand.php index 39f322a..0038190 100644 --- a/src/Command/ExportAssetsCommand.php +++ b/src/Command/ExportAssetsCommand.php @@ -79,7 +79,7 @@ protected function configure(): void protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format')); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['includeIds' => $input->getOption('includeIds')]); $zipFilename = $input->getOption('output'); try { diff --git a/src/Command/ExportDataObjectsCommand.php b/src/Command/ExportDataObjectsCommand.php index 30d6b3b..9bb2368 100644 --- a/src/Command/ExportDataObjectsCommand.php +++ b/src/Command/ExportDataObjectsCommand.php @@ -81,7 +81,7 @@ className: SocialMediaItem protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format')); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['includeIds' => $input->getOption('includeIds')]); $exportFilename = $input->getOption('output'); // Validate filename to prevent directory traversal diff --git a/src/Command/ExportDocumentsCommand.php b/src/Command/ExportDocumentsCommand.php index 07f4025..f53e52f 100644 --- a/src/Command/ExportDocumentsCommand.php +++ b/src/Command/ExportDocumentsCommand.php @@ -85,7 +85,7 @@ protected function configure(): void protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format')); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['includeIds' => $input->getOption('includeIds')]); $exportFilename = $input->getOption('output'); // Validate filename to prevent directory traversal diff --git a/src/Controller/Admin/ExportAssetsController.php b/src/Controller/Admin/ExportAssetsController.php index 0fc8120..69c9b46 100644 --- a/src/Controller/Admin/ExportAssetsController.php +++ b/src/Controller/Admin/ExportAssetsController.php @@ -37,7 +37,12 @@ public function export(Request $request): Response ); } - return $this->exportAssets([$asset], $request->query->getString('filename'), 'yaml'); + return $this->exportAssets( + [$asset], + $request->query->getString('filename'), + 'yaml', + $request->query->getBoolean('ids_included', false), + ); } #[Route( @@ -58,16 +63,25 @@ public function exportWithChildren(Request $request): Response // We need the list two times so generate an array first: $assets = iterator_to_array($this->assetRepository->findAllInTree($asset), false); - return $this->exportAssets($assets, $request->query->getString('filename'), $request->query->getString('format')); + return $this->exportAssets( + $assets, + $request->query->getString('filename'), + $request->query->getString('format'), + $request->query->getBoolean('ids_included', false) + ); } /** * @param array $assets */ - private function exportAssets(array $assets, string $filename, string $format): Response + private function exportAssets(array $assets, string $filename, string $format, bool $includeIds): Response { try { - $yaml = $this->exporter->export($assets, $format); + $yaml = $this->exporter->export( + $assets, + $format, + ['includeIds' => $includeIds], + ); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/src/Controller/Admin/ExportDataObjectsController.php b/src/Controller/Admin/ExportDataObjectsController.php index 5557a61..87a630b 100644 --- a/src/Controller/Admin/ExportDataObjectsController.php +++ b/src/Controller/Admin/ExportDataObjectsController.php @@ -34,7 +34,12 @@ public function export(Request $request): Response ); } - return $this->exportObjects([$object], $request->query->getString('filename'), 'yaml'); + return $this->exportObjects( + [$object], + $request->query->getString('filename'), + 'yaml', + $request->query->getBoolean('ids_included', false), + ); } #[Route( @@ -54,16 +59,27 @@ public function exportWithChildren(Request $request): Response $objects = $this->objectRepository->findAllInTree($object); - return $this->exportObjects($objects, $request->query->getString('filename'), $request->query->getString('format')); + return $this->exportObjects( + $objects, + $request->query->getString('filename'), + $request->query->getString('format'), + $request->query->getBoolean('ids_included', false), + ); } /** * @param iterable $objects */ - private function exportObjects(iterable $objects, string $filename, string $format): Response + private function exportObjects(iterable $objects, string $filename, string $format, bool $includeIds): Response { try { - $yaml = $this->exporter->export($objects, $format); + $yaml = $this->exporter->export( + $objects, + $format, + [ + 'includeIds' => $includeIds, + ], + ); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/src/Controller/Admin/ExportDocumentsController.php b/src/Controller/Admin/ExportDocumentsController.php index 1f0a6a9..81b8566 100644 --- a/src/Controller/Admin/ExportDocumentsController.php +++ b/src/Controller/Admin/ExportDocumentsController.php @@ -34,7 +34,12 @@ public function export(Request $request): Response ); } - return $this->exportDocuments([$document], $request->query->getString('filename'), 'yaml'); + return $this->exportDocuments( + [$document], + $request->query->getString('filename'), + 'yaml', + $request->query->getBoolean('ids_included', false), + ); } #[Route( @@ -54,16 +59,27 @@ public function exportWithChildren(Request $request): Response $documents = $this->documentRepository->findAllInTree($document); - return $this->exportDocuments($documents, $request->query->getString('filename'), $request->query->getString('format')); + return $this->exportDocuments( + $documents, + $request->query->getString('filename'), + $request->query->getString('format'), + $request->query->getBoolean('ids_included', false), + ); } /** * @param iterable $documents */ - private function exportDocuments(iterable $documents, string $filename, string $format): Response + private function exportDocuments(iterable $documents, string $filename, string $format, bool $includeIds): Response { try { - $yaml = $this->exporter->export($documents, $format); + $yaml = $this->exporter->export( + $documents, + $format, + [ + 'includeIds' => $includeIds, + ] + ); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/src/Export/Exporter.php b/src/Export/Exporter.php index 2d7935c..2d8a52c 100644 --- a/src/Export/Exporter.php +++ b/src/Export/Exporter.php @@ -27,16 +27,22 @@ public function __construct( * Exports one or more Pimcore Elements in the given format (yaml, json, ...)). * * @param iterable $elements + * @param array $ctxParams * * @throws ConverterException */ - public function export(iterable $elements, string $format): string + public function export(iterable $elements, string $format, array $ctxParams = []): string { + $ctx = new GenericContext(); + foreach ($ctxParams as $key => $value) { + $ctx->setValue($key, $value); + } + $yamlExportElements = []; foreach ($elements as $element) { foreach (array_keys($this->typeToConverterMap) as $type) { if ($element instanceof $type) { - $yamlExportElements[] = [$type => $this->typeToConverterMap[$type]->convert($element)]; + $yamlExportElements[] = [$type => $this->typeToConverterMap[$type]->convert($element, $ctx)]; continue 2; } } diff --git a/src/Model/Element.php b/src/Model/Element.php index a911207..b3d0367 100644 --- a/src/Model/Element.php +++ b/src/Model/Element.php @@ -6,8 +6,8 @@ class Element { public const ELEMENTS = 'elements'; - public ?int $id = null; - public int $parentId = 0; + public ?int $id; // important not to set default value here to avoid exporting null values automatically + public ?int $parentId; // important not to set default value here to avoid exporting null values automatically public string $type = 'element'; public string $path = ''; public string $language = ''; diff --git a/src/Populator/IdsPopulator.php b/src/Populator/IdsPopulator.php new file mode 100644 index 0000000..8490da2 --- /dev/null +++ b/src/Populator/IdsPopulator.php @@ -0,0 +1,21 @@ + + */ +class IdsPopulator implements Populator +{ + public function populate(object $target, object $source, ?object $ctx = null): void + { + if ($ctx?->hasKey('includeIds') && true === $ctx->getValue('includeIds')) { + $target->id = $source->getId(); + $target->parentId = $source->getParentId(); + } + } +} diff --git a/tests/DummyKernelTest.php b/tests/DummyKernelTest.php old mode 100644 new mode 100755 diff --git a/tests/Integration/Export/ExporterTest.php b/tests/Integration/Export/ExporterTest.php old mode 100644 new mode 100755 index 5b97925..e0d2772 --- a/tests/Integration/Export/ExporterTest.php +++ b/tests/Integration/Export/ExporterTest.php @@ -35,6 +35,19 @@ public function test_single_image_export(): void $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } + public function test_single_image_export_with_ids(): void + { + $asset = new Image(); + $asset->setId(999); + $asset->setParentId(1); + $asset->setKey('image_1'); + $asset->setType('image'); + $asset->setPath('/'); + + $yaml = $this->exporter->export([$asset], 'yaml', ['includeIds' => true]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + public function test_single_page_export(): void { $page = $this->createPageWithInputEditable(); @@ -43,6 +56,14 @@ public function test_single_page_export(): void $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } + public function test_single_page_export_with_ids(): void + { + $page = $this->createPageWithInputEditable(); + + $yaml = $this->exporter->export([$page], 'yaml', ['includeIds' => true]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + public function test_single_page_export_json(): void { $page = $this->createPageWithInputEditable(); @@ -87,6 +108,21 @@ public function test_tree_pages_export(): void $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } + public function test_tree_pages_export_with_ids(): void + { + $page1 = $this->createSimplePage('1', 1, '/will/be/overwritten/'); + $page1->save(); + + $page2 = $this->createSimplePage('2', $page1->getId(), '/will/be/overwritten/'); + $page2->save(); + + $page3 = $this->createSimplePage('3', $page2->getId(), '/will/be/overwritten/'); + $page3->save(); + + $yaml = $this->exporter->export([$page1, $page2, $page3], 'yaml', ['includeIds' => true]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + private function createPageWithInputEditable(): Page { $page = new Page(); diff --git a/tests/Integration/Export/ImportExportYamlDriver.php b/tests/Integration/Export/ImportExportYamlDriver.php old mode 100644 new mode 100755 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml old mode 100644 new mode 100755 index e843983..c0c88dc --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml @@ -7,8 +7,6 @@ elements: title: 'Test Document_1' controller: 'App\Controller\DefaultController::defaultAction' editables: { } - id: 2 - parentId: 1 type: page path: / language: '' @@ -21,8 +19,6 @@ elements: title: 'Test Document_2' controller: 'App\Controller\DefaultController::defaultAction' editables: { } - id: 3 - parentId: 1 type: page path: / language: '' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml old mode 100644 new mode 100755 index b4d6f2e..e46ab64 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml @@ -7,8 +7,6 @@ elements: title: 'Test Document_1' controller: 'App\Controller\DefaultController::defaultAction' editables: { } - id: ~ - parentId: 1 type: page path: /will/not/overwritten/ language: '' @@ -21,8 +19,6 @@ elements: title: 'Test Document_2' controller: 'App\Controller\DefaultController::defaultAction' editables: { } - id: ~ - parentId: 1 type: page path: /will/not/overwritten/ language: '' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml old mode 100644 new mode 100755 index 0d228cd..0bc0ef6 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml @@ -2,8 +2,6 @@ elements: - Pimcore\Model\Asset\Image: filename: image_1 - id: 999 - parentId: 1 type: image path: / language: '' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml old mode 100644 new mode 100755 index 8f7e1d3..1c92d0b --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml @@ -11,8 +11,6 @@ elements: type: input name: textInput data: 'some text input' - id: 999 - parentId: 4 type: page path: /test/ language: en diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json old mode 100644 new mode 100755 index b454b0b..ef33936 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json @@ -14,8 +14,6 @@ "data": "some text input" } ], - "id": 999, - "parentId": 4, "type": "page", "path": "\/test\/", "language": "en", diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml old mode 100644 new mode 100755 index 1d8686f..843f187 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml @@ -7,8 +7,6 @@ elements: title: 'Test Document_1' controller: 'App\Controller\DefaultController::defaultAction' editables: { } - id: 2 - parentId: 1 type: page path: / language: '' @@ -21,8 +19,6 @@ elements: title: 'Test Document_2' controller: 'App\Controller\DefaultController::defaultAction' editables: { } - id: 3 - parentId: 2 type: page path: /test_document_1/ language: '' @@ -35,8 +31,6 @@ elements: title: 'Test Document_3' controller: 'App\Controller\DefaultController::defaultAction' editables: { } - id: 4 - parentId: 3 type: page path: /test_document_1/test_document_2/ language: '' diff --git a/tests/Integration/Import/ImporterTest.php b/tests/Integration/Import/ImporterTest.php old mode 100644 new mode 100755 diff --git a/tests/Integration/Import/ParentRelationResolverTest.php b/tests/Integration/Import/ParentRelationResolverTest.php old mode 100644 new mode 100755 diff --git a/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php b/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php old mode 100644 new mode 100755 index c756f0d..018bc05 --- a/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php +++ b/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php @@ -38,14 +38,14 @@ protected function setUp(): void $this->exporter->reveal(), $this->documentRepository->reveal(), ); - $this->request = new Request(['doc_id' => 17]); + $this->request = new Request(['doc_id' => 17, 'ids_included' => false]); } public function testExportPage_regular_case(): void { $page = $this->prophesize(Page::class); $this->documentRepository->getById(17)->willReturn($page->reveal()); - $this->exporter->export([$page->reveal()], 'yaml')->willReturn('TEST_YAML'); + $this->exporter->export([$page->reveal()], 'yaml', ['includeIds' => false])->willReturn('TEST_YAML'); $response = $this->controller->export($this->request); @@ -59,7 +59,7 @@ public function testExportPage_exceptional_case(): void { $page = $this->prophesize(Page::class); $this->documentRepository->getById(17)->willReturn($page->reveal()); - $this->exporter->export([$page->reveal()], 'yaml')->willThrow(new \Exception('Problem')); + $this->exporter->export([$page->reveal()], 'yaml', ['includeIds' => false])->willThrow(new \Exception('Problem')); $response = $this->controller->export($this->request); diff --git a/tests/Unit/Converter/TypeStrategyConverterTest.php b/tests/Unit/Converter/TypeStrategyConverterTest.php old mode 100644 new mode 100755 diff --git a/tests/Unit/PimcoreConverter/Fixture/TestDataObject.php b/tests/Unit/PimcoreConverter/Fixture/TestDataObject.php old mode 100644 new mode 100755 diff --git a/tests/Unit/PimcoreConverter/Populator/PropertyBasedMappingPopulatorTest.php b/tests/Unit/PimcoreConverter/Populator/PropertyBasedMappingPopulatorTest.php old mode 100644 new mode 100755 diff --git a/tests/app/.env b/tests/app/.env old mode 100644 new mode 100755 diff --git a/tests/app/.gitignore b/tests/app/.gitignore old mode 100644 new mode 100755 diff --git a/tests/app/TestKernel.php b/tests/app/TestKernel.php old mode 100644 new mode 100755 diff --git a/tests/app/config/.gitkeep b/tests/app/config/.gitkeep old mode 100644 new mode 100755 diff --git a/tests/app/config/services.yaml b/tests/app/config/services.yaml old mode 100644 new mode 100755 diff --git a/tests/bootstrap.php b/tests/bootstrap.php old mode 100644 new mode 100755 diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index bda7ff6..c0258db 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -1,3 +1,4 @@ +neusta_pimcore_import_export_exclude_ids_question: 'Es werden keine IDs (id/parentId) exportiert. Die Zuordnung geschieht über Key und Pfad.' neusta_pimcore_import_export_enter_filename: 'Bitte gib einen Dateinamen ein (Standard: Document Key)' neusta_pimcore_import_export_export_menu_label: 'Exportiere in YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Exportiere in YAML mit Kindelementen' diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index 98cc95a..0299908 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -1,3 +1,4 @@ +neusta_pimcore_import_export_exclude_ids_question: 'No IDs (id/parentId) will be exported. The mapping is done via key and path.' neusta_pimcore_import_export_enter_filename: 'Please enter a filename (default: Document Key)' neusta_pimcore_import_export_export_menu_label: 'Export to YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Export to YAML with children' From fa730303c6344ae397254799889a7c1b6a92ad6f Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 00:02:27 +0200 Subject: [PATCH 02/25] [With or Without IDs][Import] import with or without IDs (id, parentsId); new failure --- .../Base/AbstractImportBaseController.php | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Controller/Admin/Base/AbstractImportBaseController.php b/src/Controller/Admin/Base/AbstractImportBaseController.php index dada4c1..844df49 100644 --- a/src/Controller/Admin/Base/AbstractImportBaseController.php +++ b/src/Controller/Admin/Base/AbstractImportBaseController.php @@ -20,6 +20,7 @@ abstract class AbstractImportBaseController public const SUCCESS_ELEMENT_REPLACEMENT = 2; public const SUCCESS_WITHOUT_REPLACEMENT = 3; public const SUCCESS_NEW_ELEMENT = 4; + public const FAILURE_INCONSISTENCY = 5; /** * @var string[] Map of error codes to messages @@ -29,6 +30,7 @@ abstract class AbstractImportBaseController self::SUCCESS_ELEMENT_REPLACEMENT => 'replaced successfully', self::SUCCESS_WITHOUT_REPLACEMENT => 'not replaced', self::SUCCESS_NEW_ELEMENT => 'imported successfully', + self::FAILURE_INCONSISTENCY => 'failed due to inconsistency in the data', ]; protected bool $overwrite = false; @@ -48,6 +50,7 @@ public function __construct( self::SUCCESS_ELEMENT_REPLACEMENT => 0, self::SUCCESS_WITHOUT_REPLACEMENT => 0, self::SUCCESS_NEW_ELEMENT => 0, + self::FAILURE_INCONSISTENCY => 0, ]; } @@ -92,8 +95,20 @@ protected function replaceIfExists(AbstractElement $element): int $oldElement = $this->repository->getByPath('/' . $element->getFullPath()); if (null !== $oldElement) { if ($this->overwrite) { - $oldElement->delete(); - $element->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); + if (null === $element->getId() || $oldElement->getId() === $element->getId()) { + $oldElement->delete(); + $element->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); + } else { + $this->logger->error( + \sprintf('Two pages with same key (%s) and path (%s) but different IDs (new ID: %d, old ID: %d) found. This seems to be an inconsistency of your importing data. Please check your import file.', + $element->getKey(), + $element->getPath(), + $element->getId(), + $oldElement->getId() + )); + + return self::FAILURE_INCONSISTENCY; + } return self::SUCCESS_ELEMENT_REPLACEMENT; } @@ -107,21 +122,16 @@ protected function replaceIfExists(AbstractElement $element): int protected function createResultMessage(): string { - $resultMessage = ''; + $resultMessage = ''; foreach ($this->resultStatistics as $resultCode => $result) { if ($result > 0) { - if (1 === $result) { - $start = 'One ' . $this->elementType; - } else { - $start = \sprintf('%d ' . $this->elementType . 's', $result); - } - $message = \sprintf('%s %s', $start, $this->messagesMap[$resultCode]); - $resultMessage .= $message . '

'; + $resultMessage .= ''; } } - return '

' . $resultMessage . '

'; + return $resultMessage . '
' . $this->elementType . 'Count
'; + $resultMessage .= $this->messagesMap[$resultCode] . '' . $result . '
'; } /** From 1dbc6999c8e5f205e2c6b26bf821811e7493ae01 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 09:15:11 +0200 Subject: [PATCH 03/25] [With or Without IDs][Export][Documents] Add ignore ID checkbox to menu --- config/services.yaml | 17 +++++ public/js/exportDocument.js | 68 +++++++++++++++++-- .../Base/AbstractImportBaseController.php | 3 +- .../PrioritizedAttributesNormalizer.php | 56 +++++++++++++++ src/Serializer/YamlSerializer.php | 3 + translations/admin.de.yaml | 8 ++- translations/admin.en.yaml | 10 ++- 7 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php diff --git a/config/services.yaml b/config/services.yaml index aac9b09..525c298 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -121,6 +121,23 @@ services: Neusta\Pimcore\ImportExportBundle\Serializer\YamlSerializer: ~ Neusta\Pimcore\ImportExportBundle\Serializer\JsonSerializer: ~ + Neusta\Pimcore\ImportExportBundle\Serializer\Normalizer\PrioritizedAttributesNormalizer: + arguments: + $priorities: + - 'type' + - 'id' + - 'parentId' + - 'path' + - 'key' + - 'published' + - 'controller' + - 'language' + - 'navigation_name' + - 'navigation_title' + - 'title' + - 'editables' + tags: [ 'serializer.normalizer' ] + ################# # EventListener # ################# diff --git a/public/js/exportDocument.js b/public/js/exportDocument.js index 352996a..28091f3 100755 --- a/public/js/exportDocument.js +++ b/public/js/exportDocument.js @@ -21,14 +21,72 @@ neusta_pimcore_import_export.plugin.document.export = Class.create({ iconCls: icon, handler: function () { let defaultFilename = document.data.key + '.yaml'; - let includeIds = !confirm(t('neusta_pimcore_import_export_exclude_ids_question')); // Yes = false, No = true - let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); - if (filename) { - pimcore.helpers.download(Routing.generate(route, {doc_id: document.data.id, filename: filename, format: 'yaml', ids_included: includeIds})); - } + let win = Ext.create('Ext.window.Window', { + title: t('neusta_pimcore_import_export_dialog_title'), + modal: true, + width: 400, + layout: 'fit', + items: [{ + xtype: 'form', + bodyPadding: 10, + defaults: { + anchor: '100%', + labelAlign: 'top' + }, + items: [ + { + xtype: 'textfield', + name: 'filename', + fieldLabel: t('neusta_pimcore_import_export_filename_label'), + value: defaultFilename, + allowBlank: false + }, + { + xtype: 'checkbox', + name: 'includeIds', + boxLabel: t('neusta_pimcore_import_export_exclude_ids_label') + + ' ', + inputValue: true + } + ] + }], + buttons: [{ + text: t('neusta_pimcore_import_export_dialog_confirm'), + handler: function () { + let form = win.down('form').getForm(); + if (form.isValid()) { + let values = form.getValues(); + pimcore.helpers.download( + Routing.generate(route, { + doc_id: document.data.id, + filename: values.filename, + format: 'yaml', + ids_included: !!values.includeIds + }) + ); + win.close(); + } + } + }, { + text: t('neusta_pimcore_import_export_dialog_cancel'), + handler: function () { + win.close(); + } + }] + }); + + win.show(); } })); + + // let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); + // if (filename) { + // pimcore.helpers.download(Routing.generate(route, {doc_id: document.data.id, filename: filename, format: 'yaml', ids_included: includeIds})); + // } + // } + // })); } }); diff --git a/src/Controller/Admin/Base/AbstractImportBaseController.php b/src/Controller/Admin/Base/AbstractImportBaseController.php index 844df49..f6ea049 100644 --- a/src/Controller/Admin/Base/AbstractImportBaseController.php +++ b/src/Controller/Admin/Base/AbstractImportBaseController.php @@ -100,7 +100,8 @@ protected function replaceIfExists(AbstractElement $element): int $element->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); } else { $this->logger->error( - \sprintf('Two pages with same key (%s) and path (%s) but different IDs (new ID: %d, old ID: %d) found. This seems to be an inconsistency of your importing data. Please check your import file.', + \sprintf('Two %ss with same key (%s) and path (%s) but different IDs (new ID: %d, old ID: %d) found. This seems to be an inconsistency of your importing data. Please check your import file.', + strtolower($this->elementType), $element->getKey(), $element->getPath(), $element->getId(), diff --git a/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php new file mode 100644 index 0000000..03a5c38 --- /dev/null +++ b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php @@ -0,0 +1,56 @@ + $priorities */ + private array $priorities; + + /** + * @param array $priorities + */ + public function __construct(ObjectNormalizer $normalizer, array $priorities = []) + { + $this->normalizer = $normalizer; + $this->priorities = $priorities; // z.B.: ['type', 'id', 'parentId', 'path', ...] + } + + /** + * @param object $object + * @param string|null $format + * @param array $context + * + * @return array + */ + public function normalize($object, ?string $format = null, array $context = []) + { + $data = $this->normalizer->normalize($object, $format, $context); + + if (!\is_array($data)) { + return $data; + } + + $sorted = []; + + foreach ($this->priorities as $key) { + if (\array_key_exists($key, $data)) { + $sorted[$key] = $data[$key]; + unset($data[$key]); + } + } + + // Hänge alle restlichen Properties hinten dran + return array_merge($sorted, $data); + } + + public function supportsNormalization($data, ?string $format = null, array $context = []): bool + { + return \is_object($data) && $this->normalizer->supportsNormalization($data, $format, $context); + } +} diff --git a/src/Serializer/YamlSerializer.php b/src/Serializer/YamlSerializer.php index 8b07511..dbc42f0 100644 --- a/src/Serializer/YamlSerializer.php +++ b/src/Serializer/YamlSerializer.php @@ -23,6 +23,9 @@ public function serialize(mixed $data, string $format): string 'yaml_inline' => 6, 'yaml_indent' => 0, 'yaml_flags' => self::YAML_DUMP_FLAGS, + 'attributes' => [ + 'type', 'id', 'parentId', 'path', 'language', 'key', 'published', 'controller', 'navigation_name', 'navigation_title', 'title', 'editables', + ], ]); } diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index c0258db..1ee5471 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -1,5 +1,9 @@ -neusta_pimcore_import_export_exclude_ids_question: 'Es werden keine IDs (id/parentId) exportiert. Die Zuordnung geschieht über Key und Pfad.' -neusta_pimcore_import_export_enter_filename: 'Bitte gib einen Dateinamen ein (Standard: Document Key)' +neusta_pimcore_import_export_dialog_title: 'Export Einstellungen' +neusta_pimcore_import_export_dialog_confirm: 'Export ausführen' +neusta_pimcore_import_export_dialog_cancel: 'Abbrechen' +neusta_pimcore_import_export_filename_label: 'Bitte gib einen Dateinamen ein (Standard: Document Key)' +neusta_pimcore_import_export_exclude_ids_label: 'Keine IDs exportieren' +neusta_pimcore_import_export_exclude_ids_info: 'Es werden keine IDs (id/parentId) exportiert. Die Zuordnung geschieht über Key und Pfad.
Um IDs zu exportieren, aktiviere bitte diese Option.' neusta_pimcore_import_export_export_menu_label: 'Exportiere in YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Exportiere in YAML mit Kindelementen' neusta_pimcore_import_export_import_dialog_title: 'Importiere Seite aus YAML' diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index 0299908..384392a 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -1,8 +1,12 @@ -neusta_pimcore_import_export_exclude_ids_question: 'No IDs (id/parentId) will be exported. The mapping is done via key and path.' -neusta_pimcore_import_export_enter_filename: 'Please enter a filename (default: Document Key)' +neusta_pimcore_import_export_dialog_title: 'Export Settings' +neusta_pimcore_import_export_dialog_confirm: 'execute Export' +neusta_pimcore_import_export_dialog_cancel: 'cancel Export' +neusta_pimcore_import_export_filename_label: 'Please fill in filename (default: Document Key)' +neusta_pimcore_import_export_exclude_ids_label: 'No exported IDs' +neusta_pimcore_import_export_exclude_ids_info: 'No IDs (id/parentId) will be exported. The mapping is based on key and path.
To export IDs, please enable this option.' neusta_pimcore_import_export_export_menu_label: 'Export to YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Export to YAML with children' -neusta_pimcore_import_export_import_dialog_title: 'Import Page from YAML' +neusta_pimcore_import_export_import_dialog_title: 'Import page from YAML' neusta_pimcore_import_export_import_menu_label: 'Import (YAML)' neusta_pimcore_import_export_import_menu_label_asset: 'Import Assets' neusta_pimcore_import_export_import_menu_label_document: 'Import Documents' From 097669f78e843ddfd41476473db72998ad3d620c Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 10:05:49 +0200 Subject: [PATCH 04/25] [With or Without IDs][Export] manage sequence of exported attributes --- .../Normalizer/PrioritizedAttributesNormalizer.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php index 03a5c38..f73ccb8 100644 --- a/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php +++ b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php @@ -9,7 +9,7 @@ class PrioritizedAttributesNormalizer implements NormalizerInterface { private ObjectNormalizer $normalizer; - /** @var array $priorities */ + /** @var array */ private array $priorities; /** @@ -22,11 +22,8 @@ public function __construct(ObjectNormalizer $normalizer, array $priorities = [] } /** - * @param object $object - * @param string|null $format + * @param object $object * @param array $context - * - * @return array */ public function normalize($object, ?string $format = null, array $context = []) { @@ -49,8 +46,8 @@ public function normalize($object, ?string $format = null, array $context = []) return array_merge($sorted, $data); } - public function supportsNormalization($data, ?string $format = null, array $context = []): bool + public function supportsNormalization($data, ?string $format = null): bool { - return \is_object($data) && $this->normalizer->supportsNormalization($data, $format, $context); + return \is_object($data) && $this->normalizer->supportsNormalization($data, $format); } } From e1a82bdc39fe0c176e820e93eb07c37e30345c93 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 10:18:01 +0200 Subject: [PATCH 05/25] [With or Without IDs][Export] export menu adapted for Assets & DataObjects --- public/js/exportAsset.js | 60 +++++++++++++++++++++++++++++++--- public/js/exportDataObjects.js | 60 +++++++++++++++++++++++++++++++--- public/js/exportDocument.js | 7 ---- 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/public/js/exportAsset.js b/public/js/exportAsset.js index 444a67c..5c46cd7 100644 --- a/public/js/exportAsset.js +++ b/public/js/exportAsset.js @@ -23,10 +23,62 @@ neusta_pimcore_import_export.plugin.asset.export = Class.create({ let defaultFilename = asset.data.key + '.yaml'; let includeIds = !confirm(t('neusta_pimcore_import_export_exclude_ids_question')); // Yes = false, No = true - let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); - if (filename) { - pimcore.helpers.download(Routing.generate(route, {asset_id: asset.data.id, filename: filename, format: 'yaml', ids_included: includeIds})); - } + let win = Ext.create('Ext.window.Window', { + title: t('neusta_pimcore_import_export_dialog_title'), + modal: true, + width: 400, + layout: 'fit', + items: [{ + xtype: 'form', + bodyPadding: 10, + defaults: { + anchor: '100%', + labelAlign: 'top' + }, + items: [ + { + xtype: 'textfield', + name: 'filename', + fieldLabel: t('neusta_pimcore_import_export_filename_label'), + value: defaultFilename, + allowBlank: false + }, + { + xtype: 'checkbox', + name: 'includeIds', + boxLabel: t('neusta_pimcore_import_export_exclude_ids_label') + + ' ', + inputValue: true + } + ] + }], + buttons: [{ + text: t('neusta_pimcore_import_export_dialog_confirm'), + handler: function () { + let form = win.down('form').getForm(); + if (form.isValid()) { + let values = form.getValues(); + pimcore.helpers.download( + Routing.generate(route, { + asset_id: asset.data.id, + filename: values.filename, + format: 'yaml', + ids_included: !!values.includeIds + }) + ); + win.close(); + } + } + }, { + text: t('neusta_pimcore_import_export_dialog_cancel'), + handler: function () { + win.close(); + } + }] + }); + + win.show(); } })); } diff --git a/public/js/exportDataObjects.js b/public/js/exportDataObjects.js index 2ca6fd9..eeff148 100644 --- a/public/js/exportDataObjects.js +++ b/public/js/exportDataObjects.js @@ -23,10 +23,62 @@ neusta_pimcore_import_export.plugin.object.export = Class.create({ let defaultFilename = object.data.key + '.yaml'; let includeIds = !confirm(t('neusta_pimcore_import_export_exclude_ids_question')); // Yes = false, No = true - let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); - if (filename) { - pimcore.helpers.download(Routing.generate(route, {object_id: object.data.id, filename: filename, format: 'yaml', ids_included: includeIds})); - } + let win = Ext.create('Ext.window.Window', { + title: t('neusta_pimcore_import_export_dialog_title'), + modal: true, + width: 400, + layout: 'fit', + items: [{ + xtype: 'form', + bodyPadding: 10, + defaults: { + anchor: '100%', + labelAlign: 'top' + }, + items: [ + { + xtype: 'textfield', + name: 'filename', + fieldLabel: t('neusta_pimcore_import_export_filename_label'), + value: defaultFilename, + allowBlank: false + }, + { + xtype: 'checkbox', + name: 'includeIds', + boxLabel: t('neusta_pimcore_import_export_exclude_ids_label') + + ' ', + inputValue: true + } + ] + }], + buttons: [{ + text: t('neusta_pimcore_import_export_dialog_confirm'), + handler: function () { + let form = win.down('form').getForm(); + if (form.isValid()) { + let values = form.getValues(); + pimcore.helpers.download( + Routing.generate(route, { + object_id: object.data.id, + filename: values.filename, + format: 'yaml', + ids_included: !!values.includeIds + }) + ); + win.close(); + } + } + }, { + text: t('neusta_pimcore_import_export_dialog_cancel'), + handler: function () { + win.close(); + } + }] + }); + + win.show(); } })); } diff --git a/public/js/exportDocument.js b/public/js/exportDocument.js index 28091f3..eb46c60 100755 --- a/public/js/exportDocument.js +++ b/public/js/exportDocument.js @@ -80,13 +80,6 @@ neusta_pimcore_import_export.plugin.document.export = Class.create({ win.show(); } })); - - // let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); - // if (filename) { - // pimcore.helpers.download(Routing.generate(route, {doc_id: document.data.id, filename: filename, format: 'yaml', ids_included: includeIds})); - // } - // } - // })); } }); From 36bc9c2308b552fbe604b65164cae82efb7ac1f1 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 10:21:41 +0200 Subject: [PATCH 06/25] [With or Without IDs][Export] doc adapted --- docs/images/filename_dialog.png | Bin 8979 -> 11434 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/filename_dialog.png b/docs/images/filename_dialog.png index 7fcca1d6626822dcec5863eabfbfca13e8698670..b19f505124381185fbf8992dde80051e984797d8 100644 GIT binary patch literal 11434 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 M1r2$KEHLbU0Xpw(uK)l5 literal 8979 zcmcI~WmKDAvu}V>TC@~`OMy~cinllgQmlBOXbHuF6!!ov?$F{AC=Nl3JEa6IZpDkc z2G8QsDvs00M=#vKjyY1_J#Z@#rD? z^N)Gu6ac_DtspC{FZjZ_gPlw+;>#1WrTc{BJ zcpE{ULYd6djvrbvADeR`u*_6aiuoSvq~;9uJY9Ve@l=&VEIDkGbhk#9lB}tjbm4?6 ziY){ngt7FOBAAqhmF)Ri{XPoLbg?lW>m0>7(=~F&kNUlFJ#^Kw4jc4nU5Cxnud6;n z3uiClo3)320@5CKpvCuF{;Z3EUKM-7fnJNkfnWmwZHH3D=pQ0@#Xa)uCW+8&^|aJe9~G1-BqOTFa-aeaq)|Y{03Kg+8!lp^ zgubynK`82elg_J*7uz!rQ<1@B;kWr~!x^frDj;PPooYZpFgEm;F|kYORtit<>y5ze z7h$PGztq|OsbI?h&!;BP@UxYHA=Vn-AFRqcjI5<5PdnHp;hVWEODwhFeM(7va(2hWroZUhSlO>%dmbROO`uk%A=GULL!?8h!{!XQ zTx|ZC@zavp$_cu!@JfDTlD>P+8XKaioRF;3WM9`!9i}<$xTfmn@!5fIZ2U#<5Op5+ zdkUEc{#j{;Mz~e-tYqQpoFnqz*UWLPS%#eBG7P0F{O8IMXEbA&n!Y?*B-;>Xx)^Wq zcbXkM^I&h{!@dXzrKZiDmO2QsYMWi!)J(Y*9o)?{&j+(LBt!W&^x>^^=M*598GW_N z3UMG~oO%SeAr~YCFHcr2bkue}2=8o(<5aheE#QrR#DoZPa@;snXvj(RQA}M=^sh#T zH6u^0@tx&YQ=?N;cjf`rVK1!f*Dl|xrcGl?9EIG~8Q2}2r?CA=8TzF)nrkZ@QvPP@ z&fS~TQc0~rSvGexuk+h?xGo;{H6vlIQcF*Lvk$J^Yvh%^MDDL@x+r2)a2#dc$B>0i z|G1%X&m4Z`eu-%3#JX9!qhQ9kUTlfjqasdK#r!VL2tYOSYc`;5uHSivbMRq{m~gfH z=5b1(VUh&DVzB;uiGuE1s>F<}_qK|lZl2qjG$4;=SYpPFw~$hH!TC26+{;%qyVLq# zBpiGr@f7RbM;_Y|Ko12DP%|{++bMh|7M`plJ8VzF}CN%i$dXzG!uSB-UR- z8X2DFBPMUVljU5ik?$H*uN!Kgs?SIo-o-{n7^@oa)1NyGXmC*l4DF)MdCSQ?3bb0h zThZYL|KcK#QalXS@)C6st}A5Dl_cDJ5SaZ^u^jPyTE?fgF4XCUgM_WD_bA8N^M^?Q zYx`w{F$C5bmVRgTve|4j|AU~x%tdBQ)Okm^_Gt^*$A*G_ z4t82M{=jcmZ;)m=?icU-)<>*p-qnPVh1H!(rMlnB&O7C9n+?CL^5N_xS#$rRO}_v2 zLQ80eczA@=M9_WOxz>`w749EfD)gSn{rDWRm{~)sxW?{X`e{W%eZjt^!&E(XkJ|wV z?tnJz&pkbB$8T%Ur5qEn-t%+E>J?|$!m=`QXFj_!)Y$Cm3lYtGV~b%`*pYsr zqk^%X3V5-Ot7afC5__aET589?TE;9X{=y{UIooq~u8d(?p(ojXeeRL*If-nEEsE`E zPL78kS;yN=)>iwxU6qA*|kQrbtLFI_5M&E zE~U|N`Op0JJAv#a$#T~rcd2m6 z>!8-)5cH~}Oy1qm=11sul1@p|Bx0|Smysmn4>AoalfcP(g)omO-l@gFPJ3r8nIX)d zQXsd!O8+P+MK1*g>f}E&v0&n)s5?7^CjLJEgOl$ zqcI4+bk*yQn@xi4f$gvnggPG#7>Ka5j*DP9%LvdY?e zlD~JY^5OG%o=j_mQ_=B5};cw#&?VHf$Ar%Ed;DcsJsN%jMN??GrI9yvgBfc9R zH=#_#TAyf3I4F7BZPA3zN=#`pecS^+%Z++?O&BVJCQJw7lg30 z@Q0ESrPlpqC!BmvL!v02bL|Rs_QiLr;7NlW(A0*D(Vea)5+lM!O3|dftqCLdTB(|{ zTD>(T3T$3#ojNLJZJugP=-v)Pd#_SDJ|w@}S1U+Vu(nP#30Tdz+O9O+LViA(mz$Kie>(gxW2|utq zgP>{H&R~mpfM{WUKFh+0+?p(@K%yY~Q$(x{M~Q4-(Z}gA`*GXIrqui_dzJQ=-_PH? zp0D7X<8_>mdb0T;;Ay%c-`;>_+rgUXze3P@#ZYPLI4RI%@pBn#GZizcA?tqs*TW}` z7;_^)tp@nSfc2A4v*~>NVR6QC7X~Z0+ImzTUo=d_654D0R@XCi5elE)D+n8wR$GwO zcU!pjg(f~QHPi-H6m8IrcDLq-&EoL~JPy_N<&-LYNL1aMFVoLyTlK-jSF_?~C)>40 znourH^F@fv38kpD;7+)b7d+H{E5GXD5H1x%8yS z@8|!O#<{Gqy3mdrgi=*|uS=Mk>U@G{YEeYzmV0S)NWmN_rA4uqaK2)Cr2AEtL69WkMd?2B`5t zyixx#0InXGdDN6FC?2Qf>oGL0&-!YyXohNasi#|U9#n5P4(GjS>K}U=)vI1|ZBT;^ z`w?*?L;Is5HR0Z=Nu4{7Ue~q;GG11L^znc7aYS@!`~ico^gIY3Fs0LE^nDcgz^GGC zl&>fE9%U)@?wiEcJ6}3J#^$4>TVLCcKMv&%fjvF%om4up zV%H4o#^f5kyEg{z(U9t4P&TkSrF6&p!LIk_8}d7=&>|EFmn~gLkj_Q_O|u7u*CI42 z(DeDIWJ-rf$K>Q^Jp7s>9UhtdG7PVL-=u9XT~JCKh2wBfDi}NbD_F*?+0yAMd5gsl zBASn(1)Kc8jP9dl%AIevY^yhtrVrm)qnIl$^;cYEtqT36_^l-Bs4`=4&5C9(bbXDs z$A0CjJaMm)5A)$iHcFwv%A-c5fyy5!8eYA-6p6&1<$Sd|dw9IiLQnoUy~iNdARdWJ zY#I;qmI5~s95t0dduiudqUpU)R!?q@na}|>|Jm=L`ZGoc0R0Rvw|wQ%SC5HNAa35I zOOTUA8hZ3m#;BI}-EfVcJIY>DjqnnX*@;fW-6ce(;AjlBU%%Ze6uTO0nNpZE2Xv4HGXCDFh11WjHXtWHa|35)906;_ZFQn~|qfr&}MWG)(_gYDXPQc`~V0R55^7@{3?X)1jr;Lb|1Uv~MGhw?VZ&jk4ieO6nIF2QsC z?rld+r;pbp59HAx<3u~W83P)5wxL9CR)FYdlEh6s3%!h}`R(h@R6)+$R|i4}t) zWIAR7>G8K?GroX9^yns~K&Lny7!EQ|dOdSq+5O_nI6ZUgHC29S{g=2QRXP)$52vHE zmx-#WtHMQH|Fk{45Y^(91PL3%N8o4X=KzBmGvHnh91izZI*Z2B&kLV!;yw!WI`+^G zWV#a&&KyohcTkz|LaAa3m`u4PL$!;!8LJ9$YxpeL+S^9 zJhUng*-1I1^Mc!D(x?J9o}UorFKXd=P#dfeRA~q#WXdwWt9YBVB;Nb%48tz4BD{PE zyMUnm@6GCSA^38G2iB+i@*(cJ&k&BOqvDoAhwG+K>6;s_w&ii-mkibS_D}CiX+5UHW%U}=_>-;f=1D#Hb@Ox5J%Cx@;aCPZM4W{D&O86CuHYU>jAVOjG)(B1Z^b zAH?Mbl{!WyscP&|z$<-EEUKN87fS9An`AT=jL)2uJhEgYsNl(pyy=Z5Y6xrTKnw4b9ud{PpoS%xrAn zyR#K~vrJ?CaU}~$`EYa>-PR$(@1A`7ZSU#MYY%O4gSf8{;C0V?%D)TEo{InQ(CTRL zm0+u0y%HTL3=Y%KC~iuv1}rGPCwhWDfE77lY)$apE2C!q2|Jqo>Zj;lfNDlC9{Qz} zi^#>ISs@p}f@BdLyP`rxZ&WloT>y$g4PpP-J;`xrOKH4gYom?A<|#fp0}+c}wo4V$ z*rB}^{%4r{A!Lev>2OXPMo@HCkqE|Jq225+6nB|wj1UJIf)1+Y8j zZ%*m9w5r^h`sB^>UMK%e%nzxyn1Sjw@s;Wv1zX)1My||_SU8qe5}?Ng1vvj=mrHe@ z<fOYZqj%p9Ra(=%*r#p!MAvV8E3msmM@0YMn#onX z1`YB&1s)wu7KnQ!y>?}>>}M>EA=h~J^~n{X`c>4)3J`eJOk9Fw;#h0^aieV0Rp4U6 z)W6`aOH8OD=2~iUsH@7eCn4{3N}qRnidC;Tr(C=ynVcn#b7fgp4(qDsJ+*nSSUE>D zWcrCFt3woL11T6Da4se_Nd16`MVWTna?e(2bIO0&synPu2mlFrO3Sw8zRt`1RRLImV6DRUM_>mi3e~=phwvb^J~$xSt}tsa;W)y{v*8;yhpk&s^AWkU@`iPWMthwLn2w)*UPq1#sZ-1g|Xla%-eka3a%5Rttv20}$b zo^uZNPx^+?HTuiRA+8&z1MBK1d&2bLiB8Mn$~>oG%Z(Y?UV-kv4eL_qW?txN|ET`> z0$;8JdMEk#Wse(?x$W&Y^OAa{r~0k59a6e-tMwhm_u@r1ExdP(`241vxtU<1`G}p!Yb5GC;_DYx%uVcWL+Un)mMIC+Kx(j)_UeNUcmp1-qz%q(bWpvLC-$+b(8~@PHzBiR=={eZCTN3L zkt4Ekj7dszv8(c!(3Jz36Z8|LWEo@Uay6qlcE?E)6|O+#W)2nXRp}8{+*9H zF(AzBJ8tm23$ZFMl_J@2ix4B2H=AI6waMSgxp2$P<;T;eG#(bDL3^a)HQo&Us}gKy zGJ2eP@_K(RhfW&nd=;qQ?GrQ=-VGWQyMK6iCb$}HK+{LB$TM2yx5i@t;z?!m6Ss({QY+HWOn@D5Sm9{Fh>drv*szm?B}-Vay-y2#R&e`c)8uUt>C@0ULG zRv>toky|y@T^~PKJOqusL6b?zdE5KQLh-C4v=zg~f0X2#`8D3br2bt{X08SmY~SCu zzC-*|P~e*saTpR;qY@|gB}I)`e_B<+_HpY?g=YaPn(@wo%OwgFrMBF_bbQESkBu&T zNfh*k6yNn*eFN+~&L5>yw|uALMi&@}(KRwgt8HKG2JU%3F)l}nOYX#`{IDIwK2cNX z4I3vnm0j@M3c$Zcm&N0s7kyi7_p&PJit4`6RT3* zNJ+^92AIf$$!BOzIyVOE=m<dcMsbs3l<06Q;Fq9gp1M@yIMJ)sd}Devo&P0OIP~`9E^??A{#sqFaI&GF ze6s26AS8vEhSc?WYY-2g+ZiQn zAYXA6b0ZI8%JT93Io+W@+j}X%)9!Q$w4HWOZ^jpL@z_mut(HA>)7)I|g*82Z5ITkH$k~db%Bvlugp1176-BEFs_4G=RVPz1)^#^gL$ig0F2joCIHVrJ8&%KnvTWvg_15eSLTtDc)?-^|Vr z+JJ+`Me;g#8Za5afUzf78DQz+(=JOmX&&%+w`4AEYx3;OIA<;ts|qo>A~^~j2O?0u ztUTcFI)gy17EKf5xl6Y6c(yv}{k_*J3uA$6+o=P?gAyaM*2iNY0iH8@?P4F*g^O{) zuc*x6JE@;yCaHQ?SHcgg0^UEkSI1%PTYK*$JaUKwK_ACe^r?8C`_%`AlZig(^*N4@ z;oC})L>kLKN{)XUWry1b%zM64UN|kwdhaBlvy`}2oFzJWgSRUIr=R%T+?pAj5Q-8h ze#K!qa0WR}SWLt6n$KhQ;o^1$%}=_!}wEdMRx;R+>phy`jJ5Jk68W6w_SJNCZAjk&KCe z7JOC!wB13K>pnaW}u?lS`1tDBw{Zp)zQRi0t?88E%%XG2)1_;ta zH%e4k4)t z66gLhbOa>oY~7Gw){q9N9Dfee4zS^7s0 zdc^E`-4t_6$Zu!Ih_!^#O=s6mN73gnqmV7H^qozLOLZOS68sYhH8>U&Y6<+E1DIMR zWUN)LlB0&)1O&5{T5tSXiuU`$;yWp5FIEK}m4rIbO=H}9`8&9&rUbKM=|Tm^6J&Wd z5#xLUyE@qc;FDB9)JM+(Ps4ZjYO8N68oTs0JQBnu?#w(_|31rYm>L-2k1ppe9n$w5 z2XfRn76A9-{yac#jcDKCwdj-$6XH}A|Jecj;}ZM6qPu5YiKPwNDM z`s>~Yt);@zKRv(ZwVii~F;cgwrAgK`tXLA`1dS5S^Avca$JbRah1;@JKfR)~`2{+r zE|e4Au+Z+3$+?C;Delg;UvpN6r3styZ)}= z%_On~)W!0^SdoY#s9v??Kb|~XM6&;`Tw+*P4ctg5ZY(*JP7(CQ?jHdi3D)FpTAuN2 zyql^$dDidoT)A)3HuuVVIz-9~A}EhDk2}gaSHc3qx_om?Fbwg^^SZ7hRV4#?Tt7~6 zI`cn{beZ!^%06zLGy^dOu?k54ytmr{%V_1+8&`Oog9dLIB=*W@C6`#Q_2 zvn*s``YGTcn!6X%F|^ed>ct|ZT8#2a?h*-U7R&+NZA16qB1BWs;mb*wa)4KZ>JrV_FF`y7TD~! zfy5jpC|9Fl+5Jo4H<1?$w--965-vpd;rf?nz9^pOH~js3>X=q{1h|FQ%XN`&;^BkL z(!y5lNrZ*23k-^nD8&L4ZQvG5qKi)NaRa7Bjl=vD8m3ASeBr^K5gydLxU)ka`NsPv7e0JgjL)(x^ zcVox+H}KHYlKdSmBkTKo9LeXIjoZgF{@H=f!AQGze=urfGSBoPm(n~91R7j5516`# z8w5^fq;IDBrs5+~tWT%hR`@S_-vj~$l=%lzeXnkSf-^SvS+a9cp%jWuF#5aq!3V^_ zbDiq1L00B&nB|mcLALx5lywgxJ3W4SXxN_8H)AGd{uJSP9j#I<7k{r`ddx-IiWFw` zQmOE=_P9rGbkEYt1$xfJON;m++UKtAVzim9caM~q?B>U2znq8=ZQn@u?{<0cPDRYu zDU>y%9@oR)l4~th+f~x5jv`ubW!Izy@9TfQx97==^ul%)ximZPNZ&fWxv){f3K;pO zZ=T-fZiTB||{9#kI) zs>pHlvd@nERrkL;KkYi+IORWH5YEUcE0fSu+Z3JqH!vh?$%EEr{YE3I|Ls3K{x_%> ben*6*{l^?YkS2?U)&K>$ce152AHMt-JEI%0 From 18a0acea0f946e7b282b614c8cd0a31618bedf00 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 10:38:37 +0200 Subject: [PATCH 07/25] [With or Without IDs][Export] label for checkbox corrected --- translations/admin.de.yaml | 4 ++-- translations/admin.en.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index 1ee5471..aa2dfd1 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -2,8 +2,8 @@ neusta_pimcore_import_export_dialog_title: 'Export Einstellungen' neusta_pimcore_import_export_dialog_confirm: 'Export ausführen' neusta_pimcore_import_export_dialog_cancel: 'Abbrechen' neusta_pimcore_import_export_filename_label: 'Bitte gib einen Dateinamen ein (Standard: Document Key)' -neusta_pimcore_import_export_exclude_ids_label: 'Keine IDs exportieren' -neusta_pimcore_import_export_exclude_ids_info: 'Es werden keine IDs (id/parentId) exportiert. Die Zuordnung geschieht über Key und Pfad.
Um IDs zu exportieren, aktiviere bitte diese Option.' +neusta_pimcore_import_export_exclude_ids_label: 'Exportiere mit IDs' +neusta_pimcore_import_export_exclude_ids_info: 'Standardmäßig werden keine IDs (id/parentId) exportiert. Die Zuordnung geschieht über Key und Pfad.
Um IDs zu exportieren, aktiviere bitte diese Option.' neusta_pimcore_import_export_export_menu_label: 'Exportiere in YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Exportiere in YAML mit Kindelementen' neusta_pimcore_import_export_import_dialog_title: 'Importiere Seite aus YAML' diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index 384392a..57f0294 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -2,8 +2,8 @@ neusta_pimcore_import_export_dialog_title: 'Export Settings' neusta_pimcore_import_export_dialog_confirm: 'execute Export' neusta_pimcore_import_export_dialog_cancel: 'cancel Export' neusta_pimcore_import_export_filename_label: 'Please fill in filename (default: Document Key)' -neusta_pimcore_import_export_exclude_ids_label: 'No exported IDs' -neusta_pimcore_import_export_exclude_ids_info: 'No IDs (id/parentId) will be exported. The mapping is based on key and path.
To export IDs, please enable this option.' +neusta_pimcore_import_export_exclude_ids_label: 'Export with IDs' +neusta_pimcore_import_export_exclude_ids_info: 'By default no IDs (id/parentId) will be exported. The mapping is based on key and path.
To export IDs, please enable this option.' neusta_pimcore_import_export_export_menu_label: 'Export to YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Export to YAML with children' neusta_pimcore_import_export_import_dialog_title: 'Import page from YAML' From dcf9c6efa699e109f0acdb843624620908ca0b18 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 15:00:06 +0200 Subject: [PATCH 08/25] [With or Without IDs][Import] use ApplicationLogger for errors and infos --- .../Base/AbstractImportBaseController.php | 60 +++++++++++++------ .../Admin/ImportAssetsController.php | 11 ++-- .../Admin/ImportDataObjectsController.php | 11 ++-- .../Admin/ImportDocumentsController.php | 8 ++- src/Import/Importer.php | 2 +- 5 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/Controller/Admin/Base/AbstractImportBaseController.php b/src/Controller/Admin/Base/AbstractImportBaseController.php index f6ea049..77f5bf9 100644 --- a/src/Controller/Admin/Base/AbstractImportBaseController.php +++ b/src/Controller/Admin/Base/AbstractImportBaseController.php @@ -3,10 +3,11 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base; use Neusta\ConverterBundle\Exception\ConverterException; +use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ImportRepositoryInterface; +use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Pimcore\Model\Element\AbstractElement; use Pimcore\Model\Element\DuplicateFullPathException; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -42,8 +43,9 @@ abstract class AbstractImportBaseController * @param ImportRepositoryInterface $repository */ public function __construct( - protected LoggerInterface $logger, + protected ApplicationLogger $applicationLogger, protected ImportRepositoryInterface $repository, + protected ParentRelationResolver $parentRelationResolver, protected string $elementType = 'Element', ) { $this->resultStatistics = [ @@ -75,7 +77,7 @@ public function import(Request $request): JsonResponse try { $this->cleanUp(); } catch (\Throwable $cleanupError) { - $this->logger->warning($cleanupError->getMessage()); + $this->applicationLogger->warning($cleanupError->getMessage()); } } @@ -92,32 +94,39 @@ protected function createJsonResponse(bool $success, string $message, int $statu */ protected function replaceIfExists(AbstractElement $element): int { - $oldElement = $this->repository->getByPath('/' . $element->getFullPath()); + $oldElement = $this->repository->getByPath($element->getFullPath()); if (null !== $oldElement) { if ($this->overwrite) { - if (null === $element->getId() || $oldElement->getId() === $element->getId()) { + if (0 === $element->getId() || null === $element->getId() || $oldElement->getId() === $element->getId()) { $oldElement->delete(); + $this->parentRelationResolver->resolve($element); $element->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); - } else { - $this->logger->error( - \sprintf('Two %ss with same key (%s) and path (%s) but different IDs (new ID: %d, old ID: %d) found. This seems to be an inconsistency of your importing data. Please check your import file.', - strtolower($this->elementType), - $element->getKey(), - $element->getPath(), - $element->getId(), - $oldElement->getId() - )); - - return self::FAILURE_INCONSISTENCY; + $this->writeApplicationLog('[REPLACE] ', $element, $oldElement); + + return self::SUCCESS_ELEMENT_REPLACEMENT; } - return self::SUCCESS_ELEMENT_REPLACEMENT; + $this->applicationLogger->error(\sprintf('Two %ss with same key (%s) and path (%s) but different IDs (new ID: %d, old ID: %d) found. This seems to be an inconsistency of your importing data. Please check your import file.', + strtolower($this->elementType), + $element->getKey(), + $element->getPath(), + $element->getId(), + $oldElement->getId() + )); + + return self::FAILURE_INCONSISTENCY; } + $this->writeApplicationLog('[SKIP] ', $element, $oldElement); + return self::SUCCESS_WITHOUT_REPLACEMENT; } + + $this->parentRelationResolver->resolve($element); $element->save(['versionNote' => 'added by pimcore-import-export-bundle']); + $this->writeApplicationLog('[NEW] ', $element, null); + return self::SUCCESS_NEW_ELEMENT; } @@ -147,4 +156,21 @@ protected function cleanUp(): void { // implement clean ups in subclasses if necessary } + + private function writeApplicationLog(string $prefix, AbstractElement $newElement, ?AbstractElement $oldElement = null): void + { + $this->applicationLogger->info( + <<elementType} + key: {$newElement->getKey()} + path: {$newElement->getPath()} + id: {$newElement->getId()} + MESSAGE, + [ + 'relatedObject' => $oldElement, + 'component' => 'Pimcore Import Export Bundle', + ] + ); + } } diff --git a/src/Controller/Admin/ImportAssetsController.php b/src/Controller/Admin/ImportAssetsController.php index 8dcee04..d542252 100644 --- a/src/Controller/Admin/ImportAssetsController.php +++ b/src/Controller/Admin/ImportAssetsController.php @@ -4,10 +4,11 @@ use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; use Neusta\Pimcore\ImportExportBundle\Import\Importer; +use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Import\ZipImporter; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository; +use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Pimcore\Model\Asset; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -22,12 +23,13 @@ final class ImportAssetsController extends AbstractImportBaseController * @param Importer<\ArrayObject, Asset> $importer */ public function __construct( - LoggerInterface $logger, + ApplicationLogger $applicationLogger, private Importer $importer, + ParentRelationResolver $parentRelationResolver, private ZipImporter $zipImporter, AssetRepository $assetRepository, ) { - parent::__construct($logger, $assetRepository, 'Asset'); + parent::__construct($applicationLogger, $assetRepository, $parentRelationResolver, 'Asset'); } #[Route( @@ -47,7 +49,7 @@ protected function importByFile(UploadedFile $file, string $format): array // if file is ZIP add physical files to Assets if ('zip' === $extension) { $zipContent = $this->zipImporter->import($file->getPathname()); - $assets = $this->importer->import($zipContent['yaml'], $format); + $assets = $this->importer->import($zipContent['yaml'], $format, true); foreach ($assets as $asset) { if ( \array_key_exists($asset->getType(), $zipContent) @@ -67,6 +69,7 @@ protected function importByFile(UploadedFile $file, string $format): array return $this->importer->import($content, $format); } catch (\Exception $e) { + $this->applicationLogger->error($e->getMessage()); throw new \Exception('Error reading uploaded file: ' . $e->getMessage(), 0, $e); } } diff --git a/src/Controller/Admin/ImportDataObjectsController.php b/src/Controller/Admin/ImportDataObjectsController.php index 7af4034..18548b7 100644 --- a/src/Controller/Admin/ImportDataObjectsController.php +++ b/src/Controller/Admin/ImportDataObjectsController.php @@ -4,9 +4,10 @@ use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; use Neusta\Pimcore\ImportExportBundle\Import\Importer; +use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository; +use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Pimcore\Model\DataObject; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -21,11 +22,12 @@ final class ImportDataObjectsController extends AbstractImportBaseController * @param Importer<\ArrayObject, DataObject> $importer */ public function __construct( - LoggerInterface $logger, + ApplicationLogger $applicationLogger, DataObjectRepository $repository, + ParentRelationResolver $parentRelationResolver, private Importer $importer, ) { - parent::__construct($logger, $repository, 'DataObject'); + parent::__construct($applicationLogger, $repository, $parentRelationResolver, 'DataObject'); } #[Route( @@ -43,8 +45,9 @@ protected function importByFile(UploadedFile $file, string $format): array try { $content = $file->getContent(); - return $this->importer->import($content, $format); + return $this->importer->import($content, $format, true); } catch (\Exception $e) { + $this->applicationLogger->error($e->getMessage()); throw new \Exception('Error reading uploaded file: ' . $e->getMessage(), 0, $e); } } diff --git a/src/Controller/Admin/ImportDocumentsController.php b/src/Controller/Admin/ImportDocumentsController.php index 0b3f60e..b39fd81 100644 --- a/src/Controller/Admin/ImportDocumentsController.php +++ b/src/Controller/Admin/ImportDocumentsController.php @@ -4,9 +4,10 @@ use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; use Neusta\Pimcore\ImportExportBundle\Import\Importer; +use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository; +use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Pimcore\Model\Document; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -21,11 +22,12 @@ final class ImportDocumentsController extends AbstractImportBaseController * @param Importer<\ArrayObject, Document> $importer */ public function __construct( - LoggerInterface $logger, + ApplicationLogger $applicationLogger, DocumentRepository $repository, + ParentRelationResolver $parentRelationResolver, private Importer $importer, ) { - parent::__construct($logger, $repository, 'Document'); + parent::__construct($applicationLogger, $repository, $parentRelationResolver, 'Document'); } #[Route( diff --git a/src/Import/Importer.php b/src/Import/Importer.php index 0bb33f0..64cceb2 100644 --- a/src/Import/Importer.php +++ b/src/Import/Importer.php @@ -34,7 +34,7 @@ public function __construct( * @throws \DomainException * @throws \InvalidArgumentException */ - public function import(string $yamlInput, string $format, bool $forcedSave = true): array + public function import(string $yamlInput, string $format, bool $forcedSave = false): array { $config = $this->serializer->deserialize($yamlInput, $format); From c8d32ae60d5fd4a402cecc344bb228d4628be7be Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 15:05:52 +0200 Subject: [PATCH 09/25] [With or Without IDs][Export] use kebap-case for commandline option --- src/Command/Base/AbstractExportBaseCommand.php | 2 +- src/Command/ExportAssetsCommand.php | 2 +- src/Command/ExportDataObjectsCommand.php | 2 +- src/Command/ExportDocumentsCommand.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Command/Base/AbstractExportBaseCommand.php b/src/Command/Base/AbstractExportBaseCommand.php index 15bcddc..a584b62 100644 --- a/src/Command/Base/AbstractExportBaseCommand.php +++ b/src/Command/Base/AbstractExportBaseCommand.php @@ -38,7 +38,7 @@ protected function configure(): void { $this ->addOption( - 'includeIds', + 'include-ids', null, InputOption::VALUE_NONE, 'If set, the export will include asset/document/object IDs and ParentIDs - be aware with re-importing' diff --git a/src/Command/ExportAssetsCommand.php b/src/Command/ExportAssetsCommand.php index 0038190..15b7a97 100644 --- a/src/Command/ExportAssetsCommand.php +++ b/src/Command/ExportAssetsCommand.php @@ -79,7 +79,7 @@ protected function configure(): void protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['includeIds' => $input->getOption('includeIds')]); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('includeIds')]); $zipFilename = $input->getOption('output'); try { diff --git a/src/Command/ExportDataObjectsCommand.php b/src/Command/ExportDataObjectsCommand.php index 9bb2368..bf33eac 100644 --- a/src/Command/ExportDataObjectsCommand.php +++ b/src/Command/ExportDataObjectsCommand.php @@ -81,7 +81,7 @@ className: SocialMediaItem protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['includeIds' => $input->getOption('includeIds')]); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('includeIds')]); $exportFilename = $input->getOption('output'); // Validate filename to prevent directory traversal diff --git a/src/Command/ExportDocumentsCommand.php b/src/Command/ExportDocumentsCommand.php index f53e52f..06feb55 100644 --- a/src/Command/ExportDocumentsCommand.php +++ b/src/Command/ExportDocumentsCommand.php @@ -85,7 +85,7 @@ protected function configure(): void protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['includeIds' => $input->getOption('includeIds')]); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('includeIds')]); $exportFilename = $input->getOption('output'); // Validate filename to prevent directory traversal From b47a1abc581255e2e224f608a6f953faffffafa9 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 15:07:06 +0200 Subject: [PATCH 10/25] [With or Without IDs][Export] remove unused code in JS --- public/js/exportAsset.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/js/exportAsset.js b/public/js/exportAsset.js index 5c46cd7..5628530 100644 --- a/public/js/exportAsset.js +++ b/public/js/exportAsset.js @@ -21,7 +21,6 @@ neusta_pimcore_import_export.plugin.asset.export = Class.create({ iconCls: "pimcore_icon_asset pimcore_icon_overlay_download", handler: function () { let defaultFilename = asset.data.key + '.yaml'; - let includeIds = !confirm(t('neusta_pimcore_import_export_exclude_ids_question')); // Yes = false, No = true let win = Ext.create('Ext.window.Window', { title: t('neusta_pimcore_import_export_dialog_title'), From 73340c72311a2b6aa86b7c09a4fd09be78c5ca28 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 15:12:05 +0200 Subject: [PATCH 11/25] [With or Without IDs][Export] try to fix integration tests by loading PimcoreApplicationLoggerBundle --- tests/app/TestKernel.php | 2 ++ tests/app/config/services.yaml | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/app/TestKernel.php b/tests/app/TestKernel.php index 767b351..12d5006 100755 --- a/tests/app/TestKernel.php +++ b/tests/app/TestKernel.php @@ -2,12 +2,14 @@ use Neusta\Pimcore\ImportExportBundle\NeustaPimcoreImportExportBundle; use Neusta\Pimcore\TestingFramework\Kernel\TestKernel as TestingFrameworkTestKernel; +use Pimcore\Bundle\ApplicationLoggerBundle\PimcoreApplicationLoggerBundle; use Pimcore\HttpKernel\BundleCollection\BundleCollection; class TestKernel extends TestingFrameworkTestKernel { public function registerBundlesToCollection(BundleCollection $collection): void { + $collection->addBundle(new PimcoreApplicationLoggerBundle()); $collection->addBundle(new NeustaPimcoreImportExportBundle()); } } diff --git a/tests/app/config/services.yaml b/tests/app/config/services.yaml index 3a80bdf..8606fc7 100755 --- a/tests/app/config/services.yaml +++ b/tests/app/config/services.yaml @@ -50,4 +50,3 @@ services: $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver: ~ - From e36b2319c3c957a7182cb67c21fda2f429034f9b Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 22 Jul 2025 15:37:36 +0200 Subject: [PATCH 12/25] [With or Without IDs][Export] fix tests --- config/services.yaml | 1 + src/Serializer/YamlSerializer.php | 3 -- ...st__test_simple_saved_pages_export__1.yaml | 20 ++++----- ...__test_simple_unsaved_pages_export__1.yaml | 20 ++++----- ...rterTest__test_single_image_export__1.yaml | 4 +- ..._test_single_image_export_with_ids__1.yaml | 10 +++++ ...orterTest__test_single_page_export__1.yaml | 10 ++--- ...Test__test_single_page_export_json__1.json | 12 +++--- ...__test_single_page_export_with_ids__1.yaml | 19 ++++++++ ...porterTest__test_tree_pages_export__1.yaml | 30 ++++++------- ...t__test_tree_pages_export_with_ids__1.yaml | 43 +++++++++++++++++++ tests/Integration/Import/ImporterTest.php | 12 +++--- 12 files changed, 127 insertions(+), 57 deletions(-) create mode 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml create mode 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml create mode 100755 tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml diff --git a/config/services.yaml b/config/services.yaml index 525c298..ea67072 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -136,6 +136,7 @@ services: - 'navigation_title' - 'title' - 'editables' + - 'filename' tags: [ 'serializer.normalizer' ] ################# diff --git a/src/Serializer/YamlSerializer.php b/src/Serializer/YamlSerializer.php index dbc42f0..8b07511 100644 --- a/src/Serializer/YamlSerializer.php +++ b/src/Serializer/YamlSerializer.php @@ -23,9 +23,6 @@ public function serialize(mixed $data, string $format): string 'yaml_inline' => 6, 'yaml_indent' => 0, 'yaml_flags' => self::YAML_DUMP_FLAGS, - 'attributes' => [ - 'type', 'id', 'parentId', 'path', 'language', 'key', 'published', 'controller', 'navigation_name', 'navigation_title', 'title', 'editables', - ], ]); } diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml index c0c88dc..f2ddca3 100755 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml @@ -1,25 +1,25 @@ elements: - Pimcore\Model\Document\Page: + type: page + path: / + key: test_document_1 published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' navigation_name: ~ navigation_title: ~ title: 'Test Document_1' - controller: 'App\Controller\DefaultController::defaultAction' editables: { } - type: page - path: / - language: '' - key: test_document_1 - Pimcore\Model\Document\Page: + type: page + path: / + key: test_document_2 published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' navigation_name: ~ navigation_title: ~ title: 'Test Document_2' - controller: 'App\Controller\DefaultController::defaultAction' editables: { } - type: page - path: / - language: '' - key: test_document_2 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml index e46ab64..cfdba17 100755 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml @@ -1,25 +1,25 @@ elements: - Pimcore\Model\Document\Page: + type: page + path: /will/not/overwritten/ + key: test_document_1 published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' navigation_name: ~ navigation_title: ~ title: 'Test Document_1' - controller: 'App\Controller\DefaultController::defaultAction' editables: { } - type: page - path: /will/not/overwritten/ - language: '' - key: test_document_1 - Pimcore\Model\Document\Page: + type: page + path: /will/not/overwritten/ + key: test_document_2 published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' navigation_name: ~ navigation_title: ~ title: 'Test Document_2' - controller: 'App\Controller\DefaultController::defaultAction' editables: { } - type: page - path: /will/not/overwritten/ - language: '' - key: test_document_2 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml index 0bc0ef6..2cb9a78 100755 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml @@ -1,8 +1,8 @@ elements: - Pimcore\Model\Asset\Image: - filename: image_1 type: image path: / - language: '' key: image_1 + language: '' + filename: image_1 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml new file mode 100755 index 0000000..6cc0962 --- /dev/null +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml @@ -0,0 +1,10 @@ +elements: + - + Pimcore\Model\Asset\Image: + type: image + id: 999 + parentId: 1 + path: / + key: image_1 + language: '' + filename: image_1 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml index 1c92d0b..f9fdd34 100755 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml @@ -1,17 +1,17 @@ elements: - Pimcore\Model\Document\Page: + type: page + path: /test/ + key: test_document_1 published: false + controller: /Some/Controller + language: en navigation_name: 'My Document' navigation_title: 'My Document - Title' title: 'The Title of my document' - controller: /Some/Controller editables: - type: input name: textInput data: 'some text input' - type: page - path: /test/ - language: en - key: test_document_1 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json index ef33936..80b367f 100755 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json @@ -2,22 +2,22 @@ "elements": [ { "Pimcore\\Model\\Document\\Page": { + "type": "page", + "path": "\/test\/", + "key": "test_document_1", "published": false, + "controller": "\/Some\/Controller", + "language": "en", "navigation_name": "My Document", "navigation_title": "My Document - Title", "title": "The Title of my document", - "controller": "\/Some\/Controller", "editables": [ { "type": "input", "name": "textInput", "data": "some text input" } - ], - "type": "page", - "path": "\/test\/", - "language": "en", - "key": "test_document_1" + ] } } ] diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml new file mode 100755 index 0000000..67161c8 --- /dev/null +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml @@ -0,0 +1,19 @@ +elements: + - + Pimcore\Model\Document\Page: + type: page + id: 999 + parentId: 4 + path: /test/ + key: test_document_1 + published: false + controller: /Some/Controller + language: en + navigation_name: 'My Document' + navigation_title: 'My Document - Title' + title: 'The Title of my document' + editables: + - + type: input + name: textInput + data: 'some text input' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml index 843f187..1fe6ff7 100755 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml @@ -1,37 +1,37 @@ elements: - Pimcore\Model\Document\Page: + type: page + path: / + key: test_document_1 published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' navigation_name: ~ navigation_title: ~ title: 'Test Document_1' - controller: 'App\Controller\DefaultController::defaultAction' editables: { } - type: page - path: / - language: '' - key: test_document_1 - Pimcore\Model\Document\Page: + type: page + path: /test_document_1/ + key: test_document_2 published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' navigation_name: ~ navigation_title: ~ title: 'Test Document_2' - controller: 'App\Controller\DefaultController::defaultAction' editables: { } - type: page - path: /test_document_1/ - language: '' - key: test_document_2 - Pimcore\Model\Document\Page: + type: page + path: /test_document_1/test_document_2/ + key: test_document_3 published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' navigation_name: ~ navigation_title: ~ title: 'Test Document_3' - controller: 'App\Controller\DefaultController::defaultAction' editables: { } - type: page - path: /test_document_1/test_document_2/ - language: '' - key: test_document_3 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml new file mode 100755 index 0000000..b665af6 --- /dev/null +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml @@ -0,0 +1,43 @@ +elements: + - + Pimcore\Model\Document\Page: + type: page + id: 2 + parentId: 1 + path: / + key: test_document_1 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' + navigation_name: ~ + navigation_title: ~ + title: 'Test Document_1' + editables: { } + - + Pimcore\Model\Document\Page: + type: page + id: 3 + parentId: 2 + path: /test_document_1/ + key: test_document_2 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' + navigation_name: ~ + navigation_title: ~ + title: 'Test Document_2' + editables: { } + - + Pimcore\Model\Document\Page: + type: page + id: 4 + parentId: 3 + path: /test_document_1/test_document_2/ + key: test_document_3 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + language: '' + navigation_name: ~ + navigation_title: ~ + title: 'Test Document_3' + editables: { } diff --git a/tests/Integration/Import/ImporterTest.php b/tests/Integration/Import/ImporterTest.php index 3dc4c7d..6e33c63 100755 --- a/tests/Integration/Import/ImporterTest.php +++ b/tests/Integration/Import/ImporterTest.php @@ -33,7 +33,7 @@ public function testSinglePageImport_exceptional_case(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Neither parentId nor path leads to a valid parent element'); - $this->importer->import($yaml, 'yaml'); + $this->importer->import($yaml, 'yaml', true); } public function testSinglePageExport_regular_case_parent_id(): void @@ -56,7 +56,7 @@ public function testSinglePageExport_regular_case_parent_id(): void controller: /Some/Controller YAML; - $pages = $this->importer->import($yaml, 'yaml'); + $pages = $this->importer->import($yaml, 'yaml', true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); @@ -101,7 +101,7 @@ public function testSinglePageExport_regular_case_json(): void } JSON; - $pages = $this->importer->import($json, 'json'); + $pages = $this->importer->import($json, 'json', true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); @@ -134,7 +134,7 @@ public function testSinglePageExport_regular_case_path(): void controller: /Some/Controller YAML; - $pages = $this->importer->import($yaml, 'yaml'); + $pages = $this->importer->import($yaml, 'yaml', true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); self::assertEquals(1, $pages[0]->getParentId()); @@ -173,7 +173,7 @@ public function testSinglePageImport_tree_case(): void key: test_document_1_1_1 YAML; - $pages = $this->importer->import($yaml, 'yaml'); + $pages = $this->importer->import($yaml, 'yaml', true); self::assertEquals('/test_document_1/test_document_1_1/', $pages[2]->getPath()); } @@ -209,7 +209,7 @@ public function testSinglePageImport_tree_case_by_path(): void key: test_document_1_1_1 YAML; - $pages = $this->importer->import($yaml, 'yaml'); + $pages = $this->importer->import($yaml, 'yaml', true); self::assertEquals('/test_document_1/test_document_1_1/', $pages[3]->getPath()); } From 5da074a976684124067c690393084b268bc51646 Mon Sep 17 00:00:00 2001 From: Luka Dschaak Date: Tue, 22 Jul 2025 16:39:45 +0200 Subject: [PATCH 13/25] [With or Without IDs][Export] add EventSubscriber, Event Dispatching and Strategy-Service-Locators. --- .../documents/converters_populators.yaml | 22 ++++++ config/services.yaml | 42 +++++++++-- src/Command/ImportAssetsCommand.php | 2 +- src/Command/ImportDataObjectsCommand.php | 2 +- src/Command/ImportDocumentsCommand.php | 2 +- .../Base/AbstractImportBaseController.php | 15 ++-- .../Admin/ImportAssetsController.php | 6 +- .../Admin/ImportDataObjectsController.php | 4 +- .../Admin/ImportDocumentsController.php | 4 +- .../RegisterTaggedConverterPass.php | 30 ++++++++ src/Exception/InconsistencyException.php | 7 ++ src/Import/Event/ImportEvent.php | 48 +++++++++++++ src/Import/Event/ImportStatus.php | 11 +++ .../ImportLoggingEventSubscriber.php | 70 +++++++++++++++++++ src/Import/Importer.php | 66 ++++++++++++++--- src/Import/Strategy/MergeElementStrategy.php | 17 +++++ .../Page/UpdateExistingPageStrategy.php | 36 ++++++++++ .../ReplaceExistingElementStrategy.php | 22 ++++++ src/NeustaPimcoreImportExportBundle.php | 11 +++ tests/Integration/Import/ImporterTest.php | 51 ++++++++++++-- tests/app/config/services.yaml | 56 +++++++++++++-- 21 files changed, 481 insertions(+), 43 deletions(-) create mode 100644 src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php create mode 100644 src/Exception/InconsistencyException.php create mode 100644 src/Import/Event/ImportEvent.php create mode 100644 src/Import/Event/ImportStatus.php create mode 100644 src/Import/EventSubscriber/ImportLoggingEventSubscriber.php create mode 100644 src/Import/Strategy/MergeElementStrategy.php create mode 100644 src/Import/Strategy/Page/UpdateExistingPageStrategy.php create mode 100644 src/Import/Strategy/ReplaceExistingElementStrategy.php diff --git a/config/pimcore/import/documents/converters_populators.yaml b/config/pimcore/import/documents/converters_populators.yaml index 79a539d..03d2483 100644 --- a/config/pimcore/import/documents/converters_populators.yaml +++ b/config/pimcore/import/documents/converters_populators.yaml @@ -7,6 +7,16 @@ neusta_converter: # Import Converter (Page -> PimcorePage) ########################################################### neusta_pimcore_import_export.import_document: + target: Pimcore\Model\Document + populators: + - neusta_pimcore_import_export.documents.import.populator.id + - neusta_pimcore_import_export.documents.import.populator.key + - neusta_pimcore_import_export.documents.import.populator.type + - neusta_pimcore_import_export.documents.import.populator.published + - neusta_pimcore_import_export.documents.import.populator.path + - neusta_pimcore_import_export.documents.import.populator.parentId + + neusta_pimcore_import_export.import_page: target: Pimcore\Model\Document\Page populators: - neusta_pimcore_import_export.documents.import.populator.id @@ -19,6 +29,18 @@ neusta_converter: - neusta_pimcore_import_export.documents.import.populator.parentId - Neusta\Pimcore\ImportExportBundle\Populator\PageImportPopulator + neusta_pimcore_import_export.import_page_snippet: + target: Pimcore\Model\Document\PageSnippet + populators: + - neusta_pimcore_import_export.documents.import.populator.id + - neusta_pimcore_import_export.documents.import.populator.key + - neusta_pimcore_import_export.documents.import.populator.controller + - neusta_pimcore_import_export.documents.import.populator.type + - neusta_pimcore_import_export.documents.import.populator.published + - neusta_pimcore_import_export.documents.import.populator.path + - neusta_pimcore_import_export.documents.import.populator.parentId + - Neusta\Pimcore\ImportExportBundle\Populator\PageImportPopulator + services: _defaults: autowire: true diff --git a/config/services.yaml b/config/services.yaml index ea67072..aa40402 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -73,10 +73,9 @@ services: Neusta\Pimcore\ImportExportBundle\Import\Importer: class: Neusta\Pimcore\ImportExportBundle\Import\Importer arguments: - $typeToConverterMap: - Pimcore\Model\Asset: '@neusta_pimcore_import_export.import_asset' - Pimcore\Model\Document: '@neusta_pimcore_import_export.import_document' - Pimcore\Model\DataObject: '@neusta_pimcore_import_export.import_object' + $repositoryLocator: !tagged_locator { tag: 'neusta.import_export.repository', index_by: 'type' } + $converterLocator: !tagged_locator { tag: 'neusta.import_export.converter', index_by: 'type' } + $mergeStrategyLocator: !tagged_locator { tag: 'neusta.import_export.merge_strategy', index_by: 'type' } $parentRelationResolver: '@Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver' $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' @@ -94,6 +93,20 @@ services: Pimcore\Model\DataObject: '@neusta_pimcore_import_export.export_object' $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' + ################## + # Merge Strategy # + ################## + Neusta\Pimcore\ImportExportBundle\Import\Strategy\Page\UpdateExistingPageStrategy: + tags: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Page' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\PageSnippet' } + + Neusta\Pimcore\ImportExportBundle\Import\Strategy\ReplaceElementStrategy: + tags: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Asset' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Concrete' } + ############## # Service # ############## @@ -101,9 +114,22 @@ services: Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver: ~ - Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository: ~ - Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository: ~ - Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository: ~ + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository: + public: true + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Asset' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository: + public: true + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Concrete' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository: + public: true + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\Page' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\PageSnippet' } ############## @@ -145,3 +171,5 @@ services: Neusta\Pimcore\ImportExportBundle\EventListener\PimcoreAdminListener: tags: - { name: kernel.event_listener, event: pimcore.bundle_manager.paths.js, method: addJSFiles } + + Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\ImportLoggingEventSubscriber: ~ diff --git a/src/Command/ImportAssetsCommand.php b/src/Command/ImportAssetsCommand.php index d047bc7..5e5bd99 100644 --- a/src/Command/ImportAssetsCommand.php +++ b/src/Command/ImportAssetsCommand.php @@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $assets = $this->importer->import($yamlInput, $format, false); + $assets = $this->importer->import($yamlInput, $format, false, $input->getOption('overwrite')); } catch (\DomainException $e) { $this->io->error(\sprintf('Invalid %s format: %s', $format, $e->getMessage())); diff --git a/src/Command/ImportDataObjectsCommand.php b/src/Command/ImportDataObjectsCommand.php index 5fbd13e..0db0de4 100644 --- a/src/Command/ImportDataObjectsCommand.php +++ b/src/Command/ImportDataObjectsCommand.php @@ -49,7 +49,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $documents = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run')); + $documents = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run'), $input->getOption('overwrite')); } catch (\DomainException $e) { $this->io->error(\sprintf('Invalid %s format: %s', $format, $e->getMessage())); diff --git a/src/Command/ImportDocumentsCommand.php b/src/Command/ImportDocumentsCommand.php index 725062e..1fc9cf9 100644 --- a/src/Command/ImportDocumentsCommand.php +++ b/src/Command/ImportDocumentsCommand.php @@ -60,7 +60,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $documents = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run')); + $documents = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run'), $input->getOption('overwrite')); } catch (\DomainException $e) { $this->io->error(\sprintf('Invalid %s format: %s', $format, $e->getMessage())); diff --git a/src/Controller/Admin/Base/AbstractImportBaseController.php b/src/Controller/Admin/Base/AbstractImportBaseController.php index 77f5bf9..9ded88f 100644 --- a/src/Controller/Admin/Base/AbstractImportBaseController.php +++ b/src/Controller/Admin/Base/AbstractImportBaseController.php @@ -67,11 +67,8 @@ public function import(Request $request): JsonResponse $this->overwrite = $request->request->getBoolean('overwrite'); try { - $elements = $this->importByFile($file, $format); - foreach ($elements as $element) { - ++$this->resultStatistics[$this->replaceIfExists($element)]; - } - } catch (\Exception $e) { + $elements = $this->importByFile($file, $format, true, $this->overwrite); + } catch (\Throwable $e) { return $this->createJsonResponse(false, $e->getMessage(), 500); } finally { try { @@ -98,6 +95,12 @@ protected function replaceIfExists(AbstractElement $element): int if (null !== $oldElement) { if ($this->overwrite) { if (0 === $element->getId() || null === $element->getId() || $oldElement->getId() === $element->getId()) { + $children = method_exists($oldElement, 'getChildren') ? $oldElement->getChildren() : []; + foreach ($children as $child) { + // reassign children to the new element + $child->setParent($element); + $child->save(); + } $oldElement->delete(); $this->parentRelationResolver->resolve($element); $element->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); @@ -150,7 +153,7 @@ protected function createResultMessage(): string * @throws ConverterException * @throws DuplicateFullPathException */ - abstract protected function importByFile(UploadedFile $file, string $format): array; + abstract protected function importByFile(UploadedFile $file, string $format, bool $forcedSave = true, bool $overwrite = false): array; protected function cleanUp(): void { diff --git a/src/Controller/Admin/ImportAssetsController.php b/src/Controller/Admin/ImportAssetsController.php index d542252..ada6b8e 100644 --- a/src/Controller/Admin/ImportAssetsController.php +++ b/src/Controller/Admin/ImportAssetsController.php @@ -42,14 +42,14 @@ public function import(Request $request): JsonResponse return parent::import($request); } - protected function importByFile(UploadedFile $file, string $format): array + protected function importByFile(UploadedFile $file, string $format, bool $forcedSave = true, bool $overwrite = false): array { $extension = pathinfo($file->getClientOriginalName(), \PATHINFO_EXTENSION); // if file is ZIP add physical files to Assets if ('zip' === $extension) { $zipContent = $this->zipImporter->import($file->getPathname()); - $assets = $this->importer->import($zipContent['yaml'], $format, true); + $assets = $this->importer->import($zipContent['yaml'], $format, $forcedSave, $overwrite); foreach ($assets as $asset) { if ( \array_key_exists($asset->getType(), $zipContent) @@ -67,7 +67,7 @@ protected function importByFile(UploadedFile $file, string $format): array try { $content = $file->getContent(); - return $this->importer->import($content, $format); + return $this->importer->import($content, $format, $forcedSave, $overwrite); } catch (\Exception $e) { $this->applicationLogger->error($e->getMessage()); throw new \Exception('Error reading uploaded file: ' . $e->getMessage(), 0, $e); diff --git a/src/Controller/Admin/ImportDataObjectsController.php b/src/Controller/Admin/ImportDataObjectsController.php index 18548b7..5e9980f 100644 --- a/src/Controller/Admin/ImportDataObjectsController.php +++ b/src/Controller/Admin/ImportDataObjectsController.php @@ -40,12 +40,12 @@ public function import(Request $request): JsonResponse return parent::import($request); } - protected function importByFile(UploadedFile $file, string $format): array + protected function importByFile(UploadedFile $file, string $format, bool $forcedSave = true, bool $overwrite = false): array { try { $content = $file->getContent(); - return $this->importer->import($content, $format, true); + return $this->importer->import($content, $format, $forcedSave, $overwrite); } catch (\Exception $e) { $this->applicationLogger->error($e->getMessage()); throw new \Exception('Error reading uploaded file: ' . $e->getMessage(), 0, $e); diff --git a/src/Controller/Admin/ImportDocumentsController.php b/src/Controller/Admin/ImportDocumentsController.php index b39fd81..4d77a21 100644 --- a/src/Controller/Admin/ImportDocumentsController.php +++ b/src/Controller/Admin/ImportDocumentsController.php @@ -40,12 +40,12 @@ public function import(Request $request): JsonResponse return parent::import($request); } - protected function importByFile(UploadedFile $file, string $format): array + protected function importByFile(UploadedFile $file, string $format, bool $forcedSave = true, bool $overwrite = false): array { try { $content = $file->getContent(); - return $this->importer->import($content, $format); + return $this->importer->import($content, $format, $forcedSave, $overwrite); } catch (\Exception $e) { throw new \Exception('Error reading uploaded file: ' . $e->getMessage(), 0, $e); } diff --git a/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php b/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php new file mode 100644 index 0000000..0aa3655 --- /dev/null +++ b/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php @@ -0,0 +1,30 @@ +getExtensionConfig('neusta_converter'); + $flattenedConverterDefs = []; + + foreach ($converterDefs as $entry) { + if (isset($entry['converter']) && \is_array($entry['converter'])) { + $flattenedConverterDefs = array_merge($flattenedConverterDefs, $entry['converter']); + } + } + foreach ($flattenedConverterDefs as $serviceId => $arguments) { + $definition = $container->getDefinition($serviceId); + + if (\array_key_exists('target', $arguments)) { + $definition->addTag('neusta.import_export.converter', ['type' => $arguments['target']]); + } + } + } +} diff --git a/src/Exception/InconsistencyException.php b/src/Exception/InconsistencyException.php new file mode 100644 index 0000000..c68ed7c --- /dev/null +++ b/src/Exception/InconsistencyException.php @@ -0,0 +1,7 @@ + $yamlContent + */ + public function __construct( + protected readonly ImportStatus $status, + protected readonly string $type, + protected readonly array $yamlContent, + protected readonly ?AbstractElement $newElement, + protected readonly ?AbstractElement $oldElement, + ) { + } + + public function getStatus(): ImportStatus + { + return $this->status; + } + + public function getType(): string + { + return $this->type; + } + + /** + * @return array + */ + public function getYamlContent(): array + { + return $this->yamlContent; + } + + public function getNewElement(): ?AbstractElement + { + return $this->newElement; + } + + public function getOldElement(): ?AbstractElement + { + return $this->oldElement; + } +} diff --git a/src/Import/Event/ImportStatus.php b/src/Import/Event/ImportStatus.php new file mode 100644 index 0000000..e0028f1 --- /dev/null +++ b/src/Import/Event/ImportStatus.php @@ -0,0 +1,11 @@ + [ + // ['logImportEvent', 100], + // ], + ]; + } + + public function logImportEvent(ImportEvent $event): void + { + if (ImportStatus::FAILED === $event->getStatus()) { + $this->writeApplicationError($event); + } else { + $this->writeApplicationLog($event); + } + } + + private function writeApplicationLog(ImportEvent $event): void + { + $prefix = \sprintf('[%s]', $event->getStatus()->value); + + $this->applicationLogger->info( + <<getType()} + key: {$event->getNewElement()?->getKey()} + path: {$event->getNewElement()?->getPath()} + id: {$event->getNewElement()?->getId()} + MESSAGE, + [ + 'relatedObject' => $event->getOldElement(), + 'component' => 'Pimcore Import Export Bundle', + ] + ); + } + + private function writeApplicationError(ImportEvent $event): void + { + $this->applicationLogger->error( + \sprintf('Two %ss with same key (%s) and path (%s) but different IDs (new ID: %d, old ID: %d) found. This seems to be an inconsistency of your importing data. Please check your import file.', + $event->getType(), + $event->getNewElement()?->getKey(), + $event->getNewElement()?->getPath(), + $event->getNewElement()?->getId(), + $event->getOldElement()?->getId(), + ), + [ + 'relatedObject' => $event->getOldElement(), + 'component' => 'Pimcore Import Export Bundle', + ] + ); + } +} diff --git a/src/Import/Importer.php b/src/Import/Importer.php index 64cceb2..ddac2f7 100644 --- a/src/Import/Importer.php +++ b/src/Import/Importer.php @@ -3,12 +3,18 @@ namespace Neusta\Pimcore\ImportExportBundle\Import; use Neusta\ConverterBundle\Converter; -use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Exception\ConverterException; +use Neusta\Pimcore\ImportExportBundle\Exception\InconsistencyException; +use Neusta\Pimcore\ImportExportBundle\Import\Event\ImportEvent; +use Neusta\Pimcore\ImportExportBundle\Import\Event\ImportStatus; +use Neusta\Pimcore\ImportExportBundle\Import\Strategy\MergeElementStrategy; use Neusta\Pimcore\ImportExportBundle\Model\Element; use Neusta\Pimcore\ImportExportBundle\Serializer\SerializerInterface; +use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ImportRepositoryInterface; use Pimcore\Model\Element\AbstractElement; use Pimcore\Model\Element\DuplicateFullPathException; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @template TSource of \ArrayObject @@ -17,24 +23,30 @@ class Importer { /** - * @param array, Converter> $typeToConverterMap + * @param ServiceLocator $repositoryLocator + * @param ServiceLocator $converterLocator + * @param ServiceLocator $mergeStrategyLocator */ public function __construct( - private readonly array $typeToConverterMap, + private readonly ServiceLocator $repositoryLocator, + private readonly ServiceLocator $converterLocator, + private readonly ServiceLocator $mergeStrategyLocator, private readonly ParentRelationResolver $parentRelationResolver, private readonly SerializerInterface $serializer, + private readonly EventDispatcherInterface $dispatcher, ) { } /** * @return array * + * @throws InconsistencyException * @throws ConverterException * @throws DuplicateFullPathException * @throws \DomainException * @throws \InvalidArgumentException */ - public function import(string $yamlInput, string $format, bool $forcedSave = false): array + public function import(string $yamlInput, string $format, bool $forcedSave, bool $overwrite): array { $config = $this->serializer->deserialize($yamlInput, $format); @@ -47,11 +59,37 @@ public function import(string $yamlInput, string $format, bool $forcedSave = fal foreach ($config[Element::ELEMENTS] as $element) { $result = null; $typeKey = key($element); - if (\array_key_exists($typeKey, $this->typeToConverterMap)) { - $result = $this->typeToConverterMap[$typeKey]->convert(new \ArrayObject($element[$typeKey])); // @phpstan-ignore-line + + $repository = $this->repositoryLocator->get($typeKey); + $converter = $this->converterLocator->get($typeKey); + $mergeStrategy = $this->mergeStrategyLocator->get($typeKey); + + if ( + $repository instanceof ImportRepositoryInterface + && $converter instanceof Converter + && $mergeStrategy instanceof MergeElementStrategy + ) { + /** @var AbstractElement $result */ + $result = $converter->convert(new \ArrayObject($element[$typeKey])); if ($forcedSave) { - $this->parentRelationResolver->resolve($result); - $result->save(); + $oldElement = $repository->getByPath($result->getFullPath()); + if (!$oldElement) { + // New element - save it + $this->parentRelationResolver->resolve($result); + $result->save(['versionNote' => 'created by pimcore-import-export-bundle']); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::CREATED, $typeKey, $element, $result, null)); + } elseif ($overwrite) { + if ($this->newElementHasNoValidId($result) || $this->bothHaveSameId($oldElement, $result)) { + // Update existing element by new one + $mergeStrategy->mergeAndSave($oldElement, $result); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::UPDATED, $typeKey, $element, $result, $oldElement)); + } else { + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $typeKey, $element, $result, $oldElement)); + } + } else { + // Don't overwrite existing element + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::SKIPPED, $typeKey, $element, $result, $oldElement)); + } } } if ($result) { @@ -59,6 +97,16 @@ public function import(string $yamlInput, string $format, bool $forcedSave = fal } } - return $elements; + return $elements; // @phpstan-ignore-line + } + + private function newElementHasNoValidId(AbstractElement $result): bool + { + return null === $result->getId() || 0 === $result->getId(); + } + + private function bothHaveSameId(AbstractElement $oldElement, AbstractElement $result): bool + { + return $oldElement->getId() === $result->getId(); } } diff --git a/src/Import/Strategy/MergeElementStrategy.php b/src/Import/Strategy/MergeElementStrategy.php new file mode 100644 index 0000000..a265131 --- /dev/null +++ b/src/Import/Strategy/MergeElementStrategy.php @@ -0,0 +1,17 @@ + + */ +class UpdateExistingPageStrategy implements MergeElementStrategy +{ + /** + * @param Document\PageSnippet $oldElement + * @param Document\PageSnippet $newElement + * + * @throws DuplicateFullPathException + */ + public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newElement): void + { + $oldElement->setPublished($newElement->getPublished()); + $oldElement->setController($newElement->getController()); + + if ($oldElement instanceof Document\Page && $newElement instanceof Document\Page) { + $oldElement->setTitle($newElement->getTitle()); + } + + $oldElement->setProperty('language', 'text', $newElement->getProperty('language')); + $oldElement->setProperty('navigation_name', 'text', $newElement->getProperty('navigation_name')); + $oldElement->setProperty('navigation_title', 'text', $newElement->getProperty('navigation_title')); + $oldElement->setEditables($newElement->getEditables()); + $oldElement->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); + } +} diff --git a/src/Import/Strategy/ReplaceExistingElementStrategy.php b/src/Import/Strategy/ReplaceExistingElementStrategy.php new file mode 100644 index 0000000..ab1d136 --- /dev/null +++ b/src/Import/Strategy/ReplaceExistingElementStrategy.php @@ -0,0 +1,22 @@ + + */ +class ReplaceExistingElementStrategy implements MergeElementStrategy +{ + /** + * @throws DuplicateFullPathException + */ + public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newElement): void + { + $oldElement->delete(); + $newElement->save(); + } +} diff --git a/src/NeustaPimcoreImportExportBundle.php b/src/NeustaPimcoreImportExportBundle.php index 4d2f4d1..6312ad3 100644 --- a/src/NeustaPimcoreImportExportBundle.php +++ b/src/NeustaPimcoreImportExportBundle.php @@ -3,10 +3,13 @@ namespace Neusta\Pimcore\ImportExportBundle; use Neusta\ConverterBundle\NeustaConverterBundle; +use Neusta\Pimcore\ImportExportBundle\DependencyInjection\CompilerPass\RegisterTaggedConverterPass; +use Pimcore\Bundle\ApplicationLoggerBundle\PimcoreApplicationLoggerBundle; use Pimcore\Extension\Bundle\AbstractPimcoreBundle; use Pimcore\Extension\Bundle\Traits\PackageVersionTrait; use Pimcore\HttpKernel\Bundle\DependentBundleInterface; use Pimcore\HttpKernel\BundleCollection\BundleCollection; +use Symfony\Component\DependencyInjection\ContainerBuilder; final class NeustaPimcoreImportExportBundle extends AbstractPimcoreBundle implements DependentBundleInterface { @@ -17,8 +20,16 @@ public function getPath(): string return \dirname(__DIR__); } + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new RegisterTaggedConverterPass()); + } + public static function registerDependentBundles(BundleCollection $collection): void { $collection->addBundle(NeustaConverterBundle::class); + $collection->addBundle(PimcoreApplicationLoggerBundle::class); } } diff --git a/tests/Integration/Import/ImporterTest.php b/tests/Integration/Import/ImporterTest.php index 6e33c63..bff0eaf 100755 --- a/tests/Integration/Import/ImporterTest.php +++ b/tests/Integration/Import/ImporterTest.php @@ -4,6 +4,8 @@ use Neusta\Pimcore\ImportExportBundle\Import\Importer; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; +use Pimcore\Model\Document\Page; +use Pimcore\Model\Version; use Pimcore\Test\KernelTestCase; use Spatie\Snapshots\MatchesSnapshots; @@ -16,6 +18,7 @@ class ImporterTest extends KernelTestCase protected function setUp(): void { + Version::disable(); $this->importer = self::getContainer()->get(Importer::class); } @@ -33,7 +36,7 @@ public function testSinglePageImport_exceptional_case(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Neither parentId nor path leads to a valid parent element'); - $this->importer->import($yaml, 'yaml', true); + $this->importer->import($yaml, 'yaml', true, true); } public function testSinglePageExport_regular_case_parent_id(): void @@ -56,7 +59,7 @@ public function testSinglePageExport_regular_case_parent_id(): void controller: /Some/Controller YAML; - $pages = $this->importer->import($yaml, 'yaml', true); + $pages = $this->importer->import($yaml, 'yaml', true, true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); @@ -101,7 +104,7 @@ public function testSinglePageExport_regular_case_json(): void } JSON; - $pages = $this->importer->import($json, 'json', true); + $pages = $this->importer->import($json, 'json', true, true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); @@ -134,7 +137,7 @@ public function testSinglePageExport_regular_case_path(): void controller: /Some/Controller YAML; - $pages = $this->importer->import($yaml, 'yaml', true); + $pages = $this->importer->import($yaml, 'yaml', true, true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); self::assertEquals(1, $pages[0]->getParentId()); @@ -173,7 +176,7 @@ public function testSinglePageImport_tree_case(): void key: test_document_1_1_1 YAML; - $pages = $this->importer->import($yaml, 'yaml', true); + $pages = $this->importer->import($yaml, 'yaml', true, true); self::assertEquals('/test_document_1/test_document_1_1/', $pages[2]->getPath()); } @@ -209,8 +212,44 @@ public function testSinglePageImport_tree_case_by_path(): void key: test_document_1_1_1 YAML; - $pages = $this->importer->import($yaml, 'yaml', true); + $pages = $this->importer->import($yaml, 'yaml', true, true); self::assertEquals('/test_document_1/test_document_1_1/', $pages[3]->getPath()); } + + public function testMultiPagesImport_multi_children(): void + { + $parentPage = new Page(); + $parentPage->setKey('test_document_1'); + $parentPage->setParentId(1); + $parentPage->save(); + + $childPage = new Page(); + $childPage->setKey('child_1'); + $childPage->setParentId($parentPage->getId()); + $childPage->save(); + + $yaml = + <<importer->import($yaml, 'yaml', true, true); + + self::assertEquals($parentPage->getId(), $pages[1]->getParentId()); + self::assertEquals($parentPage->getId(), $pages[2]->getParentId()); + self::assertEquals($parentPage->getId(), $childPage->getParentId()); + } } diff --git a/tests/app/config/services.yaml b/tests/app/config/services.yaml index 8606fc7..29a7118 100755 --- a/tests/app/config/services.yaml +++ b/tests/app/config/services.yaml @@ -1,3 +1,23 @@ +flysystem: + storages: + pimcore.asset.storage: + # Storage for asset source files, directory structure is equal to the asset tree structure + adapter: 'local' + visibility: public + directory_visibility: public + options: + directory: '%kernel.project_dir%/var/assets_test' + permissions: + file: + public: 0644 + private: 0600 + dir: + public: 0755 + private: 0700 + +imports: + - { resource: '../../../config/services.yaml' } + services: _defaults: autowire: true @@ -41,12 +61,38 @@ services: Neusta\Pimcore\ImportExportBundle\Import\Importer: class: Neusta\Pimcore\ImportExportBundle\Import\Importer arguments: - $typeToConverterMap: - Pimcore\Model\Asset\Image: '@neusta_pimcore_import_export.import_asset' - Pimcore\Model\Document\Page: '@neusta_pimcore_import_export.import_document' - Pimcore\Model\Document\PageSnippet: '@neusta_pimcore_import_export.import_document' - Pimcore\Model\Concrete: '@neusta_pimcore_import_export.import_object' + $repositoryLocator: !tagged_locator { tag: 'neusta.import_export.repository', index_by: 'type' } + $converterLocator: !tagged_locator { tag: 'neusta.import_export.converter', index_by: 'type' } + $mergeStrategyLocator: !tagged_locator { tag: 'neusta.import_export.merge_strategy', index_by: 'type' } $parentRelationResolver: '@Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver' $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver: ~ + + ################## + # Merge Strategy # + ################## + Neusta\Pimcore\ImportExportBundle\Import\Strategy\Page\UpdateExistingPageStrategy: + tags: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Page' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\PageSnippet' } + + Neusta\Pimcore\ImportExportBundle\Import\Strategy\ReplaceElementStrategy: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Asset' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Concrete' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository: + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Asset' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository: + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Concrete' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository: + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\Page' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\PageSnippet' } + From 0b0f28b3c4ee1b4dee0150b9ded17ba81471092d Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Wed, 23 Jul 2025 17:53:21 +0200 Subject: [PATCH 14/25] [With or Without IDs][Export][Import] fixed all tests, cs and phpstan --- .../documents/converters_populators.yaml | 4 - config/services.yaml | 3 + .../Base/AbstractExportBaseCommand.php | 19 +++-- src/Command/ExportAssetsCommand.php | 5 +- src/Command/ExportDataObjectsCommand.php | 3 +- src/Command/ExportDocumentsCommand.php | 5 +- .../Base/AbstractImportBaseController.php | 83 ++----------------- .../Admin/ExportAssetsController.php | 12 ++- .../Admin/ExportDataObjectsController.php | 12 ++- .../Admin/ExportDocumentsController.php | 12 ++- src/Export/Exporter.php | 11 +-- .../StatisticsEventSubscriber.php | 44 ++++++++++ 12 files changed, 103 insertions(+), 110 deletions(-) create mode 100644 src/Import/EventSubscriber/StatisticsEventSubscriber.php diff --git a/config/pimcore/export/documents/converters_populators.yaml b/config/pimcore/export/documents/converters_populators.yaml index cd5333e..1b92898 100644 --- a/config/pimcore/export/documents/converters_populators.yaml +++ b/config/pimcore/export/documents/converters_populators.yaml @@ -9,16 +9,12 @@ neusta_converter: neusta_pimcore_import_export.export_document: target: Neusta\Pimcore\ImportExportBundle\Model\Document\Document populators: - - neusta_pimcore_import_export.page.property.language.populator - - neusta_pimcore_import_export.page.property.navigation_title.populator - - neusta_pimcore_import_export.page.property.navigation_name.populator - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator properties: key: ~ type: ~ published: ~ path: ~ - controller: ~ neusta_pimcore_import_export.editable_converter: target: Neusta\Pimcore\ImportExportBundle\Model\Document\Editable diff --git a/config/services.yaml b/config/services.yaml index aa40402..1219728 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -89,6 +89,8 @@ services: arguments: $typeToConverterMap: Pimcore\Model\Asset: '@neusta_pimcore_import_export.strategy.export_asset' + Pimcore\Model\Document\Page: '@neusta_pimcore_import_export.strategy.export_document' + Pimcore\Model\Document\PageSnippet: '@neusta_pimcore_import_export.strategy.export_document' Pimcore\Model\Document: '@neusta_pimcore_import_export.strategy.export_document' Pimcore\Model\DataObject: '@neusta_pimcore_import_export.export_object' $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' @@ -173,3 +175,4 @@ services: - { name: kernel.event_listener, event: pimcore.bundle_manager.paths.js, method: addJSFiles } Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\ImportLoggingEventSubscriber: ~ + Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber: ~ diff --git a/src/Command/Base/AbstractExportBaseCommand.php b/src/Command/Base/AbstractExportBaseCommand.php index a584b62..ab48145 100644 --- a/src/Command/Base/AbstractExportBaseCommand.php +++ b/src/Command/Base/AbstractExportBaseCommand.php @@ -3,6 +3,7 @@ namespace Neusta\Pimcore\ImportExportBundle\Command\Base; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Element; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ExportRepositoryInterface; use Pimcore\Console\AbstractCommand; use Pimcore\Model\Asset; @@ -16,14 +17,16 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * @template TElement of AbstractElement + * @template TSource of AbstractElement + * @template TTarget of Element */ abstract class AbstractExportBaseCommand extends AbstractCommand { /** - * @param class-string $elementType - * @param array $supportedFormats - * @param ExportRepositoryInterface $repository + * @param class-string $elementType + * @param array $supportedFormats + * @param ExportRepositoryInterface $repository + * @param Exporter $exporter */ public function __construct( protected ExportRepositoryInterface $repository, @@ -107,10 +110,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @param array $allElements - * @param TElement $rootElement + * @param array $allElements + * @param TSource $rootElement * - * @return array + * @return array */ private function addElements(AbstractElement $rootElement, array $allElements): array { @@ -131,7 +134,7 @@ private function addElements(AbstractElement $rootElement, array $allElements): } /** - * @param array $allElements + * @param array $allElements * * @throws \Neusta\ConverterBundle\Exception\ConverterException */ diff --git a/src/Command/ExportAssetsCommand.php b/src/Command/ExportAssetsCommand.php index 15b7a97..bae39ab 100644 --- a/src/Command/ExportAssetsCommand.php +++ b/src/Command/ExportAssetsCommand.php @@ -5,13 +5,14 @@ use Neusta\Pimcore\ImportExportBundle\Command\Base\AbstractExportBaseCommand; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; use Neusta\Pimcore\ImportExportBundle\Export\Service\ZipService; +use Neusta\Pimcore\ImportExportBundle\Model\Asset\Asset; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ExportRepositoryInterface; -use Pimcore\Model\Asset; +use Pimcore\Model\Asset as PimcoreAsset; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; /** - * @extends AbstractExportBaseCommand + * @extends AbstractExportBaseCommand */ #[AsCommand( name: 'neusta:pimcore:export:assets', diff --git a/src/Command/ExportDataObjectsCommand.php b/src/Command/ExportDataObjectsCommand.php index bf33eac..f2aab7d 100644 --- a/src/Command/ExportDataObjectsCommand.php +++ b/src/Command/ExportDataObjectsCommand.php @@ -4,13 +4,14 @@ use Neusta\Pimcore\ImportExportBundle\Command\Base\AbstractExportBaseCommand; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Object\DataObject; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ExportRepositoryInterface; use Pimcore\Model\DataObject\Concrete; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; /** - * @extends AbstractExportBaseCommand + * @extends AbstractExportBaseCommand */ #[AsCommand( name: 'neusta:pimcore:export:objects', diff --git a/src/Command/ExportDocumentsCommand.php b/src/Command/ExportDocumentsCommand.php index 06feb55..4bd104e 100644 --- a/src/Command/ExportDocumentsCommand.php +++ b/src/Command/ExportDocumentsCommand.php @@ -4,13 +4,14 @@ use Neusta\Pimcore\ImportExportBundle\Command\Base\AbstractExportBaseCommand; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Document\Document; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ExportRepositoryInterface; -use Pimcore\Model\Document; +use Pimcore\Model\Document as PimcoreDocument; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; /** - * @extends AbstractExportBaseCommand + * @extends AbstractExportBaseCommand */ #[AsCommand( name: 'neusta:pimcore:export:documents', diff --git a/src/Controller/Admin/Base/AbstractImportBaseController.php b/src/Controller/Admin/Base/AbstractImportBaseController.php index 9ded88f..0c92142 100644 --- a/src/Controller/Admin/Base/AbstractImportBaseController.php +++ b/src/Controller/Admin/Base/AbstractImportBaseController.php @@ -3,6 +3,7 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base; use Neusta\ConverterBundle\Exception\ConverterException; +use Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber; use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ImportRepositoryInterface; use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; @@ -36,9 +37,6 @@ abstract class AbstractImportBaseController protected bool $overwrite = false; - /** @var array */ - private array $resultStatistics; - /** * @param ImportRepositoryInterface $repository */ @@ -48,12 +46,6 @@ public function __construct( protected ParentRelationResolver $parentRelationResolver, protected string $elementType = 'Element', ) { - $this->resultStatistics = [ - self::SUCCESS_ELEMENT_REPLACEMENT => 0, - self::SUCCESS_WITHOUT_REPLACEMENT => 0, - self::SUCCESS_NEW_ELEMENT => 0, - self::FAILURE_INCONSISTENCY => 0, - ]; } public function import(Request $request): JsonResponse @@ -67,7 +59,7 @@ public function import(Request $request): JsonResponse $this->overwrite = $request->request->getBoolean('overwrite'); try { - $elements = $this->importByFile($file, $format, true, $this->overwrite); + $this->importByFile($file, $format, true, $this->overwrite); } catch (\Throwable $e) { return $this->createJsonResponse(false, $e->getMessage(), 500); } finally { @@ -78,7 +70,7 @@ public function import(Request $request): JsonResponse } } - return $this->createJsonResponse(true, $this->createResultMessage()); + return $this->createJsonResponse(true, $this->createResultMessage(StatisticsEventSubscriber::getStatistics())); } protected function createJsonResponse(bool $success, string $message, int $statusCode = 200): JsonResponse @@ -87,60 +79,16 @@ protected function createJsonResponse(bool $success, string $message, int $statu } /** - * @param TElement $element + * @param array $stats */ - protected function replaceIfExists(AbstractElement $element): int - { - $oldElement = $this->repository->getByPath($element->getFullPath()); - if (null !== $oldElement) { - if ($this->overwrite) { - if (0 === $element->getId() || null === $element->getId() || $oldElement->getId() === $element->getId()) { - $children = method_exists($oldElement, 'getChildren') ? $oldElement->getChildren() : []; - foreach ($children as $child) { - // reassign children to the new element - $child->setParent($element); - $child->save(); - } - $oldElement->delete(); - $this->parentRelationResolver->resolve($element); - $element->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); - $this->writeApplicationLog('[REPLACE] ', $element, $oldElement); - - return self::SUCCESS_ELEMENT_REPLACEMENT; - } - - $this->applicationLogger->error(\sprintf('Two %ss with same key (%s) and path (%s) but different IDs (new ID: %d, old ID: %d) found. This seems to be an inconsistency of your importing data. Please check your import file.', - strtolower($this->elementType), - $element->getKey(), - $element->getPath(), - $element->getId(), - $oldElement->getId() - )); - - return self::FAILURE_INCONSISTENCY; - } - - $this->writeApplicationLog('[SKIP] ', $element, $oldElement); - - return self::SUCCESS_WITHOUT_REPLACEMENT; - } - - $this->parentRelationResolver->resolve($element); - $element->save(['versionNote' => 'added by pimcore-import-export-bundle']); - - $this->writeApplicationLog('[NEW] ', $element, null); - - return self::SUCCESS_NEW_ELEMENT; - } - - protected function createResultMessage(): string + protected function createResultMessage(array $stats): string { $resultMessage = ''; - foreach ($this->resultStatistics as $resultCode => $result) { + foreach ($stats as $resultCode => $result) { if ($result > 0) { $resultMessage .= ''; + $resultMessage .= $resultCode . ''; } } @@ -159,21 +107,4 @@ protected function cleanUp(): void { // implement clean ups in subclasses if necessary } - - private function writeApplicationLog(string $prefix, AbstractElement $newElement, ?AbstractElement $oldElement = null): void - { - $this->applicationLogger->info( - <<elementType} - key: {$newElement->getKey()} - path: {$newElement->getPath()} - id: {$newElement->getId()} - MESSAGE, - [ - 'relatedObject' => $oldElement, - 'component' => 'Pimcore Import Export Bundle', - ] - ); - } } diff --git a/src/Controller/Admin/ExportAssetsController.php b/src/Controller/Admin/ExportAssetsController.php index 69c9b46..6aa4af9 100644 --- a/src/Controller/Admin/ExportAssetsController.php +++ b/src/Controller/Admin/ExportAssetsController.php @@ -4,8 +4,9 @@ use Neusta\Pimcore\ImportExportBundle\Export\Exporter; use Neusta\Pimcore\ImportExportBundle\Export\Service\ZipService; +use Neusta\Pimcore\ImportExportBundle\Model\Asset\Asset; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository; -use Pimcore\Model\Asset; +use Pimcore\Model\Asset as PimcoreAsset; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -15,6 +16,9 @@ final class ExportAssetsController { + /** + * @param Exporter $exporter + */ public function __construct( private Exporter $exporter, private AssetRepository $assetRepository, @@ -30,7 +34,7 @@ public function __construct( public function export(Request $request): Response { $asset = $this->assetRepository->getById($request->query->getInt('asset_id')); - if (!$asset instanceof Asset) { + if (!$asset instanceof PimcoreAsset) { return new JsonResponse( \sprintf('Asset with id "%s" was not found', $request->query->getInt('asset_id')), Response::HTTP_NOT_FOUND, @@ -53,7 +57,7 @@ public function export(Request $request): Response public function exportWithChildren(Request $request): Response { $asset = $this->assetRepository->getById($request->query->getInt('asset_id')); - if (!$asset instanceof Asset) { + if (!$asset instanceof PimcoreAsset) { return new JsonResponse( \sprintf('Asset with id "%s" was not found', $request->query->getInt('asset_id')), Response::HTTP_NOT_FOUND, @@ -72,7 +76,7 @@ public function exportWithChildren(Request $request): Response } /** - * @param array $assets + * @param array $assets */ private function exportAssets(array $assets, string $filename, string $format, bool $includeIds): Response { diff --git a/src/Controller/Admin/ExportDataObjectsController.php b/src/Controller/Admin/ExportDataObjectsController.php index 87a630b..a1b9f59 100644 --- a/src/Controller/Admin/ExportDataObjectsController.php +++ b/src/Controller/Admin/ExportDataObjectsController.php @@ -3,8 +3,9 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Object\DataObject; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository; -use Pimcore\Model\DataObject; +use Pimcore\Model\DataObject as PimcoreDataObject; use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -13,6 +14,9 @@ final class ExportDataObjectsController { + /** + * @param Exporter $exporter + */ public function __construct( private Exporter $exporter, private DataObjectRepository $objectRepository, @@ -27,7 +31,7 @@ public function __construct( public function export(Request $request): Response { $object = $this->objectRepository->getById($request->query->getInt('object_id')); - if (!$object instanceof DataObject) { + if (!$object instanceof PimcoreDataObject) { return new JsonResponse( \sprintf('Data Object with id "%s" was not found', $request->query->getInt('object_id')), Response::HTTP_NOT_FOUND, @@ -50,7 +54,7 @@ public function export(Request $request): Response public function exportWithChildren(Request $request): Response { $object = $this->objectRepository->getById($request->query->getInt('object_id')); - if (!$object instanceof DataObject) { + if (!$object instanceof PimcoreDataObject) { return new JsonResponse( \sprintf('Data Object with id "%s" was not found', $request->query->getInt('object_id')), Response::HTTP_NOT_FOUND, @@ -68,7 +72,7 @@ public function exportWithChildren(Request $request): Response } /** - * @param iterable $objects + * @param iterable $objects */ private function exportObjects(iterable $objects, string $filename, string $format, bool $includeIds): Response { diff --git a/src/Controller/Admin/ExportDocumentsController.php b/src/Controller/Admin/ExportDocumentsController.php index 81b8566..4c7e99b 100644 --- a/src/Controller/Admin/ExportDocumentsController.php +++ b/src/Controller/Admin/ExportDocumentsController.php @@ -3,8 +3,9 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Document\Document; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository; -use Pimcore\Model\Document; +use Pimcore\Model\Document as PimcoreDocument; use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -13,6 +14,9 @@ final class ExportDocumentsController { + /** + * @param Exporter $exporter + */ public function __construct( private Exporter $exporter, private DocumentRepository $documentRepository, @@ -27,7 +31,7 @@ public function __construct( public function export(Request $request): Response { $document = $this->documentRepository->getById($request->query->getInt('doc_id')); - if (!$document instanceof Document) { + if (!$document instanceof PimcoreDocument) { return new JsonResponse( \sprintf('Document with id "%s" was not found', $request->query->getInt('doc_id')), Response::HTTP_NOT_FOUND, @@ -50,7 +54,7 @@ public function export(Request $request): Response public function exportWithChildren(Request $request): Response { $document = $this->documentRepository->getById($request->query->getInt('doc_id')); - if (!$document instanceof Document) { + if (!$document instanceof PimcoreDocument) { return new JsonResponse( \sprintf('Document with id "%s" was not found', $request->query->getInt('doc_id')), Response::HTTP_NOT_FOUND, @@ -68,7 +72,7 @@ public function exportWithChildren(Request $request): Response } /** - * @param iterable $documents + * @param iterable $documents */ private function exportDocuments(iterable $documents, string $filename, string $format, bool $includeIds): Response { diff --git a/src/Export/Exporter.php b/src/Export/Exporter.php index 2d8a52c..5624cc7 100644 --- a/src/Export/Exporter.php +++ b/src/Export/Exporter.php @@ -9,12 +9,13 @@ use Neusta\Pimcore\ImportExportBundle\Serializer\SerializerInterface; use Pimcore\Model\Element\AbstractElement; +/** + * @template TSource of AbstractElement + * @template TTarget of Element + */ class Exporter { /** - * @template TSource of AbstractElement - * @template TTarget of Element - * * @param array, Converter > $typeToConverterMap */ public function __construct( @@ -26,8 +27,8 @@ public function __construct( /** * Exports one or more Pimcore Elements in the given format (yaml, json, ...)). * - * @param iterable $elements - * @param array $ctxParams + * @param iterable $elements + * @param array $ctxParams * * @throws ConverterException */ diff --git a/src/Import/EventSubscriber/StatisticsEventSubscriber.php b/src/Import/EventSubscriber/StatisticsEventSubscriber.php new file mode 100644 index 0000000..6dd3024 --- /dev/null +++ b/src/Import/EventSubscriber/StatisticsEventSubscriber.php @@ -0,0 +1,44 @@ + */ + protected static array $statistics = []; + + public static function getSubscribedEvents() + { + return [ + ImportEvent::class => [ + ['countStatistics', 0], + ], + ]; + } + + /** + * @return array + */ + public static function getStatistics(): array + { + return self::$statistics; + } + + public function countStatistics(ImportEvent $event): void + { + $this->incrementCounter($event->getStatus()); + } + + public function incrementCounter(ImportStatus $status): void + { + if (\array_key_exists($status->value, self::$statistics)) { + ++self::$statistics[$status->value]; + } else { + self::$statistics[$status->value] = 1; + } + } +} From 079b56b254048fad1ed8394da4bb1558d62d2423 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Thu, 24 Jul 2025 15:50:43 +0200 Subject: [PATCH 15/25] [Exporter][Fix] * include properties * export different Document subtypes (folder, ...) different --- .../documents/converters_populators.yaml | 99 +++++---- config/services.yaml | 2 + public/js/exportAsset.js | 2 +- public/js/exportDataObjects.js | 2 +- public/js/exportDocument.js | 2 +- src/Command/ExportAssetsCommand.php | 2 +- src/Command/ExportDataObjectsCommand.php | 2 +- src/Command/ExportDocumentsCommand.php | 2 +- src/Command/ImportDataObjectsCommand.php | 6 +- .../Admin/ExportAssetsController.php | 4 +- .../Admin/ExportDataObjectsController.php | 4 +- .../Admin/ExportDocumentsController.php | 4 +- .../RegisterTaggedConverterPass.php | 6 + src/Exception/InconsistencyException.php | 2 +- src/Export/Exporter.php | 11 +- src/Import/Event/ImportEvent.php | 6 + src/Import/Event/ImportStatus.php | 1 + .../ImportLoggingEventSubscriber.php | 42 ++-- src/Import/Importer.php | 23 ++- .../Page/UpdateExistingPageStrategy.php | 14 +- .../ReplaceExistingElementStrategy.php | 9 +- src/Model/Document/Document.php | 10 +- src/Model/Document/Folder.php | 7 + src/Model/Document/Page.php | 8 + src/Model/Document/PageSnippet.php | 10 + src/Model/Element.php | 1 - src/Model/Property.php | 10 + src/Populator/PageImportPopulator.php | 28 ++- src/Populator/PropertyValuePopulator.php | 23 +++ ...st__test_simple_saved_pages_export__1.yaml | 8 +- ...__test_simple_unsaved_pages_export__1.yaml | 8 +- ...rterTest__test_single_image_export__1.yaml | 1 - ..._test_single_image_export_with_ids__1.yaml | 1 - ...orterTest__test_single_page_export__1.yaml | 16 +- ...Test__test_single_page_export_json__1.json | 22 +- ...__test_single_page_export_with_ids__1.yaml | 16 +- ...porterTest__test_tree_pages_export__1.yaml | 12 +- ...t__test_tree_pages_export_with_ids__1.yaml | 12 +- .../Import/ImporterExporterTest.php | 52 +++++ tests/Integration/data/Text Editor.yaml | 195 ++++++++++++++++++ .../Admin/ExportDocumentsControllerTest.php | 2 +- tests/app/config/services.yaml | 34 +-- translations/admin.de.yaml | 2 +- translations/admin.en.yaml | 2 +- 44 files changed, 548 insertions(+), 177 deletions(-) create mode 100644 src/Model/Document/Folder.php create mode 100644 src/Model/Document/Page.php create mode 100644 src/Model/Document/PageSnippet.php create mode 100644 src/Model/Property.php create mode 100644 src/Populator/PropertyValuePopulator.php mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml mode change 100755 => 100644 tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml create mode 100755 tests/Integration/Import/ImporterExporterTest.php create mode 100644 tests/Integration/data/Text Editor.yaml diff --git a/config/pimcore/export/documents/converters_populators.yaml b/config/pimcore/export/documents/converters_populators.yaml index 1b92898..94322ff 100644 --- a/config/pimcore/export/documents/converters_populators.yaml +++ b/config/pimcore/export/documents/converters_populators.yaml @@ -10,6 +10,45 @@ neusta_converter: target: Neusta\Pimcore\ImportExportBundle\Model\Document\Document populators: - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator + - neusta_pimcore_import_export.document.properties.populator + properties: + key: ~ + type: ~ + published: ~ + path: ~ + + neusta_pimcore_import_export.export_folder: + target: Neusta\Pimcore\ImportExportBundle\Model\Document\Folder + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator + - neusta_pimcore_import_export.document.properties.populator + properties: + key: ~ + type: ~ + published: ~ + path: ~ + + neusta_pimcore_import_export.export_page_snippet: + target: Neusta\Pimcore\ImportExportBundle\Model\Document\PageSnippet + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator + - neusta_pimcore_import_export.page_snippet.controller.populator + - neusta_pimcore_import_export.page_snippet.editables.populator + - neusta_pimcore_import_export.document.properties.populator + properties: + key: ~ + type: ~ + published: ~ + path: ~ + + neusta_pimcore_import_export.export_page: + target: Neusta\Pimcore\ImportExportBundle\Model\Document\Page + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator + - neusta_pimcore_import_export.page_snippet.controller.populator + - neusta_pimcore_import_export.page_snippet.editables.populator + - neusta_pimcore_import_export.page.title.populator + - neusta_pimcore_import_export.document.properties.populator properties: key: ~ type: ~ @@ -24,6 +63,14 @@ neusta_converter: type: ~ name: ~ + neusta_pimcore_import_export.property_converter: + target: Neusta\Pimcore\ImportExportBundle\Model\Property + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\PropertyValuePopulator + properties: + key: name + type: ~ + services: _defaults: autowire: true @@ -37,35 +84,9 @@ services: arguments: $typeToConverterMap: Pimcore\Model\Document\Page: '@neusta_pimcore_import_export.export_page' - Pimcore\Model\Document\Snippet: '@neusta_pimcore_import_export.export_page_snippet' + Pimcore\Model\Document\PageSnippet: '@neusta_pimcore_import_export.export_page_snippet' Pimcore\Model\Document\Folder: '@neusta_pimcore_import_export.export_folder' - - ########################################################### - # Export Converter (Document/Folder -> Page) - ########################################################### - neusta_pimcore_import_export.export_folder: - alias: neusta_pimcore_import_export.export_document - - ########################################################### - # Export Converter (Document/PageSnippet -> Page) - ########################################################### - neusta_pimcore_import_export.export_page_snippet: - class: Neusta\Pimcore\ImportExportBundle\Converter\ExtendedConverter - arguments: - $converter: '@neusta_pimcore_import_export.export_document' - $postPopulators: - - '@neusta_pimcore_import_export.page.editables.populator' - - ########################################################### - # Export Converter (Document/Page -> Page) - ########################################################### - neusta_pimcore_import_export.export_page: - class: Neusta\Pimcore\ImportExportBundle\Converter\ExtendedConverter - arguments: - $converter: '@neusta_pimcore_import_export.export_page_snippet' - $postPopulators: - - '@neusta_pimcore_import_export.page.controller.populator' - - '@neusta_pimcore_import_export.page.title.populator' + Pimcore\Model\Document: '@neusta_pimcore_import_export.export_document' ########################################################### # Export Populator (Page -> Page) @@ -78,13 +99,13 @@ services: $sourceProperty: title $targetProperty: title - neusta_pimcore_import_export.page.controller.populator: + neusta_pimcore_import_export.page_snippet.controller.populator: class: Neusta\ConverterBundle\Populator\PropertyMappingPopulator arguments: $sourceProperty: controller $targetProperty: controller - neusta_pimcore_import_export.page.editables.populator: + neusta_pimcore_import_export.page_snippet.editables.populator: class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator arguments: $converter: '@neusta_pimcore_import_export.editable_converter' @@ -98,19 +119,12 @@ services: $targetProperty: 'language' $skipNull: true - neusta_pimcore_import_export.page.property.navigation_title.populator: - class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator - arguments: - $propertyKey: 'navigation_title' - $targetProperty: 'navigation_title' - $skipNull: true - - neusta_pimcore_import_export.page.property.navigation_name.populator: - class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator + neusta_pimcore_import_export.document.properties.populator: + class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator arguments: - $propertyKey: 'navigation_name' - $targetProperty: 'navigation_name' - $skipNull: true + $converter: '@neusta_pimcore_import_export.property_converter' + $sourceArrayPropertyName: 'properties' + $targetPropertyName: 'properties' ########################################################### # Export Populator (Editable -> YamlEditable) @@ -118,3 +132,4 @@ services: neusta_pimcore_import_export.editable.data.populator: class: Neusta\Pimcore\ImportExportBundle\Populator\EditableDataPopulator + Neusta\Pimcore\ImportExportBundle\Populator\PropertyValuePopulator: ~ diff --git a/config/services.yaml b/config/services.yaml index 1219728..5058c49 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -101,6 +101,7 @@ services: Neusta\Pimcore\ImportExportBundle\Import\Strategy\Page\UpdateExistingPageStrategy: tags: - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Folder' } - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Page' } - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\PageSnippet' } @@ -130,6 +131,7 @@ services: public: true tags: - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\Folder' } - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\Page' } - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\PageSnippet' } diff --git a/public/js/exportAsset.js b/public/js/exportAsset.js index 5628530..9d00f38 100644 --- a/public/js/exportAsset.js +++ b/public/js/exportAsset.js @@ -63,7 +63,7 @@ neusta_pimcore_import_export.plugin.asset.export = Class.create({ asset_id: asset.data.id, filename: values.filename, format: 'yaml', - ids_included: !!values.includeIds + include_ids: !!values.includeIds }) ); win.close(); diff --git a/public/js/exportDataObjects.js b/public/js/exportDataObjects.js index eeff148..dedad26 100644 --- a/public/js/exportDataObjects.js +++ b/public/js/exportDataObjects.js @@ -64,7 +64,7 @@ neusta_pimcore_import_export.plugin.object.export = Class.create({ object_id: object.data.id, filename: values.filename, format: 'yaml', - ids_included: !!values.includeIds + include_ids: !!values.includeIds }) ); win.close(); diff --git a/public/js/exportDocument.js b/public/js/exportDocument.js index eb46c60..2a933a8 100755 --- a/public/js/exportDocument.js +++ b/public/js/exportDocument.js @@ -63,7 +63,7 @@ neusta_pimcore_import_export.plugin.document.export = Class.create({ doc_id: document.data.id, filename: values.filename, format: 'yaml', - ids_included: !!values.includeIds + include_ids: !!values.includeIds }) ); win.close(); diff --git a/src/Command/ExportAssetsCommand.php b/src/Command/ExportAssetsCommand.php index bae39ab..284e6f4 100644 --- a/src/Command/ExportAssetsCommand.php +++ b/src/Command/ExportAssetsCommand.php @@ -80,7 +80,7 @@ protected function configure(): void protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('includeIds')]); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('include-ids')]); $zipFilename = $input->getOption('output'); try { diff --git a/src/Command/ExportDataObjectsCommand.php b/src/Command/ExportDataObjectsCommand.php index f2aab7d..3c3de1f 100644 --- a/src/Command/ExportDataObjectsCommand.php +++ b/src/Command/ExportDataObjectsCommand.php @@ -82,7 +82,7 @@ className: SocialMediaItem protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('includeIds')]); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('include-ids')]); $exportFilename = $input->getOption('output'); // Validate filename to prevent directory traversal diff --git a/src/Command/ExportDocumentsCommand.php b/src/Command/ExportDocumentsCommand.php index 4bd104e..fbff355 100644 --- a/src/Command/ExportDocumentsCommand.php +++ b/src/Command/ExportDocumentsCommand.php @@ -86,7 +86,7 @@ protected function configure(): void protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('includeIds')]); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('include-ids')]); $exportFilename = $input->getOption('output'); // Validate filename to prevent directory traversal diff --git a/src/Command/ImportDataObjectsCommand.php b/src/Command/ImportDataObjectsCommand.php index 0db0de4..0e9a077 100644 --- a/src/Command/ImportDataObjectsCommand.php +++ b/src/Command/ImportDataObjectsCommand.php @@ -32,7 +32,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $this->io->title('Import Pimcore Data Objects given by file'); - $this->io->writeln('Start importing objects from file'); + $this->io->writeln('Start importing dataObjects from file'); $this->io->newLine(); $filename = $input->getOption('input'); @@ -49,7 +49,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $documents = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run'), $input->getOption('overwrite')); + $dataObjects = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run'), $input->getOption('overwrite')); } catch (\DomainException $e) { $this->io->error(\sprintf('Invalid %s format: %s', $format, $e->getMessage())); @@ -64,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $this->io->success(\sprintf('%d Pimcore Data Objects have been imported successfully', \count($documents))); + $this->io->success(\sprintf('%d Pimcore Data Objects have been imported successfully', \count($dataObjects))); return Command::SUCCESS; } diff --git a/src/Controller/Admin/ExportAssetsController.php b/src/Controller/Admin/ExportAssetsController.php index 6aa4af9..d8c2f75 100644 --- a/src/Controller/Admin/ExportAssetsController.php +++ b/src/Controller/Admin/ExportAssetsController.php @@ -45,7 +45,7 @@ public function export(Request $request): Response [$asset], $request->query->getString('filename'), 'yaml', - $request->query->getBoolean('ids_included', false), + $request->query->getBoolean('include_ids', false), ); } @@ -71,7 +71,7 @@ public function exportWithChildren(Request $request): Response $assets, $request->query->getString('filename'), $request->query->getString('format'), - $request->query->getBoolean('ids_included', false) + $request->query->getBoolean('include_ids', false) ); } diff --git a/src/Controller/Admin/ExportDataObjectsController.php b/src/Controller/Admin/ExportDataObjectsController.php index a1b9f59..f49df2e 100644 --- a/src/Controller/Admin/ExportDataObjectsController.php +++ b/src/Controller/Admin/ExportDataObjectsController.php @@ -42,7 +42,7 @@ public function export(Request $request): Response [$object], $request->query->getString('filename'), 'yaml', - $request->query->getBoolean('ids_included', false), + $request->query->getBoolean('include_ids', false), ); } @@ -67,7 +67,7 @@ public function exportWithChildren(Request $request): Response $objects, $request->query->getString('filename'), $request->query->getString('format'), - $request->query->getBoolean('ids_included', false), + $request->query->getBoolean('include_ids', false), ); } diff --git a/src/Controller/Admin/ExportDocumentsController.php b/src/Controller/Admin/ExportDocumentsController.php index 4c7e99b..4248562 100644 --- a/src/Controller/Admin/ExportDocumentsController.php +++ b/src/Controller/Admin/ExportDocumentsController.php @@ -42,7 +42,7 @@ public function export(Request $request): Response [$document], $request->query->getString('filename'), 'yaml', - $request->query->getBoolean('ids_included', false), + $request->query->getBoolean('include_ids', false), ); } @@ -67,7 +67,7 @@ public function exportWithChildren(Request $request): Response $documents, $request->query->getString('filename'), $request->query->getString('format'), - $request->query->getBoolean('ids_included', false), + $request->query->getBoolean('include_ids', false), ); } diff --git a/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php b/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php index 0aa3655..faa122f 100644 --- a/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php +++ b/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php @@ -20,9 +20,15 @@ public function process(ContainerBuilder $container) } } foreach ($flattenedConverterDefs as $serviceId => $arguments) { + if (!$container->hasDefinition($serviceId)) { + continue; + } $definition = $container->getDefinition($serviceId); if (\array_key_exists('target', $arguments)) { + if (!\is_string($arguments['target'])) { + continue; + } $definition->addTag('neusta.import_export.converter', ['type' => $arguments['target']]); } } diff --git a/src/Exception/InconsistencyException.php b/src/Exception/InconsistencyException.php index c68ed7c..79ccc30 100644 --- a/src/Exception/InconsistencyException.php +++ b/src/Exception/InconsistencyException.php @@ -2,6 +2,6 @@ namespace Neusta\Pimcore\ImportExportBundle\Exception; -class InconsistencyException extends \Exception +class InconsistencyException extends \LogicException { } diff --git a/src/Export/Exporter.php b/src/Export/Exporter.php index 5624cc7..a5bffdb 100644 --- a/src/Export/Exporter.php +++ b/src/Export/Exporter.php @@ -5,9 +5,12 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Exception\ConverterException; +use Neusta\Pimcore\ImportExportBundle\Import\Event\ImportEvent; +use Neusta\Pimcore\ImportExportBundle\Import\Event\ImportStatus; use Neusta\Pimcore\ImportExportBundle\Model\Element; use Neusta\Pimcore\ImportExportBundle\Serializer\SerializerInterface; use Pimcore\Model\Element\AbstractElement; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @template TSource of AbstractElement @@ -21,6 +24,7 @@ class Exporter public function __construct( private readonly array $typeToConverterMap, private readonly SerializerInterface $serializer, + private readonly EventDispatcherInterface $dispatcher, ) { } @@ -43,7 +47,12 @@ public function export(iterable $elements, string $format, array $ctxParams = [] foreach ($elements as $element) { foreach (array_keys($this->typeToConverterMap) as $type) { if ($element instanceof $type) { - $yamlExportElements[] = [$type => $this->typeToConverterMap[$type]->convert($element, $ctx)]; + try { + $yamlContent = $this->typeToConverterMap[$type]->convert($element, $ctx); + $yamlExportElements[] = [$type => $yamlContent]; + } catch (ConverterException $e) { + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $type, [], $element, null, $e->getMessage())); + } continue 2; } } diff --git a/src/Import/Event/ImportEvent.php b/src/Import/Event/ImportEvent.php index fa6673f..5e49adc 100644 --- a/src/Import/Event/ImportEvent.php +++ b/src/Import/Event/ImportEvent.php @@ -15,6 +15,7 @@ public function __construct( protected readonly array $yamlContent, protected readonly ?AbstractElement $newElement, protected readonly ?AbstractElement $oldElement, + protected readonly ?string $errorMessage = null, ) { } @@ -45,4 +46,9 @@ public function getOldElement(): ?AbstractElement { return $this->oldElement; } + + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } } diff --git a/src/Import/Event/ImportStatus.php b/src/Import/Event/ImportStatus.php index e0028f1..61ae7a3 100644 --- a/src/Import/Event/ImportStatus.php +++ b/src/Import/Event/ImportStatus.php @@ -7,5 +7,6 @@ enum ImportStatus: string case SKIPPED = 'SKIPPED'; case CREATED = 'CREATED'; case UPDATED = 'UPDATED'; + case INCONSISTENCY = 'INCONSISTENCY'; case FAILED = 'FAILED'; } diff --git a/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php b/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php index df71c79..a56f3fa 100644 --- a/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php +++ b/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php @@ -25,7 +25,10 @@ public static function getSubscribedEvents() public function logImportEvent(ImportEvent $event): void { - if (ImportStatus::FAILED === $event->getStatus()) { + if (\in_array( + $event->getStatus(), + [ImportStatus::INCONSISTENCY, ImportStatus::FAILED] + )) { $this->writeApplicationError($event); } else { $this->writeApplicationLog($event); @@ -35,17 +38,20 @@ public function logImportEvent(ImportEvent $event): void private function writeApplicationLog(ImportEvent $event): void { $prefix = \sprintf('[%s]', $event->getStatus()->value); + $key = $event->getNewElement()?->getKey() ?? 'N/A'; + $path = $event->getNewElement()?->getPath() ?? 'N/A'; + $id = $event->getNewElement()?->getId() ?? 'N/A'; $this->applicationLogger->info( <<getType()} - key: {$event->getNewElement()?->getKey()} - path: {$event->getNewElement()?->getPath()} - id: {$event->getNewElement()?->getId()} + key: {$key} + path: {$path} + id: {$id} MESSAGE, [ - 'relatedObject' => $event->getOldElement(), + 'relatedObject' => $event->getOldElement() ?? 'N/A', 'component' => 'Pimcore Import Export Bundle', ] ); @@ -53,16 +59,26 @@ private function writeApplicationLog(ImportEvent $event): void private function writeApplicationError(ImportEvent $event): void { + $prefix = \sprintf('[%s]', $event->getStatus()->value); + $key = $event->getNewElement()?->getKey() ?? 'N/A'; + $path = $event->getNewElement()?->getPath() ?? 'N/A'; + $newId = $event->getNewElement()?->getId() ?? 'N/A'; + $oldId = $event->getOldElement()?->getId() ?? 'N/A'; + $errorMessage = $event->getErrorMessage() ?? 'N/A'; + $this->applicationLogger->error( - \sprintf('Two %ss with same key (%s) and path (%s) but different IDs (new ID: %d, old ID: %d) found. This seems to be an inconsistency of your importing data. Please check your import file.', - $event->getType(), - $event->getNewElement()?->getKey(), - $event->getNewElement()?->getPath(), - $event->getNewElement()?->getId(), - $event->getOldElement()?->getId(), - ), + <<getType()} + key: {$key} + path: {$path} + id (new element): {$newId} + id (old element): {$oldId} + MESSAGE, [ - 'relatedObject' => $event->getOldElement(), + 'relatedObject' => $event->getOldElement() ?? 'N/A', 'component' => 'Pimcore Import Export Bundle', ] ); diff --git a/src/Import/Importer.php b/src/Import/Importer.php index ddac2f7..c8be435 100644 --- a/src/Import/Importer.php +++ b/src/Import/Importer.php @@ -76,15 +76,31 @@ public function import(string $yamlInput, string $format, bool $forcedSave, bool if (!$oldElement) { // New element - save it $this->parentRelationResolver->resolve($result); - $result->save(['versionNote' => 'created by pimcore-import-export-bundle']); + try { + $result->save(['versionNote' => 'created by pimcore-import-export-bundle']); + } catch (\Exception $e) { + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $typeKey, $element, $result, $oldElement, $e->getMessage())); + } $this->dispatcher->dispatch(new ImportEvent(ImportStatus::CREATED, $typeKey, $element, $result, null)); } elseif ($overwrite) { if ($this->newElementHasNoValidId($result) || $this->bothHaveSameId($oldElement, $result)) { // Update existing element by new one - $mergeStrategy->mergeAndSave($oldElement, $result); + try { + $mergeStrategy->mergeAndSave($oldElement, $result); + } catch (\Exception $e) { + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $typeKey, $element, $result, $oldElement, $e->getMessage())); + } $this->dispatcher->dispatch(new ImportEvent(ImportStatus::UPDATED, $typeKey, $element, $result, $oldElement)); } else { - $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $typeKey, $element, $result, $oldElement)); + $this->dispatcher->dispatch(new ImportEvent( + ImportStatus::INCONSISTENCY, $typeKey, $element, $result, $oldElement, + << $elements */ return $elements; // @phpstan-ignore-line } diff --git a/src/Import/Strategy/Page/UpdateExistingPageStrategy.php b/src/Import/Strategy/Page/UpdateExistingPageStrategy.php index 694ca27..9ad074e 100644 --- a/src/Import/Strategy/Page/UpdateExistingPageStrategy.php +++ b/src/Import/Strategy/Page/UpdateExistingPageStrategy.php @@ -21,16 +21,18 @@ class UpdateExistingPageStrategy implements MergeElementStrategy public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newElement): void { $oldElement->setPublished($newElement->getPublished()); - $oldElement->setController($newElement->getController()); + + if ($oldElement instanceof Document\PageSnippet && $newElement instanceof Document\PageSnippet) { + $oldElement->setEditables($newElement->getEditables()); + $oldElement->setController($newElement->getController()); + } if ($oldElement instanceof Document\Page && $newElement instanceof Document\Page) { $oldElement->setTitle($newElement->getTitle()); } - - $oldElement->setProperty('language', 'text', $newElement->getProperty('language')); - $oldElement->setProperty('navigation_name', 'text', $newElement->getProperty('navigation_name')); - $oldElement->setProperty('navigation_title', 'text', $newElement->getProperty('navigation_title')); - $oldElement->setEditables($newElement->getEditables()); + foreach ($newElement->getProperties() as $property) { + $oldElement->setProperty($property->getName() ?? 'N/A', $property->getType() ?? 'N/A', $property->getData()); + } $oldElement->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); } } diff --git a/src/Import/Strategy/ReplaceExistingElementStrategy.php b/src/Import/Strategy/ReplaceExistingElementStrategy.php index ab1d136..f78ddd2 100644 --- a/src/Import/Strategy/ReplaceExistingElementStrategy.php +++ b/src/Import/Strategy/ReplaceExistingElementStrategy.php @@ -16,7 +16,12 @@ class ReplaceExistingElementStrategy implements MergeElementStrategy */ public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newElement): void { - $oldElement->delete(); - $newElement->save(); + try { + $oldElement->delete(); + $newElement->save(); + } catch (\Exception $e) { + // If save fails after delete, we're in an inconsistent state + throw new \RuntimeException('Failed to replace element: ' . $e->getMessage(), 0, $e); + } } } diff --git a/src/Model/Document/Document.php b/src/Model/Document/Document.php index 39bd1e4..eaf7dfb 100644 --- a/src/Model/Document/Document.php +++ b/src/Model/Document/Document.php @@ -3,16 +3,14 @@ namespace Neusta\Pimcore\ImportExportBundle\Model\Document; use Neusta\Pimcore\ImportExportBundle\Model\Element; +use Neusta\Pimcore\ImportExportBundle\Model\Property; class Document extends Element { public bool $published = false; - public ?string $navigation_name = null; - public ?string $navigation_title = null; - public ?string $title = null; - public ?string $controller = null; - /** @var array */ - public array $editables = []; + + /** @var array - property key will be mapped to */ + public array $properties = []; /** * @param array|null $yamlConfig diff --git a/src/Model/Document/Folder.php b/src/Model/Document/Folder.php new file mode 100644 index 0000000..9c7bcdb --- /dev/null +++ b/src/Model/Document/Folder.php @@ -0,0 +1,7 @@ + */ + public array $editables = []; +} diff --git a/src/Model/Element.php b/src/Model/Element.php index b3d0367..eb9b572 100644 --- a/src/Model/Element.php +++ b/src/Model/Element.php @@ -10,6 +10,5 @@ class Element public ?int $parentId; // important not to set default value here to avoid exporting null values automatically public string $type = 'element'; public string $path = ''; - public string $language = ''; public string $key = ''; } diff --git a/src/Model/Property.php b/src/Model/Property.php new file mode 100644 index 0000000..4f9db24 --- /dev/null +++ b/src/Model/Property.php @@ -0,0 +1,10 @@ +setProperty($property, 'text', $source[$property]); + if ($target instanceof PimcoreDocument\PageSnippet && \is_array($source['properties'])) { + foreach ($source['properties'] as $property) { + if ($property['value'] && 'asset' === $property['type']) { + $value = $this->assetRepository->getByPath($property['value']); + } elseif ($property['value'] && 'document' === $property['type']) { + $value = $this->documentRepository->getByPath($property['value']); + } elseif ($property['value'] && 'object' === $property['type']) { + $value = $this->objectRepository->getByPath($property['value']); + } else { + $value = $property['value']; } + $target->setProperty($property['key'], $property['type'], $value); } /** @var array{type: string, data: mixed} $editable */ diff --git a/src/Populator/PropertyValuePopulator.php b/src/Populator/PropertyValuePopulator.php new file mode 100644 index 0000000..c699d8f --- /dev/null +++ b/src/Populator/PropertyValuePopulator.php @@ -0,0 +1,23 @@ + + */ +class PropertyValuePopulator implements Populator +{ + public function populate(object $target, object $source, ?object $ctx = null): void + { + if ($source->getData() instanceof AbstractElement) { + $target->value = $source->getData()->getPath() . $source->getData()->getKey(); + } else { + $target->value = $source->getData(); + } + } +} diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml old mode 100755 new mode 100644 index f2ddca3..d3bb843 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml @@ -6,11 +6,9 @@ elements: key: test_document_1 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_1' editables: { } + properties: { } - Pimcore\Model\Document\Page: type: page @@ -18,8 +16,6 @@ elements: key: test_document_2 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_2' editables: { } + properties: { } diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml old mode 100755 new mode 100644 index cfdba17..4b1f77c --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml @@ -6,11 +6,9 @@ elements: key: test_document_1 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_1' editables: { } + properties: { } - Pimcore\Model\Document\Page: type: page @@ -18,8 +16,6 @@ elements: key: test_document_2 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_2' editables: { } + properties: { } diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml old mode 100755 new mode 100644 index 2cb9a78..8ff19c2 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml @@ -4,5 +4,4 @@ elements: type: image path: / key: image_1 - language: '' filename: image_1 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml old mode 100755 new mode 100644 index 6cc0962..b042423 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml @@ -6,5 +6,4 @@ elements: parentId: 1 path: / key: image_1 - language: '' filename: image_1 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml old mode 100755 new mode 100644 index f9fdd34..ecaca87 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml @@ -6,12 +6,22 @@ elements: key: test_document_1 published: false controller: /Some/Controller - language: en - navigation_name: 'My Document' - navigation_title: 'My Document - Title' title: 'The Title of my document' editables: - type: input name: textInput data: 'some text input' + properties: + language: + type: string + key: language + value: en + navigation_name: + type: string + key: navigation_name + value: 'My Document' + navigation_title: + type: string + key: navigation_title + value: 'My Document - Title' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json old mode 100755 new mode 100644 index 80b367f..47d39a0 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json @@ -7,9 +7,6 @@ "key": "test_document_1", "published": false, "controller": "\/Some\/Controller", - "language": "en", - "navigation_name": "My Document", - "navigation_title": "My Document - Title", "title": "The Title of my document", "editables": [ { @@ -17,7 +14,24 @@ "name": "textInput", "data": "some text input" } - ] + ], + "properties": { + "language": { + "type": "string", + "key": "language", + "value": "en" + }, + "navigation_name": { + "type": "string", + "key": "navigation_name", + "value": "My Document" + }, + "navigation_title": { + "type": "string", + "key": "navigation_title", + "value": "My Document - Title" + } + } } } ] diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml old mode 100755 new mode 100644 index 67161c8..898673a --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml @@ -8,12 +8,22 @@ elements: key: test_document_1 published: false controller: /Some/Controller - language: en - navigation_name: 'My Document' - navigation_title: 'My Document - Title' title: 'The Title of my document' editables: - type: input name: textInput data: 'some text input' + properties: + language: + type: string + key: language + value: en + navigation_name: + type: string + key: navigation_name + value: 'My Document' + navigation_title: + type: string + key: navigation_title + value: 'My Document - Title' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml old mode 100755 new mode 100644 index 1fe6ff7..6331642 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml @@ -6,11 +6,9 @@ elements: key: test_document_1 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_1' editables: { } + properties: { } - Pimcore\Model\Document\Page: type: page @@ -18,11 +16,9 @@ elements: key: test_document_2 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_2' editables: { } + properties: { } - Pimcore\Model\Document\Page: type: page @@ -30,8 +26,6 @@ elements: key: test_document_3 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_3' editables: { } + properties: { } diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml old mode 100755 new mode 100644 index b665af6..69724b2 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml @@ -8,11 +8,9 @@ elements: key: test_document_1 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_1' editables: { } + properties: { } - Pimcore\Model\Document\Page: type: page @@ -22,11 +20,9 @@ elements: key: test_document_2 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_2' editables: { } + properties: { } - Pimcore\Model\Document\Page: type: page @@ -36,8 +32,6 @@ elements: key: test_document_3 published: true controller: 'App\Controller\DefaultController::defaultAction' - language: '' - navigation_name: ~ - navigation_title: ~ title: 'Test Document_3' editables: { } + properties: { } diff --git a/tests/Integration/Import/ImporterExporterTest.php b/tests/Integration/Import/ImporterExporterTest.php new file mode 100755 index 0000000..35aa3cc --- /dev/null +++ b/tests/Integration/Import/ImporterExporterTest.php @@ -0,0 +1,52 @@ +importer = self::getContainer()->get(Importer::class); + $this->exporter = self::getContainer()->get(Exporter::class); + + $asset = new Asset(); + $asset->setParentId(1); + $asset->setPath('/'); + $asset->setKey('logo_desktop.svg'); + $asset->save(); + + $document = new Page(); + $document->setParentId(1); + $document->setPath('/'); + $document->setKey('Text für viele'); + $document->save(); + } + + public function testImportExport_regular_case(): void + { + $yamlToImport = file_get_contents(__DIR__ . '/../data/Text Editor.yaml'); + $this->importer->import($yamlToImport, 'yaml', true, true); + $document = Page::getByPath('/Test-Import-Export'); + $yamlExported = $this->exporter->export([$document], 'yaml'); + + self::assertEquals($yamlToImport, $yamlExported); + } +} diff --git a/tests/Integration/data/Text Editor.yaml b/tests/Integration/data/Text Editor.yaml new file mode 100644 index 0000000..869593b --- /dev/null +++ b/tests/Integration/data/Text Editor.yaml @@ -0,0 +1,195 @@ +elements: + - + Pimcore\Model\Document\Page: + type: page + path: / + key: Test-Import-Export + published: true + controller: 'Neusta\TrinityPimcoreBundle\Controller\DefaultController::indexAction' + title: 'Text Editor' + editables: + main: + type: areablock + name: main + data: [{ key: '1', type: trinity-layout-grid, hidden: false }] + 'main:1.-col1': + type: areablock + name: 'main:1.-col1' + data: [{ key: '1', type: trinity-headline, hidden: false }, { key: '2', type: trinity-text-editor, hidden: false }] + 'main:1.-col1:1.headline-text': + type: input + name: 'main:1.-col1:1.headline-text' + data: 'Text Editor' + 'main:1.-col1:1.modifier': + type: input + name: 'main:1.-col1:1.modifier' + data: '' + 'main:1.-col1:1.position': + type: select + name: 'main:1.-col1:1.position' + data: '' + 'main:1.-col1:1.sectionID': + type: input + name: 'main:1.-col1:1.sectionID' + data: '' + 'main:1.-col1:1.variant': + type: select + name: 'main:1.-col1:1.variant' + data: h2 + 'main:1.-col1:2.modifier': + type: input + name: 'main:1.-col1:2.modifier' + data: '' + 'main:1.-col1:2.text': + type: wysiwyg + name: 'main:1.-col1:2.text' + data: |- +

📝 Was ist ein Text Editor (WYSIWYG Editor) im CMS?

+

Ein Text Editor – auch oft WYSIWYG Editor genannt („What You See Is What You Get“) – ist ein zentrales Werkzeug in jedem CMS, mit dem Redakteur:innen Inhalte erstellen und bearbeiten können, ohne selbst HTML schreiben zu müssen.

+

Statt reinen Code zu tippen, arbeiten Nutzer:innen in einer Oberfläche, die ähnlich wie ein Textverarbeitungsprogramm aussieht – mit Buttons für Fett, Kursiv, Überschriften, Links, Listen, Bilder usw.

+

Man sieht also direkt beim Bearbeiten, wie der fertige Text später auf der Webseite aussehen wird – daher der Name WYSIWYG.

+
+

🎯 Warum sind WYSIWYG-Editoren so wichtig?

+
    +
  • +

    Einfache Bedienung: Auch ohne technisches Wissen können Inhalte gepflegt werden.

    +
  • +
  • +

    Schnelle Ergebnisse: Texte, Bilder und Links lassen sich intuitiv einfügen und formatieren.

    +
  • +
  • +

    Konsistentes Layout: Vorformatierte Stile sorgen dafür, dass Inhalte überall gleich gut aussehen.

    +
  • +
  • +

    Produktivität: Redakteur:innen müssen sich nicht um HTML-Fehler oder fehlerhafte Formatierungen kümmern.

    +
  • +
+
+

🛠️ Typische Funktionen eines CMS-Texteditors:

+
+
+
' . $this->elementType . 'Count
'; - $resultMessage .= $this->messagesMap[$resultCode] . '' . $result . '
' . $result . '
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunktionBeschreibung
Bold/ItalicText fett oder kursiv setzen
ÜberschriftenStrukturieren von Inhalten (H2, H3 usw.)
Links einfügenTexte oder Bilder verlinken
Bilder hochladenMedien direkt in den Text integrieren
ListenAufzählungen und Nummerierungen
TabellenDaten übersichtlich darstellen
Code-AnsichtOptional: HTML manuell bearbeiten
+ + +
+

📚 Verwendeter WYSIWYG-Editor

+

In Pimcore 11 wechselte der Standard-WYSIWYG-Editor von CKEditor zu TinyMCE und ist Teil des  PimcoreTinymceBundle. 

+

Dieser Editor lässt sich anpassen, z. B. durch eigene Toolbars, spezielle Styles oder Integrationen (z. B. Mediabrowser, Linkbrowser im CMS).

+
+

✨ Fazit

+

Ein guter Text Editor im CMS ist das Herzstück für die tägliche Arbeit von Redakteur:innen.
Er sorgt dafür, dass Inhalte leicht erstellt, schön formatiert und technisch sauber ausgeliefert werden – ohne dass jemand HTML-Profi sein muss.

+

Kurz gesagt:
👉 WYSIWYG-Editoren sind der Dreh- und Angelpunkt zwischen Inhalt, Technik und guter Nutzererfahrung.

+ 'main:1.-col1:2.variant': + type: select + name: 'main:1.-col1:2.variant' + data: '' + 'main:1.modifier': + type: input + name: 'main:1.modifier' + data: '' + 'main:1.sectionID': + type: input + name: 'main:1.sectionID' + data: '' + 'main:1.variant': + type: select + name: 'main:1.variant' + data: 1col + properties: + navigation_accesskey: + type: text + key: navigation_accesskey + value: '' + navigation_anchor: + type: text + key: navigation_anchor + value: '' + navigation_class: + type: text + key: navigation_class + value: '' + navigation_exclude: + type: bool + key: navigation_exclude + value: false + navigation_name: + type: text + key: navigation_name + value: '' + navigation_parameters: + type: text + key: navigation_parameters + value: '' + navigation_relation: + type: text + key: navigation_relation + value: '' + navigation_tabindex: + type: text + key: navigation_tabindex + value: '' + navigation_target: + type: text + key: navigation_target + value: ~ + navigation_title: + type: text + key: navigation_title + value: '' + testPropertyAsset: + type: asset + key: testPropertyAsset + value: /logo_desktop.svg + testPropertyCheckbox: + type: bool + key: testPropertyCheckbox + value: false + testPropertyKey: + type: document + key: testPropertyKey + value: '/Text für viele' + testPropertyObject: + type: object + key: testPropertyObject + value: ~ + testPropertyString: + type: text + key: testPropertyString + value: 'Ich bin eine Zeichenkette' diff --git a/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php b/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php index 018bc05..e2a1fb7 100755 --- a/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php +++ b/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php @@ -38,7 +38,7 @@ protected function setUp(): void $this->exporter->reveal(), $this->documentRepository->reveal(), ); - $this->request = new Request(['doc_id' => 17, 'ids_included' => false]); + $this->request = new Request(['doc_id' => 17, 'include_ids' => false]); } public function testExportPage_regular_case(): void diff --git a/tests/app/config/services.yaml b/tests/app/config/services.yaml index 29a7118..f630cd8 100755 --- a/tests/app/config/services.yaml +++ b/tests/app/config/services.yaml @@ -1,20 +1,3 @@ -flysystem: - storages: - pimcore.asset.storage: - # Storage for asset source files, directory structure is equal to the asset tree structure - adapter: 'local' - visibility: public - directory_visibility: public - options: - directory: '%kernel.project_dir%/var/assets_test' - permissions: - file: - public: 0644 - private: 0600 - dir: - public: 0755 - private: 0700 - imports: - { resource: '../../../config/services.yaml' } @@ -29,18 +12,6 @@ services: Neusta\Pimcore\ImportExportBundle\Tests\Integration\Documents\ImportExportYamlDriver: ~ - ########################################################### - # Export Converter (Document/Page -> Page) - ########################################################### - neusta_pimcore_import_export.export_page: - class: Neusta\Pimcore\ImportExportBundle\Converter\ExtendedConverter - arguments: - $converter: '@neusta_pimcore_import_export.export_document' - $postPopulators: - - '@neusta_pimcore_import_export.page.title.populator' - - '@neusta_pimcore_import_export.page.controller.populator' - - '@neusta_pimcore_import_export.page.editables.populator' - ############ # Exporter # ############ @@ -79,8 +50,9 @@ services: - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\PageSnippet' } Neusta\Pimcore\ImportExportBundle\Import\Strategy\ReplaceElementStrategy: - - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Asset' } - - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Concrete' } + tags: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Asset' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Concrete' } Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository: tags: diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index aa2dfd1..268b5b0 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -3,7 +3,7 @@ neusta_pimcore_import_export_dialog_confirm: 'Export ausführen' neusta_pimcore_import_export_dialog_cancel: 'Abbrechen' neusta_pimcore_import_export_filename_label: 'Bitte gib einen Dateinamen ein (Standard: Document Key)' neusta_pimcore_import_export_exclude_ids_label: 'Exportiere mit IDs' -neusta_pimcore_import_export_exclude_ids_info: 'Standardmäßig werden keine IDs (id/parentId) exportiert. Die Zuordnung geschieht über Key und Pfad.
Um IDs zu exportieren, aktiviere bitte diese Option.' +neusta_pimcore_import_export_exclude_ids_info: 'Standardmäßig werden keine IDs (id/parentId) exportiert. Die Zuordnung geschieht über Key und Pfad.
Um IDs zu exportieren, aktiviere bitte diese Option.' neusta_pimcore_import_export_export_menu_label: 'Exportiere in YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Exportiere in YAML mit Kindelementen' neusta_pimcore_import_export_import_dialog_title: 'Importiere Seite aus YAML' diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index 57f0294..8924c96 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -3,7 +3,7 @@ neusta_pimcore_import_export_dialog_confirm: 'execute Export' neusta_pimcore_import_export_dialog_cancel: 'cancel Export' neusta_pimcore_import_export_filename_label: 'Please fill in filename (default: Document Key)' neusta_pimcore_import_export_exclude_ids_label: 'Export with IDs' -neusta_pimcore_import_export_exclude_ids_info: 'By default no IDs (id/parentId) will be exported. The mapping is based on key and path.
To export IDs, please enable this option.' +neusta_pimcore_import_export_exclude_ids_info: 'By default no IDs (id/parentId) will be exported. The mapping is based on key and path.
To export IDs, please enable this option.' neusta_pimcore_import_export_export_menu_label: 'Export to YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Export to YAML with children' neusta_pimcore_import_export_import_dialog_title: 'Import page from YAML' From 306ae4946e143965a1f48e0b1aea9a1d4c937625 Mon Sep 17 00:00:00 2001 From: Luka Dschaak Date: Fri, 25 Jul 2025 16:53:05 +0200 Subject: [PATCH 16/25] [With or Without IDs][Export] fix phpstan --- src/Import/Strategy/Page/UpdateExistingPageStrategy.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Import/Strategy/Page/UpdateExistingPageStrategy.php b/src/Import/Strategy/Page/UpdateExistingPageStrategy.php index 9ad074e..9a5d223 100644 --- a/src/Import/Strategy/Page/UpdateExistingPageStrategy.php +++ b/src/Import/Strategy/Page/UpdateExistingPageStrategy.php @@ -13,8 +13,8 @@ class UpdateExistingPageStrategy implements MergeElementStrategy { /** - * @param Document\PageSnippet $oldElement - * @param Document\PageSnippet $newElement + * @param Document $oldElement + * @param Document $newElement * * @throws DuplicateFullPathException */ From 385ced071965e5d7d3a2bf3f8b23820cd7835eb0 Mon Sep 17 00:00:00 2001 From: Luka Dschaak Date: Fri, 25 Jul 2025 16:59:36 +0200 Subject: [PATCH 17/25] [With or Without IDs][Export] remove Kernel_CLASS from phpunit.xml.dist --- phpunit.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7ab7d2c..148fef9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,7 +15,6 @@ - From 933956f472491fc10fdf0db1941fa67cbde69b50 Mon Sep 17 00:00:00 2001 From: Luka Dschaak Date: Fri, 25 Jul 2025 17:06:40 +0200 Subject: [PATCH 18/25] [With or Without IDs][Export] fix services --- config/services.yaml | 4 ++-- .../{Page => Document}/UpdateExistingPageStrategy.php | 2 +- src/Import/Strategy/ReplaceExistingElementStrategy.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/Import/Strategy/{Page => Document}/UpdateExistingPageStrategy.php (95%) diff --git a/config/services.yaml b/config/services.yaml index 5058c49..33c7585 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -98,14 +98,14 @@ services: ################## # Merge Strategy # ################## - Neusta\Pimcore\ImportExportBundle\Import\Strategy\Page\UpdateExistingPageStrategy: + Neusta\Pimcore\ImportExportBundle\Import\Strategy\Document\UpdateExistingPageStrategy: tags: - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document' } - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Folder' } - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Page' } - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\PageSnippet' } - Neusta\Pimcore\ImportExportBundle\Import\Strategy\ReplaceElementStrategy: + Neusta\Pimcore\ImportExportBundle\Import\Strategy\ReplaceExistingElementStrategy: tags: - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Asset' } - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Concrete' } diff --git a/src/Import/Strategy/Page/UpdateExistingPageStrategy.php b/src/Import/Strategy/Document/UpdateExistingPageStrategy.php similarity index 95% rename from src/Import/Strategy/Page/UpdateExistingPageStrategy.php rename to src/Import/Strategy/Document/UpdateExistingPageStrategy.php index 9a5d223..65ec917 100644 --- a/src/Import/Strategy/Page/UpdateExistingPageStrategy.php +++ b/src/Import/Strategy/Document/UpdateExistingPageStrategy.php @@ -1,6 +1,6 @@ Date: Fri, 25 Jul 2025 17:18:46 +0200 Subject: [PATCH 19/25] [With or Without IDs][Export] small clean up --- src/Import/Strategy/ReplaceExistingElementStrategy.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Import/Strategy/ReplaceExistingElementStrategy.php b/src/Import/Strategy/ReplaceExistingElementStrategy.php index 96bfae5..3e11fad 100644 --- a/src/Import/Strategy/ReplaceExistingElementStrategy.php +++ b/src/Import/Strategy/ReplaceExistingElementStrategy.php @@ -2,9 +2,7 @@ namespace Neusta\Pimcore\ImportExportBundle\Import\Strategy; -use Neusta\Pimcore\ImportExportBundle\Import\Strategy\MergeElementStrategy; use Pimcore\Model\Element\AbstractElement; -use Pimcore\Model\Element\DuplicateFullPathException; /** * @implements MergeElementStrategy @@ -12,7 +10,7 @@ class ReplaceExistingElementStrategy implements MergeElementStrategy { /** - * @throws DuplicateFullPathException + * @throws \RuntimeException */ public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newElement): void { From fb7551765de6cdaa1aa7c3a17efb2cbb1177e974 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Mon, 28 Jul 2025 09:34:24 +0200 Subject: [PATCH 20/25] [With or With IDs][Review] some resolved conversations --- src/Command/Base/AbstractImportBaseCommand.php | 7 +++++++ .../Normalizer/PrioritizedAttributesNormalizer.php | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Command/Base/AbstractImportBaseCommand.php b/src/Command/Base/AbstractImportBaseCommand.php index dfc7376..2005001 100644 --- a/src/Command/Base/AbstractImportBaseCommand.php +++ b/src/Command/Base/AbstractImportBaseCommand.php @@ -37,6 +37,13 @@ protected function configure(): void 'The format of the input file: yaml, json', '' ) + ->addOption( + 'overwrite', + null, + InputOption::VALUE_NONE, + 'Overwrite or update existing elements in case of key-path-id matching [default: false]', + false + ) ->addOption( 'dry-run', null, diff --git a/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php index f73ccb8..c8cc96c 100644 --- a/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php +++ b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php @@ -18,7 +18,7 @@ class PrioritizedAttributesNormalizer implements NormalizerInterface public function __construct(ObjectNormalizer $normalizer, array $priorities = []) { $this->normalizer = $normalizer; - $this->priorities = $priorities; // z.B.: ['type', 'id', 'parentId', 'path', ...] + $this->priorities = $priorities; // e.g.: ['type', 'id', 'parentId', 'path', ...] } /** @@ -42,7 +42,7 @@ public function normalize($object, ?string $format = null, array $context = []) } } - // Hänge alle restlichen Properties hinten dran + // Append all remaining properties at the end return array_merge($sorted, $data); } From fd5b1dd4c01952ee15fbd6b7678788e5a60153cf Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Mon, 28 Jul 2025 14:21:25 +0200 Subject: [PATCH 21/25] [With or With IDs][Review] fix failing tests --- composer.json | 2 +- .../ImportLoggingEventSubscriber.php | 12 +++-- src/Import/Importer.php | 2 +- src/Populator/PageImportPopulator.php | 24 ++++----- tests/Integration/Import/ImporterTest.php | 54 +++++++++++++++---- tests/app/config/services.yaml | 3 +- 6 files changed, 68 insertions(+), 29 deletions(-) diff --git a/composer.json b/composer.json index 73a7d6a..625e86c 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0", "ext-zip": "*", - "pimcore/pimcore": "^11.0", + "pimcore/pimcore": "^11.5.4", "symfony/config": "^6.4", "symfony/console": "^6.4", "symfony/dependency-injection": "^6.4", diff --git a/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php b/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php index a56f3fa..623d6b5 100644 --- a/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php +++ b/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php @@ -6,25 +6,31 @@ use Neusta\Pimcore\ImportExportBundle\Import\Event\ImportStatus; use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelInterface; class ImportLoggingEventSubscriber implements EventSubscriberInterface { public function __construct( private readonly ApplicationLogger $applicationLogger, + private readonly KernelInterface $kernel, ) { } public static function getSubscribedEvents() { return [ - // ImportEvent::class => [ - // ['logImportEvent', 100], - // ], + ImportEvent::class => [ + ['logImportEvent', 100], + ], ]; } public function logImportEvent(ImportEvent $event): void { + if ('test' === $this->kernel->getEnvironment()) { + return; // In our integration tests we have no installed Pimcore Application Logger bundle at the moment + } + if (\in_array( $event->getStatus(), [ImportStatus::INCONSISTENCY, ImportStatus::FAILED] diff --git a/src/Import/Importer.php b/src/Import/Importer.php index c8be435..a00f082 100644 --- a/src/Import/Importer.php +++ b/src/Import/Importer.php @@ -78,10 +78,10 @@ public function import(string $yamlInput, string $format, bool $forcedSave, bool $this->parentRelationResolver->resolve($result); try { $result->save(['versionNote' => 'created by pimcore-import-export-bundle']); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::CREATED, $typeKey, $element, $result, null)); } catch (\Exception $e) { $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $typeKey, $element, $result, $oldElement, $e->getMessage())); } - $this->dispatcher->dispatch(new ImportEvent(ImportStatus::CREATED, $typeKey, $element, $result, null)); } elseif ($overwrite) { if ($this->newElementHasNoValidId($result) || $this->bothHaveSameId($oldElement, $result)) { // Update existing element by new one diff --git a/src/Populator/PageImportPopulator.php b/src/Populator/PageImportPopulator.php index ccd3886..baba0e2 100644 --- a/src/Populator/PageImportPopulator.php +++ b/src/Populator/PageImportPopulator.php @@ -30,20 +30,20 @@ public function __construct( */ public function populate(object $target, object $source, ?object $ctx = null): void { - if ($target instanceof PimcoreDocument\PageSnippet && \is_array($source['properties'])) { - foreach ($source['properties'] as $property) { - if ($property['value'] && 'asset' === $property['type']) { - $value = $this->assetRepository->getByPath($property['value']); - } elseif ($property['value'] && 'document' === $property['type']) { - $value = $this->documentRepository->getByPath($property['value']); - } elseif ($property['value'] && 'object' === $property['type']) { - $value = $this->objectRepository->getByPath($property['value']); - } else { - $value = $property['value']; - } - $target->setProperty($property['key'], $property['type'], $value); + foreach ($source['properties'] ?? [] as $property) { + if ($property['value'] && 'asset' === $property['type']) { + $value = $this->assetRepository->getByPath($property['value']); + } elseif ($property['value'] && 'document' === $property['type']) { + $value = $this->documentRepository->getByPath($property['value']); + } elseif ($property['value'] && 'object' === $property['type']) { + $value = $this->objectRepository->getByPath($property['value']); + } else { + $value = $property['value']; } + $target->setProperty($property['key'], $property['type'], $value); + } + if ($target instanceof PimcoreDocument\PageSnippet) { /** @var array{type: string, data: mixed} $editable */ foreach ($source['editables'] ?? [] as $key => $editable) { if (!isset($editable['data'])) { diff --git a/tests/Integration/Import/ImporterTest.php b/tests/Integration/Import/ImporterTest.php index bff0eaf..f5119e9 100755 --- a/tests/Integration/Import/ImporterTest.php +++ b/tests/Integration/Import/ImporterTest.php @@ -51,12 +51,22 @@ public function testSinglePageExport_regular_case_parent_id(): void type: email published: false path: /path/will/be/overwritten/by/parent_id/ - language: fr - navigation_name: 'My Document' - navigation_title: 'My Document - Title' key: test_document_1 title: 'The Title of My Document' controller: /Some/Controller + properties: + language: + type: string + key: language + value: fr + navigation_name: + type: string + key: navigation_name + value: 'My Document' + navigation_title: + type: string + key: navigation_title + value: 'My Document - Title' YAML; $pages = $this->importer->import($yaml, 'yaml', true, true); @@ -85,9 +95,6 @@ public function testSinglePageExport_regular_case_json(): void "type": "email", "published": false, "path": "\/path\/will\/be\/overwritten\/by\/parent_id/\/", - "language": "fr", - "navigation_name": "My Document", - "navigation_title": "My Document - Title", "key": "test_document_1", "title": "The Title of My Document", "controller": "\/Some\/Controller", @@ -97,7 +104,24 @@ public function testSinglePageExport_regular_case_json(): void "name": "textInput", "data": "some text input" } - ] + ], + "properties": { + "language": { + "type": "string", + "key": "language", + "value": "fr" + }, + "navigation_name": { + "type": "string", + "key": "navigation_name", + "value": "My Document" + }, + "navigation_title": { + "type": "string", + "key": "navigation_title", + "value": "My Document - Title" + } + } } } ] @@ -129,12 +153,22 @@ public function testSinglePageExport_regular_case_path(): void type: email published: false path: / - language: en - navigation_name: 'My Document' - navigation_title: 'My Document - Title' key: test_document_1 title: 'The Title of My Document' controller: /Some/Controller + properties: + language: + type: string + key: language + value: en + navigation_name: + type: string + key: navigation_name + value: 'My Document' + navigation_title: + type: string + key: navigation_title + value: 'My Document - Title' YAML; $pages = $this->importer->import($yaml, 'yaml', true, true); diff --git a/tests/app/config/services.yaml b/tests/app/config/services.yaml index f630cd8..c361b74 100755 --- a/tests/app/config/services.yaml +++ b/tests/app/config/services.yaml @@ -43,7 +43,7 @@ services: ################## # Merge Strategy # ################## - Neusta\Pimcore\ImportExportBundle\Import\Strategy\Page\UpdateExistingPageStrategy: + Neusta\Pimcore\ImportExportBundle\Import\Strategy\Document\UpdateExistingPageStrategy: tags: - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document' } - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Page' } @@ -67,4 +67,3 @@ services: - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document' } - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\Page' } - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\PageSnippet' } - From baa2e8b5b08807ac8af49973d2856fb5704776b7 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Mon, 28 Jul 2025 14:21:25 +0200 Subject: [PATCH 22/25] [With or With IDs][Review] CodeRabbit review fixed --- .../Admin/Base/AbstractImportBaseController.php | 3 ++- src/Controller/Admin/ImportAssetsController.php | 4 +++- src/Controller/Admin/ImportDataObjectsController.php | 4 +++- src/Controller/Admin/ImportDocumentsController.php | 4 +++- .../EventSubscriber/StatisticsEventSubscriber.php | 12 ++++++------ src/Import/Importer.php | 2 +- .../Strategy/Document/UpdateExistingPageStrategy.php | 8 +++++++- tests/Integration/Import/ImporterExporterTest.php | 8 +++++--- 8 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/Controller/Admin/Base/AbstractImportBaseController.php b/src/Controller/Admin/Base/AbstractImportBaseController.php index 0c92142..b7f5c29 100644 --- a/src/Controller/Admin/Base/AbstractImportBaseController.php +++ b/src/Controller/Admin/Base/AbstractImportBaseController.php @@ -42,6 +42,7 @@ abstract class AbstractImportBaseController */ public function __construct( protected ApplicationLogger $applicationLogger, + protected StatisticsEventSubscriber $statisticsEventSubscriber, protected ImportRepositoryInterface $repository, protected ParentRelationResolver $parentRelationResolver, protected string $elementType = 'Element', @@ -70,7 +71,7 @@ public function import(Request $request): JsonResponse } } - return $this->createJsonResponse(true, $this->createResultMessage(StatisticsEventSubscriber::getStatistics())); + return $this->createJsonResponse(true, $this->createResultMessage($this->statisticsEventSubscriber->getStatistics())); } protected function createJsonResponse(bool $success, string $message, int $statusCode = 200): JsonResponse diff --git a/src/Controller/Admin/ImportAssetsController.php b/src/Controller/Admin/ImportAssetsController.php index ada6b8e..14ccc91 100644 --- a/src/Controller/Admin/ImportAssetsController.php +++ b/src/Controller/Admin/ImportAssetsController.php @@ -3,6 +3,7 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; +use Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber; use Neusta\Pimcore\ImportExportBundle\Import\Importer; use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Import\ZipImporter; @@ -24,12 +25,13 @@ final class ImportAssetsController extends AbstractImportBaseController */ public function __construct( ApplicationLogger $applicationLogger, + StatisticsEventSubscriber $statisticsEventSubscriber, private Importer $importer, ParentRelationResolver $parentRelationResolver, private ZipImporter $zipImporter, AssetRepository $assetRepository, ) { - parent::__construct($applicationLogger, $assetRepository, $parentRelationResolver, 'Asset'); + parent::__construct($applicationLogger, $statisticsEventSubscriber, $assetRepository, $parentRelationResolver, 'Asset'); } #[Route( diff --git a/src/Controller/Admin/ImportDataObjectsController.php b/src/Controller/Admin/ImportDataObjectsController.php index 5e9980f..afbdae8 100644 --- a/src/Controller/Admin/ImportDataObjectsController.php +++ b/src/Controller/Admin/ImportDataObjectsController.php @@ -3,6 +3,7 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; +use Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber; use Neusta\Pimcore\ImportExportBundle\Import\Importer; use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository; @@ -23,11 +24,12 @@ final class ImportDataObjectsController extends AbstractImportBaseController */ public function __construct( ApplicationLogger $applicationLogger, + StatisticsEventSubscriber $statisticsEventSubscriber, DataObjectRepository $repository, ParentRelationResolver $parentRelationResolver, private Importer $importer, ) { - parent::__construct($applicationLogger, $repository, $parentRelationResolver, 'DataObject'); + parent::__construct($applicationLogger, $statisticsEventSubscriber, $repository, $parentRelationResolver, 'DataObject'); } #[Route( diff --git a/src/Controller/Admin/ImportDocumentsController.php b/src/Controller/Admin/ImportDocumentsController.php index 4d77a21..df4a54c 100644 --- a/src/Controller/Admin/ImportDocumentsController.php +++ b/src/Controller/Admin/ImportDocumentsController.php @@ -3,6 +3,7 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; +use Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber; use Neusta\Pimcore\ImportExportBundle\Import\Importer; use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository; @@ -23,11 +24,12 @@ final class ImportDocumentsController extends AbstractImportBaseController */ public function __construct( ApplicationLogger $applicationLogger, + StatisticsEventSubscriber $statisticsEventSubscriber, DocumentRepository $repository, ParentRelationResolver $parentRelationResolver, private Importer $importer, ) { - parent::__construct($applicationLogger, $repository, $parentRelationResolver, 'Document'); + parent::__construct($applicationLogger, $statisticsEventSubscriber, $repository, $parentRelationResolver, 'Document'); } #[Route( diff --git a/src/Import/EventSubscriber/StatisticsEventSubscriber.php b/src/Import/EventSubscriber/StatisticsEventSubscriber.php index 6dd3024..a644082 100644 --- a/src/Import/EventSubscriber/StatisticsEventSubscriber.php +++ b/src/Import/EventSubscriber/StatisticsEventSubscriber.php @@ -9,7 +9,7 @@ class StatisticsEventSubscriber implements EventSubscriberInterface { /** @var array */ - protected static array $statistics = []; + private array $statistics = []; public static function getSubscribedEvents() { @@ -23,9 +23,9 @@ public static function getSubscribedEvents() /** * @return array */ - public static function getStatistics(): array + public function getStatistics(): array { - return self::$statistics; + return $this->statistics; } public function countStatistics(ImportEvent $event): void @@ -35,10 +35,10 @@ public function countStatistics(ImportEvent $event): void public function incrementCounter(ImportStatus $status): void { - if (\array_key_exists($status->value, self::$statistics)) { - ++self::$statistics[$status->value]; + if (\array_key_exists($status->value, $this->statistics)) { + ++$this->statistics[$status->value]; } else { - self::$statistics[$status->value] = 1; + $this->statistics[$status->value] = 1; } } } diff --git a/src/Import/Importer.php b/src/Import/Importer.php index a00f082..426cf6d 100644 --- a/src/Import/Importer.php +++ b/src/Import/Importer.php @@ -87,10 +87,10 @@ public function import(string $yamlInput, string $format, bool $forcedSave, bool // Update existing element by new one try { $mergeStrategy->mergeAndSave($oldElement, $result); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::UPDATED, $typeKey, $element, $result, $oldElement)); } catch (\Exception $e) { $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $typeKey, $element, $result, $oldElement, $e->getMessage())); } - $this->dispatcher->dispatch(new ImportEvent(ImportStatus::UPDATED, $typeKey, $element, $result, $oldElement)); } else { $this->dispatcher->dispatch(new ImportEvent( ImportStatus::INCONSISTENCY, $typeKey, $element, $result, $oldElement, diff --git a/src/Import/Strategy/Document/UpdateExistingPageStrategy.php b/src/Import/Strategy/Document/UpdateExistingPageStrategy.php index 65ec917..3e301ad 100644 --- a/src/Import/Strategy/Document/UpdateExistingPageStrategy.php +++ b/src/Import/Strategy/Document/UpdateExistingPageStrategy.php @@ -17,6 +17,7 @@ class UpdateExistingPageStrategy implements MergeElementStrategy * @param Document $newElement * * @throws DuplicateFullPathException + * @throws \InvalidArgumentException */ public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newElement): void { @@ -31,7 +32,12 @@ public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newEl $oldElement->setTitle($newElement->getTitle()); } foreach ($newElement->getProperties() as $property) { - $oldElement->setProperty($property->getName() ?? 'N/A', $property->getType() ?? 'N/A', $property->getData()); + $name = $property->getName(); + $type = $property->getType(); + if (null === $name || null === $type) { + continue; // Skip invalid properties + } + $oldElement->setProperty($name, $type, $property->getData()); } $oldElement->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); } diff --git a/tests/Integration/Import/ImporterExporterTest.php b/tests/Integration/Import/ImporterExporterTest.php index 35aa3cc..77e29f7 100755 --- a/tests/Integration/Import/ImporterExporterTest.php +++ b/tests/Integration/Import/ImporterExporterTest.php @@ -1,6 +1,6 @@ setParentId(1); $asset->setPath('/'); - $asset->setKey('logo_desktop.svg'); + $asset->setKey('logo_desktop_' . uniqid() . '.svg'); $asset->save(); + self::assertNotNull($asset->getId(), 'Asset should be saved successfully'); $document = new Page(); $document->setParentId(1); $document->setPath('/'); $document->setKey('Text für viele'); $document->save(); + self::assertNotNull($document->getId(), 'Page should be saved successfully'); } public function testImportExport_regular_case(): void @@ -47,6 +49,6 @@ public function testImportExport_regular_case(): void $document = Page::getByPath('/Test-Import-Export'); $yamlExported = $this->exporter->export([$document], 'yaml'); - self::assertEquals($yamlToImport, $yamlExported); + self::assertEquals(yaml_parse($yamlToImport), yaml_parse($yamlExported)); } } From f65536da27b4da86a76339213908e39fcb9a1221 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 5 Aug 2025 10:05:11 +0200 Subject: [PATCH 23/25] Update src/Import/EventSubscriber/StatisticsEventSubscriber.php Co-authored-by: Jacob Dreesen --- src/Import/EventSubscriber/StatisticsEventSubscriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Import/EventSubscriber/StatisticsEventSubscriber.php b/src/Import/EventSubscriber/StatisticsEventSubscriber.php index a644082..ede4b53 100644 --- a/src/Import/EventSubscriber/StatisticsEventSubscriber.php +++ b/src/Import/EventSubscriber/StatisticsEventSubscriber.php @@ -33,7 +33,7 @@ public function countStatistics(ImportEvent $event): void $this->incrementCounter($event->getStatus()); } - public function incrementCounter(ImportStatus $status): void + private function incrementCounter(ImportStatus $status): void { if (\array_key_exists($status->value, $this->statistics)) { ++$this->statistics[$status->value]; From 267f49d75863db20fb33dce15e8e0d7e5695d080 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 5 Aug 2025 11:56:48 +0200 Subject: [PATCH 24/25] [With or With IDs][Review] by J-Cop --- composer.json | 2 +- src/Export/Exporter.php | 2 +- src/Import/Event/ImportStatus.php | 10 ++-- .../ImportLoggingEventSubscriber.php | 2 +- src/Import/Importer.php | 12 ++--- .../PrioritizedAttributesNormalizer.php | 3 +- ...orterTest__test_single_page_export__1.yaml | 6 +-- ...__test_single_page_export_with_ids__1.yaml | 6 +-- .../Import/ImporterExporterTest.php | 4 +- tests/Integration/data/Text Editor.yaml | 30 ++++++------ tests/app/config/pimcore.yaml | 46 +++++++++++++++++++ 11 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 tests/app/config/pimcore.yaml diff --git a/composer.json b/composer.json index 625e86c..73a7d6a 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0", "ext-zip": "*", - "pimcore/pimcore": "^11.5.4", + "pimcore/pimcore": "^11.0", "symfony/config": "^6.4", "symfony/console": "^6.4", "symfony/dependency-injection": "^6.4", diff --git a/src/Export/Exporter.php b/src/Export/Exporter.php index a5bffdb..51b15d3 100644 --- a/src/Export/Exporter.php +++ b/src/Export/Exporter.php @@ -51,7 +51,7 @@ public function export(iterable $elements, string $format, array $ctxParams = [] $yamlContent = $this->typeToConverterMap[$type]->convert($element, $ctx); $yamlExportElements[] = [$type => $yamlContent]; } catch (ConverterException $e) { - $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $type, [], $element, null, $e->getMessage())); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Failed, $type, [], $element, null, $e->getMessage())); } continue 2; } diff --git a/src/Import/Event/ImportStatus.php b/src/Import/Event/ImportStatus.php index 61ae7a3..29e1b18 100644 --- a/src/Import/Event/ImportStatus.php +++ b/src/Import/Event/ImportStatus.php @@ -4,9 +4,9 @@ enum ImportStatus: string { - case SKIPPED = 'SKIPPED'; - case CREATED = 'CREATED'; - case UPDATED = 'UPDATED'; - case INCONSISTENCY = 'INCONSISTENCY'; - case FAILED = 'FAILED'; + case Skipped = 'SKIPPED'; + case Created = 'CREATED'; + case Updated = 'UPDATED'; + case Inconsistency = 'INCONSISTENCY'; + case Failed = 'FAILED'; } diff --git a/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php b/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php index 623d6b5..73b44e0 100644 --- a/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php +++ b/src/Import/EventSubscriber/ImportLoggingEventSubscriber.php @@ -33,7 +33,7 @@ public function logImportEvent(ImportEvent $event): void if (\in_array( $event->getStatus(), - [ImportStatus::INCONSISTENCY, ImportStatus::FAILED] + [ImportStatus::Inconsistency, ImportStatus::Failed] )) { $this->writeApplicationError($event); } else { diff --git a/src/Import/Importer.php b/src/Import/Importer.php index 426cf6d..3637790 100644 --- a/src/Import/Importer.php +++ b/src/Import/Importer.php @@ -78,22 +78,22 @@ public function import(string $yamlInput, string $format, bool $forcedSave, bool $this->parentRelationResolver->resolve($result); try { $result->save(['versionNote' => 'created by pimcore-import-export-bundle']); - $this->dispatcher->dispatch(new ImportEvent(ImportStatus::CREATED, $typeKey, $element, $result, null)); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Created, $typeKey, $element, $result, null)); } catch (\Exception $e) { - $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $typeKey, $element, $result, $oldElement, $e->getMessage())); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Failed, $typeKey, $element, $result, $oldElement, $e->getMessage())); } } elseif ($overwrite) { if ($this->newElementHasNoValidId($result) || $this->bothHaveSameId($oldElement, $result)) { // Update existing element by new one try { $mergeStrategy->mergeAndSave($oldElement, $result); - $this->dispatcher->dispatch(new ImportEvent(ImportStatus::UPDATED, $typeKey, $element, $result, $oldElement)); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Updated, $typeKey, $element, $result, $oldElement)); } catch (\Exception $e) { - $this->dispatcher->dispatch(new ImportEvent(ImportStatus::FAILED, $typeKey, $element, $result, $oldElement, $e->getMessage())); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Failed, $typeKey, $element, $result, $oldElement, $e->getMessage())); } } else { $this->dispatcher->dispatch(new ImportEvent( - ImportStatus::INCONSISTENCY, $typeKey, $element, $result, $oldElement, + ImportStatus::Inconsistency, $typeKey, $element, $result, $oldElement, <<dispatcher->dispatch(new ImportEvent(ImportStatus::SKIPPED, $typeKey, $element, $result, $oldElement)); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Skipped, $typeKey, $element, $result, $oldElement)); } } } diff --git a/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php index c8cc96c..bafd7b2 100644 --- a/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php +++ b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php @@ -2,6 +2,7 @@ namespace Neusta\Pimcore\ImportExportBundle\Serializer\Normalizer; +use Neusta\Pimcore\ImportExportBundle\Model\Element; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -48,6 +49,6 @@ public function normalize($object, ?string $format = null, array $context = []) public function supportsNormalization($data, ?string $format = null): bool { - return \is_object($data) && $this->normalizer->supportsNormalization($data, $format); + return $data instanceof Element && $this->normalizer->supportsNormalization($data, $format); } } diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml index ecaca87..25ca2b8 100644 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml @@ -14,14 +14,14 @@ elements: data: 'some text input' properties: language: - type: string key: language + type: string value: en navigation_name: - type: string key: navigation_name + type: string value: 'My Document' navigation_title: - type: string key: navigation_title + type: string value: 'My Document - Title' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml index 898673a..6e50a63 100644 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml @@ -16,14 +16,14 @@ elements: data: 'some text input' properties: language: - type: string key: language + type: string value: en navigation_name: - type: string key: navigation_name + type: string value: 'My Document' navigation_title: - type: string key: navigation_title + type: string value: 'My Document - Title' diff --git a/tests/Integration/Import/ImporterExporterTest.php b/tests/Integration/Import/ImporterExporterTest.php index 77e29f7..98c3b3e 100755 --- a/tests/Integration/Import/ImporterExporterTest.php +++ b/tests/Integration/Import/ImporterExporterTest.php @@ -30,7 +30,7 @@ protected function setUp(): void $asset = new Asset(); $asset->setParentId(1); $asset->setPath('/'); - $asset->setKey('logo_desktop_' . uniqid() . '.svg'); + $asset->setKey('logo_desktop.svg'); $asset->save(); self::assertNotNull($asset->getId(), 'Asset should be saved successfully'); @@ -49,6 +49,6 @@ public function testImportExport_regular_case(): void $document = Page::getByPath('/Test-Import-Export'); $yamlExported = $this->exporter->export([$document], 'yaml'); - self::assertEquals(yaml_parse($yamlToImport), yaml_parse($yamlExported)); + self::assertEquals($yamlToImport, $yamlExported); } } diff --git a/tests/Integration/data/Text Editor.yaml b/tests/Integration/data/Text Editor.yaml index 869593b..69d0e88 100644 --- a/tests/Integration/data/Text Editor.yaml +++ b/tests/Integration/data/Text Editor.yaml @@ -134,62 +134,62 @@ elements: data: 1col properties: navigation_accesskey: - type: text key: navigation_accesskey + type: text value: '' navigation_anchor: - type: text key: navigation_anchor + type: text value: '' navigation_class: - type: text key: navigation_class + type: text value: '' navigation_exclude: - type: bool key: navigation_exclude + type: bool value: false navigation_name: - type: text key: navigation_name + type: text value: '' navigation_parameters: - type: text key: navigation_parameters + type: text value: '' navigation_relation: - type: text key: navigation_relation + type: text value: '' navigation_tabindex: - type: text key: navigation_tabindex + type: text value: '' navigation_target: - type: text key: navigation_target + type: text value: ~ navigation_title: - type: text key: navigation_title + type: text value: '' testPropertyAsset: - type: asset key: testPropertyAsset + type: asset value: /logo_desktop.svg testPropertyCheckbox: - type: bool key: testPropertyCheckbox + type: bool value: false testPropertyKey: - type: document key: testPropertyKey + type: document value: '/Text für viele' testPropertyObject: - type: object key: testPropertyObject + type: object value: ~ testPropertyString: - type: text key: testPropertyString + type: text value: 'Ich bin eine Zeichenkette' diff --git a/tests/app/config/pimcore.yaml b/tests/app/config/pimcore.yaml new file mode 100644 index 0000000..5145eda --- /dev/null +++ b/tests/app/config/pimcore.yaml @@ -0,0 +1,46 @@ +framework: + html_sanitizer: + sanitizers: + pimcore.wysiwyg_sanitizer: + max_input_length: -1 + allow_attributes: + pimcore_type: '*' + pimcore_id: '*' + allow_relative_links: true + allow_relative_medias: true + allow_elements: + span: [ 'class', 'style', 'id' ] + div: [ 'class', 'style', 'id' ] + p: [ 'class', 'style', 'id' ] + strong: 'class' + em: 'class' + h1: [ 'class', 'id' ] + h2: [ 'class', 'id' ] + h3: [ 'class', 'id' ] + h4: [ 'class', 'id' ] + h5: [ 'class', 'id' ] + h6: [ 'class', 'id' ] + a: [ 'class', 'id', 'href', 'target', 'title', 'rel', 'style' ] + table: [ 'class', 'style', 'cellspacing', 'cellpadding', 'border', 'width', 'height', 'id' ] + colgroup: 'class' + col: [ 'class', 'style', 'id' ] + thead: [ 'class', 'id' ] + tbody: [ 'class', 'id' ] + tr: [ 'class', 'id' ] + td: [ 'class', 'id' ] + th: [ 'class', 'id', 'scope' ] + ul: [ 'class', 'style', 'id' ] + li: [ 'class', 'style', 'id' ] + ol: [ 'class', 'style', 'id' ] + u: [ 'class', 'id' ] + i: [ 'class', 'id' ] + b: [ 'class', 'id' ] + caption: [ 'class', 'id' ] + sub: [ 'class', 'id' ] + sup: [ 'class', 'id' ] + blockquote: [ 'class', 'id' ] + s: [ 'class', 'id' ] + iframe: [ 'frameborder', 'height', 'longdesc', 'name', 'sandbox', 'scrolling', 'src', 'title', 'width' ] + br: '' + img: [ 'class', 'id', 'alt', 'style', 'src' ] + hr: '' From bdb26355782719e4883d1db8ae0729734d7e9d8a Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Tue, 5 Aug 2025 13:07:35 +0200 Subject: [PATCH 25/25] [With or With IDs][Review] fixed test for WYSIWYG editable --- README.md | 12 ++++++++++++ composer.json | 7 +++++-- tests/app/config/{ => packages}/pimcore.yaml | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) rename tests/app/config/{ => packages}/pimcore.yaml (91%) diff --git a/README.md b/README.md index 776ebe6..3dffd4f 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,18 @@ All import commands follow a similar structure and support the following common - `--input` or `-i`: Path to the input YAML file (required for import commands). - `--dry-run`: Perform the operation without persisting data (only available for import commands). +### Notice: WYSIWYG - Editable + +For using a correct exporting and importing of WYSIWYG editables in older Pimcore versions (e.g. using `symfony/framework < 6.2.2`) you probably need to configure your Symfony HTML sanitizer as documented here: +https://docs.pimcore.com/platform/2024.4/Pimcore/Documents/Editables/WYSIWYG#extending-symfony-html-sanitizer-configuration + +In our integration test we have used the following configuration: + +```yaml +tests/app/config/packages/pimcore.yaml +``` +because of already known bugs with `` - HTML tag. + ## Concepts ### Page Export diff --git a/composer.json b/composer.json index 73a7d6a..ee088fe 100644 --- a/composer.json +++ b/composer.json @@ -34,11 +34,14 @@ "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", "phpstan/phpstan-symfony": "^1.3.8", - "phpunit/phpunit": "^9.5", - "pimcore/admin-ui-classic-bundle": "^1.6", + "phpunit/phpunit": "^9.6", + "pimcore/admin-ui-classic-bundle": "^1.0", "spatie/phpunit-snapshot-assertions": "^4.2", "teamneusta/pimcore-testing-framework": "^0.12" }, + "conflict": { + "masterminds/html5": "< 2.8.1" + }, "autoload": { "psr-4": { "Neusta\\Pimcore\\ImportExportBundle\\": "src/" diff --git a/tests/app/config/pimcore.yaml b/tests/app/config/packages/pimcore.yaml similarity index 91% rename from tests/app/config/pimcore.yaml rename to tests/app/config/packages/pimcore.yaml index 5145eda..5c0ad5f 100644 --- a/tests/app/config/pimcore.yaml +++ b/tests/app/config/packages/pimcore.yaml @@ -6,6 +6,7 @@ framework: allow_attributes: pimcore_type: '*' pimcore_id: '*' + allowed_link_schemes: ['http', 'https', 'mailto'] # necessary for symfony/framework < 6.2.2 [https://github.com/symfony/symfony/issues/48556] allow_relative_links: true allow_relative_medias: true allow_elements: