diff --git a/README.md b/README.md index 776ebe6..3dffd4f 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,18 @@ All import commands follow a similar structure and support the following common - `--input` or `-i`: Path to the input YAML file (required for import commands). - `--dry-run`: Perform the operation without persisting data (only available for import commands). +### Notice: WYSIWYG - Editable + +For using a correct exporting and importing of WYSIWYG editables in older Pimcore versions (e.g. using `symfony/framework < 6.2.2`) you probably need to configure your Symfony HTML sanitizer as documented here: +https://docs.pimcore.com/platform/2024.4/Pimcore/Documents/Editables/WYSIWYG#extending-symfony-html-sanitizer-configuration + +In our integration test we have used the following configuration: + +```yaml +tests/app/config/packages/pimcore.yaml +``` +because of already known bugs with `` - HTML tag. + ## Concepts ### Page Export diff --git a/composer.json b/composer.json index 73a7d6a..ee088fe 100644 --- a/composer.json +++ b/composer.json @@ -34,11 +34,14 @@ "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", "phpstan/phpstan-symfony": "^1.3.8", - "phpunit/phpunit": "^9.5", - "pimcore/admin-ui-classic-bundle": "^1.6", + "phpunit/phpunit": "^9.6", + "pimcore/admin-ui-classic-bundle": "^1.0", "spatie/phpunit-snapshot-assertions": "^4.2", "teamneusta/pimcore-testing-framework": "^0.12" }, + "conflict": { + "masterminds/html5": "< 2.8.1" + }, "autoload": { "psr-4": { "Neusta\\Pimcore\\ImportExportBundle\\": "src/" diff --git a/config/pimcore/export/assets/converters_populators.yaml b/config/pimcore/export/assets/converters_populators.yaml index 09ca22a..68680f6 100644 --- a/config/pimcore/export/assets/converters_populators.yaml +++ b/config/pimcore/export/assets/converters_populators.yaml @@ -8,12 +8,12 @@ neusta_converter: ########################################################### neusta_pimcore_import_export.export_asset_folder: target: Neusta\Pimcore\ImportExportBundle\Model\Asset\Asset + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator properties: - id: ~ key: ~ type: ~ path: ~ - parentId: ~ services: _defaults: @@ -87,6 +87,8 @@ services: ########################################################### # Export Populator (Pimcore Asset -> Asset) ########################################################### + Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator: ~ + neusta_pimcore_import_export.asset.filename.populator: class: Neusta\ConverterBundle\Populator\PropertyMappingPopulator arguments: diff --git a/config/pimcore/export/documents/converters_populators.yaml b/config/pimcore/export/documents/converters_populators.yaml index 1a7192f..94322ff 100644 --- a/config/pimcore/export/documents/converters_populators.yaml +++ b/config/pimcore/export/documents/converters_populators.yaml @@ -9,17 +9,51 @@ neusta_converter: neusta_pimcore_import_export.export_document: target: Neusta\Pimcore\ImportExportBundle\Model\Document\Document populators: - - neusta_pimcore_import_export.page.property.language.populator - - neusta_pimcore_import_export.page.property.navigation_title.populator - - neusta_pimcore_import_export.page.property.navigation_name.populator + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator + - neusta_pimcore_import_export.document.properties.populator + properties: + key: ~ + type: ~ + published: ~ + path: ~ + + neusta_pimcore_import_export.export_folder: + target: Neusta\Pimcore\ImportExportBundle\Model\Document\Folder + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator + - neusta_pimcore_import_export.document.properties.populator + properties: + key: ~ + type: ~ + published: ~ + path: ~ + + neusta_pimcore_import_export.export_page_snippet: + target: Neusta\Pimcore\ImportExportBundle\Model\Document\PageSnippet + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator + - neusta_pimcore_import_export.page_snippet.controller.populator + - neusta_pimcore_import_export.page_snippet.editables.populator + - neusta_pimcore_import_export.document.properties.populator + properties: + key: ~ + type: ~ + published: ~ + path: ~ + + neusta_pimcore_import_export.export_page: + target: Neusta\Pimcore\ImportExportBundle\Model\Document\Page + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator + - neusta_pimcore_import_export.page_snippet.controller.populator + - neusta_pimcore_import_export.page_snippet.editables.populator + - neusta_pimcore_import_export.page.title.populator + - neusta_pimcore_import_export.document.properties.populator properties: - id: ~ key: ~ type: ~ published: ~ path: ~ - parentId: ~ - controller: ~ neusta_pimcore_import_export.editable_converter: target: Neusta\Pimcore\ImportExportBundle\Model\Document\Editable @@ -29,6 +63,14 @@ neusta_converter: type: ~ name: ~ + neusta_pimcore_import_export.property_converter: + target: Neusta\Pimcore\ImportExportBundle\Model\Property + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\PropertyValuePopulator + properties: + key: name + type: ~ + services: _defaults: autowire: true @@ -42,52 +84,28 @@ services: arguments: $typeToConverterMap: Pimcore\Model\Document\Page: '@neusta_pimcore_import_export.export_page' - Pimcore\Model\Document\Snippet: '@neusta_pimcore_import_export.export_page_snippet' + Pimcore\Model\Document\PageSnippet: '@neusta_pimcore_import_export.export_page_snippet' Pimcore\Model\Document\Folder: '@neusta_pimcore_import_export.export_folder' - - ########################################################### - # Export Converter (Document/Folder -> Page) - ########################################################### - neusta_pimcore_import_export.export_folder: - alias: neusta_pimcore_import_export.export_document - - ########################################################### - # Export Converter (Document/PageSnippet -> Page) - ########################################################### - neusta_pimcore_import_export.export_page_snippet: - class: Neusta\Pimcore\ImportExportBundle\Converter\ExtendedConverter - arguments: - $converter: '@neusta_pimcore_import_export.export_document' - $postPopulators: - - '@neusta_pimcore_import_export.page.editables.populator' - - ########################################################### - # Export Converter (Document/Page -> Page) - ########################################################### - neusta_pimcore_import_export.export_page: - class: Neusta\Pimcore\ImportExportBundle\Converter\ExtendedConverter - arguments: - $converter: '@neusta_pimcore_import_export.export_page_snippet' - $postPopulators: - - '@neusta_pimcore_import_export.page.controller.populator' - - '@neusta_pimcore_import_export.page.title.populator' + Pimcore\Model\Document: '@neusta_pimcore_import_export.export_document' ########################################################### # Export Populator (Page -> Page) ########################################################### + Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator: ~ + neusta_pimcore_import_export.page.title.populator: class: Neusta\ConverterBundle\Populator\PropertyMappingPopulator arguments: $sourceProperty: title $targetProperty: title - neusta_pimcore_import_export.page.controller.populator: + neusta_pimcore_import_export.page_snippet.controller.populator: class: Neusta\ConverterBundle\Populator\PropertyMappingPopulator arguments: $sourceProperty: controller $targetProperty: controller - neusta_pimcore_import_export.page.editables.populator: + neusta_pimcore_import_export.page_snippet.editables.populator: class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator arguments: $converter: '@neusta_pimcore_import_export.editable_converter' @@ -101,19 +119,12 @@ services: $targetProperty: 'language' $skipNull: true - neusta_pimcore_import_export.page.property.navigation_title.populator: - class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator - arguments: - $propertyKey: 'navigation_title' - $targetProperty: 'navigation_title' - $skipNull: true - - neusta_pimcore_import_export.page.property.navigation_name.populator: - class: Neusta\Pimcore\ImportExportBundle\PimcoreConverter\Populator\PropertyBasedMappingPopulator + neusta_pimcore_import_export.document.properties.populator: + class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator arguments: - $propertyKey: 'navigation_name' - $targetProperty: 'navigation_name' - $skipNull: true + $converter: '@neusta_pimcore_import_export.property_converter' + $sourceArrayPropertyName: 'properties' + $targetPropertyName: 'properties' ########################################################### # Export Populator (Editable -> YamlEditable) @@ -121,3 +132,4 @@ services: neusta_pimcore_import_export.editable.data.populator: class: Neusta\Pimcore\ImportExportBundle\Populator\EditableDataPopulator + Neusta\Pimcore\ImportExportBundle\Populator\PropertyValuePopulator: ~ diff --git a/config/pimcore/export/elements/converters_populators.yaml b/config/pimcore/export/elements/converters_populators.yaml index e19f2db..00d9c38 100644 --- a/config/pimcore/export/elements/converters_populators.yaml +++ b/config/pimcore/export/elements/converters_populators.yaml @@ -8,9 +8,16 @@ neusta_converter: ########################################################### neusta_pimcore_import_export.export_element: target: Neusta\Pimcore\ImportExportBundle\Model\Element + populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator properties: - id: ~ key: ~ type: ~ path: ~ - parentId: ~ + +services: + _defaults: + autowire: true + autoconfigure: true + + Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator: ~ diff --git a/config/pimcore/export/objects/converters_populators.yaml b/config/pimcore/export/objects/converters_populators.yaml index 4672aa4..04067d5 100644 --- a/config/pimcore/export/objects/converters_populators.yaml +++ b/config/pimcore/export/objects/converters_populators.yaml @@ -9,16 +9,15 @@ neusta_converter: neusta_pimcore_import_export.export_object: target: Neusta\Pimcore\ImportExportBundle\Model\Object\DataObject populators: + - Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator - neusta_pimcore_import_export.export_object.fields.populator - neusta_pimcore_import_export.export_object.relations.populator properties: className: ~ - id: ~ key: ~ type: ~ published: ~ path: ~ - parentId: ~ neusta_pimcore_import_export.export_object.without.relations: target: Neusta\Pimcore\ImportExportBundle\Model\Object\DataObject @@ -47,6 +46,8 @@ services: $typeToConverterMap: Pimcore\Model\DataObject\Concrete: '@neusta_pimcore_import_export.export_object' + Neusta\Pimcore\ImportExportBundle\Populator\IdsPopulator: ~ + neusta_pimcore_import_export.export_object.fields.populator: class: Neusta\Pimcore\ImportExportBundle\Populator\DataObjectExportFieldsPopulator diff --git a/config/pimcore/import/documents/converters_populators.yaml b/config/pimcore/import/documents/converters_populators.yaml index 79a539d..03d2483 100644 --- a/config/pimcore/import/documents/converters_populators.yaml +++ b/config/pimcore/import/documents/converters_populators.yaml @@ -7,6 +7,16 @@ neusta_converter: # Import Converter (Page -> PimcorePage) ########################################################### neusta_pimcore_import_export.import_document: + target: Pimcore\Model\Document + populators: + - neusta_pimcore_import_export.documents.import.populator.id + - neusta_pimcore_import_export.documents.import.populator.key + - neusta_pimcore_import_export.documents.import.populator.type + - neusta_pimcore_import_export.documents.import.populator.published + - neusta_pimcore_import_export.documents.import.populator.path + - neusta_pimcore_import_export.documents.import.populator.parentId + + neusta_pimcore_import_export.import_page: target: Pimcore\Model\Document\Page populators: - neusta_pimcore_import_export.documents.import.populator.id @@ -19,6 +29,18 @@ neusta_converter: - neusta_pimcore_import_export.documents.import.populator.parentId - Neusta\Pimcore\ImportExportBundle\Populator\PageImportPopulator + neusta_pimcore_import_export.import_page_snippet: + target: Pimcore\Model\Document\PageSnippet + populators: + - neusta_pimcore_import_export.documents.import.populator.id + - neusta_pimcore_import_export.documents.import.populator.key + - neusta_pimcore_import_export.documents.import.populator.controller + - neusta_pimcore_import_export.documents.import.populator.type + - neusta_pimcore_import_export.documents.import.populator.published + - neusta_pimcore_import_export.documents.import.populator.path + - neusta_pimcore_import_export.documents.import.populator.parentId + - Neusta\Pimcore\ImportExportBundle\Populator\PageImportPopulator + services: _defaults: autowire: true diff --git a/config/services.yaml b/config/services.yaml index aac9b09..33c7585 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -73,10 +73,9 @@ services: Neusta\Pimcore\ImportExportBundle\Import\Importer: class: Neusta\Pimcore\ImportExportBundle\Import\Importer arguments: - $typeToConverterMap: - Pimcore\Model\Asset: '@neusta_pimcore_import_export.import_asset' - Pimcore\Model\Document: '@neusta_pimcore_import_export.import_document' - Pimcore\Model\DataObject: '@neusta_pimcore_import_export.import_object' + $repositoryLocator: !tagged_locator { tag: 'neusta.import_export.repository', index_by: 'type' } + $converterLocator: !tagged_locator { tag: 'neusta.import_export.converter', index_by: 'type' } + $mergeStrategyLocator: !tagged_locator { tag: 'neusta.import_export.merge_strategy', index_by: 'type' } $parentRelationResolver: '@Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver' $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' @@ -90,10 +89,27 @@ services: arguments: $typeToConverterMap: Pimcore\Model\Asset: '@neusta_pimcore_import_export.strategy.export_asset' + Pimcore\Model\Document\Page: '@neusta_pimcore_import_export.strategy.export_document' + Pimcore\Model\Document\PageSnippet: '@neusta_pimcore_import_export.strategy.export_document' Pimcore\Model\Document: '@neusta_pimcore_import_export.strategy.export_document' Pimcore\Model\DataObject: '@neusta_pimcore_import_export.export_object' $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' + ################## + # Merge Strategy # + ################## + Neusta\Pimcore\ImportExportBundle\Import\Strategy\Document\UpdateExistingPageStrategy: + tags: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Folder' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Page' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\PageSnippet' } + + Neusta\Pimcore\ImportExportBundle\Import\Strategy\ReplaceExistingElementStrategy: + tags: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Asset' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Concrete' } + ############## # Service # ############## @@ -101,9 +117,23 @@ services: Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver: ~ - Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository: ~ - Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository: ~ - Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository: ~ + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository: + public: true + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Asset' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository: + public: true + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Concrete' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository: + public: true + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\Folder' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\Page' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\PageSnippet' } ############## @@ -121,9 +151,30 @@ services: Neusta\Pimcore\ImportExportBundle\Serializer\YamlSerializer: ~ Neusta\Pimcore\ImportExportBundle\Serializer\JsonSerializer: ~ + Neusta\Pimcore\ImportExportBundle\Serializer\Normalizer\PrioritizedAttributesNormalizer: + arguments: + $priorities: + - 'type' + - 'id' + - 'parentId' + - 'path' + - 'key' + - 'published' + - 'controller' + - 'language' + - 'navigation_name' + - 'navigation_title' + - 'title' + - 'editables' + - 'filename' + tags: [ 'serializer.normalizer' ] + ################# # EventListener # ################# Neusta\Pimcore\ImportExportBundle\EventListener\PimcoreAdminListener: tags: - { name: kernel.event_listener, event: pimcore.bundle_manager.paths.js, method: addJSFiles } + + Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\ImportLoggingEventSubscriber: ~ + Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber: ~ diff --git a/docs/images/filename_dialog.png b/docs/images/filename_dialog.png index 7fcca1d..b19f505 100644 Binary files a/docs/images/filename_dialog.png and b/docs/images/filename_dialog.png differ diff --git a/public/js/exportAsset.js b/public/js/exportAsset.js index 459941a..9d00f38 100644 --- a/public/js/exportAsset.js +++ b/public/js/exportAsset.js @@ -21,10 +21,63 @@ neusta_pimcore_import_export.plugin.asset.export = Class.create({ iconCls: "pimcore_icon_asset pimcore_icon_overlay_download", handler: function () { let defaultFilename = asset.data.key + '.yaml'; - let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); - if (filename) { - pimcore.helpers.download(Routing.generate(route, {asset_id: asset.data.id, filename: filename, format: 'yaml'})); - } + + let win = Ext.create('Ext.window.Window', { + title: t('neusta_pimcore_import_export_dialog_title'), + modal: true, + width: 400, + layout: 'fit', + items: [{ + xtype: 'form', + bodyPadding: 10, + defaults: { + anchor: '100%', + labelAlign: 'top' + }, + items: [ + { + xtype: 'textfield', + name: 'filename', + fieldLabel: t('neusta_pimcore_import_export_filename_label'), + value: defaultFilename, + allowBlank: false + }, + { + xtype: 'checkbox', + name: 'includeIds', + boxLabel: t('neusta_pimcore_import_export_exclude_ids_label') + + ' ', + inputValue: true + } + ] + }], + buttons: [{ + text: t('neusta_pimcore_import_export_dialog_confirm'), + handler: function () { + let form = win.down('form').getForm(); + if (form.isValid()) { + let values = form.getValues(); + pimcore.helpers.download( + Routing.generate(route, { + asset_id: asset.data.id, + filename: values.filename, + format: 'yaml', + include_ids: !!values.includeIds + }) + ); + win.close(); + } + } + }, { + text: t('neusta_pimcore_import_export_dialog_cancel'), + handler: function () { + win.close(); + } + }] + }); + + win.show(); } })); } diff --git a/public/js/exportDataObjects.js b/public/js/exportDataObjects.js index 722173a..dedad26 100644 --- a/public/js/exportDataObjects.js +++ b/public/js/exportDataObjects.js @@ -21,10 +21,64 @@ neusta_pimcore_import_export.plugin.object.export = Class.create({ iconCls: icon, handler: function () { let defaultFilename = object.data.key + '.yaml'; - let filename = prompt(t('neusta_pimcore_import_export_enter_filename'), defaultFilename); - if (filename) { - pimcore.helpers.download(Routing.generate(route, {object_id: object.data.id, filename: filename, format: 'yaml'})); - } + let includeIds = !confirm(t('neusta_pimcore_import_export_exclude_ids_question')); // Yes = false, No = true + + let win = Ext.create('Ext.window.Window', { + title: t('neusta_pimcore_import_export_dialog_title'), + modal: true, + width: 400, + layout: 'fit', + items: [{ + xtype: 'form', + bodyPadding: 10, + defaults: { + anchor: '100%', + labelAlign: 'top' + }, + items: [ + { + xtype: 'textfield', + name: 'filename', + fieldLabel: t('neusta_pimcore_import_export_filename_label'), + value: defaultFilename, + allowBlank: false + }, + { + xtype: 'checkbox', + name: 'includeIds', + boxLabel: t('neusta_pimcore_import_export_exclude_ids_label') + + ' ', + inputValue: true + } + ] + }], + buttons: [{ + text: t('neusta_pimcore_import_export_dialog_confirm'), + handler: function () { + let form = win.down('form').getForm(); + if (form.isValid()) { + let values = form.getValues(); + pimcore.helpers.download( + Routing.generate(route, { + object_id: object.data.id, + filename: values.filename, + format: 'yaml', + include_ids: !!values.includeIds + }) + ); + win.close(); + } + } + }, { + text: t('neusta_pimcore_import_export_dialog_cancel'), + handler: function () { + win.close(); + } + }] + }); + + win.show(); } })); } diff --git a/public/js/exportDocument.js b/public/js/exportDocument.js index 1c0bbe8..2a933a8 100755 --- a/public/js/exportDocument.js +++ b/public/js/exportDocument.js @@ -21,10 +21,63 @@ neusta_pimcore_import_export.plugin.document.export = Class.create({ iconCls: icon, handler: function () { 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, {doc_id: document.data.id, filename: filename, format: 'yaml'})); - } + + let win = Ext.create('Ext.window.Window', { + title: t('neusta_pimcore_import_export_dialog_title'), + modal: true, + width: 400, + layout: 'fit', + items: [{ + xtype: 'form', + bodyPadding: 10, + defaults: { + anchor: '100%', + labelAlign: 'top' + }, + items: [ + { + xtype: 'textfield', + name: 'filename', + fieldLabel: t('neusta_pimcore_import_export_filename_label'), + value: defaultFilename, + allowBlank: false + }, + { + xtype: 'checkbox', + name: 'includeIds', + boxLabel: t('neusta_pimcore_import_export_exclude_ids_label') + + ' ', + inputValue: true + } + ] + }], + buttons: [{ + text: t('neusta_pimcore_import_export_dialog_confirm'), + handler: function () { + let form = win.down('form').getForm(); + if (form.isValid()) { + let values = form.getValues(); + pimcore.helpers.download( + Routing.generate(route, { + doc_id: document.data.id, + filename: values.filename, + format: 'yaml', + include_ids: !!values.includeIds + }) + ); + win.close(); + } + } + }, { + text: t('neusta_pimcore_import_export_dialog_cancel'), + handler: function () { + win.close(); + } + }] + }); + + win.show(); } })); } diff --git a/src/Command/Base/AbstractExportBaseCommand.php b/src/Command/Base/AbstractExportBaseCommand.php index 1f0d5ed..ab48145 100644 --- a/src/Command/Base/AbstractExportBaseCommand.php +++ b/src/Command/Base/AbstractExportBaseCommand.php @@ -3,6 +3,7 @@ namespace Neusta\Pimcore\ImportExportBundle\Command\Base; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Element; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ExportRepositoryInterface; use Pimcore\Console\AbstractCommand; use Pimcore\Model\Asset; @@ -16,14 +17,16 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * @template TElement of AbstractElement + * @template TSource of AbstractElement + * @template TTarget of Element */ abstract class AbstractExportBaseCommand extends AbstractCommand { /** - * @param class-string $elementType - * @param array $supportedFormats - * @param ExportRepositoryInterface $repository + * @param class-string $elementType + * @param array $supportedFormats + * @param ExportRepositoryInterface $repository + * @param Exporter $exporter */ public function __construct( protected ExportRepositoryInterface $repository, @@ -37,6 +40,12 @@ public function __construct( protected function configure(): void { $this + ->addOption( + 'include-ids', + null, + InputOption::VALUE_NONE, + 'If set, the export will include asset/document/object IDs and ParentIDs - be aware with re-importing' + ) ->addOption( 'output', 'o', @@ -101,10 +110,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @param array $allElements - * @param TElement $rootElement + * @param array $allElements + * @param TSource $rootElement * - * @return array + * @return array */ private function addElements(AbstractElement $rootElement, array $allElements): array { @@ -125,7 +134,7 @@ private function addElements(AbstractElement $rootElement, array $allElements): } /** - * @param array $allElements + * @param array $allElements * * @throws \Neusta\ConverterBundle\Exception\ConverterException */ diff --git a/src/Command/Base/AbstractImportBaseCommand.php b/src/Command/Base/AbstractImportBaseCommand.php index dfc7376..2005001 100644 --- a/src/Command/Base/AbstractImportBaseCommand.php +++ b/src/Command/Base/AbstractImportBaseCommand.php @@ -37,6 +37,13 @@ protected function configure(): void 'The format of the input file: yaml, json', '' ) + ->addOption( + 'overwrite', + null, + InputOption::VALUE_NONE, + 'Overwrite or update existing elements in case of key-path-id matching [default: false]', + false + ) ->addOption( 'dry-run', null, diff --git a/src/Command/ExportAssetsCommand.php b/src/Command/ExportAssetsCommand.php index 39f322a..284e6f4 100644 --- a/src/Command/ExportAssetsCommand.php +++ b/src/Command/ExportAssetsCommand.php @@ -5,13 +5,14 @@ use Neusta\Pimcore\ImportExportBundle\Command\Base\AbstractExportBaseCommand; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; use Neusta\Pimcore\ImportExportBundle\Export\Service\ZipService; +use Neusta\Pimcore\ImportExportBundle\Model\Asset\Asset; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ExportRepositoryInterface; -use Pimcore\Model\Asset; +use Pimcore\Model\Asset as PimcoreAsset; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; /** - * @extends AbstractExportBaseCommand + * @extends AbstractExportBaseCommand */ #[AsCommand( name: 'neusta:pimcore:export:assets', @@ -79,7 +80,7 @@ protected function configure(): void protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format')); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('include-ids')]); $zipFilename = $input->getOption('output'); try { diff --git a/src/Command/ExportDataObjectsCommand.php b/src/Command/ExportDataObjectsCommand.php index 30d6b3b..3c3de1f 100644 --- a/src/Command/ExportDataObjectsCommand.php +++ b/src/Command/ExportDataObjectsCommand.php @@ -4,13 +4,14 @@ use Neusta\Pimcore\ImportExportBundle\Command\Base\AbstractExportBaseCommand; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Object\DataObject; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ExportRepositoryInterface; use Pimcore\Model\DataObject\Concrete; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; /** - * @extends AbstractExportBaseCommand + * @extends AbstractExportBaseCommand */ #[AsCommand( name: 'neusta:pimcore:export:objects', @@ -81,7 +82,7 @@ className: SocialMediaItem protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format')); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('include-ids')]); $exportFilename = $input->getOption('output'); // Validate filename to prevent directory traversal diff --git a/src/Command/ExportDocumentsCommand.php b/src/Command/ExportDocumentsCommand.php index 07f4025..fbff355 100644 --- a/src/Command/ExportDocumentsCommand.php +++ b/src/Command/ExportDocumentsCommand.php @@ -4,13 +4,14 @@ use Neusta\Pimcore\ImportExportBundle\Command\Base\AbstractExportBaseCommand; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Document\Document; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ExportRepositoryInterface; -use Pimcore\Model\Document; +use Pimcore\Model\Document as PimcoreDocument; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; /** - * @extends AbstractExportBaseCommand + * @extends AbstractExportBaseCommand */ #[AsCommand( name: 'neusta:pimcore:export:documents', @@ -85,7 +86,7 @@ protected function configure(): void protected function exportInFile(array $allElements, InputInterface $input): bool { - $yamlContent = $this->exporter->export($allElements, $input->getOption('format')); + $yamlContent = $this->exporter->export($allElements, $input->getOption('format'), ['include-ids' => $input->getOption('include-ids')]); $exportFilename = $input->getOption('output'); // Validate filename to prevent directory traversal diff --git a/src/Command/ImportAssetsCommand.php b/src/Command/ImportAssetsCommand.php index d047bc7..5e5bd99 100644 --- a/src/Command/ImportAssetsCommand.php +++ b/src/Command/ImportAssetsCommand.php @@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $assets = $this->importer->import($yamlInput, $format, false); + $assets = $this->importer->import($yamlInput, $format, false, $input->getOption('overwrite')); } catch (\DomainException $e) { $this->io->error(\sprintf('Invalid %s format: %s', $format, $e->getMessage())); diff --git a/src/Command/ImportDataObjectsCommand.php b/src/Command/ImportDataObjectsCommand.php index 5fbd13e..0e9a077 100644 --- a/src/Command/ImportDataObjectsCommand.php +++ b/src/Command/ImportDataObjectsCommand.php @@ -32,7 +32,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $this->io->title('Import Pimcore Data Objects given by file'); - $this->io->writeln('Start importing objects from file'); + $this->io->writeln('Start importing dataObjects from file'); $this->io->newLine(); $filename = $input->getOption('input'); @@ -49,7 +49,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $documents = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run')); + $dataObjects = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run'), $input->getOption('overwrite')); } catch (\DomainException $e) { $this->io->error(\sprintf('Invalid %s format: %s', $format, $e->getMessage())); @@ -64,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $this->io->success(\sprintf('%d Pimcore Data Objects have been imported successfully', \count($documents))); + $this->io->success(\sprintf('%d Pimcore Data Objects have been imported successfully', \count($dataObjects))); return Command::SUCCESS; } diff --git a/src/Command/ImportDocumentsCommand.php b/src/Command/ImportDocumentsCommand.php index 725062e..1fc9cf9 100644 --- a/src/Command/ImportDocumentsCommand.php +++ b/src/Command/ImportDocumentsCommand.php @@ -60,7 +60,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $documents = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run')); + $documents = $this->importer->import($yamlInput, $format, !$input->getOption('dry-run'), $input->getOption('overwrite')); } catch (\DomainException $e) { $this->io->error(\sprintf('Invalid %s format: %s', $format, $e->getMessage())); diff --git a/src/Controller/Admin/Base/AbstractImportBaseController.php b/src/Controller/Admin/Base/AbstractImportBaseController.php index dada4c1..b7f5c29 100644 --- a/src/Controller/Admin/Base/AbstractImportBaseController.php +++ b/src/Controller/Admin/Base/AbstractImportBaseController.php @@ -3,10 +3,12 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base; use Neusta\ConverterBundle\Exception\ConverterException; +use Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber; +use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ImportRepositoryInterface; +use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Pimcore\Model\Element\AbstractElement; use Pimcore\Model\Element\DuplicateFullPathException; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -20,6 +22,7 @@ abstract class AbstractImportBaseController public const SUCCESS_ELEMENT_REPLACEMENT = 2; public const SUCCESS_WITHOUT_REPLACEMENT = 3; public const SUCCESS_NEW_ELEMENT = 4; + public const FAILURE_INCONSISTENCY = 5; /** * @var string[] Map of error codes to messages @@ -29,26 +32,21 @@ abstract class AbstractImportBaseController self::SUCCESS_ELEMENT_REPLACEMENT => 'replaced successfully', self::SUCCESS_WITHOUT_REPLACEMENT => 'not replaced', self::SUCCESS_NEW_ELEMENT => 'imported successfully', + self::FAILURE_INCONSISTENCY => 'failed due to inconsistency in the data', ]; protected bool $overwrite = false; - /** @var array */ - private array $resultStatistics; - /** * @param ImportRepositoryInterface $repository */ public function __construct( - protected LoggerInterface $logger, + protected ApplicationLogger $applicationLogger, + protected StatisticsEventSubscriber $statisticsEventSubscriber, protected ImportRepositoryInterface $repository, + protected ParentRelationResolver $parentRelationResolver, protected string $elementType = 'Element', ) { - $this->resultStatistics = [ - self::SUCCESS_ELEMENT_REPLACEMENT => 0, - self::SUCCESS_WITHOUT_REPLACEMENT => 0, - self::SUCCESS_NEW_ELEMENT => 0, - ]; } public function import(Request $request): JsonResponse @@ -62,21 +60,18 @@ public function import(Request $request): JsonResponse $this->overwrite = $request->request->getBoolean('overwrite'); try { - $elements = $this->importByFile($file, $format); - foreach ($elements as $element) { - ++$this->resultStatistics[$this->replaceIfExists($element)]; - } - } catch (\Exception $e) { + $this->importByFile($file, $format, true, $this->overwrite); + } catch (\Throwable $e) { return $this->createJsonResponse(false, $e->getMessage(), 500); } finally { try { $this->cleanUp(); } catch (\Throwable $cleanupError) { - $this->logger->warning($cleanupError->getMessage()); + $this->applicationLogger->warning($cleanupError->getMessage()); } } - return $this->createJsonResponse(true, $this->createResultMessage()); + return $this->createJsonResponse(true, $this->createResultMessage($this->statisticsEventSubscriber->getStatistics())); } protected function createJsonResponse(bool $success, string $message, int $statusCode = 200): JsonResponse @@ -85,43 +80,20 @@ protected function createJsonResponse(bool $success, string $message, int $statu } /** - * @param TElement $element + * @param array $stats */ - protected function replaceIfExists(AbstractElement $element): int - { - $oldElement = $this->repository->getByPath('/' . $element->getFullPath()); - if (null !== $oldElement) { - if ($this->overwrite) { - $oldElement->delete(); - $element->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); - - return self::SUCCESS_ELEMENT_REPLACEMENT; - } - - return self::SUCCESS_WITHOUT_REPLACEMENT; - } - $element->save(['versionNote' => 'added by pimcore-import-export-bundle']); - - return self::SUCCESS_NEW_ELEMENT; - } - - protected function createResultMessage(): string + protected function createResultMessage(array $stats): string { - $resultMessage = ''; + $resultMessage = ''; - foreach ($this->resultStatistics as $resultCode => $result) { + foreach ($stats as $resultCode => $result) { if ($result > 0) { - if (1 === $result) { - $start = 'One ' . $this->elementType; - } else { - $start = \sprintf('%d ' . $this->elementType . 's', $result); - } - $message = \sprintf('%s %s', $start, $this->messagesMap[$resultCode]); - $resultMessage .= $message . '

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

' . $resultMessage . '

'; + return $resultMessage . '
' . $this->elementType . 'Count
'; + $resultMessage .= $resultCode . '' . $result . '
'; } /** @@ -130,7 +102,7 @@ protected function createResultMessage(): string * @throws ConverterException * @throws DuplicateFullPathException */ - abstract protected function importByFile(UploadedFile $file, string $format): array; + abstract protected function importByFile(UploadedFile $file, string $format, bool $forcedSave = true, bool $overwrite = false): array; protected function cleanUp(): void { diff --git a/src/Controller/Admin/ExportAssetsController.php b/src/Controller/Admin/ExportAssetsController.php index 0fc8120..d8c2f75 100644 --- a/src/Controller/Admin/ExportAssetsController.php +++ b/src/Controller/Admin/ExportAssetsController.php @@ -4,8 +4,9 @@ use Neusta\Pimcore\ImportExportBundle\Export\Exporter; use Neusta\Pimcore\ImportExportBundle\Export\Service\ZipService; +use Neusta\Pimcore\ImportExportBundle\Model\Asset\Asset; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository; -use Pimcore\Model\Asset; +use Pimcore\Model\Asset as PimcoreAsset; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -15,6 +16,9 @@ final class ExportAssetsController { + /** + * @param Exporter $exporter + */ public function __construct( private Exporter $exporter, private AssetRepository $assetRepository, @@ -30,14 +34,19 @@ public function __construct( public function export(Request $request): Response { $asset = $this->assetRepository->getById($request->query->getInt('asset_id')); - if (!$asset instanceof Asset) { + if (!$asset instanceof PimcoreAsset) { return new JsonResponse( \sprintf('Asset with id "%s" was not found', $request->query->getInt('asset_id')), Response::HTTP_NOT_FOUND, ); } - return $this->exportAssets([$asset], $request->query->getString('filename'), 'yaml'); + return $this->exportAssets( + [$asset], + $request->query->getString('filename'), + 'yaml', + $request->query->getBoolean('include_ids', false), + ); } #[Route( @@ -48,7 +57,7 @@ public function export(Request $request): Response public function exportWithChildren(Request $request): Response { $asset = $this->assetRepository->getById($request->query->getInt('asset_id')); - if (!$asset instanceof Asset) { + if (!$asset instanceof PimcoreAsset) { return new JsonResponse( \sprintf('Asset with id "%s" was not found', $request->query->getInt('asset_id')), Response::HTTP_NOT_FOUND, @@ -58,16 +67,25 @@ public function exportWithChildren(Request $request): Response // We need the list two times so generate an array first: $assets = iterator_to_array($this->assetRepository->findAllInTree($asset), false); - return $this->exportAssets($assets, $request->query->getString('filename'), $request->query->getString('format')); + return $this->exportAssets( + $assets, + $request->query->getString('filename'), + $request->query->getString('format'), + $request->query->getBoolean('include_ids', false) + ); } /** - * @param array $assets + * @param array $assets */ - private function exportAssets(array $assets, string $filename, string $format): Response + private function exportAssets(array $assets, string $filename, string $format, bool $includeIds): Response { try { - $yaml = $this->exporter->export($assets, $format); + $yaml = $this->exporter->export( + $assets, + $format, + ['includeIds' => $includeIds], + ); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/src/Controller/Admin/ExportDataObjectsController.php b/src/Controller/Admin/ExportDataObjectsController.php index 5557a61..f49df2e 100644 --- a/src/Controller/Admin/ExportDataObjectsController.php +++ b/src/Controller/Admin/ExportDataObjectsController.php @@ -3,8 +3,9 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Object\DataObject; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository; -use Pimcore\Model\DataObject; +use Pimcore\Model\DataObject as PimcoreDataObject; use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -13,6 +14,9 @@ final class ExportDataObjectsController { + /** + * @param Exporter $exporter + */ public function __construct( private Exporter $exporter, private DataObjectRepository $objectRepository, @@ -27,14 +31,19 @@ public function __construct( public function export(Request $request): Response { $object = $this->objectRepository->getById($request->query->getInt('object_id')); - if (!$object instanceof DataObject) { + if (!$object instanceof PimcoreDataObject) { return new JsonResponse( \sprintf('Data Object with id "%s" was not found', $request->query->getInt('object_id')), Response::HTTP_NOT_FOUND, ); } - return $this->exportObjects([$object], $request->query->getString('filename'), 'yaml'); + return $this->exportObjects( + [$object], + $request->query->getString('filename'), + 'yaml', + $request->query->getBoolean('include_ids', false), + ); } #[Route( @@ -45,7 +54,7 @@ public function export(Request $request): Response public function exportWithChildren(Request $request): Response { $object = $this->objectRepository->getById($request->query->getInt('object_id')); - if (!$object instanceof DataObject) { + if (!$object instanceof PimcoreDataObject) { return new JsonResponse( \sprintf('Data Object with id "%s" was not found', $request->query->getInt('object_id')), Response::HTTP_NOT_FOUND, @@ -54,16 +63,27 @@ public function exportWithChildren(Request $request): Response $objects = $this->objectRepository->findAllInTree($object); - return $this->exportObjects($objects, $request->query->getString('filename'), $request->query->getString('format')); + return $this->exportObjects( + $objects, + $request->query->getString('filename'), + $request->query->getString('format'), + $request->query->getBoolean('include_ids', false), + ); } /** - * @param iterable $objects + * @param iterable $objects */ - private function exportObjects(iterable $objects, string $filename, string $format): Response + private function exportObjects(iterable $objects, string $filename, string $format, bool $includeIds): Response { try { - $yaml = $this->exporter->export($objects, $format); + $yaml = $this->exporter->export( + $objects, + $format, + [ + 'includeIds' => $includeIds, + ], + ); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/src/Controller/Admin/ExportDocumentsController.php b/src/Controller/Admin/ExportDocumentsController.php index 1f0a6a9..4248562 100644 --- a/src/Controller/Admin/ExportDocumentsController.php +++ b/src/Controller/Admin/ExportDocumentsController.php @@ -3,8 +3,9 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Export\Exporter; +use Neusta\Pimcore\ImportExportBundle\Model\Document\Document; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository; -use Pimcore\Model\Document; +use Pimcore\Model\Document as PimcoreDocument; use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -13,6 +14,9 @@ final class ExportDocumentsController { + /** + * @param Exporter $exporter + */ public function __construct( private Exporter $exporter, private DocumentRepository $documentRepository, @@ -27,14 +31,19 @@ public function __construct( public function export(Request $request): Response { $document = $this->documentRepository->getById($request->query->getInt('doc_id')); - if (!$document instanceof Document) { + if (!$document instanceof PimcoreDocument) { return new JsonResponse( \sprintf('Document with id "%s" was not found', $request->query->getInt('doc_id')), Response::HTTP_NOT_FOUND, ); } - return $this->exportDocuments([$document], $request->query->getString('filename'), 'yaml'); + return $this->exportDocuments( + [$document], + $request->query->getString('filename'), + 'yaml', + $request->query->getBoolean('include_ids', false), + ); } #[Route( @@ -45,7 +54,7 @@ public function export(Request $request): Response public function exportWithChildren(Request $request): Response { $document = $this->documentRepository->getById($request->query->getInt('doc_id')); - if (!$document instanceof Document) { + if (!$document instanceof PimcoreDocument) { return new JsonResponse( \sprintf('Document with id "%s" was not found', $request->query->getInt('doc_id')), Response::HTTP_NOT_FOUND, @@ -54,16 +63,27 @@ public function exportWithChildren(Request $request): Response $documents = $this->documentRepository->findAllInTree($document); - return $this->exportDocuments($documents, $request->query->getString('filename'), $request->query->getString('format')); + return $this->exportDocuments( + $documents, + $request->query->getString('filename'), + $request->query->getString('format'), + $request->query->getBoolean('include_ids', false), + ); } /** - * @param iterable $documents + * @param iterable $documents */ - private function exportDocuments(iterable $documents, string $filename, string $format): Response + private function exportDocuments(iterable $documents, string $filename, string $format, bool $includeIds): Response { try { - $yaml = $this->exporter->export($documents, $format); + $yaml = $this->exporter->export( + $documents, + $format, + [ + 'includeIds' => $includeIds, + ] + ); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/src/Controller/Admin/ImportAssetsController.php b/src/Controller/Admin/ImportAssetsController.php index 8dcee04..14ccc91 100644 --- a/src/Controller/Admin/ImportAssetsController.php +++ b/src/Controller/Admin/ImportAssetsController.php @@ -3,11 +3,13 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; +use Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber; use Neusta\Pimcore\ImportExportBundle\Import\Importer; +use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Import\ZipImporter; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository; +use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Pimcore\Model\Asset; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -22,12 +24,14 @@ final class ImportAssetsController extends AbstractImportBaseController * @param Importer<\ArrayObject, Asset> $importer */ public function __construct( - LoggerInterface $logger, + ApplicationLogger $applicationLogger, + StatisticsEventSubscriber $statisticsEventSubscriber, private Importer $importer, + ParentRelationResolver $parentRelationResolver, private ZipImporter $zipImporter, AssetRepository $assetRepository, ) { - parent::__construct($logger, $assetRepository, 'Asset'); + parent::__construct($applicationLogger, $statisticsEventSubscriber, $assetRepository, $parentRelationResolver, 'Asset'); } #[Route( @@ -40,14 +44,14 @@ public function import(Request $request): JsonResponse return parent::import($request); } - protected function importByFile(UploadedFile $file, string $format): array + protected function importByFile(UploadedFile $file, string $format, bool $forcedSave = true, bool $overwrite = false): array { $extension = pathinfo($file->getClientOriginalName(), \PATHINFO_EXTENSION); // if file is ZIP add physical files to Assets if ('zip' === $extension) { $zipContent = $this->zipImporter->import($file->getPathname()); - $assets = $this->importer->import($zipContent['yaml'], $format); + $assets = $this->importer->import($zipContent['yaml'], $format, $forcedSave, $overwrite); foreach ($assets as $asset) { if ( \array_key_exists($asset->getType(), $zipContent) @@ -65,8 +69,9 @@ protected function importByFile(UploadedFile $file, string $format): array try { $content = $file->getContent(); - return $this->importer->import($content, $format); + return $this->importer->import($content, $format, $forcedSave, $overwrite); } catch (\Exception $e) { + $this->applicationLogger->error($e->getMessage()); throw new \Exception('Error reading uploaded file: ' . $e->getMessage(), 0, $e); } } diff --git a/src/Controller/Admin/ImportDataObjectsController.php b/src/Controller/Admin/ImportDataObjectsController.php index 7af4034..afbdae8 100644 --- a/src/Controller/Admin/ImportDataObjectsController.php +++ b/src/Controller/Admin/ImportDataObjectsController.php @@ -3,10 +3,12 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; +use Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber; use Neusta\Pimcore\ImportExportBundle\Import\Importer; +use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository; +use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Pimcore\Model\DataObject; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -21,11 +23,13 @@ final class ImportDataObjectsController extends AbstractImportBaseController * @param Importer<\ArrayObject, DataObject> $importer */ public function __construct( - LoggerInterface $logger, + ApplicationLogger $applicationLogger, + StatisticsEventSubscriber $statisticsEventSubscriber, DataObjectRepository $repository, + ParentRelationResolver $parentRelationResolver, private Importer $importer, ) { - parent::__construct($logger, $repository, 'DataObject'); + parent::__construct($applicationLogger, $statisticsEventSubscriber, $repository, $parentRelationResolver, 'DataObject'); } #[Route( @@ -38,13 +42,14 @@ public function import(Request $request): JsonResponse return parent::import($request); } - protected function importByFile(UploadedFile $file, string $format): array + protected function importByFile(UploadedFile $file, string $format, bool $forcedSave = true, bool $overwrite = false): array { try { $content = $file->getContent(); - return $this->importer->import($content, $format); + return $this->importer->import($content, $format, $forcedSave, $overwrite); } catch (\Exception $e) { + $this->applicationLogger->error($e->getMessage()); throw new \Exception('Error reading uploaded file: ' . $e->getMessage(), 0, $e); } } diff --git a/src/Controller/Admin/ImportDocumentsController.php b/src/Controller/Admin/ImportDocumentsController.php index 0b3f60e..df4a54c 100644 --- a/src/Controller/Admin/ImportDocumentsController.php +++ b/src/Controller/Admin/ImportDocumentsController.php @@ -3,10 +3,12 @@ namespace Neusta\Pimcore\ImportExportBundle\Controller\Admin; use Neusta\Pimcore\ImportExportBundle\Controller\Admin\Base\AbstractImportBaseController; +use Neusta\Pimcore\ImportExportBundle\Import\EventSubscriber\StatisticsEventSubscriber; use Neusta\Pimcore\ImportExportBundle\Import\Importer; +use Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver; use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository; +use Pimcore\Bundle\ApplicationLoggerBundle\ApplicationLogger; use Pimcore\Model\Document; -use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -21,11 +23,13 @@ final class ImportDocumentsController extends AbstractImportBaseController * @param Importer<\ArrayObject, Document> $importer */ public function __construct( - LoggerInterface $logger, + ApplicationLogger $applicationLogger, + StatisticsEventSubscriber $statisticsEventSubscriber, DocumentRepository $repository, + ParentRelationResolver $parentRelationResolver, private Importer $importer, ) { - parent::__construct($logger, $repository, 'Document'); + parent::__construct($applicationLogger, $statisticsEventSubscriber, $repository, $parentRelationResolver, 'Document'); } #[Route( @@ -38,12 +42,12 @@ public function import(Request $request): JsonResponse return parent::import($request); } - protected function importByFile(UploadedFile $file, string $format): array + protected function importByFile(UploadedFile $file, string $format, bool $forcedSave = true, bool $overwrite = false): array { try { $content = $file->getContent(); - return $this->importer->import($content, $format); + return $this->importer->import($content, $format, $forcedSave, $overwrite); } catch (\Exception $e) { throw new \Exception('Error reading uploaded file: ' . $e->getMessage(), 0, $e); } diff --git a/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php b/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php new file mode 100644 index 0000000..faa122f --- /dev/null +++ b/src/DependencyInjection/CompilerPass/RegisterTaggedConverterPass.php @@ -0,0 +1,36 @@ +getExtensionConfig('neusta_converter'); + $flattenedConverterDefs = []; + + foreach ($converterDefs as $entry) { + if (isset($entry['converter']) && \is_array($entry['converter'])) { + $flattenedConverterDefs = array_merge($flattenedConverterDefs, $entry['converter']); + } + } + foreach ($flattenedConverterDefs as $serviceId => $arguments) { + if (!$container->hasDefinition($serviceId)) { + continue; + } + $definition = $container->getDefinition($serviceId); + + if (\array_key_exists('target', $arguments)) { + if (!\is_string($arguments['target'])) { + continue; + } + $definition->addTag('neusta.import_export.converter', ['type' => $arguments['target']]); + } + } + } +} diff --git a/src/Exception/InconsistencyException.php b/src/Exception/InconsistencyException.php new file mode 100644 index 0000000..79ccc30 --- /dev/null +++ b/src/Exception/InconsistencyException.php @@ -0,0 +1,7 @@ +, Converter > $typeToConverterMap */ public function __construct( private readonly array $typeToConverterMap, private readonly SerializerInterface $serializer, + private readonly EventDispatcherInterface $dispatcher, ) { } /** * Exports one or more Pimcore Elements in the given format (yaml, json, ...)). * - * @param iterable $elements + * @param iterable $elements + * @param array $ctxParams * * @throws ConverterException */ - public function export(iterable $elements, string $format): string + public function export(iterable $elements, string $format, array $ctxParams = []): string { + $ctx = new GenericContext(); + foreach ($ctxParams as $key => $value) { + $ctx->setValue($key, $value); + } + $yamlExportElements = []; foreach ($elements as $element) { foreach (array_keys($this->typeToConverterMap) as $type) { if ($element instanceof $type) { - $yamlExportElements[] = [$type => $this->typeToConverterMap[$type]->convert($element)]; + try { + $yamlContent = $this->typeToConverterMap[$type]->convert($element, $ctx); + $yamlExportElements[] = [$type => $yamlContent]; + } catch (ConverterException $e) { + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Failed, $type, [], $element, null, $e->getMessage())); + } continue 2; } } diff --git a/src/Import/Event/ImportEvent.php b/src/Import/Event/ImportEvent.php new file mode 100644 index 0000000..5e49adc --- /dev/null +++ b/src/Import/Event/ImportEvent.php @@ -0,0 +1,54 @@ + $yamlContent + */ + public function __construct( + protected readonly ImportStatus $status, + protected readonly string $type, + protected readonly array $yamlContent, + protected readonly ?AbstractElement $newElement, + protected readonly ?AbstractElement $oldElement, + protected readonly ?string $errorMessage = null, + ) { + } + + public function getStatus(): ImportStatus + { + return $this->status; + } + + public function getType(): string + { + return $this->type; + } + + /** + * @return array + */ + public function getYamlContent(): array + { + return $this->yamlContent; + } + + public function getNewElement(): ?AbstractElement + { + return $this->newElement; + } + + public function getOldElement(): ?AbstractElement + { + return $this->oldElement; + } + + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } +} diff --git a/src/Import/Event/ImportStatus.php b/src/Import/Event/ImportStatus.php new file mode 100644 index 0000000..29e1b18 --- /dev/null +++ b/src/Import/Event/ImportStatus.php @@ -0,0 +1,12 @@ + [ + ['logImportEvent', 100], + ], + ]; + } + + public function logImportEvent(ImportEvent $event): void + { + if ('test' === $this->kernel->getEnvironment()) { + return; // In our integration tests we have no installed Pimcore Application Logger bundle at the moment + } + + if (\in_array( + $event->getStatus(), + [ImportStatus::Inconsistency, ImportStatus::Failed] + )) { + $this->writeApplicationError($event); + } else { + $this->writeApplicationLog($event); + } + } + + private function writeApplicationLog(ImportEvent $event): void + { + $prefix = \sprintf('[%s]', $event->getStatus()->value); + $key = $event->getNewElement()?->getKey() ?? 'N/A'; + $path = $event->getNewElement()?->getPath() ?? 'N/A'; + $id = $event->getNewElement()?->getId() ?? 'N/A'; + + $this->applicationLogger->info( + <<getType()} + key: {$key} + path: {$path} + id: {$id} + MESSAGE, + [ + 'relatedObject' => $event->getOldElement() ?? 'N/A', + 'component' => 'Pimcore Import Export Bundle', + ] + ); + } + + private function writeApplicationError(ImportEvent $event): void + { + $prefix = \sprintf('[%s]', $event->getStatus()->value); + $key = $event->getNewElement()?->getKey() ?? 'N/A'; + $path = $event->getNewElement()?->getPath() ?? 'N/A'; + $newId = $event->getNewElement()?->getId() ?? 'N/A'; + $oldId = $event->getOldElement()?->getId() ?? 'N/A'; + $errorMessage = $event->getErrorMessage() ?? 'N/A'; + + $this->applicationLogger->error( + <<getType()} + key: {$key} + path: {$path} + id (new element): {$newId} + id (old element): {$oldId} + MESSAGE, + [ + 'relatedObject' => $event->getOldElement() ?? 'N/A', + 'component' => 'Pimcore Import Export Bundle', + ] + ); + } +} diff --git a/src/Import/EventSubscriber/StatisticsEventSubscriber.php b/src/Import/EventSubscriber/StatisticsEventSubscriber.php new file mode 100644 index 0000000..ede4b53 --- /dev/null +++ b/src/Import/EventSubscriber/StatisticsEventSubscriber.php @@ -0,0 +1,44 @@ + */ + private array $statistics = []; + + public static function getSubscribedEvents() + { + return [ + ImportEvent::class => [ + ['countStatistics', 0], + ], + ]; + } + + /** + * @return array + */ + public function getStatistics(): array + { + return $this->statistics; + } + + public function countStatistics(ImportEvent $event): void + { + $this->incrementCounter($event->getStatus()); + } + + private function incrementCounter(ImportStatus $status): void + { + if (\array_key_exists($status->value, $this->statistics)) { + ++$this->statistics[$status->value]; + } else { + $this->statistics[$status->value] = 1; + } + } +} diff --git a/src/Import/Importer.php b/src/Import/Importer.php index 0bb33f0..3637790 100644 --- a/src/Import/Importer.php +++ b/src/Import/Importer.php @@ -3,12 +3,18 @@ namespace Neusta\Pimcore\ImportExportBundle\Import; use Neusta\ConverterBundle\Converter; -use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Exception\ConverterException; +use Neusta\Pimcore\ImportExportBundle\Exception\InconsistencyException; +use Neusta\Pimcore\ImportExportBundle\Import\Event\ImportEvent; +use Neusta\Pimcore\ImportExportBundle\Import\Event\ImportStatus; +use Neusta\Pimcore\ImportExportBundle\Import\Strategy\MergeElementStrategy; use Neusta\Pimcore\ImportExportBundle\Model\Element; use Neusta\Pimcore\ImportExportBundle\Serializer\SerializerInterface; +use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\ImportRepositoryInterface; use Pimcore\Model\Element\AbstractElement; use Pimcore\Model\Element\DuplicateFullPathException; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @template TSource of \ArrayObject @@ -17,24 +23,30 @@ class Importer { /** - * @param array, Converter> $typeToConverterMap + * @param ServiceLocator $repositoryLocator + * @param ServiceLocator $converterLocator + * @param ServiceLocator $mergeStrategyLocator */ public function __construct( - private readonly array $typeToConverterMap, + private readonly ServiceLocator $repositoryLocator, + private readonly ServiceLocator $converterLocator, + private readonly ServiceLocator $mergeStrategyLocator, private readonly ParentRelationResolver $parentRelationResolver, private readonly SerializerInterface $serializer, + private readonly EventDispatcherInterface $dispatcher, ) { } /** * @return array * + * @throws InconsistencyException * @throws ConverterException * @throws DuplicateFullPathException * @throws \DomainException * @throws \InvalidArgumentException */ - public function import(string $yamlInput, string $format, bool $forcedSave = true): array + public function import(string $yamlInput, string $format, bool $forcedSave, bool $overwrite): array { $config = $this->serializer->deserialize($yamlInput, $format); @@ -47,11 +59,53 @@ public function import(string $yamlInput, string $format, bool $forcedSave = tru foreach ($config[Element::ELEMENTS] as $element) { $result = null; $typeKey = key($element); - if (\array_key_exists($typeKey, $this->typeToConverterMap)) { - $result = $this->typeToConverterMap[$typeKey]->convert(new \ArrayObject($element[$typeKey])); // @phpstan-ignore-line + + $repository = $this->repositoryLocator->get($typeKey); + $converter = $this->converterLocator->get($typeKey); + $mergeStrategy = $this->mergeStrategyLocator->get($typeKey); + + if ( + $repository instanceof ImportRepositoryInterface + && $converter instanceof Converter + && $mergeStrategy instanceof MergeElementStrategy + ) { + /** @var AbstractElement $result */ + $result = $converter->convert(new \ArrayObject($element[$typeKey])); if ($forcedSave) { - $this->parentRelationResolver->resolve($result); - $result->save(); + $oldElement = $repository->getByPath($result->getFullPath()); + if (!$oldElement) { + // New element - save it + $this->parentRelationResolver->resolve($result); + try { + $result->save(['versionNote' => 'created by pimcore-import-export-bundle']); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Created, $typeKey, $element, $result, null)); + } catch (\Exception $e) { + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Failed, $typeKey, $element, $result, $oldElement, $e->getMessage())); + } + } elseif ($overwrite) { + if ($this->newElementHasNoValidId($result) || $this->bothHaveSameId($oldElement, $result)) { + // Update existing element by new one + try { + $mergeStrategy->mergeAndSave($oldElement, $result); + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Updated, $typeKey, $element, $result, $oldElement)); + } catch (\Exception $e) { + $this->dispatcher->dispatch(new ImportEvent(ImportStatus::Failed, $typeKey, $element, $result, $oldElement, $e->getMessage())); + } + } else { + $this->dispatcher->dispatch(new ImportEvent( + ImportStatus::Inconsistency, $typeKey, $element, $result, $oldElement, + <<dispatcher->dispatch(new ImportEvent(ImportStatus::Skipped, $typeKey, $element, $result, $oldElement)); + } } } if ($result) { @@ -59,6 +113,17 @@ public function import(string $yamlInput, string $format, bool $forcedSave = tru } } - return $elements; + /* @var array $elements */ + return $elements; // @phpstan-ignore-line + } + + private function newElementHasNoValidId(AbstractElement $result): bool + { + return null === $result->getId() || 0 === $result->getId(); + } + + private function bothHaveSameId(AbstractElement $oldElement, AbstractElement $result): bool + { + return $oldElement->getId() === $result->getId(); } } diff --git a/src/Import/Strategy/Document/UpdateExistingPageStrategy.php b/src/Import/Strategy/Document/UpdateExistingPageStrategy.php new file mode 100644 index 0000000..3e301ad --- /dev/null +++ b/src/Import/Strategy/Document/UpdateExistingPageStrategy.php @@ -0,0 +1,44 @@ + + */ +class UpdateExistingPageStrategy implements MergeElementStrategy +{ + /** + * @param Document $oldElement + * @param Document $newElement + * + * @throws DuplicateFullPathException + * @throws \InvalidArgumentException + */ + public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newElement): void + { + $oldElement->setPublished($newElement->getPublished()); + + if ($oldElement instanceof Document\PageSnippet && $newElement instanceof Document\PageSnippet) { + $oldElement->setEditables($newElement->getEditables()); + $oldElement->setController($newElement->getController()); + } + + if ($oldElement instanceof Document\Page && $newElement instanceof Document\Page) { + $oldElement->setTitle($newElement->getTitle()); + } + foreach ($newElement->getProperties() as $property) { + $name = $property->getName(); + $type = $property->getType(); + if (null === $name || null === $type) { + continue; // Skip invalid properties + } + $oldElement->setProperty($name, $type, $property->getData()); + } + $oldElement->save(['versionNote' => 'overwritten by pimcore-import-export-bundle']); + } +} diff --git a/src/Import/Strategy/MergeElementStrategy.php b/src/Import/Strategy/MergeElementStrategy.php new file mode 100644 index 0000000..a265131 --- /dev/null +++ b/src/Import/Strategy/MergeElementStrategy.php @@ -0,0 +1,17 @@ + + */ +class ReplaceExistingElementStrategy implements MergeElementStrategy +{ + /** + * @throws \RuntimeException + */ + public function mergeAndSave(AbstractElement $oldElement, AbstractElement $newElement): void + { + try { + $oldElement->delete(); + $newElement->save(); + } catch (\Exception $e) { + // If save fails after delete, we're in an inconsistent state + throw new \RuntimeException('Failed to replace element: ' . $e->getMessage(), 0, $e); + } + } +} diff --git a/src/Model/Document/Document.php b/src/Model/Document/Document.php index 39bd1e4..eaf7dfb 100644 --- a/src/Model/Document/Document.php +++ b/src/Model/Document/Document.php @@ -3,16 +3,14 @@ namespace Neusta\Pimcore\ImportExportBundle\Model\Document; use Neusta\Pimcore\ImportExportBundle\Model\Element; +use Neusta\Pimcore\ImportExportBundle\Model\Property; class Document extends Element { public bool $published = false; - public ?string $navigation_name = null; - public ?string $navigation_title = null; - public ?string $title = null; - public ?string $controller = null; - /** @var array */ - public array $editables = []; + + /** @var array - property key will be mapped to */ + public array $properties = []; /** * @param array|null $yamlConfig diff --git a/src/Model/Document/Folder.php b/src/Model/Document/Folder.php new file mode 100644 index 0000000..9c7bcdb --- /dev/null +++ b/src/Model/Document/Folder.php @@ -0,0 +1,7 @@ + */ + public array $editables = []; +} diff --git a/src/Model/Element.php b/src/Model/Element.php index a911207..eb9b572 100644 --- a/src/Model/Element.php +++ b/src/Model/Element.php @@ -6,10 +6,9 @@ class Element { public const ELEMENTS = 'elements'; - public ?int $id = null; - public int $parentId = 0; + public ?int $id; // important not to set default value here to avoid exporting null values automatically + public ?int $parentId; // important not to set default value here to avoid exporting null values automatically public string $type = 'element'; public string $path = ''; - public string $language = ''; public string $key = ''; } diff --git a/src/Model/Property.php b/src/Model/Property.php new file mode 100644 index 0000000..4f9db24 --- /dev/null +++ b/src/Model/Property.php @@ -0,0 +1,10 @@ +addCompilerPass(new RegisterTaggedConverterPass()); + } + public static function registerDependentBundles(BundleCollection $collection): void { $collection->addBundle(NeustaConverterBundle::class); + $collection->addBundle(PimcoreApplicationLoggerBundle::class); } } diff --git a/src/Populator/IdsPopulator.php b/src/Populator/IdsPopulator.php new file mode 100644 index 0000000..8490da2 --- /dev/null +++ b/src/Populator/IdsPopulator.php @@ -0,0 +1,21 @@ + + */ +class IdsPopulator implements Populator +{ + public function populate(object $target, object $source, ?object $ctx = null): void + { + if ($ctx?->hasKey('includeIds') && true === $ctx->getValue('includeIds')) { + $target->id = $source->getId(); + $target->parentId = $source->getParentId(); + } + } +} diff --git a/src/Populator/PageImportPopulator.php b/src/Populator/PageImportPopulator.php index 85b3e43..baba0e2 100644 --- a/src/Populator/PageImportPopulator.php +++ b/src/Populator/PageImportPopulator.php @@ -4,6 +4,9 @@ use Neusta\ConverterBundle\Converter\Context\GenericContext; use Neusta\ConverterBundle\Populator; +use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository; +use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository; +use Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository; use Pimcore\Model\Document as PimcoreDocument; use Psr\Log\LoggerInterface; @@ -12,14 +15,10 @@ */ class PageImportPopulator implements Populator { - private const TEXT_PROPERTIES = [ - 'language', - 'navigation_title', - 'navigation_name', - // Add more properties here if necessary - ]; - public function __construct( + private readonly AssetRepository $assetRepository, + private readonly DataObjectRepository $objectRepository, + private readonly DocumentRepository $documentRepository, private readonly ?LoggerInterface $logger = null, ) { } @@ -31,13 +30,20 @@ public function __construct( */ public function populate(object $target, object $source, ?object $ctx = null): void { - if ($target instanceof PimcoreDocument\PageSnippet) { - foreach (self::TEXT_PROPERTIES as $property) { - if (isset($source[$property])) { - $target->setProperty($property, 'text', $source[$property]); - } + foreach ($source['properties'] ?? [] as $property) { + if ($property['value'] && 'asset' === $property['type']) { + $value = $this->assetRepository->getByPath($property['value']); + } elseif ($property['value'] && 'document' === $property['type']) { + $value = $this->documentRepository->getByPath($property['value']); + } elseif ($property['value'] && 'object' === $property['type']) { + $value = $this->objectRepository->getByPath($property['value']); + } else { + $value = $property['value']; } + $target->setProperty($property['key'], $property['type'], $value); + } + if ($target instanceof PimcoreDocument\PageSnippet) { /** @var array{type: string, data: mixed} $editable */ foreach ($source['editables'] ?? [] as $key => $editable) { if (!isset($editable['data'])) { diff --git a/src/Populator/PropertyValuePopulator.php b/src/Populator/PropertyValuePopulator.php new file mode 100644 index 0000000..c699d8f --- /dev/null +++ b/src/Populator/PropertyValuePopulator.php @@ -0,0 +1,23 @@ + + */ +class PropertyValuePopulator implements Populator +{ + public function populate(object $target, object $source, ?object $ctx = null): void + { + if ($source->getData() instanceof AbstractElement) { + $target->value = $source->getData()->getPath() . $source->getData()->getKey(); + } else { + $target->value = $source->getData(); + } + } +} diff --git a/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php new file mode 100644 index 0000000..bafd7b2 --- /dev/null +++ b/src/Serializer/Normalizer/PrioritizedAttributesNormalizer.php @@ -0,0 +1,54 @@ + */ + private array $priorities; + + /** + * @param array $priorities + */ + public function __construct(ObjectNormalizer $normalizer, array $priorities = []) + { + $this->normalizer = $normalizer; + $this->priorities = $priorities; // e.g.: ['type', 'id', 'parentId', 'path', ...] + } + + /** + * @param object $object + * @param array $context + */ + public function normalize($object, ?string $format = null, array $context = []) + { + $data = $this->normalizer->normalize($object, $format, $context); + + if (!\is_array($data)) { + return $data; + } + + $sorted = []; + + foreach ($this->priorities as $key) { + if (\array_key_exists($key, $data)) { + $sorted[$key] = $data[$key]; + unset($data[$key]); + } + } + + // Append all remaining properties at the end + return array_merge($sorted, $data); + } + + public function supportsNormalization($data, ?string $format = null): bool + { + return $data instanceof Element && $this->normalizer->supportsNormalization($data, $format); + } +} diff --git a/tests/DummyKernelTest.php b/tests/DummyKernelTest.php old mode 100644 new mode 100755 diff --git a/tests/Integration/Export/ExporterTest.php b/tests/Integration/Export/ExporterTest.php old mode 100644 new mode 100755 index 5b97925..e0d2772 --- a/tests/Integration/Export/ExporterTest.php +++ b/tests/Integration/Export/ExporterTest.php @@ -35,6 +35,19 @@ public function test_single_image_export(): void $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } + public function test_single_image_export_with_ids(): void + { + $asset = new Image(); + $asset->setId(999); + $asset->setParentId(1); + $asset->setKey('image_1'); + $asset->setType('image'); + $asset->setPath('/'); + + $yaml = $this->exporter->export([$asset], 'yaml', ['includeIds' => true]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + public function test_single_page_export(): void { $page = $this->createPageWithInputEditable(); @@ -43,6 +56,14 @@ public function test_single_page_export(): void $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } + public function test_single_page_export_with_ids(): void + { + $page = $this->createPageWithInputEditable(); + + $yaml = $this->exporter->export([$page], 'yaml', ['includeIds' => true]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + public function test_single_page_export_json(): void { $page = $this->createPageWithInputEditable(); @@ -87,6 +108,21 @@ public function test_tree_pages_export(): void $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); } + public function test_tree_pages_export_with_ids(): void + { + $page1 = $this->createSimplePage('1', 1, '/will/be/overwritten/'); + $page1->save(); + + $page2 = $this->createSimplePage('2', $page1->getId(), '/will/be/overwritten/'); + $page2->save(); + + $page3 = $this->createSimplePage('3', $page2->getId(), '/will/be/overwritten/'); + $page3->save(); + + $yaml = $this->exporter->export([$page1, $page2, $page3], 'yaml', ['includeIds' => true]); + $this->assertMatchesSnapshot($yaml, new ImportExportYamlDriver()); + } + private function createPageWithInputEditable(): Page { $page = new Page(); diff --git a/tests/Integration/Export/ImportExportYamlDriver.php b/tests/Integration/Export/ImportExportYamlDriver.php old mode 100644 new mode 100755 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml index e843983..d3bb843 100644 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_saved_pages_export__1.yaml @@ -1,29 +1,21 @@ elements: - Pimcore\Model\Document\Page: - published: true - navigation_name: ~ - navigation_title: ~ - title: 'Test Document_1' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } - id: 2 - parentId: 1 type: page path: / - language: '' key: test_document_1 - - - Pimcore\Model\Document\Page: published: true - navigation_name: ~ - navigation_title: ~ - title: 'Test Document_2' controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_1' editables: { } - id: 3 - parentId: 1 + properties: { } + - + Pimcore\Model\Document\Page: type: page path: / - language: '' key: test_document_2 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_2' + editables: { } + properties: { } diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml index b4d6f2e..4b1f77c 100644 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_simple_unsaved_pages_export__1.yaml @@ -1,29 +1,21 @@ elements: - Pimcore\Model\Document\Page: - published: true - navigation_name: ~ - navigation_title: ~ - title: 'Test Document_1' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } - id: ~ - parentId: 1 type: page path: /will/not/overwritten/ - language: '' key: test_document_1 - - - Pimcore\Model\Document\Page: published: true - navigation_name: ~ - navigation_title: ~ - title: 'Test Document_2' controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_1' editables: { } - id: ~ - parentId: 1 + properties: { } + - + Pimcore\Model\Document\Page: type: page path: /will/not/overwritten/ - language: '' key: test_document_2 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_2' + editables: { } + properties: { } diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml index 0d228cd..8ff19c2 100644 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export__1.yaml @@ -1,10 +1,7 @@ elements: - Pimcore\Model\Asset\Image: - filename: image_1 - id: 999 - parentId: 1 type: image path: / - language: '' key: image_1 + filename: image_1 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml new file mode 100644 index 0000000..b042423 --- /dev/null +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_image_export_with_ids__1.yaml @@ -0,0 +1,9 @@ +elements: + - + Pimcore\Model\Asset\Image: + type: image + id: 999 + parentId: 1 + path: / + key: image_1 + filename: image_1 diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml index 8f7e1d3..25ca2b8 100644 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export__1.yaml @@ -1,19 +1,27 @@ elements: - Pimcore\Model\Document\Page: + type: page + path: /test/ + key: test_document_1 published: false - navigation_name: 'My Document' - navigation_title: 'My Document - Title' - title: 'The Title of my document' controller: /Some/Controller + title: 'The Title of my document' editables: - type: input name: textInput data: 'some text input' - id: 999 - parentId: 4 - type: page - path: /test/ - language: en - key: test_document_1 + properties: + language: + key: language + type: string + value: en + navigation_name: + key: navigation_name + type: string + value: 'My Document' + navigation_title: + key: navigation_title + type: string + value: 'My Document - Title' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json index b454b0b..47d39a0 100644 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_json__1.json @@ -2,11 +2,12 @@ "elements": [ { "Pimcore\\Model\\Document\\Page": { + "type": "page", + "path": "\/test\/", + "key": "test_document_1", "published": false, - "navigation_name": "My Document", - "navigation_title": "My Document - Title", - "title": "The Title of my document", "controller": "\/Some\/Controller", + "title": "The Title of my document", "editables": [ { "type": "input", @@ -14,12 +15,23 @@ "data": "some text input" } ], - "id": 999, - "parentId": 4, - "type": "page", - "path": "\/test\/", - "language": "en", - "key": "test_document_1" + "properties": { + "language": { + "type": "string", + "key": "language", + "value": "en" + }, + "navigation_name": { + "type": "string", + "key": "navigation_name", + "value": "My Document" + }, + "navigation_title": { + "type": "string", + "key": "navigation_title", + "value": "My Document - Title" + } + } } } ] diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml new file mode 100644 index 0000000..6e50a63 --- /dev/null +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_single_page_export_with_ids__1.yaml @@ -0,0 +1,29 @@ +elements: + - + Pimcore\Model\Document\Page: + type: page + id: 999 + parentId: 4 + path: /test/ + key: test_document_1 + published: false + controller: /Some/Controller + title: 'The Title of my document' + editables: + - + type: input + name: textInput + data: 'some text input' + properties: + language: + key: language + type: string + value: en + navigation_name: + key: navigation_name + type: string + value: 'My Document' + navigation_title: + key: navigation_title + type: string + value: 'My Document - Title' diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml index 1d8686f..6331642 100644 --- a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export__1.yaml @@ -1,43 +1,31 @@ elements: - Pimcore\Model\Document\Page: - published: true - navigation_name: ~ - navigation_title: ~ - title: 'Test Document_1' - controller: 'App\Controller\DefaultController::defaultAction' - editables: { } - id: 2 - parentId: 1 type: page path: / - language: '' key: test_document_1 - - - Pimcore\Model\Document\Page: published: true - navigation_name: ~ - navigation_title: ~ - title: 'Test Document_2' controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_1' editables: { } - id: 3 - parentId: 2 + properties: { } + - + Pimcore\Model\Document\Page: type: page path: /test_document_1/ - language: '' key: test_document_2 - - - Pimcore\Model\Document\Page: published: true - navigation_name: ~ - navigation_title: ~ - title: 'Test Document_3' controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_2' editables: { } - id: 4 - parentId: 3 + properties: { } + - + Pimcore\Model\Document\Page: type: page path: /test_document_1/test_document_2/ - language: '' key: test_document_3 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_3' + editables: { } + properties: { } diff --git a/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml new file mode 100644 index 0000000..69724b2 --- /dev/null +++ b/tests/Integration/Export/__snapshots__/ExporterTest__test_tree_pages_export_with_ids__1.yaml @@ -0,0 +1,37 @@ +elements: + - + Pimcore\Model\Document\Page: + type: page + id: 2 + parentId: 1 + path: / + key: test_document_1 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_1' + editables: { } + properties: { } + - + Pimcore\Model\Document\Page: + type: page + id: 3 + parentId: 2 + path: /test_document_1/ + key: test_document_2 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_2' + editables: { } + properties: { } + - + Pimcore\Model\Document\Page: + type: page + id: 4 + parentId: 3 + path: /test_document_1/test_document_2/ + key: test_document_3 + published: true + controller: 'App\Controller\DefaultController::defaultAction' + title: 'Test Document_3' + editables: { } + properties: { } diff --git a/tests/Integration/Import/ImporterExporterTest.php b/tests/Integration/Import/ImporterExporterTest.php new file mode 100755 index 0000000..98c3b3e --- /dev/null +++ b/tests/Integration/Import/ImporterExporterTest.php @@ -0,0 +1,54 @@ +importer = self::getContainer()->get(Importer::class); + $this->exporter = self::getContainer()->get(Exporter::class); + + $asset = new Asset(); + $asset->setParentId(1); + $asset->setPath('/'); + $asset->setKey('logo_desktop.svg'); + $asset->save(); + self::assertNotNull($asset->getId(), 'Asset should be saved successfully'); + + $document = new Page(); + $document->setParentId(1); + $document->setPath('/'); + $document->setKey('Text für viele'); + $document->save(); + self::assertNotNull($document->getId(), 'Page should be saved successfully'); + } + + public function testImportExport_regular_case(): void + { + $yamlToImport = file_get_contents(__DIR__ . '/../data/Text Editor.yaml'); + $this->importer->import($yamlToImport, 'yaml', true, true); + $document = Page::getByPath('/Test-Import-Export'); + $yamlExported = $this->exporter->export([$document], 'yaml'); + + self::assertEquals($yamlToImport, $yamlExported); + } +} diff --git a/tests/Integration/Import/ImporterTest.php b/tests/Integration/Import/ImporterTest.php old mode 100644 new mode 100755 index 3dc4c7d..f5119e9 --- a/tests/Integration/Import/ImporterTest.php +++ b/tests/Integration/Import/ImporterTest.php @@ -4,6 +4,8 @@ use Neusta\Pimcore\ImportExportBundle\Import\Importer; use Neusta\Pimcore\TestingFramework\Database\ResetDatabase; +use Pimcore\Model\Document\Page; +use Pimcore\Model\Version; use Pimcore\Test\KernelTestCase; use Spatie\Snapshots\MatchesSnapshots; @@ -16,6 +18,7 @@ class ImporterTest extends KernelTestCase protected function setUp(): void { + Version::disable(); $this->importer = self::getContainer()->get(Importer::class); } @@ -33,7 +36,7 @@ public function testSinglePageImport_exceptional_case(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Neither parentId nor path leads to a valid parent element'); - $this->importer->import($yaml, 'yaml'); + $this->importer->import($yaml, 'yaml', true, true); } public function testSinglePageExport_regular_case_parent_id(): void @@ -48,15 +51,25 @@ public function testSinglePageExport_regular_case_parent_id(): void type: email published: false path: /path/will/be/overwritten/by/parent_id/ - language: fr - navigation_name: 'My Document' - navigation_title: 'My Document - Title' key: test_document_1 title: 'The Title of My Document' controller: /Some/Controller + properties: + language: + type: string + key: language + value: fr + navigation_name: + type: string + key: navigation_name + value: 'My Document' + navigation_title: + type: string + key: navigation_title + value: 'My Document - Title' YAML; - $pages = $this->importer->import($yaml, 'yaml'); + $pages = $this->importer->import($yaml, 'yaml', true, true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); @@ -82,9 +95,6 @@ public function testSinglePageExport_regular_case_json(): void "type": "email", "published": false, "path": "\/path\/will\/be\/overwritten\/by\/parent_id/\/", - "language": "fr", - "navigation_name": "My Document", - "navigation_title": "My Document - Title", "key": "test_document_1", "title": "The Title of My Document", "controller": "\/Some\/Controller", @@ -94,14 +104,31 @@ public function testSinglePageExport_regular_case_json(): void "name": "textInput", "data": "some text input" } - ] + ], + "properties": { + "language": { + "type": "string", + "key": "language", + "value": "fr" + }, + "navigation_name": { + "type": "string", + "key": "navigation_name", + "value": "My Document" + }, + "navigation_title": { + "type": "string", + "key": "navigation_title", + "value": "My Document - Title" + } + } } } ] } JSON; - $pages = $this->importer->import($json, 'json'); + $pages = $this->importer->import($json, 'json', true, true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); @@ -126,15 +153,25 @@ public function testSinglePageExport_regular_case_path(): void type: email published: false path: / - language: en - navigation_name: 'My Document' - navigation_title: 'My Document - Title' key: test_document_1 title: 'The Title of My Document' controller: /Some/Controller + properties: + language: + type: string + key: language + value: en + navigation_name: + type: string + key: navigation_name + value: 'My Document' + navigation_title: + type: string + key: navigation_title + value: 'My Document - Title' YAML; - $pages = $this->importer->import($yaml, 'yaml'); + $pages = $this->importer->import($yaml, 'yaml', true, true); self::assertEquals(999, $pages[0]->getId()); self::assertEquals('/', $pages[0]->getPath()); self::assertEquals(1, $pages[0]->getParentId()); @@ -173,7 +210,7 @@ public function testSinglePageImport_tree_case(): void key: test_document_1_1_1 YAML; - $pages = $this->importer->import($yaml, 'yaml'); + $pages = $this->importer->import($yaml, 'yaml', true, true); self::assertEquals('/test_document_1/test_document_1_1/', $pages[2]->getPath()); } @@ -209,8 +246,44 @@ public function testSinglePageImport_tree_case_by_path(): void key: test_document_1_1_1 YAML; - $pages = $this->importer->import($yaml, 'yaml'); + $pages = $this->importer->import($yaml, 'yaml', true, true); self::assertEquals('/test_document_1/test_document_1_1/', $pages[3]->getPath()); } + + public function testMultiPagesImport_multi_children(): void + { + $parentPage = new Page(); + $parentPage->setKey('test_document_1'); + $parentPage->setParentId(1); + $parentPage->save(); + + $childPage = new Page(); + $childPage->setKey('child_1'); + $childPage->setParentId($parentPage->getId()); + $childPage->save(); + + $yaml = + <<importer->import($yaml, 'yaml', true, true); + + self::assertEquals($parentPage->getId(), $pages[1]->getParentId()); + self::assertEquals($parentPage->getId(), $pages[2]->getParentId()); + self::assertEquals($parentPage->getId(), $childPage->getParentId()); + } } diff --git a/tests/Integration/Import/ParentRelationResolverTest.php b/tests/Integration/Import/ParentRelationResolverTest.php old mode 100644 new mode 100755 diff --git a/tests/Integration/data/Text Editor.yaml b/tests/Integration/data/Text Editor.yaml new file mode 100644 index 0000000..69d0e88 --- /dev/null +++ b/tests/Integration/data/Text Editor.yaml @@ -0,0 +1,195 @@ +elements: + - + Pimcore\Model\Document\Page: + type: page + path: / + key: Test-Import-Export + published: true + controller: 'Neusta\TrinityPimcoreBundle\Controller\DefaultController::indexAction' + title: 'Text Editor' + editables: + main: + type: areablock + name: main + data: [{ key: '1', type: trinity-layout-grid, hidden: false }] + 'main:1.-col1': + type: areablock + name: 'main:1.-col1' + data: [{ key: '1', type: trinity-headline, hidden: false }, { key: '2', type: trinity-text-editor, hidden: false }] + 'main:1.-col1:1.headline-text': + type: input + name: 'main:1.-col1:1.headline-text' + data: 'Text Editor' + 'main:1.-col1:1.modifier': + type: input + name: 'main:1.-col1:1.modifier' + data: '' + 'main:1.-col1:1.position': + type: select + name: 'main:1.-col1:1.position' + data: '' + 'main:1.-col1:1.sectionID': + type: input + name: 'main:1.-col1:1.sectionID' + data: '' + 'main:1.-col1:1.variant': + type: select + name: 'main:1.-col1:1.variant' + data: h2 + 'main:1.-col1:2.modifier': + type: input + name: 'main:1.-col1:2.modifier' + data: '' + 'main:1.-col1:2.text': + type: wysiwyg + name: 'main:1.-col1:2.text' + data: |- +

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

+

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

+

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

+

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

+
+

🎯 Warum sind WYSIWYG-Editoren so wichtig?

+
    +
  • +

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

    +
  • +
  • +

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

    +
  • +
  • +

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

    +
  • +
  • +

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

    +
  • +
+
+

🛠️ Typische Funktionen eines CMS-Texteditors:

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunktionBeschreibung
Bold/ItalicText fett oder kursiv setzen
ÜberschriftenStrukturieren von Inhalten (H2, H3 usw.)
Links einfügenTexte oder Bilder verlinken
Bilder hochladenMedien direkt in den Text integrieren
ListenAufzählungen und Nummerierungen
TabellenDaten übersichtlich darstellen
Code-AnsichtOptional: HTML manuell bearbeiten
+
+
+
+

📚 Verwendeter WYSIWYG-Editor

+

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

+

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

+
+

✨ Fazit

+

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

+

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

+ 'main:1.-col1:2.variant': + type: select + name: 'main:1.-col1:2.variant' + data: '' + 'main:1.modifier': + type: input + name: 'main:1.modifier' + data: '' + 'main:1.sectionID': + type: input + name: 'main:1.sectionID' + data: '' + 'main:1.variant': + type: select + name: 'main:1.variant' + data: 1col + properties: + navigation_accesskey: + key: navigation_accesskey + type: text + value: '' + navigation_anchor: + key: navigation_anchor + type: text + value: '' + navigation_class: + key: navigation_class + type: text + value: '' + navigation_exclude: + key: navigation_exclude + type: bool + value: false + navigation_name: + key: navigation_name + type: text + value: '' + navigation_parameters: + key: navigation_parameters + type: text + value: '' + navigation_relation: + key: navigation_relation + type: text + value: '' + navigation_tabindex: + key: navigation_tabindex + type: text + value: '' + navigation_target: + key: navigation_target + type: text + value: ~ + navigation_title: + key: navigation_title + type: text + value: '' + testPropertyAsset: + key: testPropertyAsset + type: asset + value: /logo_desktop.svg + testPropertyCheckbox: + key: testPropertyCheckbox + type: bool + value: false + testPropertyKey: + key: testPropertyKey + type: document + value: '/Text für viele' + testPropertyObject: + key: testPropertyObject + type: object + value: ~ + testPropertyString: + key: testPropertyString + type: text + value: 'Ich bin eine Zeichenkette' diff --git a/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php b/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php old mode 100644 new mode 100755 index c756f0d..e2a1fb7 --- a/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php +++ b/tests/Unit/Controller/Admin/ExportDocumentsControllerTest.php @@ -38,14 +38,14 @@ protected function setUp(): void $this->exporter->reveal(), $this->documentRepository->reveal(), ); - $this->request = new Request(['doc_id' => 17]); + $this->request = new Request(['doc_id' => 17, 'include_ids' => false]); } public function testExportPage_regular_case(): void { $page = $this->prophesize(Page::class); $this->documentRepository->getById(17)->willReturn($page->reveal()); - $this->exporter->export([$page->reveal()], 'yaml')->willReturn('TEST_YAML'); + $this->exporter->export([$page->reveal()], 'yaml', ['includeIds' => false])->willReturn('TEST_YAML'); $response = $this->controller->export($this->request); @@ -59,7 +59,7 @@ public function testExportPage_exceptional_case(): void { $page = $this->prophesize(Page::class); $this->documentRepository->getById(17)->willReturn($page->reveal()); - $this->exporter->export([$page->reveal()], 'yaml')->willThrow(new \Exception('Problem')); + $this->exporter->export([$page->reveal()], 'yaml', ['includeIds' => false])->willThrow(new \Exception('Problem')); $response = $this->controller->export($this->request); diff --git a/tests/Unit/Converter/TypeStrategyConverterTest.php b/tests/Unit/Converter/TypeStrategyConverterTest.php old mode 100644 new mode 100755 diff --git a/tests/Unit/PimcoreConverter/Fixture/TestDataObject.php b/tests/Unit/PimcoreConverter/Fixture/TestDataObject.php old mode 100644 new mode 100755 diff --git a/tests/Unit/PimcoreConverter/Populator/PropertyBasedMappingPopulatorTest.php b/tests/Unit/PimcoreConverter/Populator/PropertyBasedMappingPopulatorTest.php old mode 100644 new mode 100755 diff --git a/tests/app/.env b/tests/app/.env old mode 100644 new mode 100755 diff --git a/tests/app/.gitignore b/tests/app/.gitignore old mode 100644 new mode 100755 diff --git a/tests/app/TestKernel.php b/tests/app/TestKernel.php old mode 100644 new mode 100755 index 767b351..12d5006 --- a/tests/app/TestKernel.php +++ b/tests/app/TestKernel.php @@ -2,12 +2,14 @@ use Neusta\Pimcore\ImportExportBundle\NeustaPimcoreImportExportBundle; use Neusta\Pimcore\TestingFramework\Kernel\TestKernel as TestingFrameworkTestKernel; +use Pimcore\Bundle\ApplicationLoggerBundle\PimcoreApplicationLoggerBundle; use Pimcore\HttpKernel\BundleCollection\BundleCollection; class TestKernel extends TestingFrameworkTestKernel { public function registerBundlesToCollection(BundleCollection $collection): void { + $collection->addBundle(new PimcoreApplicationLoggerBundle()); $collection->addBundle(new NeustaPimcoreImportExportBundle()); } } diff --git a/tests/app/config/.gitkeep b/tests/app/config/.gitkeep old mode 100644 new mode 100755 diff --git a/tests/app/config/packages/pimcore.yaml b/tests/app/config/packages/pimcore.yaml new file mode 100644 index 0000000..5c0ad5f --- /dev/null +++ b/tests/app/config/packages/pimcore.yaml @@ -0,0 +1,47 @@ +framework: + html_sanitizer: + sanitizers: + pimcore.wysiwyg_sanitizer: + max_input_length: -1 + allow_attributes: + pimcore_type: '*' + pimcore_id: '*' + allowed_link_schemes: ['http', 'https', 'mailto'] # necessary for symfony/framework < 6.2.2 [https://github.com/symfony/symfony/issues/48556] + allow_relative_links: true + allow_relative_medias: true + allow_elements: + span: [ 'class', 'style', 'id' ] + div: [ 'class', 'style', 'id' ] + p: [ 'class', 'style', 'id' ] + strong: 'class' + em: 'class' + h1: [ 'class', 'id' ] + h2: [ 'class', 'id' ] + h3: [ 'class', 'id' ] + h4: [ 'class', 'id' ] + h5: [ 'class', 'id' ] + h6: [ 'class', 'id' ] + a: [ 'class', 'id', 'href', 'target', 'title', 'rel', 'style' ] + table: [ 'class', 'style', 'cellspacing', 'cellpadding', 'border', 'width', 'height', 'id' ] + colgroup: 'class' + col: [ 'class', 'style', 'id' ] + thead: [ 'class', 'id' ] + tbody: [ 'class', 'id' ] + tr: [ 'class', 'id' ] + td: [ 'class', 'id' ] + th: [ 'class', 'id', 'scope' ] + ul: [ 'class', 'style', 'id' ] + li: [ 'class', 'style', 'id' ] + ol: [ 'class', 'style', 'id' ] + u: [ 'class', 'id' ] + i: [ 'class', 'id' ] + b: [ 'class', 'id' ] + caption: [ 'class', 'id' ] + sub: [ 'class', 'id' ] + sup: [ 'class', 'id' ] + blockquote: [ 'class', 'id' ] + s: [ 'class', 'id' ] + iframe: [ 'frameborder', 'height', 'longdesc', 'name', 'sandbox', 'scrolling', 'src', 'title', 'width' ] + br: '' + img: [ 'class', 'id', 'alt', 'style', 'src' ] + hr: '' diff --git a/tests/app/config/services.yaml b/tests/app/config/services.yaml old mode 100644 new mode 100755 index 3a80bdf..c361b74 --- a/tests/app/config/services.yaml +++ b/tests/app/config/services.yaml @@ -1,3 +1,6 @@ +imports: + - { resource: '../../../config/services.yaml' } + services: _defaults: autowire: true @@ -9,18 +12,6 @@ services: Neusta\Pimcore\ImportExportBundle\Tests\Integration\Documents\ImportExportYamlDriver: ~ - ########################################################### - # Export Converter (Document/Page -> Page) - ########################################################### - neusta_pimcore_import_export.export_page: - class: Neusta\Pimcore\ImportExportBundle\Converter\ExtendedConverter - arguments: - $converter: '@neusta_pimcore_import_export.export_document' - $postPopulators: - - '@neusta_pimcore_import_export.page.title.populator' - - '@neusta_pimcore_import_export.page.controller.populator' - - '@neusta_pimcore_import_export.page.editables.populator' - ############ # Exporter # ############ @@ -41,13 +32,38 @@ services: Neusta\Pimcore\ImportExportBundle\Import\Importer: class: Neusta\Pimcore\ImportExportBundle\Import\Importer arguments: - $typeToConverterMap: - Pimcore\Model\Asset\Image: '@neusta_pimcore_import_export.import_asset' - Pimcore\Model\Document\Page: '@neusta_pimcore_import_export.import_document' - Pimcore\Model\Document\PageSnippet: '@neusta_pimcore_import_export.import_document' - Pimcore\Model\Concrete: '@neusta_pimcore_import_export.import_object' + $repositoryLocator: !tagged_locator { tag: 'neusta.import_export.repository', index_by: 'type' } + $converterLocator: !tagged_locator { tag: 'neusta.import_export.converter', index_by: 'type' } + $mergeStrategyLocator: !tagged_locator { tag: 'neusta.import_export.merge_strategy', index_by: 'type' } $parentRelationResolver: '@Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver' $serializer: '@Neusta\Pimcore\ImportExportBundle\Serializer\SerializerStrategy' Neusta\Pimcore\ImportExportBundle\Import\ParentRelationResolver: ~ + ################## + # Merge Strategy # + ################## + Neusta\Pimcore\ImportExportBundle\Import\Strategy\Document\UpdateExistingPageStrategy: + tags: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\Page' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Document\PageSnippet' } + + Neusta\Pimcore\ImportExportBundle\Import\Strategy\ReplaceElementStrategy: + tags: + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Asset' } + - { name: 'neusta.import_export.merge_strategy', type: 'Pimcore\Model\Concrete' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\AssetRepository: + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Asset' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DataObjectRepository: + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Concrete' } + + Neusta\Pimcore\ImportExportBundle\Toolbox\Repository\DocumentRepository: + tags: + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\Page' } + - { name: 'neusta.import_export.repository', type: 'Pimcore\Model\Document\PageSnippet' } diff --git a/tests/bootstrap.php b/tests/bootstrap.php old mode 100644 new mode 100755 diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml index bda7ff6..268b5b0 100644 --- a/translations/admin.de.yaml +++ b/translations/admin.de.yaml @@ -1,4 +1,9 @@ -neusta_pimcore_import_export_enter_filename: 'Bitte gib einen Dateinamen ein (Standard: Document Key)' +neusta_pimcore_import_export_dialog_title: 'Export Einstellungen' +neusta_pimcore_import_export_dialog_confirm: 'Export ausführen' +neusta_pimcore_import_export_dialog_cancel: 'Abbrechen' +neusta_pimcore_import_export_filename_label: 'Bitte gib einen Dateinamen ein (Standard: Document Key)' +neusta_pimcore_import_export_exclude_ids_label: 'Exportiere mit IDs' +neusta_pimcore_import_export_exclude_ids_info: 'Standardmäßig werden keine IDs (id/parentId) exportiert. Die Zuordnung geschieht über Key und Pfad.
Um IDs zu exportieren, aktiviere bitte diese Option.' neusta_pimcore_import_export_export_menu_label: 'Exportiere in YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Exportiere in YAML mit Kindelementen' neusta_pimcore_import_export_import_dialog_title: 'Importiere Seite aus YAML' diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml index 98cc95a..8924c96 100644 --- a/translations/admin.en.yaml +++ b/translations/admin.en.yaml @@ -1,7 +1,12 @@ -neusta_pimcore_import_export_enter_filename: 'Please enter a filename (default: Document Key)' +neusta_pimcore_import_export_dialog_title: 'Export Settings' +neusta_pimcore_import_export_dialog_confirm: 'execute Export' +neusta_pimcore_import_export_dialog_cancel: 'cancel Export' +neusta_pimcore_import_export_filename_label: 'Please fill in filename (default: Document Key)' +neusta_pimcore_import_export_exclude_ids_label: 'Export with IDs' +neusta_pimcore_import_export_exclude_ids_info: 'By default no IDs (id/parentId) will be exported. The mapping is based on key and path.
To export IDs, please enable this option.' neusta_pimcore_import_export_export_menu_label: 'Export to YAML' neusta_pimcore_import_export_export_with_children_menu_label: 'Export to YAML with children' -neusta_pimcore_import_export_import_dialog_title: 'Import Page from YAML' +neusta_pimcore_import_export_import_dialog_title: 'Import page from YAML' neusta_pimcore_import_export_import_menu_label: 'Import (YAML)' neusta_pimcore_import_export_import_menu_label_asset: 'Import Assets' neusta_pimcore_import_export_import_menu_label_document: 'Import Documents'