diff --git a/doc/01_Installation/01_Upgrade.md b/doc/01_Installation/01_Upgrade.md index e52bea93..c9a46cb5 100644 --- a/doc/01_Installation/01_Upgrade.md +++ b/doc/01_Installation/01_Upgrade.md @@ -5,6 +5,17 @@ description: Breaking changes and migration steps per release. # Upgrade Notes +## Upgrade to 2026.3.0 + +### Skipping Element Persistence From a `PreSaveEvent` Listener + +- `DataObject\PreSaveEvent` now carries a skip flag: a listener can call `setSkipSave(true)` to stop the import + from persisting the current element. The element is not saved, `DataObject\PostSaveEvent` is not dispatched, and + the skip is written to the import log. Processing continues with the next record. +- A skipped element that already exists is reloaded afterwards, so the changes the mapping applied to it in + memory cannot leak into a later row that resolves the same element. +- The flag defaults to `false`, so imports without such a listener behave exactly as before. No migration needed. + ## Upgrade to 2026.2.6 ### Frontend Build Ships as a Packaged Archive diff --git a/doc/06_Extending/02_Events.md b/doc/06_Extending/02_Events.md index 6804dec7..2d67f4b3 100644 --- a/doc/06_Extending/02_Events.md +++ b/doc/06_Extending/02_Events.md @@ -18,7 +18,11 @@ Listening for events customizes import behaviour without replacing any component | `PostPreparationEvent` | After an import was prepared and the queue items were created. | The three `DataObject` events share a base class exposing the import configuration name, the raw source record, and the -data object. `ProcessElementExceptionEvent` adds the thrown exception, the error message, and the mapping configuration +data object. `DataObject\PreSaveEvent` additionally lets a listener skip the persistence of the current element with +`setSkipSave(true)`: the element is not saved, no `DataObject\PostSaveEvent` is dispatched, the skip is written to the +import log, and the import continues with the next record. An existing element is reloaded after the skip, so the +changes the mapping applied to it in memory are discarded rather than carried over to a later record resolving the +same element. `ProcessElementExceptionEvent` adds the thrown exception, the error message, and the mapping configuration that failed, when the failure can be attributed to one. `PostPreparationEvent` exposes the configuration name, the execution type, and whether the source file was interpreted. @@ -50,6 +54,17 @@ final class ImportListener } ``` +Skip the persistence of a record the import should not write: + +```php +public function __invoke(PreSaveEvent $event): void +{ + if (($event->getRawData()['status'] ?? null) === 'draft') { + $event->setSkipSave(true); + } +} +``` + ## Studio API Events The configuration panel is a Pimcore Studio plugin. Before one of its endpoints returns, the bundle dispatches a diff --git a/src/Event/DataObject/PreSaveEvent.php b/src/Event/DataObject/PreSaveEvent.php index 1f24ce51..afb9487d 100644 --- a/src/Event/DataObject/PreSaveEvent.php +++ b/src/Event/DataObject/PreSaveEvent.php @@ -17,4 +17,15 @@ */ final class PreSaveEvent extends AbstractDataObjectImportEvent { + private bool $skipSave = false; + + public function shouldSkipSave(): bool + { + return $this->skipSave; + } + + public function setSkipSave(bool $skipSave): void + { + $this->skipSave = $skipSave; + } } diff --git a/src/Processing/ImportProcessingService.php b/src/Processing/ImportProcessingService.php index f2283dc5..c538707d 100644 --- a/src/Processing/ImportProcessingService.php +++ b/src/Processing/ImportProcessingService.php @@ -30,6 +30,7 @@ use Pimcore\Bundle\DataImporterBundle\Resolver\ResolverFactory; use Pimcore\Bundle\DataImporterBundle\Settings\ConfigurationPreparationService; use Pimcore\Model\Element\ElementInterface; +use Pimcore\Model\Element\Service as ElementService; use Pimcore\Model\Tool\TmpStore; use Pimcore\Model\Version; use Psr\Log\LoggerAwareTrait; @@ -241,6 +242,16 @@ private function processElement( $event = new PreSaveEvent($configName, $importDataRow, $element); $this->eventDispatcher->dispatch($event); + if ($event->shouldSkipSave()) { + $this->logInfo($configName, 'Saving of element skipped by PreSaveEvent listener.', [ + 'component' => PimcoreDataImporterBundle::LOGGER_COMPONENT_PREFIX . $configName, + 'relatedObject' => $element + ]); + $this->discardUnsavedChanges($element); + + return; + } + $this->checkKey($element); $element ->setUserModification($userOwner) @@ -316,6 +327,22 @@ private function processElement( } } + /** + * A skipped element keeps the changes the mapping applied to it in memory. Pimcore hands out + * runtime-cached instances, so a later row resolving the same element would otherwise inherit - + * and save - the values of the skipped row. Reloading it registers a clean instance instead. + */ + private function discardUnsavedChanges(ElementInterface $element): void + { + $type = ElementService::getElementType($element); + + if ($type === null || !$element->getId()) { + return; + } + + ElementService::getElementById($type, $element->getId(), ['force' => true]); + } + /** * Process transformations for an element *