Repository: pimcore/pimcore (language: php)

Issue:
[Improvement]: Allow `JobStep` of `Generic Execution Engine` to change selected elements for the next `JobStep`
### Improvement description

We currently rely on passing elements via JobRunContext from one JobStep to another. While functional, this approach undermines the benefits of our new Generic Execution Engine.

### Problem:

- Elements passed via JobRunContext are processed at once in a single message, bypassing multiple worker processes.
- Multiple selected elements can lead to redundant executions of the same action, as detailed in [issue #17312](https://github.com/pimcore/pimcore/issues/17312).

### Solution:

Allow specific actions to modify the selected elements for the subsequent JobStep.

### Example:

JobStep 1: Contains a PQL action and does not require input. Using PQL, 200 elements are selected from the index. These 200 elements become the selected elements for the next JobStep.

JobStep 2: Executes 200 times in parallel, processing each element selected in JobStep 1 independently.

### Benefits

- Avoids unnecessary executions of JobSteps.
- Enables enhanced error handling.
- Facilitates parallel processing for different JobSteps.


Discussion / hints:

Fix patch:
diff --git a/bundles/GenericExecutionEngineBundle/src/Exception/JobNotFoundException.php b/bundles/GenericExecutionEngineBundle/src/Exception/JobNotFoundException.php
new file mode 100644
index 00000000000..afc6e86fff2
--- /dev/null
+++ b/bundles/GenericExecutionEngineBundle/src/Exception/JobNotFoundException.php
@@ -0,0 +1,26 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * Pimcore
+ *
+ * This source file is available under two different licenses:
+ * - GNU General Public License version 3 (GPLv3)
+ * - Pimcore Commercial License (PCL)
+ * Full copyright and license information is available in
+ * LICENSE.md which is distributed with this source code.
+ *
+ *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
+ *  @license    http://www.pimcore.org/license     GPLv3 and PCL
+ */
+
+namespace Pimcore\Bundle\GenericExecutionEngineBundle\Exception;
+
+use RuntimeException;
+
+/**
+ * @internal
+ */
+final class JobNotFoundException extends RuntimeException
+{
+}
diff --git a/bundles/GenericExecutionEngineBundle/src/Messenger/Handler/AbstractAutomationActionHandler.php b/bundles/GenericExecutionEngineBundle/src/Messenger/Handler/AbstractAutomationActionHandler.php
index d9158678bb9..97a4a63ca8c 100644
--- a/bundles/GenericExecutionEngineBundle/src/Messenger/Handler/AbstractAutomationActionHandler.php
+++ b/bundles/GenericExecutionEngineBundle/src/Messenger/Handler/AbstractAutomationActionHandler.php
@@ -208,4 +208,9 @@ protected function getSubjectsFromMessage(
 
         return $subjects;
     }
+
+    protected function setSelectedElementsForNextJobStep(JobRun $jobRun, array $selectedElements): void
+    {
+        $this->jobRunRepository->updateSelectedElements($jobRun, $selectedElements);
+    }
 }
diff --git a/bundles/GenericExecutionEngineBundle/src/Model/Job.php b/bundles/GenericExecutionEngineBundle/src/Model/Job.php
index a3e7148e0ee..c5fd8f6fdd0 100644
--- a/bundles/GenericExecutionEngineBundle/src/Model/Job.php
+++ b/bundles/GenericExecutionEngineBundle/src/Model/Job.php
@@ -28,7 +28,7 @@ final class Job
     public function __construct(
         private readonly string $name,
         private readonly array $steps,
-        private readonly array $selectedElements = [],
+        private array $selectedElements = [],
         private readonly array $environmentData = []
     ) {
         if (empty($this->steps)) {
@@ -49,6 +49,9 @@ public function getSteps(): array
         return $this->steps;
     }
 
+    /**
+     * @return ElementDescriptor[] $selectedElements
+     */
     public function getSelectedElements(): array
     {
         return $this->selectedElements;
@@ -58,4 +61,12 @@ public function getEnvironmentData(): array
     {
         return $this->environmentData;
     }
+
+    /**
+     * @param ElementDescriptor[] $selectedElements
+     */
+    public function setSelectedElements(array $selectedElements): void
+    {
+        $this->selectedElements = $selectedElements;
+    }
 }
diff --git a/bundles/GenericExecutionEngineBundle/src/Repository/JobRunRepository.php b/bundles/GenericExecutionEngineBundle/src/Repository/JobRunRepository.php
index 0c596927389..85cadfb10ba 100644
--- a/bundles/GenericExecutionEngineBundle/src/Repository/JobRunRepository.php
+++ b/bundles/GenericExecutionEngineBundle/src/Repository/JobRunRepository.php
@@ -23,6 +23,7 @@
 use Pimcore\Bundle\GenericExecutionEngineBundle\Configuration\ExecutionContextInterface;
 use Pimcore\Bundle\GenericExecutionEngineBundle\CurrentMessage\CurrentMessageProviderInterface;
 use Pimcore\Bundle\GenericExecutionEngineBundle\Entity\JobRun;
+use Pimcore\Bundle\GenericExecutionEngineBundle\Exception\JobNotFoundException;
 use Pimcore\Bundle\GenericExecutionEngineBundle\Model\Job;
 use Pimcore\Bundle\GenericExecutionEngineBundle\Model\JobRunStates;
 use Pimcore\Bundle\GenericExecutionEngineBundle\Security\PermissionServiceInterface;
@@ -30,6 +31,7 @@
 use Pimcore\Model\Exception\NotFoundException;
 use Pimcore\Translation\Translator;
 use Psr\Log\LoggerInterface;
+use function count;
 
 final class JobRunRepository implements JobRunRepositoryInterface
 {
@@ -211,4 +213,26 @@ public function getLastJobRunByName(string $name): ?JobRun
 
         return $result[0];
     }
+
+    /**
+     * @throws Exception
+     */
+    public function updateSelectedElements(JobRun $jobRun, array $selectedElements): void
+    {
+        $job = $jobRun->getJob();
+        if (!$job) {
+            throw new JobNotFoundException('Job not found for JobRun with id: ' . $jobRun->getId());
+        }
+        $currentlySelectedElements = $job->getSelectedElements();
+        $job->setSelectedElements($selectedElements);
+        $this->update($jobRun);
+        $this->updateLogLocalizedWithDomain(
+            $jobRun,
+            'gee_updated_selected_elements',
+            [
+                '%fromCount%' => count($currentlySelectedElements),
+                '%toCount%' => count($selectedElements),
+            ]
+        );
+    }
 }
diff --git a/bundles/GenericExecutionEngineBundle/src/Repository/JobRunRepositoryInterface.php b/bundles/GenericExecutionEngineBundle/src/Repository/JobRunRepositoryInterface.php
index 2f7695039d9..a4079455111 100644
--- a/bundles/GenericExecutionEngineBundle/src/Repository/JobRunRepositoryInterface.php
+++ b/bundles/GenericExecutionEngineBundle/src/Repository/JobRunRepositoryInterface.php
@@ -18,7 +18,9 @@
 
 use Doctrine\DBAL\Exception;
 use Pimcore\Bundle\GenericExecutionEngineBundle\Entity\JobRun;
+use Pimcore\Bundle\GenericExecutionEngineBundle\Exception\JobNotFoundException;
 use Pimcore\Bundle\GenericExecutionEngineBundle\Model\Job;
+use Pimcore\Model\Element\ElementDescriptor;
 
 interface JobRunRepositoryInterface
 {
@@ -74,4 +76,11 @@ public function getRunningJobsByUserId(
     ): array;
 
     public function getLastJobRunByName(string $name): ?JobRun;
+
+    /**
+     * @param ElementDescriptor[] $selectedElements
+     *
+     * @throws JobNotFoundException
+     */
+    public function updateSelectedElements(JobRun $jobRun, array $selectedElements): void;
 }
diff --git a/bundles/GenericExecutionEngineBundle/translations/admin.en.yaml b/bundles/GenericExecutionEngineBundle/translations/admin.en.yaml
index b2d1a25ea33..35f38bbd381 100644
--- a/bundles/GenericExecutionEngineBundle/translations/admin.en.yaml
+++ b/bundles/GenericExecutionEngineBundle/translations/admin.en.yaml
@@ -8,4 +8,5 @@ gee_job_failed: Job Run %job_run_name% (%job_run_id%) failed.
 gee_middleware_step_condition_not_met: Skipping step %stepName% with id %stepId% in %jobName%, condition not met.
 gee_job_started: Job Run %job_run_name% (%job_run_id%) started.
 gee_job_finished: Job Run %job_run_name% (%job_run_id%) completed successfully.
-gee_job_finished_with_errors: Job Run %job_run_name% (%job_run_id%) completed with errors.
\ No newline at end of file
+gee_job_finished_with_errors: Job Run %job_run_name% (%job_run_id%) completed with errors.
+gee_updated_selected_elements: 'Updated selected elements. (Was: %fromCount%, Now: %toCount%)'
\ No newline at end of file


Test patch: