From 12852a925d5a5851eaef2fc48e55246835af1f5d Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Fri, 28 Feb 2025 13:01:54 +0100 Subject: [PATCH 01/28] [Feature] enable exporting and importing several pages by ONE yaml file --- src/Controller/Admin/PageExportController.php | 4 +- src/Controller/Admin/PageImportController.php | 45 +++++++++++++++---- src/Documents/Export/PageExporter.php | 35 ++++++++++++--- src/Documents/Export/YamlExportPage.php | 19 ++++---- src/Documents/Import/PageImporter.php | 34 ++++++++++---- 5 files changed, 103 insertions(+), 34 deletions(-) diff --git a/src/Controller/Admin/PageExportController.php b/src/Controller/Admin/PageExportController.php index d7cbaf2..65bff58 100644 --- a/src/Controller/Admin/PageExportController.php +++ b/src/Controller/Admin/PageExportController.php @@ -9,7 +9,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; final class PageExportController { @@ -37,7 +37,7 @@ public function exportPage(Request $request): Response } try { - $yaml = $this->pageExporter->toYaml($page); + $yaml = $this->pageExporter->toYaml([$page]); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/src/Controller/Admin/PageImportController.php b/src/Controller/Admin/PageImportController.php index 1c31393..1eefba0 100644 --- a/src/Controller/Admin/PageImportController.php +++ b/src/Controller/Admin/PageImportController.php @@ -10,14 +10,28 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; final class PageImportController { + public const ERR_NO_FILE_UPLOADED = 1; + public const SUCCESS_DOCUMENT_REPLACEMENT = 2; + public const SUCCESS_WITHOUT_REPLACEMENT = 3; + public const SUCCESS_NEW_DOCUMENT = 4; + + /** string[] */ + private array $messagesMap; + public function __construct( private PageImporter $pageImporter, private PageRepository $pageRepository, ) { + $this->messagesMap = [ + self::ERR_NO_FILE_UPLOADED => 'No file uploaded', + self::SUCCESS_DOCUMENT_REPLACEMENT => 'Documents replaced successfully', + self::SUCCESS_WITHOUT_REPLACEMENT => 'Documents already exist and were not replaced', + self::SUCCESS_NEW_DOCUMENT => 'New Documents imported successfully', + ]; } #[Route( @@ -29,23 +43,28 @@ public function import(Request $request): JsonResponse { $file = $request->files->get('file'); if (!$file instanceof UploadedFile) { - return new JsonResponse(['success' => false, 'message' => 'No file uploaded'], 400); + return new JsonResponse(['success' => false, 'message' => self::ERR_NO_FILE_UPLOADED], 400); } $overwrite = $request->request->getBoolean('overwrite'); try { - $page = $this->pageImporter->parseYaml($file->getContent()); + $pages = $this->pageImporter->parseYaml($file->getContent()); - $message = $this->replaceIfExists($page, $overwrite); + $resultMessage = 'Import Summary:' . \PHP_EOL; + + foreach ($pages as $index => $page) { + $resultCode = $this->replaceIfExists($page, $overwrite); + $resultMessage = $this->appendMessage($index, $resultCode, $resultMessage); + } - return new JsonResponse(['success' => true, 'message' => $message]); + return new JsonResponse(['success' => true, 'message' => $resultMessage]); } catch (\Exception $e) { return new JsonResponse(['success' => false, 'message' => $e->getMessage()], 500); } } - protected function replaceIfExists(Page $page, bool $overwrite): string + protected function replaceIfExists(Page $page, bool $overwrite): int { $oldPage = $this->pageRepository->getByPath('/' . $page->getFullPath()); if (null !== $oldPage) { @@ -53,13 +72,21 @@ protected function replaceIfExists(Page $page, bool $overwrite): string $oldPage->delete(); $page->save(); - return 'Document replaced successfully'; + return self::SUCCESS_DOCUMENT_REPLACEMENT; } - return 'Document already exists and was not replaced'; + return self::SUCCESS_WITHOUT_REPLACEMENT; } $page->save(); - return 'New Document imported successfully'; + return self::SUCCESS_NEW_DOCUMENT; + } + + private function appendMessage(int|string $index, int $resultCode, string $resultMessage): string + { + $message = \sprintf('%d. %s', $index + 1, $this->messagesMap[$resultCode]); + $resultMessage .= $message . \PHP_EOL; + + return $resultMessage; } } diff --git a/src/Documents/Export/PageExporter.php b/src/Documents/Export/PageExporter.php index 124a3c0..101cb63 100644 --- a/src/Documents/Export/PageExporter.php +++ b/src/Documents/Export/PageExporter.php @@ -4,13 +4,13 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; +use Neusta\ConverterBundle\Exception\ConverterException; use Pimcore\Model\Document\Page; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Yaml\Yaml; class PageExporter { - private const PAGE = 'page'; private const YAML_DUMP_FLAGS = Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK | @@ -25,16 +25,39 @@ public function __construct( ) { } - public function toYaml(Page $page): string + /** + * Exports one or more pages as YAML with the following structure: + * pages: + * - page: + * key: 'page_key_1' + * - page: + * key: 'page_key_2' + * ... + * + * @param Page|iterable $pages + * + * @throws ConverterException + * + * @deprecated parameter type Page will not be allowed in further versions + */ + public function toYaml(Page|iterable $pages): string { - $yamlExportPage = $this->pageToYamlConverter->convert($page); + // @deprecated - should be removed after changing the method signature + if ($pages instanceof Page) { + $pages = [$pages]; + } + + $yamlExportPages = []; + foreach ($pages as $page) { + $yamlExportPages[] = [YamlExportPage::PAGE => $this->pageToYamlConverter->convert($page)]; + } return $this->serializer->serialize( - [self::PAGE => $yamlExportPage], + [YamlExportPage::PAGES => $yamlExportPages], 'yaml', [ - 'yaml_inline' => 4, // how many levels should be used before inline YAML - 'yaml_indent' => 0, // how many indentations should be used from the very beginning + 'yaml_inline' => 4, + 'yaml_indent' => 0, 'yaml_flags' => self::YAML_DUMP_FLAGS, ] ); diff --git a/src/Documents/Export/YamlExportPage.php b/src/Documents/Export/YamlExportPage.php index 8fb7652..a4a29c8 100644 --- a/src/Documents/Export/YamlExportPage.php +++ b/src/Documents/Export/YamlExportPage.php @@ -4,19 +4,22 @@ class YamlExportPage { + public const PAGES = 'pages'; + public const PAGE = 'page'; + public ?int $id = null; - public int $parentId; + public int $parentId = 0; public string $type = 'page'; public bool $published = false; - public string $path; - public string $language; - public string $navigation_name; + public string $path = ''; + public string $language = ''; + public string $navigation_name = ''; public ?string $navigation_title = null; - public string $key; - public string $title; - public string $controller; + public string $key = ''; + public string $title = ''; + public string $controller = ''; /** @var array */ - public array $editables; + public array $editables = []; /** * @param array|null $yamlConfig diff --git a/src/Documents/Import/PageImporter.php b/src/Documents/Import/PageImporter.php index b65ea31..d29e18b 100644 --- a/src/Documents/Import/PageImporter.php +++ b/src/Documents/Import/PageImporter.php @@ -4,14 +4,14 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; +use Neusta\ConverterBundle\Exception\ConverterException; use Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage; use Pimcore\Model\Document\Page; +use Pimcore\Model\Element\DuplicateFullPathException; use Symfony\Component\Yaml\Yaml; class PageImporter { - private const PAGE = 'page'; - /** * @param Converter $yamlToPageConverter */ @@ -20,20 +20,36 @@ public function __construct( ) { } - public function parseYaml(string $yamlInput, bool $forcedSave = true): mixed + /** + * @return array + * + * @throws ConverterException + * @throws DuplicateFullPathException + */ + public function parseYaml(string $yamlInput, bool $forcedSave = true): array { $config = Yaml::parse($yamlInput); - if (!\is_array($config) || !\is_array($config[self::PAGE] ?? null)) { - throw new \DomainException('Given YAML is not a valid page.'); + if (!\is_array($config) || !\is_array($config[YamlExportPage::PAGES] ?? null)) { + throw new \DomainException('Given YAML is not valid.'); } - $page = $this->yamlToPageConverter->convert(new YamlExportPage($config[self::PAGE])); - if ($forcedSave) { - $page->save(); + $pages = []; + + foreach ($config[YamlExportPage::PAGES] ?? [] as $configPage) { + $page = null; + if (\is_array($configPage)) { + $page = $this->yamlToPageConverter->convert(new YamlExportPage($configPage)); + if ($forcedSave) { + $page->save(); + } + } + if ($page) { + $pages[] = $page; + } } - return $page; + return $pages; } /** From 3647382e6b0ede594ef7e983846296164c2a9951 Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Fri, 28 Feb 2025 14:17:34 +0100 Subject: [PATCH 02/28] [Fix] use ['page'] instead of config --- src/Documents/Import/PageImporter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Documents/Import/PageImporter.php b/src/Documents/Import/PageImporter.php index d29e18b..a4168fd 100644 --- a/src/Documents/Import/PageImporter.php +++ b/src/Documents/Import/PageImporter.php @@ -38,8 +38,8 @@ public function parseYaml(string $yamlInput, bool $forcedSave = true): array foreach ($config[YamlExportPage::PAGES] ?? [] as $configPage) { $page = null; - if (\is_array($configPage)) { - $page = $this->yamlToPageConverter->convert(new YamlExportPage($configPage)); + if (\is_array($configPage[YamlExportPage::PAGE])) { + $page = $this->yamlToPageConverter->convert(new YamlExportPage($configPage[YamlExportPage::PAGE])); if ($forcedSave) { $page->save(); } From ac6271e1d2e66ac6698d7e6c191b00610c19a8a8 Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Sat, 1 Mar 2025 21:05:47 +0100 Subject: [PATCH 03/28] [Feature] Add integration tests for PageExporter Add Menu and Controller route for export page incl. subpages --- compose.yaml | 2 +- composer.json | 1 + config/pimcore/config.yaml | 1 + config/services.yaml | 3 + public/js/exportPage.js | 10 ++ src/Controller/Admin/PageExportController.php | 35 +++++- src/Controller/Admin/PageImportController.php | 6 +- src/Documents/Export/PageExporter.php | 34 ++++-- src/Documents/Export/YamlExportPage.php | 6 +- src/Documents/Import/PageImporter.php | 2 +- src/Toolbox/Repository/PageRepository.php | 14 +++ .../Documents/Export/PageExporterTest.php | 105 ++++++++++++++++++ .../Documents/ImportExportYamlDriver.php | 30 +++++ .../Admin/PageExportControllerTest.php | 4 +- tests/app/config/services.yaml | 7 ++ translations/admin.de.yaml | 1 + translations/admin.en.yaml | 1 + 17 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 tests/Integration/Documents/Export/PageExporterTest.php create mode 100644 tests/Integration/Documents/ImportExportYamlDriver.php create mode 100644 tests/app/config/services.yaml diff --git a/compose.yaml b/compose.yaml index a6a5d8f..4b9d516 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,7 +16,7 @@ services: timeout: 10s php: - image: pimcore/pimcore:php8.3-latest + image: pimcore/pimcore:php8.3-debug-latest volumes: - ./:/var/www/html/ environment: diff --git a/composer.json b/composer.json index a938e4f..5e776ec 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "phpstan/phpstan-symfony": "^1.3.8", "phpunit/phpunit": "^9.5", "pimcore/admin-ui-classic-bundle": "^1.6", + "spatie/phpunit-snapshot-assertions": "^4.2", "teamneusta/pimcore-testing-framework": "^0.12" }, "autoload": { diff --git a/config/pimcore/config.yaml b/config/pimcore/config.yaml index 7c843c2..2b4daea 100644 --- a/config/pimcore/config.yaml +++ b/config/pimcore/config.yaml @@ -31,6 +31,7 @@ neusta_converter: path: source: path default: '/' + skip_null: true parentId: source: parentId default: 0 diff --git a/config/services.yaml b/config/services.yaml index f9fb6ac..02cd447 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -41,18 +41,21 @@ services: arguments: $propertyKey: 'language' $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 arguments: $propertyKey: 'navigation_name' $targetProperty: 'navigation_name' + $skipNull: true Neusta\Pimcore\ImportExportBundle\Documents\Export\PageExporter: arguments: diff --git a/public/js/exportPage.js b/public/js/exportPage.js index 9a771fe..1f06883 100755 --- a/public/js/exportPage.js +++ b/public/js/exportPage.js @@ -8,6 +8,7 @@ neusta_pimcore_import_export.plugin.page.export = Class.create({ onPrepareDocumentTreeContextMenu: function (e) { let menu = e.detail.menu; let document = e.detail.document; + // Export page into yaml file menu.add("-"); menu.add(new Ext.menu.Item({ @@ -17,6 +18,15 @@ neusta_pimcore_import_export.plugin.page.export = Class.create({ pimcore.helpers.download(Routing.generate('neusta_pimcore_import_export_page_export', {page_id: document.data.id})); } })); + + // Export page and children into yaml file + menu.add(new Ext.menu.Item({ + text: t('neusta_pimcore_import_export_export_with_children_menu_label'), + iconCls: "pimcore_icon_export", + handler: function () { + pimcore.helpers.download(Routing.generate('neusta_pimcore_import_export_page_export_with_children', {page_id: document.data.id})); + } + })); }, }); diff --git a/src/Controller/Admin/PageExportController.php b/src/Controller/Admin/PageExportController.php index 65bff58..14dafc9 100644 --- a/src/Controller/Admin/PageExportController.php +++ b/src/Controller/Admin/PageExportController.php @@ -37,7 +37,40 @@ public function exportPage(Request $request): Response } try { - $yaml = $this->pageExporter->toYaml([$page]); + $yaml = $this->pageExporter->exportToYaml([$page]); + } catch (\Exception $e) { + return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $response = new Response($yaml); + $response->headers->set('Content-type', 'application/yaml'); + $response->headers->set( + 'Content-Disposition', + HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $this->createFilename($page)), + ); + + return $response; + } + + #[Route( + '/admin/neusta/import-export/page/export/with-children', + name: 'neusta_pimcore_import_export_page_export_with_children', + methods: ['GET'] + )] + public function exportPageWithChildren(Request $request): Response + { + $pageId = $request->query->getInt('page_id'); + $page = $this->pageRepository->getById($pageId); + + if (!$page instanceof Page) { + return new JsonResponse( + \sprintf('Page with id "%s" was not found', $pageId), + Response::HTTP_NOT_FOUND, + ); + } + + try { + $yaml = $this->pageExporter->exportToYaml($this->pageRepository->findAllPagesWithSubPages($page)); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/src/Controller/Admin/PageImportController.php b/src/Controller/Admin/PageImportController.php index 1eefba0..e2a50e5 100644 --- a/src/Controller/Admin/PageImportController.php +++ b/src/Controller/Admin/PageImportController.php @@ -19,7 +19,9 @@ final class PageImportController public const SUCCESS_WITHOUT_REPLACEMENT = 3; public const SUCCESS_NEW_DOCUMENT = 4; - /** string[] */ + /** + * @var string[] Map of error codes to messages + */ private array $messagesMap; public function __construct( @@ -84,7 +86,7 @@ protected function replaceIfExists(Page $page, bool $overwrite): int private function appendMessage(int|string $index, int $resultCode, string $resultMessage): string { - $message = \sprintf('%d. %s', $index + 1, $this->messagesMap[$resultCode]); + $message = \sprintf('%d. %s', (int) $index + 1, $this->messagesMap[$resultCode]); $resultMessage .= $message . \PHP_EOL; return $resultMessage; diff --git a/src/Documents/Export/PageExporter.php b/src/Documents/Export/PageExporter.php index 101cb63..496f7ed 100644 --- a/src/Documents/Export/PageExporter.php +++ b/src/Documents/Export/PageExporter.php @@ -11,7 +11,7 @@ class PageExporter { - private const YAML_DUMP_FLAGS = + public const YAML_DUMP_FLAGS = Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK | Yaml::DUMP_NULL_AS_TILDE; @@ -34,19 +34,35 @@ public function __construct( * key: 'page_key_2' * ... * - * @param Page|iterable $pages - * * @throws ConverterException * - * @deprecated parameter type Page will not be allowed in further versions + * @deprecated use PageExporter#exportToYaml([$page]) in further versions */ - public function toYaml(Page|iterable $pages): string + public function toYaml(Page $page): string { - // @deprecated - should be removed after changing the method signature - if ($pages instanceof Page) { - $pages = [$pages]; + $pages = []; + if ($page instanceof Page) { + $pages = [$page]; } + return $this->exportToYaml($pages); + } + + /** + * Exports one or more pages as YAML with the following structure: + * pages: + * - page: + * key: 'page_key_1' + * - page: + * key: 'page_key_2' + * ... + * + * @param iterable $pages + * + * @throws ConverterException + */ + public function exportToYaml(iterable $pages): string + { $yamlExportPages = []; foreach ($pages as $page) { $yamlExportPages[] = [YamlExportPage::PAGE => $this->pageToYamlConverter->convert($page)]; @@ -57,7 +73,7 @@ public function toYaml(Page|iterable $pages): string 'yaml', [ 'yaml_inline' => 4, - 'yaml_indent' => 0, + 'yaml_indent' => 2, 'yaml_flags' => self::YAML_DUMP_FLAGS, ] ); diff --git a/src/Documents/Export/YamlExportPage.php b/src/Documents/Export/YamlExportPage.php index a4a29c8..a2f387b 100644 --- a/src/Documents/Export/YamlExportPage.php +++ b/src/Documents/Export/YamlExportPage.php @@ -13,11 +13,11 @@ class YamlExportPage public bool $published = false; public string $path = ''; public string $language = ''; - public string $navigation_name = ''; + public ?string $navigation_name = null; public ?string $navigation_title = null; public string $key = ''; - public string $title = ''; - public string $controller = ''; + public ?string $title = null; + public ?string $controller = null; /** @var array */ public array $editables = []; diff --git a/src/Documents/Import/PageImporter.php b/src/Documents/Import/PageImporter.php index a4168fd..7d657a8 100644 --- a/src/Documents/Import/PageImporter.php +++ b/src/Documents/Import/PageImporter.php @@ -36,7 +36,7 @@ public function parseYaml(string $yamlInput, bool $forcedSave = true): array $pages = []; - foreach ($config[YamlExportPage::PAGES] ?? [] as $configPage) { + foreach ($config[YamlExportPage::PAGES] as $configPage) { $page = null; if (\is_array($configPage[YamlExportPage::PAGE])) { $page = $this->yamlToPageConverter->convert(new YamlExportPage($configPage[YamlExportPage::PAGE])); diff --git a/src/Toolbox/Repository/PageRepository.php b/src/Toolbox/Repository/PageRepository.php index 5453928..3b2505f 100644 --- a/src/Toolbox/Repository/PageRepository.php +++ b/src/Toolbox/Repository/PageRepository.php @@ -19,4 +19,18 @@ public function __construct() { parent::__construct(Page::class); } + + /** + * @return iterable + */ + public function findAllPagesWithSubPages(Page $page): iterable + { + yield $page; + + foreach ($page->getChildren() as $child) { + if ($child instanceof Page) { + yield from $this->findAllPagesWithSubPages($child); + } + } + } } diff --git a/tests/Integration/Documents/Export/PageExporterTest.php b/tests/Integration/Documents/Export/PageExporterTest.php new file mode 100644 index 0000000..d0a850e --- /dev/null +++ b/tests/Integration/Documents/Export/PageExporterTest.php @@ -0,0 +1,105 @@ +exporter = self::getContainer()->get(PageExporter::class); + } + + public function testSinglePageExport(): void + { + $page = new Page(); + $page->setId(999); + $page->setParentId(4); + $page->setType('email'); + $page->setPublished(false); + $page->setPath('/test/'); + $page->setKey('test_document_1'); + $page->setProperty('language', 'string', 'fr'); + $page->setProperty('navigation_name', 'string', 'Mein Dokument'); + $page->setProperty('navigation_title', 'string', 'Mein Dokument - Titel'); + $page->setTitle('Titel meines Dokuments'); + $page->setController('Irgend/ein/Controller'); + $inputEditable = new Input(); + $inputEditable->setName('textEingabe');; + $inputEditable->setDataFromResource('Texteingabe'); + $page->setEditables([$inputEditable]); + + $yaml = $this->exporter->exportToYaml([$page]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + + public function testSimpleSavedPagesExport(): void + { + $page1 = new Page(); + $page1->setParentId(1); + $page1->setKey('test_document_1'); + $page1->setTitle('Test Document_1'); + $page1->save(); + + $page2 = new Page(); + $page2->setParentId(1); + $page2->setKey('test_document_2'); + $page2->setTitle('Test Document_2'); + $page2->save(); + + $yaml = $this->exporter->exportToYaml([$page1, $page2]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + + public function testSimpleUnsavedPagesExport(): void + { + $page1 = new Page(); + $page1->setParentId(1); + $page1->setKey('test_document_1'); + $page1->setPath('/'); + + $page2 = new Page(); + $page2->setParentId(1); + $page2->setKey('test_document_2'); + $page2->setPath('/'); + + $yaml = $this->exporter->exportToYaml([$page1, $page2]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + + public function testTreePagesExport(): void + { + $page1 = new Page(); + $page1->setParentId(1); + $page1->setKey('test_document_1'); + $page1->setTitle('Test Document_1'); + $page1->save(); + + $page2 = new Page(); + $page2->setParentId($page1->getId()); + $page2->setKey('test_document_2'); + $page2->setTitle('Test Document_2'); + $page2->save(); + + $page3 = new Page(); + $page3->setParentId($page2->getId()); + $page3->setKey('test_document_2'); + $page3->setTitle('Test Document_2'); + $page3->save(); + + $yaml = $this->exporter->exportToYaml([$page1, $page2, $page3]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } +} diff --git a/tests/Integration/Documents/ImportExportYamlDriver.php b/tests/Integration/Documents/ImportExportYamlDriver.php new file mode 100644 index 0000000..55a07c0 --- /dev/null +++ b/tests/Integration/Documents/ImportExportYamlDriver.php @@ -0,0 +1,30 @@ +prophesize(Page::class); $this->pageRepository->getById(17)->willReturn($page->reveal()); - $this->pageExporter->toYaml($page->reveal())->willReturn('TEST_YAML'); + $this->pageExporter->exportToYaml($page->reveal())->willReturn('TEST_YAML'); $response = $this->controller->exportPage($this->request); @@ -57,7 +57,7 @@ public function testExportPage_exceptional_case(): void { $page = $this->prophesize(Page::class); $this->pageRepository->getById(17)->willReturn($page->reveal()); - $this->pageExporter->toYaml($page->reveal())->willThrow(new \Exception('Problem')); + $this->pageExporter->exportToYaml($page->reveal())->willThrow(new \Exception('Problem')); $response = $this->controller->exportPage($this->request); diff --git a/tests/app/config/services.yaml b/tests/app/config/services.yaml new file mode 100644 index 0000000..d158ecf --- /dev/null +++ b/tests/app/config/services.yaml @@ -0,0 +1,7 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: true + + Neusta\Pimcore\ImportExportBundle\Tests\Integration\Documents\ImportExportYamlDriver: ~ diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index 2eab111..280ba12 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -1,4 +1,5 @@ neusta_pimcore_import_export_export_menu_label: 'Exportiere in YAML' +neusta_pimcore_import_export_export_with_children_menu_label: 'Exportiere in YAML (inkl. Unterseiten)' neusta_pimcore_import_export_import_dialog_title: 'Importiere Seite aus YAML' neusta_pimcore_import_export_import_menu_label: 'Importiere aus YAML' neusta_pimcore_import_export_import_dialog_file_label: 'YAML Datei' diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index 46f94d2..e7e01e4 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -1,4 +1,5 @@ neusta_pimcore_import_export_export_menu_label: 'Export to YAML' +neusta_pimcore_import_export_export_with_children_menu_label: 'Export to YAML (incl. subpages)' neusta_pimcore_import_export_import_dialog_title: 'Import Page from YAML' neusta_pimcore_import_export_import_menu_label: 'Import from YAML' neusta_pimcore_import_export_import_dialog_file_label: 'YAML File' From d1754f91337754cb3536f29d151c2775994b943e Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Sat, 1 Mar 2025 21:44:06 +0100 Subject: [PATCH 04/28] [Feature] Moved import menu to main menu toolbar --- public/js/importPage.js | 157 ++++++++++-------- src/Controller/Admin/PageImportController.php | 40 +++-- .../Documents/Export/PageExporterTest.php | 2 +- .../Admin/PageExportControllerTest.php | 4 +- translations/admin.de.yaml | 2 +- translations/admin.en.yaml | 2 +- 6 files changed, 126 insertions(+), 81 deletions(-) diff --git a/public/js/importPage.js b/public/js/importPage.js index 4a15d5e..5862741 100644 --- a/public/js/importPage.js +++ b/public/js/importPage.js @@ -2,82 +2,109 @@ pimcore.registerNS("neusta_pimcore_import_export.plugin.page.import"); neusta_pimcore_import_export.plugin.page.import = Class.create({ initialize: function () { - document.addEventListener(pimcore.events.prepareDocumentTreeContextMenu, this.onPrepareDocumentTreeContextMenu.bind(this)); + document.addEventListener(pimcore.events.preMenuBuild, this.preMenuBuild.bind(this)); }, - onPrepareDocumentTreeContextMenu: function (e) { + preMenuBuild: function (e) { let menu = e.detail.menu; - let document = e.detail.document; - menu.add(new Ext.menu.Item({ - text: t('neusta_pimcore_import_export_import_menu_label'), - iconCls: "pimcore_icon_import", - handler: function () { - let uploadDialog = new Ext.Window({ - title: t('neusta_pimcore_import_export_import_dialog_title'), - width: 600, - layout: 'fit', - modal: true, + menu.neusta_pimcore_import_export = { + label: t('neusta_pimcore_import_export_import_menu_label'), + iconCls: 'pimcore_icon_import', // Pimcore-Icon für Konsistenz + priority: 50, // Position im Menü (höher = weiter oben) + handler: this.openImportDialog.bind(this), + noSubmenus: true // Kein Submenü, direkter Klick öffnet das Fenster + }; + }, + + openImportDialog: function () { + let uploadDialog = new Ext.Window({ + title: t('neusta_pimcore_import_export_import_dialog_title'), + width: 600, + layout: 'fit', + modal: true, + items: [ + new Ext.form.Panel({ + bodyPadding: 10, items: [ - new Ext.form.Panel({ - bodyPadding: 10, - items: [ - { - xtype: 'filefield', - name: 'file', - width: 450, - fieldLabel: t('neusta_pimcore_import_export_import_dialog_file_label'), - labelWidth: 100, - allowBlank: false, - buttonText: t('neusta_pimcore_import_export_import_dialog_file_button'), - accept: '.yaml,.yml' - }, - { - xtype: 'checkbox', - name: 'overwrite', - fieldLabel: t('neusta_pimcore_import_export_import_dialog_overwrite_label'), + { + xtype: 'filefield', + name: 'file', + width: 450, + fieldLabel: t('neusta_pimcore_import_export_import_dialog_file_label'), + labelWidth: 100, + allowBlank: false, + buttonText: t('neusta_pimcore_import_export_import_dialog_file_button'), + accept: '.yaml,.yml' + }, + { + xtype: 'checkbox', + name: 'overwrite', + fieldLabel: t('neusta_pimcore_import_export_import_dialog_overwrite_label'), + } + ], + buttons: [ + { + text: 'Import', + handler: function (btn) { + let form = btn.up('form').getForm(); + if (!form.isValid()) { + return; } - ], - buttons: [ - { - text: 'Import', - handler: function (btn) { - let form = btn.up('form').getForm(); - if (!form.isValid()) { - return; - } - form.submit({ - url: Routing.generate('neusta_pimcore_import_export_page_import'), - method: 'POST', - waitMsg: t('neusta_pimcore_import_export_import_dialog_wait_message'), - headers: { - 'X-Requested-With': 'XMLHttpRequest' // ✅ important for AJAX-Requests - }, - params: { - 'csrfToken': parent.pimcore.settings["csrfToken"] - }, - success: function (form, action) { - let response = Ext.decode(action.response.responseText); - pimcore.helpers.showNotification(t('neusta_pimcore_import_export_import_dialog_notification_success'), response.message, 'success'); - pimcore.globalmanager.get('layout_document_tree').tree.getStore().reload(); - uploadDialog.close(); - }, - failure: function (form, action) { - let response = Ext.decode(action.response.responseText); - pimcore.helpers.showNotification(t('neusta_pimcore_import_export_import_dialog_notification_error'), response.message || 'Import failed', 'error'); - } + form.submit({ + url: Routing.generate('neusta_pimcore_import_export_page_import'), + method: 'POST', + waitMsg: t('neusta_pimcore_import_export_import_dialog_wait_message'), + headers: { + 'X-Requested-With': 'XMLHttpRequest' // ✅ important for AJAX-Requests + }, + params: { + 'csrfToken': parent.pimcore.settings["csrfToken"] + }, + success: function (form, action) { + let response = Ext.decode(action.response.responseText); + // pimcore.helpers.showNotification(t('neusta_pimcore_import_export_import_dialog_notification_success'), response.message, 'success'); + let successDialog = new Ext.Window({ + title: t('neusta_pimcore_import_export_import_dialog_notification_success'), + width: 300, + height: 200, + modal: true, // ✅ Modal = Benutzer muss interagieren + layout: 'fit', + items: [ + { + xtype: 'panel', + html: `${response.message}`, + } + ], + buttons: [ + { + text: 'OK', + handler: function () { + successDialog.close(); + } + } + ] }); + + successDialog.show(); + + pimcore.globalmanager.get('layout_document_tree').tree.getStore().reload(); + uploadDialog.close(); + }, + failure: function (form, action) { + let response = Ext.decode(action.response.responseText); + pimcore.helpers.showNotification(t('neusta_pimcore_import_export_import_dialog_notification_error'), response.message || 'Import failed', 'error'); } - } - ] - }) + }); + } + } ] - }); + }) + ] + }); - uploadDialog.show(); - } - })); + uploadDialog.show(); } }); diff --git a/src/Controller/Admin/PageImportController.php b/src/Controller/Admin/PageImportController.php index e2a50e5..3e7f131 100644 --- a/src/Controller/Admin/PageImportController.php +++ b/src/Controller/Admin/PageImportController.php @@ -30,9 +30,9 @@ public function __construct( ) { $this->messagesMap = [ self::ERR_NO_FILE_UPLOADED => 'No file uploaded', - self::SUCCESS_DOCUMENT_REPLACEMENT => 'Documents replaced successfully', - self::SUCCESS_WITHOUT_REPLACEMENT => 'Documents already exist and were not replaced', - self::SUCCESS_NEW_DOCUMENT => 'New Documents imported successfully', + self::SUCCESS_DOCUMENT_REPLACEMENT => 'replaced successfully', + self::SUCCESS_WITHOUT_REPLACEMENT => 'not replaced', + self::SUCCESS_NEW_DOCUMENT => 'imported successfully', ]; } @@ -53,12 +53,16 @@ public function import(Request $request): JsonResponse try { $pages = $this->pageImporter->parseYaml($file->getContent()); - $resultMessage = 'Import Summary:' . \PHP_EOL; - - foreach ($pages as $index => $page) { + $results = [ + self::SUCCESS_DOCUMENT_REPLACEMENT => 0, + self::SUCCESS_WITHOUT_REPLACEMENT => 0, + self::SUCCESS_NEW_DOCUMENT => 0, + ]; + foreach ($pages as $page) { $resultCode = $this->replaceIfExists($page, $overwrite); - $resultMessage = $this->appendMessage($index, $resultCode, $resultMessage); + ++$results[$resultCode]; } + $resultMessage = $this->appendMessage($results); return new JsonResponse(['success' => true, 'message' => $resultMessage]); } catch (\Exception $e) { @@ -84,11 +88,25 @@ protected function replaceIfExists(Page $page, bool $overwrite): int return self::SUCCESS_NEW_DOCUMENT; } - private function appendMessage(int|string $index, int $resultCode, string $resultMessage): string + /** + * @param array $results + */ + private function appendMessage(array $results): string { - $message = \sprintf('%d. %s', (int) $index + 1, $this->messagesMap[$resultCode]); - $resultMessage .= $message . \PHP_EOL; + $resultMessage = ''; + + foreach ($results as $resultCode => $result) { + if ($result > 0) { + if (1 === $result) { + $start = 'One Document'; + } else { + $start = \sprintf('%d Documents', $result); + } + $message = \sprintf('%s %s', $start, $this->messagesMap[$resultCode]); + $resultMessage .= $message . '

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

' . $resultMessage . '

'; } } diff --git a/tests/Integration/Documents/Export/PageExporterTest.php b/tests/Integration/Documents/Export/PageExporterTest.php index d0a850e..978902f 100644 --- a/tests/Integration/Documents/Export/PageExporterTest.php +++ b/tests/Integration/Documents/Export/PageExporterTest.php @@ -37,7 +37,7 @@ public function testSinglePageExport(): void $page->setTitle('Titel meines Dokuments'); $page->setController('Irgend/ein/Controller'); $inputEditable = new Input(); - $inputEditable->setName('textEingabe');; + $inputEditable->setName('textEingabe'); $inputEditable->setDataFromResource('Texteingabe'); $page->setEditables([$inputEditable]); diff --git a/tests/Unit/Controller/Admin/PageExportControllerTest.php b/tests/Unit/Controller/Admin/PageExportControllerTest.php index 16273fc..1158f31 100644 --- a/tests/Unit/Controller/Admin/PageExportControllerTest.php +++ b/tests/Unit/Controller/Admin/PageExportControllerTest.php @@ -43,7 +43,7 @@ public function testExportPage_regular_case(): void { $page = $this->prophesize(Page::class); $this->pageRepository->getById(17)->willReturn($page->reveal()); - $this->pageExporter->exportToYaml($page->reveal())->willReturn('TEST_YAML'); + $this->pageExporter->exportToYaml([$page->reveal()])->willReturn('TEST_YAML'); $response = $this->controller->exportPage($this->request); @@ -57,7 +57,7 @@ public function testExportPage_exceptional_case(): void { $page = $this->prophesize(Page::class); $this->pageRepository->getById(17)->willReturn($page->reveal()); - $this->pageExporter->exportToYaml($page->reveal())->willThrow(new \Exception('Problem')); + $this->pageExporter->exportToYaml([$page->reveal()])->willThrow(new \Exception('Problem')); $response = $this->controller->exportPage($this->request); diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index 280ba12..ef653e4 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -6,5 +6,5 @@ neusta_pimcore_import_export_import_dialog_file_label: 'YAML Datei' neusta_pimcore_import_export_import_dialog_file_button: 'Wähle YAML Datei...' neusta_pimcore_import_export_import_dialog_overwrite_label: 'überschreibe, falls vorhanden' neusta_pimcore_import_export_import_dialog_wait_message: 'Hochladen...' -neusta_pimcore_import_export_import_dialog_notification_success: 'Import erfolgreich' +neusta_pimcore_import_export_import_dialog_notification_success: 'Import Zusammenfassung' neusta_pimcore_import_export_import_dialog_notification_error: 'Import fehlgeschlagen' diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index e7e01e4..d7c0fc4 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -6,5 +6,5 @@ neusta_pimcore_import_export_import_dialog_file_label: 'YAML File' neusta_pimcore_import_export_import_dialog_file_button: 'Select Yaml file...' neusta_pimcore_import_export_import_dialog_overwrite_label: 'overwrite if exists' neusta_pimcore_import_export_import_dialog_wait_message: 'Uploading...' -neusta_pimcore_import_export_import_dialog_notification_success: 'Import successful' +neusta_pimcore_import_export_import_dialog_notification_success: 'Import Summary' neusta_pimcore_import_export_import_dialog_notification_error: 'Import failed' From f0f8f47e8648e904676a330264b44c2b2f817718 Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Sun, 2 Mar 2025 02:31:17 +0100 Subject: [PATCH 05/28] [Feature] Test PageImporter and adapt way of saving --- README.md | 41 ++++-- config/pimcore/config.yaml | 3 +- config/services.yaml | 6 + src/Documents/Export/PageExporter.php | 2 +- .../Populator/EditableDataPopulator.php | 25 ++++ src/Documents/Import/PageImportPopulator.php | 4 +- src/Documents/Import/PageImporter.php | 24 ++++ .../Documents/Export/PageImporterTest.php | 135 ++++++++++++++++++ 8 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 src/Documents/Export/Populator/EditableDataPopulator.php create mode 100644 tests/Integration/Documents/Export/PageImporterTest.php diff --git a/README.md b/README.md index 521e992..64e3ef6 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,33 @@ ## Installation -1. **Require the bundle** - - ```shell - composer require teamneusta/pimcore-import-export-bundle - ``` +1. **Require the bundle** + + ```shell + composer require teamneusta/pimcore-import-export-bundle + ``` 2. **Enable the bundle** - - Add the Bundle to your `config/bundles.php`: - + + Add the Bundle to your `config/bundles.php`: + ```php Neusta\Pimcore\ImportExportBundle\NeustaPimcoreImportExportBundle::class => ['all' => true], ``` ## Usage -After enabling the bundle you should see a new menu item in the context menu of Pimcore Admin Backend - Section Documents: +After enabling the bundle you should see a new menu item in the context menu of Pimcore Admin Backend - Section +Documents: ![context_menu_import_export.png](docs/images/context_menu_import_export.png) (german translation) ### Page Export + The selected page will be exported into YAML format: + ```yaml page: id: 123 @@ -43,7 +46,7 @@ page: main: type: areablock name: main - data: [{ key: '1', type: text-editor, hidden: false }] + data: [ { key: '1', type: text-editor, hidden: false } ] ... ``` @@ -53,7 +56,23 @@ In the same way you can re-import your yaml file again by selecting: `Import fro ### Page Import -The import process will create a new page with the given data. +The import process will create pages with the given data. + +The following rule applies: + +If the parseYaml method of the `PageImporter` is not called with `forcedSave`, the data from the provided YAML will be +adopted, regardless of whether it makes sense or not, and without checking whether the page could be saved that way. + +If `forcedSave` is set to `true`, the ID will be retained (Caution – this can overwrite an existing page). +If a `parentId` is specified, the corresponding document will be searched for. +If it exists, it will be set as the parent (Note: This may override the `path` specification). +If the `parentId` does not exist, an attempt will be made to find a parent using the `path` specification. +If such a parent exists, the `parentId` will be set accordingly and saved. + +If neither is found, an InvalidArgumentException will be thrown, and the save operation will be aborted. + +If multiple pages are imported and a path specification changes py the applied rules, this path specification will be +replaced with the new, correct path specification in all provided page configurations. ## Contribution diff --git a/config/pimcore/config.yaml b/config/pimcore/config.yaml index 2b4daea..c8b5493 100644 --- a/config/pimcore/config.yaml +++ b/config/pimcore/config.yaml @@ -58,7 +58,8 @@ neusta_converter: neusta_pimcore_import_export.editable_converter: target: Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportEditable + populators: + - neusta_pimcore_import_export.editable.data.populator properties: type: ~ name: ~ - data: ~ diff --git a/config/services.yaml b/config/services.yaml index 02cd447..a560c05 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -57,6 +57,12 @@ services: $targetProperty: 'navigation_name' $skipNull: true + ########################################################### + # Export Populator (Editable -> YamlExportEditable) + ########################################################### + neusta_pimcore_import_export.editable.data.populator: + class: Neusta\Pimcore\ImportExportBundle\Documents\Export\Populator\EditableDataPopulator + Neusta\Pimcore\ImportExportBundle\Documents\Export\PageExporter: arguments: $pageToYamlConverter: '@neusta_pimcore_import_export.export_page' diff --git a/src/Documents/Export/PageExporter.php b/src/Documents/Export/PageExporter.php index 496f7ed..a8b55c3 100644 --- a/src/Documents/Export/PageExporter.php +++ b/src/Documents/Export/PageExporter.php @@ -73,7 +73,7 @@ public function exportToYaml(iterable $pages): string 'yaml', [ 'yaml_inline' => 4, - 'yaml_indent' => 2, + 'yaml_indent' => 0, 'yaml_flags' => self::YAML_DUMP_FLAGS, ] ); diff --git a/src/Documents/Export/Populator/EditableDataPopulator.php b/src/Documents/Export/Populator/EditableDataPopulator.php new file mode 100644 index 0000000..25cf030 --- /dev/null +++ b/src/Documents/Export/Populator/EditableDataPopulator.php @@ -0,0 +1,25 @@ + + */ +class EditableDataPopulator implements Populator +{ + public function populate(object $target, object $source, ?object $ctx = null): void + { + if ($source instanceof Editable\Relation || $source instanceof Editable\Relations) { + $target->data = $source->getDataForResource(); + } else { + $target->data = $source->getData(); + } + } +} diff --git a/src/Documents/Import/PageImportPopulator.php b/src/Documents/Import/PageImportPopulator.php index 247f0bb..8752336 100644 --- a/src/Documents/Import/PageImportPopulator.php +++ b/src/Documents/Import/PageImportPopulator.php @@ -22,8 +22,8 @@ public function populate(object $target, object $source, ?object $ctx = null): v if (property_exists($source, 'language') && isset($source->language)) { $target->setProperty('language', 'text', $source->language); } - $target->setProperty('navigation_title', 'text', $source->title); - $target->setProperty('navigation_name', 'text', $source->key); + $target->setProperty('navigation_title', 'text', $source->navigation_title); + $target->setProperty('navigation_name', 'text', $source->navigation_name); /** @var array $editable */ foreach ($source->editables ?? [] as $key => $editable) { $target->setRawEditable($key, $editable['type'], $editable['data']); diff --git a/src/Documents/Import/PageImporter.php b/src/Documents/Import/PageImporter.php index 7d657a8..7b359f2 100644 --- a/src/Documents/Import/PageImporter.php +++ b/src/Documents/Import/PageImporter.php @@ -6,6 +6,7 @@ use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Exception\ConverterException; use Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage; +use Pimcore\Model\Document; use Pimcore\Model\Document\Page; use Pimcore\Model\Element\DuplicateFullPathException; use Symfony\Component\Yaml\Yaml; @@ -41,6 +42,7 @@ public function parseYaml(string $yamlInput, bool $forcedSave = true): array if (\is_array($configPage[YamlExportPage::PAGE])) { $page = $this->yamlToPageConverter->convert(new YamlExportPage($configPage[YamlExportPage::PAGE])); if ($forcedSave) { + $this->checkAndUpdatePage($page, $config[YamlExportPage::PAGES]); $page->save(); } } @@ -67,4 +69,26 @@ public function readYamlFileAndSetParameters(string $filename, array $params = [ return ''; } + + /** + * @param array> $configPages + */ + private function checkAndUpdatePage(Page $page, array &$configPages): void + { + $oldPath = $page->getPath(); + $existingParent = Document::getById($page->getParentId() ?? -1); + if (!$existingParent) { + $existingParent = Document::getByPath($page->getPath() ?? ''); + if (!$existingParent) { + throw new \InvalidArgumentException('Neither parentId nor path leads to a valid parent element'); + } + $page->setParentId($existingParent->getId()); + $newPath = $existingParent->getPath() . $page->getKey() . '/'; + foreach ($configPages as $configPage) { + if (\array_key_exists('path', $configPage[YamlExportPage::PAGE]) && \is_string($oldPath)) { + str_replace($oldPath, $newPath, $configPage[YamlExportPage::PAGE]['path']); + } + } + } + } } diff --git a/tests/Integration/Documents/Export/PageImporterTest.php b/tests/Integration/Documents/Export/PageImporterTest.php new file mode 100644 index 0000000..6c9f075 --- /dev/null +++ b/tests/Integration/Documents/Export/PageImporterTest.php @@ -0,0 +1,135 @@ +importer = self::getContainer()->get(PageImporter::class); + } + + public function testSinglePageImport_exceptional_case(): void + { + $yaml = + <<expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Neither parentId nor path leads to a valid parent element'); + $this->importer->parseYaml($yaml); + } + + public function testSinglePageExport_regular_case_parent_id(): void + { + $yaml = + <<importer->parseYaml($yaml); + self::assertEquals(999, $pages[0]->getId()); + self::assertEquals('/', $pages[0]->getPath()); + + self::assertEquals('test_document_1', $pages[0]->getKey()); + self::assertEquals('Titel meines Dokuments', $pages[0]->getTitle()); + self::assertEquals('email', $pages[0]->getType()); + self::assertEquals('Irgend/ein/Controller', $pages[0]->getController()); + self::assertEquals('fr', $pages[0]->getProperty('language')); + self::assertEquals('Mein Dokument', $pages[0]->getProperty('navigation_name')); + self::assertEquals('Mein Dokument - Titel', $pages[0]->getProperty('navigation_title')); + } + + public function testSinglePageExport_regular_case_path(): void + { + $yaml = + <<importer->parseYaml($yaml); + self::assertEquals(999, $pages[0]->getId()); + self::assertEquals('/', $pages[0]->getPath()); + self::assertEquals(1, $pages[0]->getParentId()); + + self::assertEquals('test_document_1', $pages[0]->getKey()); + self::assertEquals('Titel meines Dokuments', $pages[0]->getTitle()); + self::assertEquals('email', $pages[0]->getType()); + self::assertEquals('Irgend/ein/Controller', $pages[0]->getController()); + self::assertEquals('fr', $pages[0]->getProperty('language')); + self::assertEquals('Mein Dokument', $pages[0]->getProperty('navigation_name')); + self::assertEquals('Mein Dokument - Titel', $pages[0]->getProperty('navigation_title')); + } + + public function testSinglePageImport_tree_case(): void + { + $yaml = + <<importer->parseYaml($yaml); + + self::assertEquals('/test_document_1/test_document_1_1/', $pages[2]->getPath()); + } +} From 884f2f1ddf3bdb3817dd672a090de14f0b4b7776 Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Fri, 14 Mar 2025 15:22:55 +0100 Subject: [PATCH 06/28] [Export All] new command for export all pages different converters for folder, snippet & pages --- config/converters_populators.yaml | 63 ++++++++++++++++ config/pimcore/config.yaml | 36 +++++++++ config/services.yaml | 58 +++------------ src/Command/ExportAllPagesCommand.php | 74 +++++++++++++++++++ .../DocumentTypeStrategyConverter.php | 33 +++++++++ src/Documents/Export/PageExporter.php | 15 +++- src/Toolbox/Repository/PageRepository.php | 2 +- 7 files changed, 230 insertions(+), 51 deletions(-) create mode 100644 config/converters_populators.yaml create mode 100644 src/Command/ExportAllPagesCommand.php create mode 100644 src/Documents/Export/Converter/DocumentTypeStrategyConverter.php diff --git a/config/converters_populators.yaml b/config/converters_populators.yaml new file mode 100644 index 0000000..57c98a5 --- /dev/null +++ b/config/converters_populators.yaml @@ -0,0 +1,63 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + ########################################################### + # Import Populator (YamlExportPage -> Page) + ########################################################### + Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImportPopulator: ~ + + Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImporter: + arguments: + $yamlToPageConverter: '@neusta_pimcore_import_export.import_page' + + ########################################################### + # Export Converter (Document -> YamlExportPage) + ########################################################### + neusta_pimcore_import_export.export_document: + class: Neusta\Pimcore\ImportExportBundle\Documents\Export\Converter\DocumentTypeStrategyConverter + arguments: + $type2ConverterMap: + Pimcore\Model\Document\Page: '@neusta_pimcore_import_export.export_page' + Pimcore\Model\Document\Snippet: '@neusta_pimcore_import_export.export_page_snippet' + Pimcore\Model\Document\Folder: '@neusta_pimcore_import_export.export_folder' + + + ########################################################### + # Export Populator (Page -> YamlExportPage) + ########################################################### + neusta_pimcore_import_export.page.editables.populator: + class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator + arguments: + $converter: '@neusta_pimcore_import_export.editable_converter' + $sourceArrayPropertyName: 'editables' + $targetPropertyName: 'editables' + + neusta_pimcore_import_export.page.property.language.populator: + class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator + arguments: + $propertyKey: 'language' + $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 + arguments: + $propertyKey: 'navigation_name' + $targetProperty: 'navigation_name' + $skipNull: true + + ########################################################### + # Export Populator (Editable -> YamlExportEditable) + ########################################################### + neusta_pimcore_import_export.editable.data.populator: + class: Neusta\Pimcore\ImportExportBundle\Documents\Export\Populator\EditableDataPopulator + diff --git a/config/pimcore/config.yaml b/config/pimcore/config.yaml index c8b5493..60f7988 100644 --- a/config/pimcore/config.yaml +++ b/config/pimcore/config.yaml @@ -36,6 +36,23 @@ neusta_converter: source: parentId default: 0 + ########################################################### + # Export Converter (Folder -> YamlExportPage) + ########################################################### + neusta_pimcore_import_export.export_folder: + target: Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage + 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 + properties: + id: ~ + key: ~ + type: ~ + published: ~ + path: ~ + parentId: ~ + ########################################################### # Export Converter (Page -> YamlExportPage) ########################################################### @@ -56,6 +73,25 @@ neusta_converter: path: ~ parentId: ~ + ########################################################### + # Export Converter (Snippet -> YamlExportPage) + ########################################################### + neusta_pimcore_import_export.export_page_snippet: + target: Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage + 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_import_export.page.editables.populator + properties: + id: ~ + key: ~ + controller: ~ + type: ~ + published: ~ + path: ~ + parentId: ~ + neusta_pimcore_import_export.editable_converter: target: Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportEditable populators: diff --git a/config/services.yaml b/config/services.yaml index a560c05..80938e7 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,3 +1,6 @@ +imports: + - { resource: 'converters_populators.yaml' } + services: _defaults: autowire: true @@ -6,6 +9,13 @@ services: Neusta\Pimcore\ImportExportBundle\Toolbox\: resource: '../src/Toolbox' + ########################################################### + # Commands + ########################################################### + Neusta\Pimcore\ImportExportBundle\Command\ExportAllPagesCommand: + public: true + tags: [ 'console.command' ] + ########################################################### # Controller ########################################################### @@ -17,55 +27,9 @@ services: public: true tags: [ 'controller.service_arguments' ] - ########################################################### - # Import Populator (YamlExportPage -> Page) - ########################################################### - Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImportPopulator: ~ - - Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImporter: - arguments: - $yamlToPageConverter: '@neusta_pimcore_import_export.import_page' - - ########################################################### - # Export Populator (Page -> YamlExportPage) - ########################################################### - neusta_pimcore_import_export.page.editables.populator: - class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator - arguments: - $converter: '@neusta_pimcore_import_export.editable_converter' - $sourceArrayPropertyName: 'editables' - $targetPropertyName: 'editables' - - neusta_pimcore_import_export.page.property.language.populator: - class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator - arguments: - $propertyKey: 'language' - $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 - arguments: - $propertyKey: 'navigation_name' - $targetProperty: 'navigation_name' - $skipNull: true - - ########################################################### - # Export Populator (Editable -> YamlExportEditable) - ########################################################### - neusta_pimcore_import_export.editable.data.populator: - class: Neusta\Pimcore\ImportExportBundle\Documents\Export\Populator\EditableDataPopulator - Neusta\Pimcore\ImportExportBundle\Documents\Export\PageExporter: arguments: - $pageToYamlConverter: '@neusta_pimcore_import_export.export_page' + $pageToYamlConverter: '@neusta_pimcore_import_export.export_document' Neusta\Pimcore\ImportExportBundle\EventListener\PimcoreAdminListener: tags: diff --git a/src/Command/ExportAllPagesCommand.php b/src/Command/ExportAllPagesCommand.php new file mode 100644 index 0000000..79355e8 --- /dev/null +++ b/src/Command/ExportAllPagesCommand.php @@ -0,0 +1,74 @@ +io->title('Export all pages into one single YAML file'); + + $rootPage = $this->pageRepository->getById(1); + if (!$rootPage) { + $this->io->error('Root page (ID: 1) not found'); + + return Command::FAILURE; + } + + $allPages = $this->addPages($rootPage, []); + + $this->io->writeln('Start exporting all results'); + $this->io->newLine(); + $yamlContent = $this->pageExporter->exportToYaml($allPages); + + $exportFilename = 'export_all_pages.yaml'; + + $this->io->writeln('Write in file <' . $exportFilename . '>'); + $this->io->newLine(); + file_put_contents($exportFilename, $yamlContent); + + $this->io->success('All pages have been exported successfully'); + + return Command::SUCCESS; + } + + /** + * @param array $allPages + * + * @return array + */ + private function addPages(Document $rootPage, array $allPages): array + { + $allPages[] = $rootPage; + foreach ($rootPage->getChildren(true) as $childPage) { + if ($childPage instanceof Document) { + $allPages = $this->addPages($childPage, $allPages); + } + } + + return $allPages; + } +} diff --git a/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php b/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php new file mode 100644 index 0000000..6571c89 --- /dev/null +++ b/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php @@ -0,0 +1,33 @@ + + */ +class DocumentTypeStrategyConverter implements Converter +{ + /** + * @param array> $type2ConverterMap + */ + public function __construct( + private array $type2ConverterMap, + ) { + } + + public function convert(object $source, ?object $ctx = null): object + { + if (!\array_key_exists($source::class, $this->type2ConverterMap)) { + throw new \InvalidArgumentException('No converter found for type ' . $source::class); + } + + return $this->type2ConverterMap[$source::class]->convert($source, $ctx); + } +} diff --git a/src/Documents/Export/PageExporter.php b/src/Documents/Export/PageExporter.php index a8b55c3..9b22d54 100644 --- a/src/Documents/Export/PageExporter.php +++ b/src/Documents/Export/PageExporter.php @@ -5,7 +5,10 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Exception\ConverterException; +use Pimcore\Model\Document; +use Pimcore\Model\Document\Folder; use Pimcore\Model\Document\Page; +use Pimcore\Model\Document\PageSnippet; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Yaml\Yaml; @@ -17,7 +20,7 @@ class PageExporter Yaml::DUMP_NULL_AS_TILDE; /** - * @param Converter $pageToYamlConverter + * @param Converter $pageToYamlConverter */ public function __construct( private readonly Converter $pageToYamlConverter, @@ -57,7 +60,7 @@ public function toYaml(Page $page): string * key: 'page_key_2' * ... * - * @param iterable $pages + * @param iterable $pages * * @throws ConverterException */ @@ -65,7 +68,13 @@ public function exportToYaml(iterable $pages): string { $yamlExportPages = []; foreach ($pages as $page) { - $yamlExportPages[] = [YamlExportPage::PAGE => $this->pageToYamlConverter->convert($page)]; + if ( + $page instanceof Page + || $page instanceof PageSnippet + || $page instanceof Folder + ) { + $yamlExportPages[] = [YamlExportPage::PAGE => $this->pageToYamlConverter->convert($page)]; + } } return $this->serializer->serialize( diff --git a/src/Toolbox/Repository/PageRepository.php b/src/Toolbox/Repository/PageRepository.php index 3b2505f..094b589 100644 --- a/src/Toolbox/Repository/PageRepository.php +++ b/src/Toolbox/Repository/PageRepository.php @@ -27,7 +27,7 @@ public function findAllPagesWithSubPages(Page $page): iterable { yield $page; - foreach ($page->getChildren() as $child) { + foreach ($page->getChildren(true) as $child) { if ($child instanceof Page) { yield from $this->findAllPagesWithSubPages($child); } From 69e2ff6f179f75d1f8cf979ce63a3685bda39366 Mon Sep 17 00:00:00 2001 From: Luka Dschaak Date: Fri, 14 Mar 2025 18:23:50 +0100 Subject: [PATCH 07/28] [Chore] fix general code styles --- README.md | 25 +++++++++---------- config/services.yaml | 18 ++++++++----- src/Command/ExportAllPagesCommand.php | 7 ++---- src/Controller/Admin/PageImportController.php | 4 +-- .../DocumentTypeStrategyConverter.php | 4 +-- .../Populator/EditableDataPopulator.php | 4 +-- 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 64e3ef6..76b2fb7 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,23 @@ ## Installation -1. **Require the bundle** +1. **Require the bundle** - ```shell - composer require teamneusta/pimcore-import-export-bundle - ``` + ```shell + composer require teamneusta/pimcore-import-export-bundle + ``` -2. **Enable the bundle** - - Add the Bundle to your `config/bundles.php`: - - ```php - Neusta\Pimcore\ImportExportBundle\NeustaPimcoreImportExportBundle::class => ['all' => true], - ``` +2. **Enable the bundle** + + Add the Bundle to your `config/bundles.php`: + + ```php + Neusta\Pimcore\ImportExportBundle\NeustaPimcoreImportExportBundle::class => ['all' => true], + ``` ## Usage -After enabling the bundle you should see a new menu item in the context menu of Pimcore Admin Backend - Section -Documents: +After enabling the bundle you should see a new menu item in the context menu of Pimcore Admin Backend - Section Documents: ![context_menu_import_export.png](docs/images/context_menu_import_export.png) diff --git a/config/services.yaml b/config/services.yaml index 80938e7..178645e 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -9,16 +9,16 @@ services: Neusta\Pimcore\ImportExportBundle\Toolbox\: resource: '../src/Toolbox' - ########################################################### - # Commands - ########################################################### + ############ + # Commands # + ############ Neusta\Pimcore\ImportExportBundle\Command\ExportAllPagesCommand: public: true tags: [ 'console.command' ] - ########################################################### - # Controller - ########################################################### + ############## + # Controller # + ############## Neusta\Pimcore\ImportExportBundle\Controller\Admin\PageExportController: public: true tags: [ 'controller.service_arguments' ] @@ -27,10 +27,16 @@ services: public: true tags: [ 'controller.service_arguments' ] + ############# + # Documents # + ############# Neusta\Pimcore\ImportExportBundle\Documents\Export\PageExporter: arguments: $pageToYamlConverter: '@neusta_pimcore_import_export.export_document' + ################# + # EventListener # + ################# Neusta\Pimcore\ImportExportBundle\EventListener\PimcoreAdminListener: tags: - { name: kernel.event_listener, event: pimcore.bundle_manager.paths.js, method: addJSFiles } diff --git a/src/Command/ExportAllPagesCommand.php b/src/Command/ExportAllPagesCommand.php index 79355e8..b1a3d03 100644 --- a/src/Command/ExportAllPagesCommand.php +++ b/src/Command/ExportAllPagesCommand.php @@ -1,6 +1,4 @@ - Date: Fri, 14 Mar 2025 18:58:24 +0100 Subject: [PATCH 08/28] [Chore] readme: edit how tests must be executed --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76b2fb7..ad89238 100644 --- a/README.md +++ b/README.md @@ -92,5 +92,10 @@ We use composer scripts for our main quality tools. They can be executed via the ```shell bin/composer cs:fix bin/composer phpstan -bin/composer tests +``` + +For the tests there is a different script, that includes a database setup. + +```shell +bin/run-tests ``` From 1654f74a37a0e877bae4ee361d5d8ab1198c7e70 Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Sat, 15 Mar 2025 09:15:19 +0100 Subject: [PATCH 09/28] [Export All][Refactoring] remove duplicated code --- src/Controller/Admin/PageExportController.php | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Controller/Admin/PageExportController.php b/src/Controller/Admin/PageExportController.php index 14dafc9..ffc8654 100644 --- a/src/Controller/Admin/PageExportController.php +++ b/src/Controller/Admin/PageExportController.php @@ -26,12 +26,11 @@ public function __construct( )] public function exportPage(Request $request): Response { - $pageId = $request->query->getInt('page_id'); - $page = $this->pageRepository->getById($pageId); - - if (!$page instanceof Page) { + try { + $page = $this->getPageByRequest($request); + } catch (\Exception $exception) { return new JsonResponse( - \sprintf('Page with id "%s" was not found', $pageId), + \sprintf('Page with id "%s" was not found', $exception->getMessage()), Response::HTTP_NOT_FOUND, ); } @@ -42,14 +41,7 @@ public function exportPage(Request $request): Response return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } - $response = new Response($yaml); - $response->headers->set('Content-type', 'application/yaml'); - $response->headers->set( - 'Content-Disposition', - HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $this->createFilename($page)), - ); - - return $response; + return $this->createJsonResponseByYaml($yaml, $this->createFilename($page)); } #[Route( @@ -59,12 +51,11 @@ public function exportPage(Request $request): Response )] public function exportPageWithChildren(Request $request): Response { - $pageId = $request->query->getInt('page_id'); - $page = $this->pageRepository->getById($pageId); - - if (!$page instanceof Page) { + try { + $page = $this->getPageByRequest($request); + } catch (\Exception $exception) { return new JsonResponse( - \sprintf('Page with id "%s" was not found', $pageId), + \sprintf('Page with id "%s" was not found', $exception->getMessage()), Response::HTTP_NOT_FOUND, ); } @@ -75,18 +66,35 @@ public function exportPageWithChildren(Request $request): Response return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } + return $this->createJsonResponseByYaml($yaml, $this->createFilename($page)); + } + + private function getPageByRequest(Request $request): Page + { + $pageId = $request->query->getInt('page_id'); + $page = $this->pageRepository->getById($pageId); + + if (!$page instanceof Page) { + throw new \Exception((string) $pageId); + } + + return $page; + } + + private function createFilename(Page $page): string + { + return \sprintf('%s.yaml', str_replace(' ', '_', (string) $page->getKey())); + } + + private function createJsonResponseByYaml(string $yaml, string $filename): Response + { $response = new Response($yaml); $response->headers->set('Content-type', 'application/yaml'); $response->headers->set( 'Content-Disposition', - HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $this->createFilename($page)), + HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $filename), ); return $response; } - - private function createFilename(Page $page): string - { - return \sprintf('%s.yaml', str_replace(' ', '_', (string) $page->getKey())); - } } From eaba65fa89e03329866f5807ab229f02c79aa135 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Sat, 15 Mar 2025 09:18:58 +0100 Subject: [PATCH 10/28] Update src/Controller/Admin/PageImportController.php Co-authored-by: Luka Dschaak --- src/Controller/Admin/PageImportController.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Controller/Admin/PageImportController.php b/src/Controller/Admin/PageImportController.php index 6e98f95..d4d7c09 100644 --- a/src/Controller/Admin/PageImportController.php +++ b/src/Controller/Admin/PageImportController.php @@ -20,18 +20,17 @@ final class PageImportController /** * @var string[] Map of error codes to messages */ - private array $messagesMap; + private array $messagesMap = [ + self::ERR_NO_FILE_UPLOADED => 'No file uploaded', + self::SUCCESS_DOCUMENT_REPLACEMENT => 'replaced successfully', + self::SUCCESS_WITHOUT_REPLACEMENT => 'not replaced', + self::SUCCESS_NEW_DOCUMENT => 'imported successfully', + ]; public function __construct( private PageImporter $pageImporter, private PageRepository $pageRepository, ) { - $this->messagesMap = [ - self::ERR_NO_FILE_UPLOADED => 'No file uploaded', - self::SUCCESS_DOCUMENT_REPLACEMENT => 'replaced successfully', - self::SUCCESS_WITHOUT_REPLACEMENT => 'not replaced', - self::SUCCESS_NEW_DOCUMENT => 'imported successfully', - ]; } #[Route( From 248bacb04c136cb9b4595cef673c0c34b8145294 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Sat, 15 Mar 2025 09:24:46 +0100 Subject: [PATCH 11/28] Update tests/Integration/Documents/Export/PageExporterTest.php thx for translations Co-authored-by: Luka Dschaak --- .../Documents/Export/PageExporterTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Integration/Documents/Export/PageExporterTest.php b/tests/Integration/Documents/Export/PageExporterTest.php index 978902f..7d0748f 100644 --- a/tests/Integration/Documents/Export/PageExporterTest.php +++ b/tests/Integration/Documents/Export/PageExporterTest.php @@ -31,14 +31,14 @@ public function testSinglePageExport(): void $page->setPublished(false); $page->setPath('/test/'); $page->setKey('test_document_1'); - $page->setProperty('language', 'string', 'fr'); - $page->setProperty('navigation_name', 'string', 'Mein Dokument'); - $page->setProperty('navigation_title', 'string', 'Mein Dokument - Titel'); - $page->setTitle('Titel meines Dokuments'); - $page->setController('Irgend/ein/Controller'); + $page->setProperty('language', 'string', 'en'); + $page->setProperty('navigation_name', 'string', 'My Document'); + $page->setProperty('navigation_title', 'string', 'My Document - Title'); + $page->setTitle('The Title of my document'); + $page->setController('/Some/Controller'); $inputEditable = new Input(); - $inputEditable->setName('textEingabe'); - $inputEditable->setDataFromResource('Texteingabe'); + $inputEditable->setName('textInput'); + $inputEditable->setDataFromResource('some text input'); $page->setEditables([$inputEditable]); $yaml = $this->exporter->exportToYaml([$page]); From 8953203dcc096234fdd7ced1f4491128a700e2ea Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Sat, 15 Mar 2025 09:25:01 +0100 Subject: [PATCH 12/28] Update tests/Integration/Documents/Export/PageImporterTest.php thx for translations Co-authored-by: Luka Dschaak --- .../Integration/Documents/Export/PageImporterTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Integration/Documents/Export/PageImporterTest.php b/tests/Integration/Documents/Export/PageImporterTest.php index 6c9f075..f2c21c0 100644 --- a/tests/Integration/Documents/Export/PageImporterTest.php +++ b/tests/Integration/Documents/Export/PageImporterTest.php @@ -61,12 +61,12 @@ public function testSinglePageExport_regular_case_parent_id(): void self::assertEquals('/', $pages[0]->getPath()); self::assertEquals('test_document_1', $pages[0]->getKey()); - self::assertEquals('Titel meines Dokuments', $pages[0]->getTitle()); + self::assertEquals('The Titel of My Document', $pages[0]->getTitle()); self::assertEquals('email', $pages[0]->getType()); - self::assertEquals('Irgend/ein/Controller', $pages[0]->getController()); - self::assertEquals('fr', $pages[0]->getProperty('language')); - self::assertEquals('Mein Dokument', $pages[0]->getProperty('navigation_name')); - self::assertEquals('Mein Dokument - Titel', $pages[0]->getProperty('navigation_title')); + self::assertEquals('/Some/Controller', $pages[0]->getController()); + self::assertEquals('en', $pages[0]->getProperty('language')); + self::assertEquals('My Document', $pages[0]->getProperty('navigation_name')); + self::assertEquals('My Document - Title', $pages[0]->getProperty('navigation_title')); } public function testSinglePageExport_regular_case_path(): void From a4a3de06492c408c28de3fba283c88e4189325cb Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Sat, 15 Mar 2025 09:25:11 +0100 Subject: [PATCH 13/28] Update tests/Integration/Documents/Export/PageImporterTest.php thx for translations Co-authored-by: Luka Dschaak --- .../Integration/Documents/Export/PageImporterTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Integration/Documents/Export/PageImporterTest.php b/tests/Integration/Documents/Export/PageImporterTest.php index f2c21c0..779e527 100644 --- a/tests/Integration/Documents/Export/PageImporterTest.php +++ b/tests/Integration/Documents/Export/PageImporterTest.php @@ -95,12 +95,12 @@ public function testSinglePageExport_regular_case_path(): void self::assertEquals(1, $pages[0]->getParentId()); self::assertEquals('test_document_1', $pages[0]->getKey()); - self::assertEquals('Titel meines Dokuments', $pages[0]->getTitle()); + self::assertEquals('The Title of My Document', $pages[0]->getTitle()); self::assertEquals('email', $pages[0]->getType()); - self::assertEquals('Irgend/ein/Controller', $pages[0]->getController()); - self::assertEquals('fr', $pages[0]->getProperty('language')); - self::assertEquals('Mein Dokument', $pages[0]->getProperty('navigation_name')); - self::assertEquals('Mein Dokument - Titel', $pages[0]->getProperty('navigation_title')); + self::assertEquals('/Some/Controller', $pages[0]->getController()); + self::assertEquals('en', $pages[0]->getProperty('language')); + self::assertEquals('My Document', $pages[0]->getProperty('navigation_name')); + self::assertEquals('My Document - Title', $pages[0]->getProperty('navigation_title')); } public function testSinglePageImport_tree_case(): void From 65eabf91446c0c4744d79ffdd60b5f800ce227cc Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Sat, 15 Mar 2025 09:26:45 +0100 Subject: [PATCH 14/28] Update translations/admin.de.yaml thx for translations Co-authored-by: Luka Dschaak --- translations/admin.de.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index ef653e4..1e710d6 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -1,5 +1,5 @@ neusta_pimcore_import_export_export_menu_label: 'Exportiere in YAML' -neusta_pimcore_import_export_export_with_children_menu_label: 'Exportiere in YAML (inkl. Unterseiten)' +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' neusta_pimcore_import_export_import_menu_label: 'Importiere aus YAML' neusta_pimcore_import_export_import_dialog_file_label: 'YAML Datei' From c9a4b4e9f61901a35f38aa7f08ea185b3575c8de Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Sat, 15 Mar 2025 09:27:00 +0100 Subject: [PATCH 15/28] Update translations/admin.en.yaml thx for translations Co-authored-by: Luka Dschaak --- translations/admin.en.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index d7c0fc4..718ac19 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -1,5 +1,5 @@ neusta_pimcore_import_export_export_menu_label: 'Export to YAML' -neusta_pimcore_import_export_export_with_children_menu_label: 'Export to YAML (incl. subpages)' +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_menu_label: 'Import from YAML' neusta_pimcore_import_export_import_dialog_file_label: 'YAML File' From 725fbb9bf0ba8591486b342d7b262a3e4a82c6c8 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Sat, 15 Mar 2025 09:27:08 +0100 Subject: [PATCH 16/28] Update tests/Integration/Documents/Export/PageImporterTest.php thx for translations Co-authored-by: Luka Dschaak --- .../Integration/Documents/Export/PageImporterTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Integration/Documents/Export/PageImporterTest.php b/tests/Integration/Documents/Export/PageImporterTest.php index 779e527..4722d32 100644 --- a/tests/Integration/Documents/Export/PageImporterTest.php +++ b/tests/Integration/Documents/Export/PageImporterTest.php @@ -81,12 +81,12 @@ public function testSinglePageExport_regular_case_path(): void type: email published: false path: / - language: fr - navigation_name: 'Mein Dokument' - navigation_title: 'Mein Dokument - Titel' + language: en + navigation_name: 'My Document' + navigation_title: 'My Document - Title' key: test_document_1 - title: 'Titel meines Dokuments' - controller: Irgend/ein/Controller + title: 'The Title of My Document' + controller: /Some/Controller YAML; $pages = $this->importer->parseYaml($yaml); From 8d987abe7fe0097dd3dc446242c102a8f8c0e61f Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Sat, 15 Mar 2025 09:27:26 +0100 Subject: [PATCH 17/28] Update tests/Integration/Documents/Export/PageImporterTest.php thx for translations Co-authored-by: Luka Dschaak --- tests/Integration/Documents/Export/PageImporterTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Integration/Documents/Export/PageImporterTest.php b/tests/Integration/Documents/Export/PageImporterTest.php index 4722d32..a6bbddc 100644 --- a/tests/Integration/Documents/Export/PageImporterTest.php +++ b/tests/Integration/Documents/Export/PageImporterTest.php @@ -49,11 +49,11 @@ public function testSinglePageExport_regular_case_parent_id(): void published: false path: /path/will/be/overwritten/by/parent_id/ language: fr - navigation_name: 'Mein Dokument' - navigation_title: 'Mein Dokument - Titel' + navigation_name: 'My Document' + navigation_title: 'My Document - Title' key: test_document_1 - title: 'Titel meines Dokuments' - controller: Irgend/ein/Controller + title: 'The Title of My Document' + controller: /Some/Controller YAML; $pages = $this->importer->parseYaml($yaml); From 9343665e31422fcfdfe4ceee48b6e337ceb163ee Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Sat, 15 Mar 2025 09:30:12 +0100 Subject: [PATCH 18/28] [Export All][Tests] add expected yaml files --- ...erTest__testSimpleSavedPagesExport__1.yaml | 29 +++++++++++++ ...Test__testSimpleUnsavedPagesExport__1.yaml | 29 +++++++++++++ ...ExporterTest__testSinglePageExport__1.yaml | 15 +++++++ ...eExporterTest__testTreePagesExport__1.yaml | 43 +++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml new file mode 100644 index 0000000..fa1de4d --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml @@ -0,0 +1,29 @@ +pages: + - + page: + id: 2 + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_1 + title: 'Test Document_1' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } + - + page: + id: 3 + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_2 + title: 'Test Document_2' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml new file mode 100644 index 0000000..cf5f19f --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml @@ -0,0 +1,29 @@ +pages: + - + page: + id: ~ + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_1 + title: '' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } + - + page: + id: ~ + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_2 + title: '' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml new file mode 100644 index 0000000..c5c7a06 --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml @@ -0,0 +1,15 @@ +pages: + - + page: + id: 999 + parentId: 4 + type: email + published: false + path: /test/ + language: en + navigation_name: 'My Document' + navigation_title: 'My Document - Title' + key: test_document_1 + title: 'The Title of my document' + controller: /Some/Controller + editables: [{ type: input, name: textInput, data: 'some text input' }] diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml new file mode 100644 index 0000000..0fe7e0c --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml @@ -0,0 +1,43 @@ +pages: + - + page: + id: 2 + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_1 + title: 'Test Document_1' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } + - + page: + id: 3 + parentId: 2 + type: page + published: true + path: /test_document_1/ + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_2 + title: 'Test Document_2' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } + - + page: + id: 4 + parentId: 3 + type: page + published: true + path: /test_document_1/test_document_2/ + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_2 + title: 'Test Document_2' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } From 158f9814dd9a2c58dfd077ae709dfd244226d3e4 Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Sat, 15 Mar 2025 09:41:04 +0100 Subject: [PATCH 19/28] [Export All][Tests] fix it --- tests/Integration/Documents/Export/PageImporterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Documents/Export/PageImporterTest.php b/tests/Integration/Documents/Export/PageImporterTest.php index a6bbddc..9aa8034 100644 --- a/tests/Integration/Documents/Export/PageImporterTest.php +++ b/tests/Integration/Documents/Export/PageImporterTest.php @@ -61,10 +61,10 @@ public function testSinglePageExport_regular_case_parent_id(): void self::assertEquals('/', $pages[0]->getPath()); self::assertEquals('test_document_1', $pages[0]->getKey()); - self::assertEquals('The Titel of My Document', $pages[0]->getTitle()); + self::assertEquals('The Title of My Document', $pages[0]->getTitle()); self::assertEquals('email', $pages[0]->getType()); self::assertEquals('/Some/Controller', $pages[0]->getController()); - self::assertEquals('en', $pages[0]->getProperty('language')); + self::assertEquals('fr', $pages[0]->getProperty('language')); self::assertEquals('My Document', $pages[0]->getProperty('navigation_name')); self::assertEquals('My Document - Title', $pages[0]->getProperty('navigation_title')); } From 3514a1f3eb8c77242ae66097e0a3cbb587b3cebe Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Wed, 19 Mar 2025 07:58:16 +0100 Subject: [PATCH 20/28] [Export All][Refactoring] (#12) * [Export All][Refactoring] fix it * [Export All][Refactoring] check import by path inheritance except parentId * [Export All][Refactoring] add option for output file * [Export All][Refactoring] rename property for Luka * [Export All][Refactoring] add some docs * [Export All][Refactoring][New Commands] for import and export * [Export All][Refactoring][New Commands] review by Bundle Dev Group * [Export All][Refactoring][New Commands] some refactorings and an additional menu for the export filename * [Export All][Refactoring][New Commands] change inlining for better readability and remove old export method * [Export All][Refactoring][New Commands] generalize serialization and add JSON input/output * [Export All][Refactoring][New Commands] fix tests * [Export All][Refactoring][New Commands] add parameter for format to Controllers and js Scripts --- README.md | 144 +++++++++++++++++- config/converters_populators.yaml | 16 +- config/pimcore/config.yaml | 18 +-- config/services.yaml | 27 +++- docs/images/context_menu_export.png | Bin 0 -> 4320 bytes docs/images/context_menu_import_export.png | Bin 3142 -> 0 bytes docs/images/filename_dialog.png | Bin 0 -> 8979 bytes docs/images/import_menu.png | Bin 0 -> 2353 bytes public/js/exportPage.js | 25 ++- public/js/importPage.js | 115 +++++++------- src/Command/ExportAllPagesCommand.php | 71 --------- src/Command/ExportPagesCommand.php | 110 +++++++++++++ src/Command/ImportPagesCommand.php | 62 ++++++++ src/Controller/Admin/PageExportController.php | 56 +++---- src/Controller/Admin/PageImportController.php | 24 ++- .../DocumentTypeStrategyConverter.php | 12 +- src/Documents/Export/PageExporter.php | 60 ++------ .../Populator/EditableDataPopulator.php | 4 +- src/Documents/Import/PageImporter.php | 58 ++----- .../{ => Populator}/PageImportPopulator.php | 14 +- .../Editable.php} | 4 +- .../YamlExportPage.php => Model/Page.php} | 10 +- src/Serializer/JsonSerializer.php | 30 ++++ src/Serializer/SerializerInterface.php | 12 ++ src/Serializer/SerializerStrategy.php | 32 ++++ src/Serializer/YamlSerializer.php | 35 +++++ .../Documents/Export/PageExporterTest.php | 31 +++- ...erTest__testSimpleSavedPagesExport__1.yaml | 29 ---- ...Test__testSimpleUnsavedPagesExport__1.yaml | 29 ---- ...ExporterTest__testSinglePageExport__1.yaml | 15 -- ...eExporterTest__testTreePagesExport__1.yaml | 43 ------ .../{Export => Import}/PageImporterTest.php | 111 ++++++++++++-- .../Admin/PageExportControllerTest.php | 4 +- translations/admin.de.yaml | 1 + translations/admin.en.yaml | 1 + 35 files changed, 750 insertions(+), 453 deletions(-) create mode 100644 docs/images/context_menu_export.png delete mode 100644 docs/images/context_menu_import_export.png create mode 100644 docs/images/filename_dialog.png create mode 100644 docs/images/import_menu.png delete mode 100644 src/Command/ExportAllPagesCommand.php create mode 100644 src/Command/ExportPagesCommand.php create mode 100644 src/Command/ImportPagesCommand.php rename src/Documents/Import/{ => Populator}/PageImportPopulator.php (65%) rename src/Documents/{Export/YamlExportEditable.php => Model/Editable.php} (56%) rename src/Documents/{Export/YamlExportPage.php => Model/Page.php} (81%) create mode 100644 src/Serializer/JsonSerializer.php create mode 100644 src/Serializer/SerializerInterface.php create mode 100644 src/Serializer/SerializerStrategy.php create mode 100644 src/Serializer/YamlSerializer.php delete mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml delete mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml delete mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml delete mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml rename tests/Integration/Documents/{Export => Import}/PageImporterTest.php (55%) diff --git a/README.md b/README.md index ad89238..bf8c4df 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,118 @@ ## Usage +### Pimcore Admin Backend + After enabling the bundle you should see a new menu item in the context menu of Pimcore Admin Backend - Section Documents: -![context_menu_import_export.png](docs/images/context_menu_import_export.png) +![context_menu_export.png](docs/images/context_menu_export.png) + +After that you will be asked for a file name and the export will start: +![filename_dialog.png](docs/images/filename_dialog.png) (german translation) +For the import you can use the main menu button: + +![import_menu.png](docs/images/import_menu.png) + +### Symfony Commands + +There are two commands: one for exporting all pages, starting from the root document with ID 1, and one for importing pages from a given YAML file. + +#### Export Command + +##### Command Name +`neusta:pimcore:export:pages:all` + +##### Description +Exports all pages into a single YAML file. Optionally, you can specify a comma-separated list of page IDs to export specific pages and their children. + +##### Options + +- `--output` or `-o` (optional): Specifies the name of the output file. Default is `export_all_pages.yaml`. +- `--ids` (optional): A comma-separated list of page IDs to export. If not provided, the command exports the root page and its children. + +##### Usage + +1. **Export all pages to the default file:** + + ```sh + php bin/console neusta:pimcore:export:pages:all + ``` + +2. **Export all pages to a specified file:** + + ```sh + php bin/console neusta:pimcore:export:pages:all --output=custom_output.yaml + ``` + +3. **Export specific pages and their children:** + + ```sh + php bin/console neusta:pimcore:export:pages:all --ids=2,3,4 + ``` + +4. **Export specific pages and their children to a specified file:** + + ```sh + php bin/console neusta:pimcore:export:pages:all --ids=2,3,4 --output=custom_output.yaml + ``` + +##### Example + +To export pages with IDs 2, 3, and 4 and their children to a file named `selected_pages.yaml`: + +```sh +php bin/console neusta:pimcore:export:pages:all --ids=2,3,4 --output=selected_pages.yaml +``` + +This command will generate a YAML file named `selected_pages.yaml` containing the specified pages and their children. If any of the specified page IDs are not found, an error message will be displayed. +#### Import Command + +##### Command Name +`neusta:pimcore:import:pages` + +##### Description +Imports pages from a given YAML file. Optionally, you can perform a dry run to see how many pages would be successfully imported without actually saving them. + +##### Options + +- `--input` or `-i` (required): Specifies the name of the input YAML file. +- `--dry-run` (optional): Perform a dry run without saving the imported pages. + +##### Usage + +1. **Import pages from a specified file:** + + ```sh + php bin/console neusta:pimcore:import:pages --input=your_input_file.yaml + ``` + +2. **Perform a dry run to see how many pages would be imported:** + + ```sh + php bin/console neusta:pimcore:import:pages --input=your_input_file.yaml --dry-run + ``` + +##### Example + +To import pages from a file named `pages_to_import.yaml`: + +```sh +php bin/console neusta:pimcore:import:pages --input=pages_to_import.yaml +``` + +To perform a dry run for the same file: + +```sh +php bin/console neusta:pimcore:import:pages --input=pages_to_import.yaml --dry-run +``` + +This command will read the specified YAML file and import the pages. If the `--dry-run` option is used, the pages will not be saved, and you will see how many pages would be successfully imported. + +## Concepts + ### Page Export The selected page will be exported into YAML format: @@ -51,8 +157,6 @@ page: In the same way you can re-import your yaml file again by selecting: `Import from YAML` in the context menu. -## Configuration - ### Page Import The import process will create pages with the given data. @@ -73,6 +177,40 @@ If neither is found, an InvalidArgumentException will be thrown, and the save op If multiple pages are imported and a path specification changes py the applied rules, this path specification will be replaced with the new, correct path specification in all provided page configurations. +### Parameterize your yaml files + +You can parameterize your yaml files with placeholders. The placeholders will be replaced by the values you provide in your fixtures. + +```yaml +pages: + - page: + id: 2 + parentId: 1 + # ...further properties + editables: + # ...several editables + 'main:1.img:1.image': + type: image + data: + id: %IMAGE_ID% + 'main:1.img:1.title': + # ... +``` + +In the case above an image has been assigned to an `Editable/Image` editable. The image id is a placeholder `%IMAGE_ID%`. + +You can use now a `Neusta\Pimcore\ImportExportBundle\Documents\Import\Filter\SearchAndReplaceFilter` instance to replace the placeholder with the actual image id (e.g. 1234). + +```php +$yamlContent = (new SearchAndReplaceFilter(['%IMAGE_ID%' => 1234]))->filterAndReplace($yamlContent); +``` + +If you want to change your yaml in a more complex way you can use the `Neusta\Pimcore\ImportExportBundle\Documents\Import\Filter\YamlFilter` interface to implement your own filter. + +With that technique you can export test pages for Fixtures, change values into placeholders (e.g. for assets and data objects) and replace them with the actual values in your tests. + +```php + ## Contribution Feel free to open issues for any bug, feature request, or other ideas. diff --git a/config/converters_populators.yaml b/config/converters_populators.yaml index 57c98a5..04f07ae 100644 --- a/config/converters_populators.yaml +++ b/config/converters_populators.yaml @@ -4,28 +4,24 @@ services: autoconfigure: true ########################################################### - # Import Populator (YamlExportPage -> Page) + # Import Populator (YamlPage -> Page) ########################################################### - Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImportPopulator: ~ - - Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImporter: - arguments: - $yamlToPageConverter: '@neusta_pimcore_import_export.import_page' + Neusta\Pimcore\ImportExportBundle\Documents\Import\Populator\PageImportPopulator: ~ ########################################################### - # Export Converter (Document -> YamlExportPage) + # Export Converter (Document -> YamlPage) ########################################################### neusta_pimcore_import_export.export_document: class: Neusta\Pimcore\ImportExportBundle\Documents\Export\Converter\DocumentTypeStrategyConverter arguments: - $type2ConverterMap: + $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\Folder: '@neusta_pimcore_import_export.export_folder' ########################################################### - # Export Populator (Page -> YamlExportPage) + # Export Populator (Page -> YamlPage) ########################################################### neusta_pimcore_import_export.page.editables.populator: class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator @@ -56,7 +52,7 @@ services: $skipNull: true ########################################################### - # Export Populator (Editable -> YamlExportEditable) + # Export Populator (Editable -> YamlEditable) ########################################################### neusta_pimcore_import_export.editable.data.populator: class: Neusta\Pimcore\ImportExportBundle\Documents\Export\Populator\EditableDataPopulator diff --git a/config/pimcore/config.yaml b/config/pimcore/config.yaml index 60f7988..842965e 100644 --- a/config/pimcore/config.yaml +++ b/config/pimcore/config.yaml @@ -4,12 +4,12 @@ neusta_converter: converter: ########################################################### - # Import Converter (YamlExportPage -> Page) + # Import Converter (Page -> PimcorePage) ########################################################### neusta_pimcore_import_export.import_page: target: Pimcore\Model\Document\Page populators: - - Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImportPopulator + - Neusta\Pimcore\ImportExportBundle\Documents\Import\Populator\PageImportPopulator properties: id: source: id @@ -37,10 +37,10 @@ neusta_converter: default: 0 ########################################################### - # Export Converter (Folder -> YamlExportPage) + # Export Converter (Document/Folder -> Page) ########################################################### neusta_pimcore_import_export.export_folder: - target: Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage + target: Neusta\Pimcore\ImportExportBundle\Documents\Model\Page populators: - neusta_pimcore_import_export.page.property.language.populator - neusta_pimcore_import_export.page.property.navigation_title.populator @@ -54,10 +54,10 @@ neusta_converter: parentId: ~ ########################################################### - # Export Converter (Page -> YamlExportPage) + # Export Converter (Pimcore Page -> Page) ########################################################### neusta_pimcore_import_export.export_page: - target: Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage + target: Neusta\Pimcore\ImportExportBundle\Documents\Model\Page populators: - neusta_pimcore_import_export.page.property.language.populator - neusta_pimcore_import_export.page.property.navigation_title.populator @@ -74,10 +74,10 @@ neusta_converter: parentId: ~ ########################################################### - # Export Converter (Snippet -> YamlExportPage) + # Export Converter (Page Snippet -> Page) ########################################################### neusta_pimcore_import_export.export_page_snippet: - target: Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage + target: Neusta\Pimcore\ImportExportBundle\Documents\Model\Page populators: - neusta_pimcore_import_export.page.property.language.populator - neusta_pimcore_import_export.page.property.navigation_title.populator @@ -93,7 +93,7 @@ neusta_converter: parentId: ~ neusta_pimcore_import_export.editable_converter: - target: Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportEditable + target: Neusta\Pimcore\ImportExportBundle\Documents\Model\Editable populators: - neusta_pimcore_import_export.editable.data.populator properties: diff --git a/config/services.yaml b/config/services.yaml index 178645e..3fd6a71 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -12,7 +12,11 @@ services: ############ # Commands # ############ - Neusta\Pimcore\ImportExportBundle\Command\ExportAllPagesCommand: + Neusta\Pimcore\ImportExportBundle\Command\ExportPagesCommand: + public: true + tags: [ 'console.command' ] + + Neusta\Pimcore\ImportExportBundle\Command\ImportPagesCommand: public: true tags: [ 'console.command' ] @@ -27,13 +31,34 @@ services: public: true tags: [ 'controller.service_arguments' ] + ############## + # Serializer # + ############## + Neusta\Pimcore\ImportExportBundle\Serializer\SerializerInterface: + alias: Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy + + Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy: + arguments: + $formatToSerializerMap: + yaml: '@Neusta\Pimcore\ImportExportBundle\Serializer\YamlSerializer' + json: '@Neusta\Pimcore\ImportExportBundle\Serializer\JsonSerializer' + + Neusta\Pimcore\ImportExportBundle\Serializer\YamlSerializer: ~ + Neusta\Pimcore\ImportExportBundle\Serializer\JsonSerializer: ~ + ############# # Documents # ############# Neusta\Pimcore\ImportExportBundle\Documents\Export\PageExporter: arguments: + $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' $pageToYamlConverter: '@neusta_pimcore_import_export.export_document' + Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImporter: + arguments: + $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' + $yamlToPageConverter: '@neusta_pimcore_import_export.import_page' + ################# # EventListener # ################# diff --git a/docs/images/context_menu_export.png b/docs/images/context_menu_export.png new file mode 100644 index 0000000000000000000000000000000000000000..dc7ebe2689d8b99c1c80ec849b5dc0ed7e12482a GIT binary patch literal 4320 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 ben*6*{l^?YkS2?U)&K>$ce152AHMt-JEI%0 literal 0 HcmV?d00001 diff --git a/docs/images/import_menu.png b/docs/images/import_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..6ad9c5c6824da3f188f1d5007b19921408946664 GIT binary patch literal 2353 zcma);c{~){8prjf8cB_584;oxLTD;`$Tnt(F&Il46uo(sZG?<16owhg2rmYW@Y)(l z_AO$BWE(|e$uiLh={1(I-Fdrz+}nFUpL_2g=bYz{^PJ!FoX_|7Ifr0jZY*?A^dKJ} zpAZsZXvteOcw-%~pEui-nm^{_1FRzr^{nqZE@Z{|V%o(!w)e^dAX1Q-8xjzpK0qL$ z@R)*u$MJm^t=;8@P9%W@BYe(6$v}WW-}mB+7(JmQ#~+?}91XB(*;5MS*Yq#=bQL|0 zD#;3;>`7fp(D*~GzH{_TZM|BWQ)qK~?@V?`Pv{HTfVHS($$;Q8kD&G}t(9NmV<2Ed zvY>S5zNDE51Wpf9dzyS5h*XMkwTRM%>5Hr1w|EqHQl%2^1XfSF9J*DrIC$3@RR-fS z>4ZEmKG4boXyr(8XD}f$5Md=F7dU9r#goW6|MBZUZom?8WzJTV`OdAjJ%HsALM3SodU)8zd z;Z>SzJ!;%$*7|Z+NgPp!RxUpRlY7*(D!*7_o~3DI=}_%ywY|7dR$O3aYfHOMeNluN zBsM9P%h+4kmBveG7u11|aX-kKQ%)CoFm=#A;>ir{?E`%aLDO_03~pkL1iOC23^(#! z4GBOg&VpBRUwCfpV-J@*pkPZ|D_$=L{3>Sq{4RMp+#yqYDbqHJ=h#m#)ZfEIH*sUT z{1-ee2%SaN@$0kUd+8#y=Q4piTY(u{sYiBAoO&r5({Vu&ZfTSZ^IJ73oWF!Xx(R^M zTrt%qRGFWi#poXoyLSEN5@AsarN}1E>cNcQc81ANRY=3NidC!u@U7D~WB2NtEUh0$ z6G#%vV*$I=A@@P0cR>Cy`-;?$kDByq=^2p1bbI4p(v+~4$ug+rwI0UGRh8upFvE+k zYg|Gc1WwI#CZmNbr63#K-}GDZGA;$no=IeS;4%h7o{}9-y{E zTYAHmk8Q{DFZXvyZkFwy_iE}_1;C^PPP3n#og#7b;h9g`h>-hiTU7E~!-gQK4iO`V zwEdgAvIAH@ne1O+>RUrOee+l;a#g=US->O2exn3$7EQSG$(%54R7LvaG3|&{ramMx z*SHVoz%%Cx;F_mk(xF%hy>1Vw|DYU!qxvD(y-~P4ztTL%SHgeuHcxgy1`$%+5to92 zYk>{Ta6@d}`FM1Ms%(8M=*$7^8bZJ18D{Z_j>ki(dJ?>CF?p6IIvIk!&`Lu(m zLMQQ;ke3Oap$BiecEek>`E3p$FL74i^bo&N0eKGBt3&rT(na2+q9mV4;A@}rrdU&<&L~G*h}!T8n{p>^wD#oJh=--f1Pkxh0c>fK45~7mkHkFQc|K`!ynBH?Hn1LhW#acHY-Z4OOZ{2U5IY?6mEx~fNma|v z3;~L`d`s*gE=S(PyeLv^GsP1W`Aeb=e;?L=UOavx!ehTje+SdS#b5BZ#q$rC$2G2V zR@e7`0?dWDcuVU$A0qy^Qsjd(_P*h-AADG>(taQ4?3Z{I|FvS;tpuR`#O2Qej-#mu zRzAO)5neCkM273;eW>ObKPv!W&6J={d#6e{9rve7m;2f|s_L!o zf+8(u$I0p8SLrj;gB=4i3RMkx1A=uMEN-vsujr=YQLe1JR!wjoEMAC$DDdmeXFm5_ zvK-2Hy75#K3z`v{Ga#$vG}1tpR-|R=cEU{J#12_zJDClGW39-}Gou2G+n)yW;n+O& zq;pPM(J~Y?Ba4AGs?i@Zdw{y_mJ`3=oH3FWBTZJ2D3q z(tz+eQ|l{iR)g(mgR`i))CfMN5Z8Li?k*85XAY62Wg8uR#~yoK7)enA;7su7oLQUq zB7VG=@OwWJrMgKUJf)kd63@MS+2io->title('Export all pages into one single YAML file'); - - $rootPage = $this->pageRepository->getById(1); - if (!$rootPage) { - $this->io->error('Root page (ID: 1) not found'); - - return Command::FAILURE; - } - - $allPages = $this->addPages($rootPage, []); - - $this->io->writeln('Start exporting all results'); - $this->io->newLine(); - $yamlContent = $this->pageExporter->exportToYaml($allPages); - - $exportFilename = 'export_all_pages.yaml'; - - $this->io->writeln('Write in file <' . $exportFilename . '>'); - $this->io->newLine(); - file_put_contents($exportFilename, $yamlContent); - - $this->io->success('All pages have been exported successfully'); - - return Command::SUCCESS; - } - - /** - * @param array $allPages - * - * @return array - */ - private function addPages(Document $rootPage, array $allPages): array - { - $allPages[] = $rootPage; - foreach ($rootPage->getChildren(true) as $childPage) { - if ($childPage instanceof Document) { - $allPages = $this->addPages($childPage, $allPages); - } - } - - return $allPages; - } -} diff --git a/src/Command/ExportPagesCommand.php b/src/Command/ExportPagesCommand.php new file mode 100644 index 0000000..9870807 --- /dev/null +++ b/src/Command/ExportPagesCommand.php @@ -0,0 +1,110 @@ +addOption( + 'output', + 'o', + InputOption::VALUE_OPTIONAL, + 'The name of the output file (default: export_all_pages.yaml)', + 'export_all_pages.yaml' + ) + ->addArgument( + 'ids', + InputArgument::IS_ARRAY, + 'List of page IDs to export' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io->title('Export all pages into one single YAML file'); + + $pageIds = $input->getArgument('ids'); + $allPages = []; + + if ($pageIds) { + $ids = array_map('intval', $pageIds); + foreach ($ids as $id) { + $page = $this->pageRepository->getById($id); + if ($page) { + $allPages = $this->addPages($page, $allPages); + } else { + $this->io->error("Page with ID $id not found"); + + return Command::FAILURE; + } + } + } else { + $rootPage = $this->pageRepository->getById(1); + if (!$rootPage) { + $this->io->error('Root page (ID: 1) not found'); + + return Command::FAILURE; + } + $allPages = $this->addPages($rootPage, []); + } + + $this->io->writeln(\sprintf('Start exporting %d pages', \count($allPages))); + $this->io->newLine(); + $yamlContent = $this->pageExporter->export($allPages, 'yaml'); + + $exportFilename = $input->getOption('output'); + + $this->io->writeln('Write in file <' . $exportFilename . '>'); + $this->io->newLine(); + if (!file_put_contents($exportFilename, $yamlContent)) { + $this->io->error('An error occurred while writing the file'); + + return Command::FAILURE; + } + + $this->io->success('All pages have been exported successfully'); + + return Command::SUCCESS; + } + + /** + * @param array $allPages + * + * @return array + */ + private function addPages(Document $rootPage, array $allPages): array + { + $allPages[] = $rootPage; + foreach ($rootPage->getChildren(true) as $childPage) { + if ($childPage instanceof Document) { + $allPages = $this->addPages($childPage, $allPages); + } + } + + return $allPages; + } +} diff --git a/src/Command/ImportPagesCommand.php b/src/Command/ImportPagesCommand.php new file mode 100644 index 0000000..718a6b8 --- /dev/null +++ b/src/Command/ImportPagesCommand.php @@ -0,0 +1,62 @@ +addOption( + 'input', + 'i', + InputOption::VALUE_REQUIRED, + 'The name of the input yaml file', + ) + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'Perform a dry run without saving the imported pages' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io->title('Import pages given by YAML file'); + + $this->io->writeln('Start exporting all results'); + $this->io->newLine(); + + $yamlInput = file_get_contents($input->getOption('input')); + if (!$yamlInput) { + $this->io->error('Input file could not be read'); + + return Command::FAILURE; + } + + $pages = $this->pageImporter->import($yamlInput, 'yaml', !$input->getOption('dry-run')); + + $this->io->success(\sprintf('%d pages have been imported successfully', \count($pages))); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/Admin/PageExportController.php b/src/Controller/Admin/PageExportController.php index ffc8654..39460e3 100644 --- a/src/Controller/Admin/PageExportController.php +++ b/src/Controller/Admin/PageExportController.php @@ -4,12 +4,12 @@ use Neusta\Pimcore\ImportExportBundle\Documents\Export\PageExporter; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\PageRepository; -use Pimcore\Model\Document\Page; +use Pimcore\Model\Document\Page as PimcorePage; use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\Annotation\Route; final class PageExportController { @@ -26,22 +26,15 @@ public function __construct( )] public function exportPage(Request $request): Response { - try { - $page = $this->getPageByRequest($request); - } catch (\Exception $exception) { + $page = $this->getPageByRequest($request); + if (!$page instanceof PimcorePage) { return new JsonResponse( - \sprintf('Page with id "%s" was not found', $exception->getMessage()), + \sprintf('Page with id "%s" was not found', $request->query->getInt('page_id')), Response::HTTP_NOT_FOUND, ); } - try { - $yaml = $this->pageExporter->exportToYaml([$page]); - } catch (\Exception $e) { - return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); - } - - return $this->createJsonResponseByYaml($yaml, $this->createFilename($page)); + return $this->exportPages([$page], $request->query->getString('filename'), 'yaml'); } #[Route( @@ -51,39 +44,38 @@ public function exportPage(Request $request): Response )] public function exportPageWithChildren(Request $request): Response { - try { - $page = $this->getPageByRequest($request); - } catch (\Exception $exception) { + $page = $this->getPageByRequest($request); + if (!$page instanceof PimcorePage) { return new JsonResponse( - \sprintf('Page with id "%s" was not found', $exception->getMessage()), + \sprintf('Page with id "%s" was not found', $request->query->getInt('page_id')), Response::HTTP_NOT_FOUND, ); } - try { - $yaml = $this->pageExporter->exportToYaml($this->pageRepository->findAllPagesWithSubPages($page)); - } catch (\Exception $e) { - return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); - } + $pages = $this->pageRepository->findAllPagesWithSubPages($page); - return $this->createJsonResponseByYaml($yaml, $this->createFilename($page)); + return $this->exportPages($pages, $request->query->getString('filename'), $request->query->getString('format')); } - private function getPageByRequest(Request $request): Page + private function getPageByRequest(Request $request): ?PimcorePage { $pageId = $request->query->getInt('page_id'); - $page = $this->pageRepository->getById($pageId); - if (!$page instanceof Page) { - throw new \Exception((string) $pageId); - } - - return $page; + return $this->pageRepository->getById($pageId); } - private function createFilename(Page $page): string + /** + * @param iterable $pages + */ + private function exportPages(iterable $pages, string $filename, string $format): Response { - return \sprintf('%s.yaml', str_replace(' ', '_', (string) $page->getKey())); + try { + $yaml = $this->pageExporter->export($pages, $format); + } catch (\Exception $e) { + return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->createJsonResponseByYaml($yaml, $filename); } private function createJsonResponseByYaml(string $yaml, string $filename): Response diff --git a/src/Controller/Admin/PageImportController.php b/src/Controller/Admin/PageImportController.php index d4d7c09..767cc1f 100644 --- a/src/Controller/Admin/PageImportController.php +++ b/src/Controller/Admin/PageImportController.php @@ -4,7 +4,7 @@ use Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImporter; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\PageRepository; -use Pimcore\Model\Document\Page; +use Pimcore\Model\Document\Page as PimcorePage; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -40,15 +40,15 @@ public function __construct( )] public function import(Request $request): JsonResponse { - $file = $request->files->get('file'); + $file = $this->getUploadedFile($request); if (!$file instanceof UploadedFile) { - return new JsonResponse(['success' => false, 'message' => self::ERR_NO_FILE_UPLOADED], 400); + return $this->createJsonResponse(false, $this->messagesMap[self::ERR_NO_FILE_UPLOADED], 400); } $overwrite = $request->request->getBoolean('overwrite'); try { - $pages = $this->pageImporter->parseYaml($file->getContent()); + $pages = $this->pageImporter->import($file->getContent(), $request->query->get('format', 'yaml')); $results = [ self::SUCCESS_DOCUMENT_REPLACEMENT => 0, @@ -61,13 +61,23 @@ public function import(Request $request): JsonResponse } $resultMessage = $this->appendMessage($results); - return new JsonResponse(['success' => true, 'message' => $resultMessage]); + return $this->createJsonResponse(true, $resultMessage); } catch (\Exception $e) { - return new JsonResponse(['success' => false, 'message' => $e->getMessage()], 500); + return $this->createJsonResponse(false, $e->getMessage(), 500); } } - protected function replaceIfExists(Page $page, bool $overwrite): int + private function getUploadedFile(Request $request): ?UploadedFile + { + return $request->files->get('file'); + } + + private function createJsonResponse(bool $success, string $message, int $statusCode = 200): JsonResponse + { + return new JsonResponse(['success' => $success, 'message' => $message], $statusCode); + } + + protected function replaceIfExists(PimcorePage $page, bool $overwrite): int { $oldPage = $this->pageRepository->getByPath('/' . $page->getFullPath()); if (null !== $oldPage) { diff --git a/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php b/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php index 5090043..6ae06c4 100644 --- a/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php +++ b/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php @@ -4,28 +4,28 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; -use Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage; +use Neusta\Pimcore\ImportExportBundle\Documents\Model\Page; use Pimcore\Model\Document; /** - * @implements Converter + * @implements Converter */ class DocumentTypeStrategyConverter implements Converter { /** - * @param array> $type2ConverterMap + * @param array> $typeToConverterMap */ public function __construct( - private array $type2ConverterMap, + private array $typeToConverterMap, ) { } public function convert(object $source, ?object $ctx = null): object { - if (!\array_key_exists($source::class, $this->type2ConverterMap)) { + if (!\array_key_exists($source::class, $this->typeToConverterMap)) { throw new \InvalidArgumentException('No converter found for type ' . $source::class); } - return $this->type2ConverterMap[$source::class]->convert($source, $ctx); + return $this->typeToConverterMap[$source::class]->convert($source, $ctx); } } diff --git a/src/Documents/Export/PageExporter.php b/src/Documents/Export/PageExporter.php index 9b22d54..75f1d39 100644 --- a/src/Documents/Export/PageExporter.php +++ b/src/Documents/Export/PageExporter.php @@ -5,22 +5,17 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Exception\ConverterException; +use Neusta\Pimcore\ImportExportBundle\Documents\Model\Page; +use Neusta\Pimcore\ImportExportBundle\Serializer\SerializerInterface; use Pimcore\Model\Document; use Pimcore\Model\Document\Folder; -use Pimcore\Model\Document\Page; +use Pimcore\Model\Document\Page as PimcorePage; use Pimcore\Model\Document\PageSnippet; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Yaml\Yaml; class PageExporter { - public const YAML_DUMP_FLAGS = - Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE | - Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK | - Yaml::DUMP_NULL_AS_TILDE; - /** - * @param Converter $pageToYamlConverter + * @param Converter $pageToYamlConverter */ public function __construct( private readonly Converter $pageToYamlConverter, @@ -29,62 +24,25 @@ public function __construct( } /** - * Exports one or more pages as YAML with the following structure: - * pages: - * - page: - * key: 'page_key_1' - * - page: - * key: 'page_key_2' - * ... - * - * @throws ConverterException - * - * @deprecated use PageExporter#exportToYaml([$page]) in further versions - */ - public function toYaml(Page $page): string - { - $pages = []; - if ($page instanceof Page) { - $pages = [$page]; - } - - return $this->exportToYaml($pages); - } - - /** - * Exports one or more pages as YAML with the following structure: - * pages: - * - page: - * key: 'page_key_1' - * - page: - * key: 'page_key_2' - * ... + * Exports one or more pages in the given format (yaml, json, ...)). * * @param iterable $pages * * @throws ConverterException */ - public function exportToYaml(iterable $pages): string + public function export(iterable $pages, string $format): string { $yamlExportPages = []; foreach ($pages as $page) { if ( - $page instanceof Page + $page instanceof PimcorePage || $page instanceof PageSnippet || $page instanceof Folder ) { - $yamlExportPages[] = [YamlExportPage::PAGE => $this->pageToYamlConverter->convert($page)]; + $yamlExportPages[] = [Page::PAGE => $this->pageToYamlConverter->convert($page)]; } } - return $this->serializer->serialize( - [YamlExportPage::PAGES => $yamlExportPages], - 'yaml', - [ - 'yaml_inline' => 4, - 'yaml_indent' => 0, - 'yaml_flags' => self::YAML_DUMP_FLAGS, - ] - ); + return $this->serializer->serialize([Page::PAGES => $yamlExportPages], $format); } } diff --git a/src/Documents/Export/Populator/EditableDataPopulator.php b/src/Documents/Export/Populator/EditableDataPopulator.php index da7ec99..a49284b 100644 --- a/src/Documents/Export/Populator/EditableDataPopulator.php +++ b/src/Documents/Export/Populator/EditableDataPopulator.php @@ -4,11 +4,11 @@ use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Populator; -use Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportEditable; +use Neusta\Pimcore\ImportExportBundle\Documents\Model\Editable as PimcoreEditable; use Pimcore\Model\Document\Editable; /** - * @implements Populator + * @implements Populator */ class EditableDataPopulator implements Populator { diff --git a/src/Documents/Import/PageImporter.php b/src/Documents/Import/PageImporter.php index 7b359f2..2d795fc 100644 --- a/src/Documents/Import/PageImporter.php +++ b/src/Documents/Import/PageImporter.php @@ -5,44 +5,45 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Exception\ConverterException; -use Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage; +use Neusta\Pimcore\ImportExportBundle\Documents\Model\Page; +use Neusta\Pimcore\ImportExportBundle\Serializer\SerializerInterface; use Pimcore\Model\Document; -use Pimcore\Model\Document\Page; +use Pimcore\Model\Document\Page as PimcorePage; use Pimcore\Model\Element\DuplicateFullPathException; -use Symfony\Component\Yaml\Yaml; class PageImporter { /** - * @param Converter $yamlToPageConverter + * @param Converter $yamlToPageConverter */ public function __construct( private readonly Converter $yamlToPageConverter, + private readonly SerializerInterface $serializer, ) { } /** - * @return array + * @return array * * @throws ConverterException * @throws DuplicateFullPathException */ - public function parseYaml(string $yamlInput, bool $forcedSave = true): array + public function import(string $yamlInput, string $format, bool $forcedSave = true): array { - $config = Yaml::parse($yamlInput); + $config = $this->serializer->deserialize($yamlInput, $format); - if (!\is_array($config) || !\is_array($config[YamlExportPage::PAGES] ?? null)) { - throw new \DomainException('Given YAML is not valid.'); + if (!\is_array($config) || !\is_array($config[Page::PAGES] ?? null)) { + throw new \DomainException(\sprintf('Given data in format %s is not valid.', $format)); } $pages = []; - foreach ($config[YamlExportPage::PAGES] as $configPage) { + foreach ($config[Page::PAGES] as $configPage) { $page = null; - if (\is_array($configPage[YamlExportPage::PAGE])) { - $page = $this->yamlToPageConverter->convert(new YamlExportPage($configPage[YamlExportPage::PAGE])); + if (\is_array($configPage[Page::PAGE])) { + $page = $this->yamlToPageConverter->convert(new Page($configPage[Page::PAGE])); if ($forcedSave) { - $this->checkAndUpdatePage($page, $config[YamlExportPage::PAGES]); + $this->checkAndUpdatePage($page); $page->save(); } } @@ -54,41 +55,14 @@ public function parseYaml(string $yamlInput, bool $forcedSave = true): array return $pages; } - /** - * @param array $params - */ - public function readYamlFileAndSetParameters(string $filename, array $params = []): string + private function checkAndUpdatePage(Document $page): void { - if (($yamlFile = file_get_contents($filename)) !== false) { - foreach ($params as $key => $paramValue) { - $yamlFile = str_replace($key, (string) $paramValue, $yamlFile); - } - - return $yamlFile; - } - - return ''; - } - - /** - * @param array> $configPages - */ - private function checkAndUpdatePage(Page $page, array &$configPages): void - { - $oldPath = $page->getPath(); - $existingParent = Document::getById($page->getParentId() ?? -1); - if (!$existingParent) { + if (!Document::getById($page->getParentId() ?? -1)) { $existingParent = Document::getByPath($page->getPath() ?? ''); if (!$existingParent) { throw new \InvalidArgumentException('Neither parentId nor path leads to a valid parent element'); } $page->setParentId($existingParent->getId()); - $newPath = $existingParent->getPath() . $page->getKey() . '/'; - foreach ($configPages as $configPage) { - if (\array_key_exists('path', $configPage[YamlExportPage::PAGE]) && \is_string($oldPath)) { - str_replace($oldPath, $newPath, $configPage[YamlExportPage::PAGE]['path']); - } - } } } } diff --git a/src/Documents/Import/PageImportPopulator.php b/src/Documents/Import/Populator/PageImportPopulator.php similarity index 65% rename from src/Documents/Import/PageImportPopulator.php rename to src/Documents/Import/Populator/PageImportPopulator.php index 8752336..2fa9e73 100644 --- a/src/Documents/Import/PageImportPopulator.php +++ b/src/Documents/Import/Populator/PageImportPopulator.php @@ -1,20 +1,20 @@ + * @implements Populator */ class PageImportPopulator implements Populator { /** - * @param YamlExportPage $source - * @param Page $target + * @param Page $source + * @param PimcorePage $target * @param GenericContext|null $ctx */ public function populate(object $target, object $source, ?object $ctx = null): void @@ -26,7 +26,7 @@ public function populate(object $target, object $source, ?object $ctx = null): v $target->setProperty('navigation_name', 'text', $source->navigation_name); /** @var array $editable */ foreach ($source->editables ?? [] as $key => $editable) { - $target->setRawEditable($key, $editable['type'], $editable['data']); + $target->setRawEditable((string) $key, $editable['type'], $editable['data']); } } } diff --git a/src/Documents/Export/YamlExportEditable.php b/src/Documents/Model/Editable.php similarity index 56% rename from src/Documents/Export/YamlExportEditable.php rename to src/Documents/Model/Editable.php index 2c0eb24..cc73b55 100644 --- a/src/Documents/Export/YamlExportEditable.php +++ b/src/Documents/Model/Editable.php @@ -1,8 +1,8 @@ */ + /** @var array */ public array $editables = []; /** diff --git a/src/Serializer/JsonSerializer.php b/src/Serializer/JsonSerializer.php new file mode 100644 index 0000000..2010b0d --- /dev/null +++ b/src/Serializer/JsonSerializer.php @@ -0,0 +1,30 @@ +jsonSerializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]); + } + + public function serialize(mixed $data, string $format): string + { + return $this->jsonSerializer->serialize($data, 'json'); + } + + public function deserialize(string $data, string $format): mixed + { + return json_decode($data, true); + } +} diff --git a/src/Serializer/SerializerInterface.php b/src/Serializer/SerializerInterface.php new file mode 100644 index 0000000..52e0f95 --- /dev/null +++ b/src/Serializer/SerializerInterface.php @@ -0,0 +1,12 @@ + $formatToSerializerMap + */ + public function __construct( + private array $formatToSerializerMap, + ) { + } + + public function serialize(mixed $data, string $format): string + { + if (\array_key_exists($format, $this->formatToSerializerMap)) { + return $this->formatToSerializerMap[$format]->serialize($data, $format); + } + throw new \InvalidArgumentException('No serializer found for format ' . $format); + } + + public function deserialize(string $data, string $format): mixed + { + if (\array_key_exists($format, $this->formatToSerializerMap)) { + return $this->formatToSerializerMap[$format]->deserialize($data, $format); + } + throw new \InvalidArgumentException('No de-serializer found for format ' . $format); + } +} diff --git a/src/Serializer/YamlSerializer.php b/src/Serializer/YamlSerializer.php new file mode 100644 index 0000000..513ca3b --- /dev/null +++ b/src/Serializer/YamlSerializer.php @@ -0,0 +1,35 @@ +serializer->serialize($data, 'yaml', [ + 'yaml_inline' => 6, + 'yaml_indent' => 0, + 'yaml_flags' => self::YAML_DUMP_FLAGS, + ]); + } + + public function deserialize(string $data, string $format): mixed + { + return Yaml::parse($data); + } +} diff --git a/tests/Integration/Documents/Export/PageExporterTest.php b/tests/Integration/Documents/Export/PageExporterTest.php index 7d0748f..776677e 100644 --- a/tests/Integration/Documents/Export/PageExporterTest.php +++ b/tests/Integration/Documents/Export/PageExporterTest.php @@ -41,10 +41,33 @@ public function testSinglePageExport(): void $inputEditable->setDataFromResource('some text input'); $page->setEditables([$inputEditable]); - $yaml = $this->exporter->exportToYaml([$page]); + $yaml = $this->exporter->export([$page], 'yaml'); $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } + public function testSinglePageExportAsJson(): void + { + $page = new Page(); + $page->setId(999); + $page->setParentId(4); + $page->setType('email'); + $page->setPublished(false); + $page->setPath('/test/'); + $page->setKey('test_document_1'); + $page->setProperty('language', 'string', 'en'); + $page->setProperty('navigation_name', 'string', 'My Document'); + $page->setProperty('navigation_title', 'string', 'My Document - Title'); + $page->setTitle('The Title of my document'); + $page->setController('/Some/Controller'); + $inputEditable = new Input(); + $inputEditable->setName('textInput'); + $inputEditable->setDataFromResource('some text input'); + $page->setEditables([$inputEditable]); + + $json = $this->exporter->export([$page], 'json'); + $this->assertMatchesJsonSnapshot($json); + } + public function testSimpleSavedPagesExport(): void { $page1 = new Page(); @@ -59,7 +82,7 @@ public function testSimpleSavedPagesExport(): void $page2->setTitle('Test Document_2'); $page2->save(); - $yaml = $this->exporter->exportToYaml([$page1, $page2]); + $yaml = $this->exporter->export([$page1, $page2], 'yaml'); $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } @@ -75,7 +98,7 @@ public function testSimpleUnsavedPagesExport(): void $page2->setKey('test_document_2'); $page2->setPath('/'); - $yaml = $this->exporter->exportToYaml([$page1, $page2]); + $yaml = $this->exporter->export([$page1, $page2], 'yaml'); $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } @@ -99,7 +122,7 @@ public function testTreePagesExport(): void $page3->setTitle('Test Document_2'); $page3->save(); - $yaml = $this->exporter->exportToYaml([$page1, $page2, $page3]); + $yaml = $this->exporter->export([$page1, $page2, $page3], 'yaml'); $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } } diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml deleted file mode 100644 index fa1de4d..0000000 --- a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml +++ /dev/null @@ -1,29 +0,0 @@ -pages: - - - page: - id: 2 - parentId: 1 - type: page - published: true - path: / - language: '' - navigation_name: ~ - navigation_title: ~ - key: test_document_1 - title: 'Test Document_1' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } - - - page: - id: 3 - parentId: 1 - type: page - published: true - path: / - language: '' - navigation_name: ~ - navigation_title: ~ - key: test_document_2 - title: 'Test Document_2' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml deleted file mode 100644 index cf5f19f..0000000 --- a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml +++ /dev/null @@ -1,29 +0,0 @@ -pages: - - - page: - id: ~ - parentId: 1 - type: page - published: true - path: / - language: '' - navigation_name: ~ - navigation_title: ~ - key: test_document_1 - title: '' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } - - - page: - id: ~ - parentId: 1 - type: page - published: true - path: / - language: '' - navigation_name: ~ - navigation_title: ~ - key: test_document_2 - title: '' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml deleted file mode 100644 index c5c7a06..0000000 --- a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml +++ /dev/null @@ -1,15 +0,0 @@ -pages: - - - page: - id: 999 - parentId: 4 - type: email - published: false - path: /test/ - language: en - navigation_name: 'My Document' - navigation_title: 'My Document - Title' - key: test_document_1 - title: 'The Title of my document' - controller: /Some/Controller - editables: [{ type: input, name: textInput, data: 'some text input' }] diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml deleted file mode 100644 index 0fe7e0c..0000000 --- a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml +++ /dev/null @@ -1,43 +0,0 @@ -pages: - - - page: - id: 2 - parentId: 1 - type: page - published: true - path: / - language: '' - navigation_name: ~ - navigation_title: ~ - key: test_document_1 - title: 'Test Document_1' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } - - - page: - id: 3 - parentId: 2 - type: page - published: true - path: /test_document_1/ - language: '' - navigation_name: ~ - navigation_title: ~ - key: test_document_2 - title: 'Test Document_2' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } - - - page: - id: 4 - parentId: 3 - type: page - published: true - path: /test_document_1/test_document_2/ - language: '' - navigation_name: ~ - navigation_title: ~ - key: test_document_2 - title: 'Test Document_2' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } diff --git a/tests/Integration/Documents/Export/PageImporterTest.php b/tests/Integration/Documents/Import/PageImporterTest.php similarity index 55% rename from tests/Integration/Documents/Export/PageImporterTest.php rename to tests/Integration/Documents/Import/PageImporterTest.php index 9aa8034..3287879 100644 --- a/tests/Integration/Documents/Export/PageImporterTest.php +++ b/tests/Integration/Documents/Import/PageImporterTest.php @@ -1,6 +1,6 @@ expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Neither parentId nor path leads to a valid parent element'); - $this->importer->parseYaml($yaml); + $this->importer->import($yaml, 'yaml'); } public function testSinglePageExport_regular_case_parent_id(): void { $yaml = <<importer->parseYaml($yaml); + $pages = $this->importer->import($yaml, 'yaml'); + self::assertEquals(999, $pages[0]->getId()); + self::assertEquals('/', $pages[0]->getPath()); + + self::assertEquals('test_document_1', $pages[0]->getKey()); + self::assertEquals('The Title of My Document', $pages[0]->getTitle()); + self::assertEquals('email', $pages[0]->getType()); + self::assertEquals('/Some/Controller', $pages[0]->getController()); + self::assertEquals('fr', $pages[0]->getProperty('language')); + self::assertEquals('My Document', $pages[0]->getProperty('navigation_name')); + self::assertEquals('My Document - Title', $pages[0]->getProperty('navigation_title')); + } + + public function testSinglePageExport_regular_case_json(): void + { + $json = + <<importer->import($json, 'json'); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); @@ -73,9 +118,9 @@ public function testSinglePageExport_regular_case_path(): void { $yaml = <<importer->parseYaml($yaml); + $pages = $this->importer->import($yaml, 'yaml'); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); self::assertEquals(1, $pages[0]->getParentId()); @@ -107,29 +152,65 @@ public function testSinglePageImport_tree_case(): void { $yaml = <<importer->parseYaml($yaml); + $pages = $this->importer->import($yaml, 'yaml'); self::assertEquals('/test_document_1/test_document_1_1/', $pages[2]->getPath()); } + + public function testSinglePageImport_tree_case_by_path(): void + { + $yaml = + <<importer->import($yaml, 'yaml'); + + self::assertEquals('/test_document_1/test_document_1_1/', $pages[3]->getPath()); + } } diff --git a/tests/Unit/Controller/Admin/PageExportControllerTest.php b/tests/Unit/Controller/Admin/PageExportControllerTest.php index 1158f31..785d7f1 100644 --- a/tests/Unit/Controller/Admin/PageExportControllerTest.php +++ b/tests/Unit/Controller/Admin/PageExportControllerTest.php @@ -43,7 +43,7 @@ public function testExportPage_regular_case(): void { $page = $this->prophesize(Page::class); $this->pageRepository->getById(17)->willReturn($page->reveal()); - $this->pageExporter->exportToYaml([$page->reveal()])->willReturn('TEST_YAML'); + $this->pageExporter->export([$page->reveal()], 'yaml')->willReturn('TEST_YAML'); $response = $this->controller->exportPage($this->request); @@ -57,7 +57,7 @@ public function testExportPage_exceptional_case(): void { $page = $this->prophesize(Page::class); $this->pageRepository->getById(17)->willReturn($page->reveal()); - $this->pageExporter->exportToYaml([$page->reveal()])->willThrow(new \Exception('Problem')); + $this->pageExporter->export([$page->reveal()], 'yaml')->willThrow(new \Exception('Problem')); $response = $this->controller->exportPage($this->request); diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index 1e710d6..195c350 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -1,3 +1,4 @@ +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' neusta_pimcore_import_export_import_dialog_title: 'Importiere Seite aus YAML' diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index 718ac19..3c435a3 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -1,3 +1,4 @@ +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' neusta_pimcore_import_export_import_dialog_title: 'Import Page from YAML' From 7b6a683aa2d9e2473f73dd68f9c06b65eb4f3035 Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Wed, 19 Mar 2025 08:02:35 +0100 Subject: [PATCH 21/28] [Export All][Refactoring][New Commands] add test results --- ...erTest__testSimpleSavedPagesExport__1.yaml | 29 +++++++++++++ ...Test__testSimpleUnsavedPagesExport__1.yaml | 29 +++++++++++++ ...erTest__testSinglePageExportAsJson__1.json | 26 +++++++++++ ...ExporterTest__testSinglePageExport__1.yaml | 19 ++++++++ ...eExporterTest__testTreePagesExport__1.yaml | 43 +++++++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExportAsJson__1.json create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml create mode 100644 tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml new file mode 100644 index 0000000..504d0e1 --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleSavedPagesExport__1.yaml @@ -0,0 +1,29 @@ +documents: + - + document: + id: 2 + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_1 + title: 'Test Document_1' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } + - + document: + id: 3 + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_2 + title: 'Test Document_2' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml new file mode 100644 index 0000000..f1f277b --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSimpleUnsavedPagesExport__1.yaml @@ -0,0 +1,29 @@ +documents: + - + document: + id: ~ + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_1 + title: '' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } + - + document: + id: ~ + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_2 + title: '' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExportAsJson__1.json b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExportAsJson__1.json new file mode 100644 index 0000000..4fd0120 --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExportAsJson__1.json @@ -0,0 +1,26 @@ +{ + "documents": [ + { + "document": { + "id": 999, + "parentId": 4, + "type": "email", + "published": false, + "path": "\/test\/", + "language": "en", + "navigation_name": "My Document", + "navigation_title": "My Document - Title", + "key": "test_document_1", + "title": "The Title of my document", + "controller": "\/Some\/Controller", + "editables": [ + { + "type": "input", + "name": "textInput", + "data": "some text input" + } + ] + } + } + ] +} diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml new file mode 100644 index 0000000..2ff3281 --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testSinglePageExport__1.yaml @@ -0,0 +1,19 @@ +documents: + - + document: + id: 999 + parentId: 4 + type: email + published: false + path: /test/ + language: en + navigation_name: 'My Document' + navigation_title: 'My Document - Title' + key: test_document_1 + title: 'The Title of my document' + controller: /Some/Controller + editables: + - + type: input + name: textInput + data: 'some text input' diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml new file mode 100644 index 0000000..29ec4d9 --- /dev/null +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml @@ -0,0 +1,43 @@ +documents: + - + document: + id: 2 + parentId: 1 + type: page + published: true + path: / + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_1 + title: 'Test Document_1' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } + - + document: + id: 3 + parentId: 2 + type: page + published: true + path: /test_document_1/ + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_2 + title: 'Test Document_2' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } + - + document: + id: 4 + parentId: 3 + type: page + published: true + path: /test_document_1/test_document_2/ + language: '' + navigation_name: ~ + navigation_title: ~ + key: test_document_2 + title: 'Test Document_2' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } From af03dc1e7f73ab74cd099525fd82c22cad39dcac Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Wed, 19 Mar 2025 08:05:46 +0100 Subject: [PATCH 22/28] Update tests/Integration/Documents/Export/PageExporterTest.php Co-authored-by: Jacob Dreesen --- tests/Integration/Documents/Export/PageExporterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Documents/Export/PageExporterTest.php b/tests/Integration/Documents/Export/PageExporterTest.php index 776677e..c809931 100644 --- a/tests/Integration/Documents/Export/PageExporterTest.php +++ b/tests/Integration/Documents/Export/PageExporterTest.php @@ -118,8 +118,8 @@ public function testTreePagesExport(): void $page3 = new Page(); $page3->setParentId($page2->getId()); - $page3->setKey('test_document_2'); - $page3->setTitle('Test Document_2'); + $page3->setKey('test_document_3'); + $page3->setTitle('Test Document_3'); $page3->save(); $yaml = $this->exporter->export([$page1, $page2, $page3], 'yaml'); From c4704c236007bea1f4f615442d0c822f7c2e2aef Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Wed, 19 Mar 2025 08:08:41 +0100 Subject: [PATCH 23/28] [Export All][Refactoring][New Commands] adapt naming import to export --- config/pimcore/config.yaml | 2 +- config/services.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/pimcore/config.yaml b/config/pimcore/config.yaml index 842965e..32ed249 100644 --- a/config/pimcore/config.yaml +++ b/config/pimcore/config.yaml @@ -6,7 +6,7 @@ neusta_converter: ########################################################### # Import Converter (Page -> PimcorePage) ########################################################### - neusta_pimcore_import_export.import_page: + neusta_pimcore_import_export.import_document: target: Pimcore\Model\Document\Page populators: - Neusta\Pimcore\ImportExportBundle\Documents\Import\Populator\PageImportPopulator diff --git a/config/services.yaml b/config/services.yaml index 3fd6a71..b9cdcda 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -57,7 +57,7 @@ services: Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImporter: arguments: $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' - $yamlToPageConverter: '@neusta_pimcore_import_export.import_page' + $yamlToPageConverter: '@neusta_pimcore_import_export.import_document' ################# # EventListener # From fa97083f880257cf5ea96f1928fd3beb302ef80e Mon Sep 17 00:00:00 2001 From: "Michael Albrecht (personal)" Date: Wed, 19 Mar 2025 10:45:12 +0100 Subject: [PATCH 24/28] [Export All][Refactoring][New Commands] fix test --- .../PageExporterTest__testTreePagesExport__1.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml index 29ec4d9..ccef157 100644 --- a/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml +++ b/tests/Integration/Documents/Export/__snapshots__/PageExporterTest__testTreePagesExport__1.yaml @@ -37,7 +37,7 @@ documents: language: '' navigation_name: ~ navigation_title: ~ - key: test_document_2 - title: 'Test Document_2' + key: test_document_3 + title: 'Test Document_3' controller: 'App\Controller\DefaultController::defaultAction' editables: { } From c0c6bb23d2985e4e5b4fcc6f7add9fb5d431a8f5 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Wed, 19 Mar 2025 10:49:07 +0100 Subject: [PATCH 25/28] Update src/Controller/Admin/PageImportController.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Controller/Admin/PageImportController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Admin/PageImportController.php b/src/Controller/Admin/PageImportController.php index 767cc1f..0ab3cad 100644 --- a/src/Controller/Admin/PageImportController.php +++ b/src/Controller/Admin/PageImportController.php @@ -48,7 +48,7 @@ public function import(Request $request): JsonResponse $overwrite = $request->request->getBoolean('overwrite'); try { - $pages = $this->pageImporter->import($file->getContent(), $request->query->get('format', 'yaml')); + $pages = $this->pageImporter->import($file->getContent(), (string)$request->query->get('format', 'yaml')); $results = [ self::SUCCESS_DOCUMENT_REPLACEMENT => 0, From a77952e431522dded136f1170b3837ef76f5ffb9 Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Wed, 19 Mar 2025 10:53:18 +0100 Subject: [PATCH 26/28] Update src/Command/ExportPagesCommand.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Command/ExportPagesCommand.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Command/ExportPagesCommand.php b/src/Command/ExportPagesCommand.php index 9870807..7963ca5 100644 --- a/src/Command/ExportPagesCommand.php +++ b/src/Command/ExportPagesCommand.php @@ -77,6 +77,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $yamlContent = $this->pageExporter->export($allPages, 'yaml'); $exportFilename = $input->getOption('output'); + // Validate filename to prevent directory traversal + $safeFilename = basename($exportFilename); + if ($safeFilename !== $exportFilename) { + $this->io->warning(sprintf( + 'For security reasons, path traversal is not allowed. Using "%s" instead of "%s".', + $safeFilename, + $exportFilename + )); + $exportFilename = $safeFilename; + } $this->io->writeln('Write in file <' . $exportFilename . '>'); $this->io->newLine(); From 8844cd1fad6ead1cc1f697e5b8f6285e31c6714c Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Wed, 19 Mar 2025 10:53:49 +0100 Subject: [PATCH 27/28] Update src/Command/ImportPagesCommand.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Command/ImportPagesCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/ImportPagesCommand.php b/src/Command/ImportPagesCommand.php index 718a6b8..c33f3ae 100644 --- a/src/Command/ImportPagesCommand.php +++ b/src/Command/ImportPagesCommand.php @@ -43,7 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $this->io->title('Import pages given by YAML file'); - $this->io->writeln('Start exporting all results'); + $this->io->writeln('Start importing pages from YAML file'); $this->io->newLine(); $yamlInput = file_get_contents($input->getOption('input')); From 57e985375a5e0071f54d4edefc1c653dd111232e Mon Sep 17 00:00:00 2001 From: Michael Albrecht Date: Wed, 19 Mar 2025 10:54:48 +0100 Subject: [PATCH 28/28] Update src/Command/ImportPagesCommand.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Command/ImportPagesCommand.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Command/ImportPagesCommand.php b/src/Command/ImportPagesCommand.php index c33f3ae..d49f805 100644 --- a/src/Command/ImportPagesCommand.php +++ b/src/Command/ImportPagesCommand.php @@ -53,7 +53,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $pages = $this->pageImporter->import($yamlInput, 'yaml', !$input->getOption('dry-run')); + try { + $pages = $this->pageImporter->import($yamlInput, 'yaml', !$input->getOption('dry-run')); + } catch (\DomainException $e) { + $this->io->error(sprintf('Invalid YAML format: %s', $e->getMessage())); + return Command::FAILURE; + } catch (\InvalidArgumentException $e) { + $this->io->error(sprintf('Import error: %s', $e->getMessage())); + return Command::FAILURE; + } catch (\Exception $e) { + $this->io->error(sprintf('Unexpected error during import: %s', $e->getMessage())); + return Command::FAILURE; + } $this->io->success(\sprintf('%d pages have been imported successfully', \count($pages)));