Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/01_Installation/01_Upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion doc/06_Extending/02_Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/Event/DataObject/PreSaveEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
27 changes: 27 additions & 0 deletions src/Processing/ImportProcessingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Comment on lines +245 to +252

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed and fixed in f1666de.

loadOrCreateAndPrepareElement() resolves through DataObject::getById()/getByPath(), which return the runtime-cached instance, and both the sequential command and the Messenger handler process many queue items in one process. Before the skip flag this was harmless because every resolved element was saved, so the in-memory state matched the database. With a skip it no longer does, and a later row resolving the same element would have persisted the skipped row's values.

The skip path now calls discardUnsavedChanges(), which reloads an existing element through Element\Service::getElementById($type, $id, ['force' => true]). The forced load re-registers a clean instance in the runtime cache, so the next row resolving the same element gets database state. Newly created elements have no ID and were never cached, so they are simply dropped.

The extra load is paid only on skipped rows that resolved an existing element, and it usually hits the Pimcore item cache rather than the database. Documented in the upgrade note and in doc/06_Extending/02_Events.md.

}

$this->checkKey($element);
$element
->setUserModification($userOwner)
Expand Down Expand Up @@ -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
*
Expand Down
Loading