diff --git a/README.md b/README.md index 521e992..bf8c4df 100644 --- a/README.md +++ b/README.md @@ -3,29 +3,137 @@ ## Installation 1. **Require the bundle** - + ```shell composer require teamneusta/pimcore-import-export-bundle ``` -2. **Enable the bundle** +2. **Enable the bundle** Add the Bundle to your `config/bundles.php`: - ```php - Neusta\Pimcore\ImportExportBundle\NeustaPimcoreImportExportBundle::class => ['all' => true], - ``` + ```php + Neusta\Pimcore\ImportExportBundle\NeustaPimcoreImportExportBundle::class => ['all' => true], + ``` ## 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: + ```yaml page: id: 123 @@ -43,17 +151,65 @@ page: main: type: areablock name: main - data: [{ key: '1', type: text-editor, hidden: false }] + data: [ { key: '1', type: text-editor, hidden: false } ] ... ``` 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 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. + +### 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 @@ -74,5 +230,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 ``` 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/converters_populators.yaml b/config/converters_populators.yaml new file mode 100644 index 0000000..04f07ae --- /dev/null +++ b/config/converters_populators.yaml @@ -0,0 +1,59 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + ########################################################### + # Import Populator (YamlPage -> Page) + ########################################################### + Neusta\Pimcore\ImportExportBundle\Documents\Import\Populator\PageImportPopulator: ~ + + ########################################################### + # Export Converter (Document -> YamlPage) + ########################################################### + neusta_pimcore_import_export.export_document: + class: Neusta\Pimcore\ImportExportBundle\Documents\Export\Converter\DocumentTypeStrategyConverter + arguments: + $typeToConverterMap: + Pimcore\Model\Document\Page: '@neusta_pimcore_import_export.export_page' + Pimcore\Model\Document\Snippet: '@neusta_pimcore_import_export.export_page_snippet' + Pimcore\Model\Document\Folder: '@neusta_pimcore_import_export.export_folder' + + + ########################################################### + # Export Populator (Page -> YamlPage) + ########################################################### + 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 -> 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 7c843c2..32ed249 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: + neusta_pimcore_import_export.import_document: target: Pimcore\Model\Document\Page populators: - - Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImportPopulator + - Neusta\Pimcore\ImportExportBundle\Documents\Import\Populator\PageImportPopulator properties: id: source: id @@ -31,15 +31,33 @@ neusta_converter: path: source: path default: '/' + skip_null: true parentId: source: parentId default: 0 ########################################################### - # Export Converter (Page -> YamlExportPage) + # Export Converter (Document/Folder -> Page) + ########################################################### + neusta_pimcore_import_export.export_folder: + 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 + - neusta_pimcore_import_export.page.property.navigation_name.populator + properties: + id: ~ + key: ~ + type: ~ + published: ~ + path: ~ + parentId: ~ + + ########################################################### + # 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 @@ -55,9 +73,29 @@ neusta_converter: path: ~ parentId: ~ + ########################################################### + # Export Converter (Page Snippet -> Page) + ########################################################### + neusta_pimcore_import_export.export_page_snippet: + 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 + - 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 + target: Neusta\Pimcore\ImportExportBundle\Documents\Model\Editable + populators: + - neusta_pimcore_import_export.editable.data.populator properties: type: ~ name: ~ - data: ~ diff --git a/config/services.yaml b/config/services.yaml index f9fb6ac..b9cdcda 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,3 +1,6 @@ +imports: + - { resource: 'converters_populators.yaml' } + services: _defaults: autowire: true @@ -6,9 +9,20 @@ services: Neusta\Pimcore\ImportExportBundle\Toolbox\: resource: '../src/Toolbox' - ########################################################### - # Controller - ########################################################### + ############ + # Commands # + ############ + Neusta\Pimcore\ImportExportBundle\Command\ExportPagesCommand: + public: true + tags: [ 'console.command' ] + + Neusta\Pimcore\ImportExportBundle\Command\ImportPagesCommand: + public: true + tags: [ 'console.command' ] + + ############## + # Controller # + ############## Neusta\Pimcore\ImportExportBundle\Controller\Admin\PageExportController: public: true tags: [ 'controller.service_arguments' ] @@ -17,47 +31,37 @@ 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' + ############## + # Serializer # + ############## + Neusta\Pimcore\ImportExportBundle\Serializer\SerializerInterface: + alias: Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy - neusta_pimcore_import_export.page.property.language.populator: - class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator + Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy: arguments: - $propertyKey: 'language' - $targetProperty: 'language' + $formatToSerializerMap: + yaml: '@Neusta\Pimcore\ImportExportBundle\Serializer\YamlSerializer' + json: '@Neusta\Pimcore\ImportExportBundle\Serializer\JsonSerializer' - neusta_pimcore_import_export.page.property.navigation_title.populator: - class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator - arguments: - $propertyKey: 'navigation_title' - $targetProperty: 'navigation_title' + Neusta\Pimcore\ImportExportBundle\Serializer\YamlSerializer: ~ + Neusta\Pimcore\ImportExportBundle\Serializer\JsonSerializer: ~ - neusta_pimcore_import_export.page.property.navigation_name.populator: - class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator + ############# + # Documents # + ############# + Neusta\Pimcore\ImportExportBundle\Documents\Export\PageExporter: arguments: - $propertyKey: 'navigation_name' - $targetProperty: 'navigation_name' + $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' + $pageToYamlConverter: '@neusta_pimcore_import_export.export_document' - Neusta\Pimcore\ImportExportBundle\Documents\Export\PageExporter: + Neusta\Pimcore\ImportExportBundle\Documents\Import\PageImporter: arguments: - $pageToYamlConverter: '@neusta_pimcore_import_export.export_page' + $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' + $yamlToPageConverter: '@neusta_pimcore_import_export.import_document' + ################# + # EventListener # + ################# Neusta\Pimcore\ImportExportBundle\EventListener\PimcoreAdminListener: tags: - { name: kernel.event_listener, event: pimcore.bundle_manager.paths.js, method: addJSFiles } diff --git a/docs/images/context_menu_export.png b/docs/images/context_menu_export.png new file mode 100644 index 0000000..dc7ebe2 Binary files /dev/null and b/docs/images/context_menu_export.png differ diff --git a/docs/images/context_menu_import_export.png b/docs/images/context_menu_import_export.png deleted file mode 100644 index a6d1a6d..0000000 Binary files a/docs/images/context_menu_import_export.png and /dev/null differ diff --git a/docs/images/filename_dialog.png b/docs/images/filename_dialog.png new file mode 100644 index 0000000..7fcca1d Binary files /dev/null and b/docs/images/filename_dialog.png differ diff --git a/docs/images/import_menu.png b/docs/images/import_menu.png new file mode 100644 index 0000000..6ad9c5c Binary files /dev/null and b/docs/images/import_menu.png differ diff --git a/public/js/exportPage.js b/public/js/exportPage.js index 9a771fe..d5784bb 100755 --- a/public/js/exportPage.js +++ b/public/js/exportPage.js @@ -8,17 +8,26 @@ 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 + + // Add menu items menu.add("-"); + this.addMenuItem(menu, document, 'neusta_pimcore_import_export_export_menu_label', 'neusta_pimcore_import_export_page_export'); + this.addMenuItem(menu, document, 'neusta_pimcore_import_export_export_with_children_menu_label', 'neusta_pimcore_import_export_page_export_with_children'); + }, + + addMenuItem: function (menu, document, label, route) { menu.add(new Ext.menu.Item({ - text: t('neusta_pimcore_import_export_export_menu_label'), + text: t(label), iconCls: "pimcore_icon_export", handler: function () { - pimcore.helpers.download(Routing.generate('neusta_pimcore_import_export_page_export', {page_id: document.data.id})); + let defaultFilename = document.data.key + '.yaml'; + let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); + if (filename) { + pimcore.helpers.download(Routing.generate(route, {page_id: document.data.id, filename: filename, format: 'yaml'})); + } } })); - }, - + } }); var pimcorePluginPageExport = new neusta_pimcore_import_export.plugin.page.export(); diff --git a/public/js/importPage.js b/public/js/importPage.js index 4a15d5e..059997c 100644 --- a/public/js/importPage.js +++ b/public/js/importPage.js @@ -2,82 +2,114 @@ 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, - 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'), - } - ], - buttons: [ - { - text: 'Import', - handler: function (btn) { - let form = btn.up('form').getForm(); - if (!form.isValid()) { - return; - } + menu.neusta_pimcore_import_export = { + label: t('neusta_pimcore_import_export_import_menu_label'), + iconCls: 'pimcore_icon_import', + priority: 50, + handler: this.openImportDialog.bind(this), + noSubmenus: true + }; + }, - 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'); - } - }); - } - } - ] - }) + 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: [ + { + 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: this.handleImport.bind(this) + } ] - }); + }) + ] + }); + + uploadDialog.show(); + }, + + handleImport: function (btn) { + let form = btn.up('form').getForm(); + if (!form.isValid()) { + return; + } + + form.submit({ + url: Routing.generate('neusta_pimcore_import_export_page_import', {format: 'yaml'}), + method: 'POST', + waitMsg: t('neusta_pimcore_import_export_import_dialog_wait_message'), + headers: { + 'X-Requested-With': 'XMLHttpRequest' + }, + params: { + 'csrfToken': parent.pimcore.settings["csrfToken"] + }, + success: this.onImportSuccess.bind(this), + failure: this.onImportFailure.bind(this) + }); + }, + + onImportSuccess: function (form, action) { + let response = Ext.decode(action.response.responseText); + let successDialog = new Ext.Window({ + title: t('neusta_pimcore_import_export_import_dialog_notification_success'), + width: 300, + height: 200, + modal: true, + 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(); + form.owner.up('window').close(); + }, - uploadDialog.show(); - } - })); + onImportFailure: 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'); } }); diff --git a/src/Command/ExportPagesCommand.php b/src/Command/ExportPagesCommand.php new file mode 100644 index 0000000..7963ca5 --- /dev/null +++ b/src/Command/ExportPagesCommand.php @@ -0,0 +1,120 @@ +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'); + // 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(); + 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..d49f805 --- /dev/null +++ b/src/Command/ImportPagesCommand.php @@ -0,0 +1,73 @@ +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 importing pages from YAML file'); + $this->io->newLine(); + + $yamlInput = file_get_contents($input->getOption('input')); + if (!$yamlInput) { + $this->io->error('Input file could not be read'); + + return Command::FAILURE; + } + + 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))); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/Admin/PageExportController.php b/src/Controller/Admin/PageExportController.php index d7cbaf2..39460e3 100644 --- a/src/Controller/Admin/PageExportController.php +++ b/src/Controller/Admin/PageExportController.php @@ -4,7 +4,7 @@ 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; @@ -26,34 +26,67 @@ public function __construct( )] public function exportPage(Request $request): Response { - $pageId = $request->query->getInt('page_id'); - $page = $this->pageRepository->getById($pageId); + $page = $this->getPageByRequest($request); + if (!$page instanceof PimcorePage) { + return new JsonResponse( + \sprintf('Page with id "%s" was not found', $request->query->getInt('page_id')), + Response::HTTP_NOT_FOUND, + ); + } + + return $this->exportPages([$page], $request->query->getString('filename'), 'yaml'); + } - if (!$page instanceof Page) { + #[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 + { + $page = $this->getPageByRequest($request); + if (!$page instanceof PimcorePage) { return new JsonResponse( - \sprintf('Page with id "%s" was not found', $pageId), + \sprintf('Page with id "%s" was not found', $request->query->getInt('page_id')), Response::HTTP_NOT_FOUND, ); } + $pages = $this->pageRepository->findAllPagesWithSubPages($page); + + return $this->exportPages($pages, $request->query->getString('filename'), $request->query->getString('format')); + } + + private function getPageByRequest(Request $request): ?PimcorePage + { + $pageId = $request->query->getInt('page_id'); + + return $this->pageRepository->getById($pageId); + } + + /** + * @param iterable $pages + */ + private function exportPages(iterable $pages, string $filename, string $format): Response + { try { - $yaml = $this->pageExporter->toYaml($page); + $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 + { $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())); - } } diff --git a/src/Controller/Admin/PageImportController.php b/src/Controller/Admin/PageImportController.php index 1c31393..0ab3cad 100644 --- a/src/Controller/Admin/PageImportController.php +++ b/src/Controller/Admin/PageImportController.php @@ -1,19 +1,32 @@ - '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, @@ -27,25 +40,44 @@ 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' => 'No file uploaded'], 400); + return $this->createJsonResponse(false, $this->messagesMap[self::ERR_NO_FILE_UPLOADED], 400); } $overwrite = $request->request->getBoolean('overwrite'); try { - $page = $this->pageImporter->parseYaml($file->getContent()); + $pages = $this->pageImporter->import($file->getContent(), (string)$request->query->get('format', 'yaml')); - $message = $this->replaceIfExists($page, $overwrite); + $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); + ++$results[$resultCode]; + } + $resultMessage = $this->appendMessage($results); - return new JsonResponse(['success' => true, 'message' => $message]); + 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): string + 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) { @@ -53,13 +85,35 @@ 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; + } + + /** + * @param array $results + */ + private function appendMessage(array $results): string + { + $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 . '

'; } } diff --git a/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php b/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php new file mode 100644 index 0000000..6ae06c4 --- /dev/null +++ b/src/Documents/Export/Converter/DocumentTypeStrategyConverter.php @@ -0,0 +1,31 @@ + + */ +class DocumentTypeStrategyConverter implements Converter +{ + /** + * @param array> $typeToConverterMap + */ + public function __construct( + private array $typeToConverterMap, + ) { + } + + public function convert(object $source, ?object $ctx = null): object + { + if (!\array_key_exists($source::class, $this->typeToConverterMap)) { + throw new \InvalidArgumentException('No converter found for type ' . $source::class); + } + + return $this->typeToConverterMap[$source::class]->convert($source, $ctx); + } +} diff --git a/src/Documents/Export/PageExporter.php b/src/Documents/Export/PageExporter.php index 124a3c0..75f1d39 100644 --- a/src/Documents/Export/PageExporter.php +++ b/src/Documents/Export/PageExporter.php @@ -4,20 +4,18 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; -use Pimcore\Model\Document\Page; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Yaml\Yaml; +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 as PimcorePage; +use Pimcore\Model\Document\PageSnippet; class PageExporter { - private const PAGE = 'page'; - private 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, @@ -25,18 +23,26 @@ public function __construct( ) { } - public function toYaml(Page $page): string + /** + * Exports one or more pages in the given format (yaml, json, ...)). + * + * @param iterable $pages + * + * @throws ConverterException + */ + public function export(iterable $pages, string $format): string { - $yamlExportPage = $this->pageToYamlConverter->convert($page); + $yamlExportPages = []; + foreach ($pages as $page) { + if ( + $page instanceof PimcorePage + || $page instanceof PageSnippet + || $page instanceof Folder + ) { + $yamlExportPages[] = [Page::PAGE => $this->pageToYamlConverter->convert($page)]; + } + } - return $this->serializer->serialize( - [self::PAGE => $yamlExportPage], - '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_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 new file mode 100644 index 0000000..a49284b --- /dev/null +++ b/src/Documents/Export/Populator/EditableDataPopulator.php @@ -0,0 +1,23 @@ + + */ +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/PageImporter.php b/src/Documents/Import/PageImporter.php index b65ea31..2d795fc 100644 --- a/src/Documents/Import/PageImporter.php +++ b/src/Documents/Import/PageImporter.php @@ -4,51 +4,65 @@ use Neusta\ConverterBundle\Converter; use Neusta\ConverterBundle\Converter\Context\GenericContext; -use Neusta\Pimcore\ImportExportBundle\Documents\Export\YamlExportPage; -use Pimcore\Model\Document\Page; -use Symfony\Component\Yaml\Yaml; +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\Page as PimcorePage; +use Pimcore\Model\Element\DuplicateFullPathException; class PageImporter { - private const PAGE = 'page'; - /** - * @param Converter $yamlToPageConverter + * @param Converter $yamlToPageConverter */ public function __construct( private readonly Converter $yamlToPageConverter, + private readonly SerializerInterface $serializer, ) { } - public function parseYaml(string $yamlInput, bool $forcedSave = true): mixed + /** + * @return array + * + * @throws ConverterException + * @throws DuplicateFullPathException + */ + 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[self::PAGE] ?? null)) { - throw new \DomainException('Given YAML is not a valid page.'); + if (!\is_array($config) || !\is_array($config[Page::PAGES] ?? null)) { + throw new \DomainException(\sprintf('Given data in format %s is not valid.', $format)); } - $page = $this->yamlToPageConverter->convert(new YamlExportPage($config[self::PAGE])); - if ($forcedSave) { - $page->save(); + $pages = []; + + foreach ($config[Page::PAGES] as $configPage) { + $page = null; + if (\is_array($configPage[Page::PAGE])) { + $page = $this->yamlToPageConverter->convert(new Page($configPage[Page::PAGE])); + if ($forcedSave) { + $this->checkAndUpdatePage($page); + $page->save(); + } + } + if ($page) { + $pages[] = $page; + } } - return $page; + 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); + 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'); } - - return $yamlFile; + $page->setParentId($existingParent->getId()); } - - return ''; } } diff --git a/src/Documents/Import/PageImportPopulator.php b/src/Documents/Import/Populator/PageImportPopulator.php similarity index 62% rename from src/Documents/Import/PageImportPopulator.php rename to src/Documents/Import/Populator/PageImportPopulator.php index 247f0bb..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 @@ -22,11 +22,11 @@ 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']); + $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 @@ */ - public array $editables; + public string $key = ''; + public ?string $title = null; + public ?string $controller = null; + /** @var array */ + public array $editables = []; /** * @param array|null $yamlConfig 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/src/Toolbox/Repository/PageRepository.php b/src/Toolbox/Repository/PageRepository.php index 5453928..094b589 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(true) 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..c809931 --- /dev/null +++ b/tests/Integration/Documents/Export/PageExporterTest.php @@ -0,0 +1,128 @@ +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', '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]); + + $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(); + $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->export([$page1, $page2], 'yaml'); + $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->export([$page1, $page2], 'yaml'); + $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_3'); + $page3->setTitle('Test Document_3'); + $page3->save(); + + $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 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..ccef157 --- /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_3 + title: 'Test Document_3' + controller: 'App\Controller\DefaultController::defaultAction' + editables: { } diff --git a/tests/Integration/Documents/Import/PageImporterTest.php b/tests/Integration/Documents/Import/PageImporterTest.php new file mode 100644 index 0000000..3287879 --- /dev/null +++ b/tests/Integration/Documents/Import/PageImporterTest.php @@ -0,0 +1,216 @@ +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->import($yaml, 'yaml'); + } + + public function testSinglePageExport_regular_case_parent_id(): void + { + $yaml = + <<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()); + + 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_path(): void + { + $yaml = + <<importer->import($yaml, '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('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('My Document', $pages[0]->getProperty('navigation_name')); + self::assertEquals('My Document - Title', $pages[0]->getProperty('navigation_title')); + } + + public function testSinglePageImport_tree_case(): void + { + $yaml = + <<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/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->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->toYaml($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/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..195c350 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -1,9 +1,11 @@ +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' neusta_pimcore_import_export_import_menu_label: 'Importiere aus YAML' 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 46f94d2..3c435a3 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -1,9 +1,11 @@ +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' neusta_pimcore_import_export_import_menu_label: 'Import from YAML' 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'