diff --git a/config/elements.yaml b/config/elements.yaml index c62a24eb2..ba9eea60c 100644 --- a/config/elements.yaml +++ b/config/elements.yaml @@ -18,4 +18,25 @@ services: class: Pimcore\Bundle\StudioBackendBundle\Element\Service\ElementDeleteService Pimcore\Bundle\StudioBackendBundle\Element\Service\ElementFolderServiceInterface: - class: Pimcore\Bundle\StudioBackendBundle\Element\Service\ElementFolderService \ No newline at end of file + class: Pimcore\Bundle\StudioBackendBundle\Element\Service\ElementFolderService + + + # + # Handler + # + + Pimcore\Bundle\StudioBackendBundle\Element\ExecutionEngine\AutomationAction\Messenger\Handler\PatchHandler: ~ + + + # + # Event Subscriber + # + + Pimcore\Bundle\StudioBackendBundle\Element\EventSubscriber\PatchSubscriber: ~ + + # + # Mercure SSE + # + + Pimcore\Bundle\StudioBackendBundle\Element\Mercure\Provider\ElementTopicProvider: + tags: [ 'pimcore.studio_backend.mercure.topic.provider' ] \ No newline at end of file diff --git a/config/patcher.yaml b/config/patcher.yaml index 6dd6df1ab..4e45b310f 100644 --- a/config/patcher.yaml +++ b/config/patcher.yaml @@ -10,6 +10,16 @@ services: Pimcore\Bundle\StudioBackendBundle\Patcher\Service\PatchServiceInterface: class: Pimcore\Bundle\StudioBackendBundle\Patcher\Service\PatchService + # + # Adapters + # + Pimcore\Bundle\StudioBackendBundle\Patcher\Adapter\ParentIdAdapter: tags : ['pimcore.studio_backend.patch_adapter'] + Pimcore\Bundle\StudioBackendBundle\Patcher\Adapter\KeyAdapter: + tags : ['pimcore.studio_backend.patch_adapter'] + + Pimcore\Bundle\StudioBackendBundle\Patcher\Adapter\LockAdapter: + tags : ['pimcore.studio_backend.patch_adapter'] + diff --git a/config/pimcore/execution_engine.yaml b/config/pimcore/execution_engine.yaml index 82190d954..17b3c3166 100644 --- a/config/pimcore/execution_engine.yaml +++ b/config/pimcore/execution_engine.yaml @@ -17,4 +17,5 @@ framework: Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\AssetCloneMessage: pimcore_generic_execution_engine Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\AssetDeleteMessage: pimcore_generic_execution_engine Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\AssetUploadMessage: pimcore_generic_execution_engine - Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\ZipUploadMessage: pimcore_generic_execution_engine \ No newline at end of file + Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\ZipUploadMessage: pimcore_generic_execution_engine + Pimcore\Bundle\StudioBackendBundle\Element\ExecutionEngine\AutomationAction\Messenger\Messages\PatchMessage: pimcore_generic_execution_engine \ No newline at end of file diff --git a/src/Asset/Controller/PatchController.php b/src/Asset/Controller/PatchController.php index c302f0ca0..91582334b 100644 --- a/src/Asset/Controller/PatchController.php +++ b/src/Asset/Controller/PatchController.php @@ -20,12 +20,17 @@ use Pimcore\Bundle\StudioBackendBundle\Asset\Attributes\Request\PatchAssetRequestBody; use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\PatchAssetParameter; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; -use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attributes\Response\Content\PatchErrorJson; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\AccessDeniedException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementSavingFailedException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\UserNotFoundException; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attributes\Response\Content\IdJson; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attributes\Response\CreatedResponse; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attributes\Response\DefaultResponses; -use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attributes\Response\PatchSuccessResponseWithErrors; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attributes\Response\SuccessResponse; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Config\Tags; use Pimcore\Bundle\StudioBackendBundle\Patcher\Service\PatchServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Util\Constants\ElementTypes; use Pimcore\Bundle\StudioBackendBundle\Util\Constants\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constants\UserPermissions; @@ -43,10 +48,14 @@ final class PatchController extends AbstractApiController public function __construct( SerializerInterface $serializer, private readonly PatchServiceInterface $patchService, + private readonly SecurityServiceInterface $securityService ) { parent::__construct($serializer); } + /** + * @throws AccessDeniedException|ElementSavingFailedException|NotFoundException|UserNotFoundException + */ #[Route('/assets', name: 'pimcore_studio_api_patch_asset', methods: ['PATCH'])] #[IsGranted(UserPermissions::ASSETS->value)] #[Patch( @@ -57,16 +66,33 @@ public function __construct( tags: [Tags::Assets->name] )] #[PatchAssetRequestBody] - #[SuccessResponse] - #[PatchSuccessResponseWithErrors(content: new PatchErrorJson())] + #[SuccessResponse( + description: 'Successfully patched asset', + )] + #[CreatedResponse( + description: 'Successfully created jobRun for patching multiple assets', + content: new IdJson('ID of created jobRun') + )] #[DefaultResponses([ HttpResponseCodes::UNAUTHORIZED, HttpResponseCodes::NOT_FOUND, ])] public function patchAssets(#[MapRequestPayload] PatchAssetParameter $patchAssetParameter): Response { - $errors = $this->patchService->patch(ElementTypes::TYPE_ASSET, $patchAssetParameter->getData()); + $status = HttpResponseCodes::SUCCESS->value; + $data = null; + $jobRunId = $this->patchService->patch( + ElementTypes::TYPE_ASSET, + $patchAssetParameter->getData(), + $this->securityService->getCurrentUser() + ); + + if ($jobRunId) { + $status = HttpResponseCodes::CREATED->value; + + return $this->jsonResponse(['id' => $jobRunId], $status); + } - return $this->patchResponse($errors); + return new Response($data, $status); } } diff --git a/src/Asset/EventSubscriber/CloneSubscriber.php b/src/Asset/EventSubscriber/CloneSubscriber.php index 648b89700..6e08e9658 100644 --- a/src/Asset/EventSubscriber/CloneSubscriber.php +++ b/src/Asset/EventSubscriber/CloneSubscriber.php @@ -49,7 +49,7 @@ public function onStateChanged(JobRunStateChangedEvent $event): void $event->getJobName() === Jobs::CLONE_ASSETS->value ) { $this->publishService->publish( - Events::DELETION_FINISHED->value, + Events::CLONING_FINISHED->value, new Finished( $event->getJobRunId(), $event->getJobName(), diff --git a/src/Asset/EventSubscriber/DeletionSubscriber.php b/src/Asset/EventSubscriber/DeletionSubscriber.php index 02913ee15..69f414fb2 100644 --- a/src/Asset/EventSubscriber/DeletionSubscriber.php +++ b/src/Asset/EventSubscriber/DeletionSubscriber.php @@ -46,7 +46,7 @@ public static function getSubscribedEvents(): array public function onStateChanged(JobRunStateChangedEvent $event): void { - if ($event->getJobName() !== Jobs::DELETE_ASSETS->value) { + if ($event->getJobName() !== Jobs::DELETE_ASSETS->value) { return; } diff --git a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php b/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php index c2f2266ac..25a22f85d 100644 --- a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php +++ b/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php @@ -27,7 +27,6 @@ use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\Config; use Pimcore\Bundle\StudioBackendBundle\Grid\Service\GridServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Mercure\Service\PublishServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Translation\Service\TranslatorService; use Pimcore\Bundle\StudioBackendBundle\Util\Constants\ElementPermissions; use Pimcore\Bundle\StudioBackendBundle\Util\Constants\ElementTypes; use Pimcore\Bundle\StudioBackendBundle\Util\Traits\HandlerProgressTrait; @@ -68,12 +67,7 @@ public function __invoke(CsvCreationMessage $message): void ); if ($validatedParameters instanceof AbortActionData) { - $this->abortAction( - $validatedParameters->getTranslationKey(), - $validatedParameters->getTranslationParameters(), - TranslatorService::DOMAIN, - $validatedParameters->getExceptionClassName() - ); + $this->abort($validatedParameters); } $context = $jobRun->getContext(); diff --git a/src/Asset/ExecutionEngine/Util/JobSteps.php b/src/Asset/ExecutionEngine/Util/JobSteps.php index c47d227d5..103b9a12f 100644 --- a/src/Asset/ExecutionEngine/Util/JobSteps.php +++ b/src/Asset/ExecutionEngine/Util/JobSteps.php @@ -25,4 +25,5 @@ enum JobSteps: string case ASSET_UPLOADING = 'studio_ee_job_step_asset_uploading'; case CSV_COLLECTION = 'studio_ee_job_step_csv_collection'; case CSV_CREATION = 'studio_ee_job_step_csv_creation'; + case ELEMENT_PATCHING = 'studio_ee_job_step_element_patching'; } diff --git a/src/Asset/Mercure/Events.php b/src/Asset/Mercure/Events.php index be9411ebb..e07e8853f 100644 --- a/src/Asset/Mercure/Events.php +++ b/src/Asset/Mercure/Events.php @@ -31,6 +31,4 @@ enum Events: string case DELETION_FINISHED = 'deletion-finished'; case CLONING_FINISHED = 'cloning-finished'; case ASSET_UPLOAD_FINISHED = 'asset-upload-finished'; - case FINISHED_WITH_ERRORS = 'job-finished-with-errors'; - case FAILED = 'job-failed'; } diff --git a/src/Asset/Mercure/Provider/AssetTopicProvider.php b/src/Asset/Mercure/Provider/AssetTopicProvider.php index 78bc757aa..ff45c071c 100644 --- a/src/Asset/Mercure/Provider/AssetTopicProvider.php +++ b/src/Asset/Mercure/Provider/AssetTopicProvider.php @@ -18,13 +18,14 @@ use Pimcore\Bundle\StudioBackendBundle\Asset\Mercure\Events as AssetEvents; use Pimcore\Bundle\StudioBackendBundle\Mercure\Provider\AbstractServerToClientProvider; +use Pimcore\Bundle\StudioBackendBundle\Mercure\Service\Loader\TaggedIteratorAdapter; use Pimcore\Bundle\StudioBackendBundle\Mercure\Util\Events; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; /** * @internal */ -#[AutoconfigureTag('pimcore.studio_backend.mercure.topic.provider')] +#[AutoconfigureTag(TaggedIteratorAdapter::TOPIC_LOADER_TAG)] final class AssetTopicProvider extends AbstractServerToClientProvider { public function getClientSubscribableTopic(): array diff --git a/src/Asset/Patcher/Adapter/MetadataAdapter.php b/src/Asset/Patcher/Adapter/MetadataAdapter.php index 9801e8c25..9780bd425 100644 --- a/src/Asset/Patcher/Adapter/MetadataAdapter.php +++ b/src/Asset/Patcher/Adapter/MetadataAdapter.php @@ -17,14 +17,17 @@ namespace Pimcore\Bundle\StudioBackendBundle\Asset\Patcher\Adapter; use Pimcore\Bundle\StudioBackendBundle\Patcher\Service\Loader\PatchAdapterInterface; +use Pimcore\Bundle\StudioBackendBundle\Patcher\Service\Loader\TaggedIteratorAdapter; use Pimcore\Bundle\StudioBackendBundle\Util\Constants\ElementTypes; use Pimcore\Model\Asset; use Pimcore\Model\Element\ElementInterface; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; use function array_key_exists; /** * @internal */ +#[AutoconfigureTag(TaggedIteratorAdapter::ADAPTER_TAG)] final class MetadataAdapter implements PatchAdapterInterface { private const INDEX_KEY = 'metadata'; diff --git a/src/Element/EventSubscriber/PatchSubscriber.php b/src/Element/EventSubscriber/PatchSubscriber.php new file mode 100644 index 000000000..41cf36651 --- /dev/null +++ b/src/Element/EventSubscriber/PatchSubscriber.php @@ -0,0 +1,71 @@ + 'onStateChanged', + ]; + } + + public function onStateChanged(JobRunStateChangedEvent $event): void + { + if ($event->getJobName() !== Jobs::PATCH_ELEMENTS->value) { + return; + } + + match ($event->getNewState()) { + JobRunStates::FINISHED->value => $this->publishService->publish( + Events::PATCH_FINISHED->value, + new Finished( + $event->getJobRunId(), + $event->getJobName(), + $event->getJobRunOwnerId(), + $event->getNewState() + ) + ), + JobRunStates::FINISHED_WITH_ERRORS->value => $this->eventSubscriberService->handleFinishedWithErrors( + $event->getJobRunId(), + $event->getJobRunOwnerId(), + $event->getJobName() + ), + default => null, + }; + } +} diff --git a/src/Element/ExecutionEngine/AutomationAction/Messenger/Handler/PatchHandler.php b/src/Element/ExecutionEngine/AutomationAction/Messenger/Handler/PatchHandler.php new file mode 100644 index 000000000..33aac73a1 --- /dev/null +++ b/src/Element/ExecutionEngine/AutomationAction/Messenger/Handler/PatchHandler.php @@ -0,0 +1,101 @@ +getJobRun($message); + + $validatedParameters = $this->validateJobParameters( + $message, + $jobRun, + $this->userResolver, + ); + + if ($validatedParameters instanceof AbortActionData) { + $this->abort($validatedParameters); + } + + $element = $this->getElementById( + $validatedParameters->getSubject(), + $validatedParameters->getUser(), + $this->elementService + ); + $elementId = $element->getId(); + $elementType = $this->getElementType($element); + $jobEnvironmentData = $jobRun->getJob()?->getEnvironmentData(); + if (!isset($jobEnvironmentData[(string)$elementId])) { + $this->abort($this->getAbortData( + Config::ELEMENT_PATCH_FAILED_MESSAGE->value, + [ + 'type' => $elementType, + 'id' => $element->getId(), + 'message' => Config::NO_ELEMENT_DATA_FOUND->value, + ], + )); + } + + try { + $this->patchService->patchElement($element, $elementType, $jobEnvironmentData[$elementId]); + } catch (Exception $exception) { + $this->abort($this->getAbortData( + Config::ELEMENT_PATCH_FAILED_MESSAGE->value, + [ + 'type' => $elementType, + 'id' => $elementId, + 'message' => $exception->getMessage(), + ], + )); + } + + $this->updateProgress($this->publishService, $jobRun, $this->getJobStep($message)->getName()); + } +} diff --git a/src/Element/ExecutionEngine/AutomationAction/Messenger/Messages/PatchMessage.php b/src/Element/ExecutionEngine/AutomationAction/Messenger/Messages/PatchMessage.php new file mode 100644 index 000000000..72066456b --- /dev/null +++ b/src/Element/ExecutionEngine/AutomationAction/Messenger/Messages/PatchMessage.php @@ -0,0 +1,26 @@ +getEvents(); + } + + public function getServerPublishableTopic(): array + { + return $this->getEvents(); + } + + private function getEvents(): array + { + return Events::values(); + } +} diff --git a/src/ExecutionEngine/EventSubscriber/FailureSubscriber.php b/src/ExecutionEngine/EventSubscriber/FailureSubscriber.php index cf509ee37..c4f701d2b 100644 --- a/src/ExecutionEngine/EventSubscriber/FailureSubscriber.php +++ b/src/ExecutionEngine/EventSubscriber/FailureSubscriber.php @@ -19,9 +19,9 @@ use Pimcore\Bundle\GenericExecutionEngineBundle\Event\JobRunStateChangedEvent; use Pimcore\Bundle\GenericExecutionEngineBundle\Model\JobRunStates; use Pimcore\Bundle\GenericExecutionEngineBundle\Repository\JobRunErrorLogRepositoryInterface; -use Pimcore\Bundle\StudioBackendBundle\Asset\Mercure\Events; use Pimcore\Bundle\StudioBackendBundle\Mercure\Schema\ExecutionEngine\Finished; use Pimcore\Bundle\StudioBackendBundle\Mercure\Service\PublishServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\Mercure\Util\Events; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** diff --git a/src/ExecutionEngine/Service/EventSubscriberService.php b/src/ExecutionEngine/Service/EventSubscriberService.php index aa3dc956e..fbbf2feaf 100644 --- a/src/ExecutionEngine/Service/EventSubscriberService.php +++ b/src/ExecutionEngine/Service/EventSubscriberService.php @@ -18,9 +18,9 @@ use Pimcore\Bundle\GenericExecutionEngineBundle\Model\JobRunStates; use Pimcore\Bundle\GenericExecutionEngineBundle\Repository\JobRunErrorLogRepositoryInterface; -use Pimcore\Bundle\StudioBackendBundle\Asset\Mercure\Events; use Pimcore\Bundle\StudioBackendBundle\Mercure\Schema\ExecutionEngine\Finished; use Pimcore\Bundle\StudioBackendBundle\Mercure\Service\PublishServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\Mercure\Util\Events; /** * @internal diff --git a/src/ExecutionEngine/Util/Config.php b/src/ExecutionEngine/Util/Config.php index 5725b9e13..55153101f 100644 --- a/src/ExecutionEngine/Util/Config.php +++ b/src/ExecutionEngine/Util/Config.php @@ -33,4 +33,6 @@ enum Config: string case FILE_NOT_FOUND_FOR_JOB_RUN = 'studio_ee_file_not_found_for_job_run'; case NO_ASSETS_FOUND_FOR_JOB_RUN = 'studio_ee_no_assets_found_for_job_run'; case ASSET_UPLOAD_FAILED_MESSAGE = 'studio_ee_asset_upload_failed'; + case ELEMENT_PATCH_FAILED_MESSAGE = 'studio_ee_element_patch_failed'; + case NO_ELEMENT_DATA_FOUND = 'studio_ee_no_element_data_found'; } diff --git a/src/ExecutionEngine/Util/Jobs.php b/src/ExecutionEngine/Util/Jobs.php index ea3581cd4..02a8a4d1e 100644 --- a/src/ExecutionEngine/Util/Jobs.php +++ b/src/ExecutionEngine/Util/Jobs.php @@ -23,4 +23,5 @@ enum Jobs: string case UPLOAD_ASSETS = 'studio_ee_job_upload_assets'; case ZIP_FILE_UPLOAD = 'studio_ee_job_upload_zip_file'; case CREATE_CSV = 'studio_ee_job_create_csv'; + case PATCH_ELEMENTS = 'studio_ee_job_patch_elements'; } diff --git a/src/Mercure/Util/Events.php b/src/Mercure/Util/Events.php index 64627a086..c83de1bec 100644 --- a/src/Mercure/Util/Events.php +++ b/src/Mercure/Util/Events.php @@ -22,4 +22,6 @@ enum Events: string use EnumToValueArrayTrait; case HANDLER_PROGRESS = 'handler-progress'; + case FINISHED_WITH_ERRORS = 'job-finished-with-errors'; + case FAILED = 'job-failed'; } diff --git a/src/Patcher/Adapter/KeyAdapter.php b/src/Patcher/Adapter/KeyAdapter.php new file mode 100644 index 000000000..cf2862f79 --- /dev/null +++ b/src/Patcher/Adapter/KeyAdapter.php @@ -0,0 +1,56 @@ +getIndexKey(), $data)) { + return; + } + + $element->setKey($data[$this->getIndexKey()]); + } + + public function getIndexKey(): string + { + return self::INDEX_KEY; + } + + public function supportedElementTypes(): array + { + return [ + ElementTypes::TYPE_ASSET, + ElementTypes::TYPE_DOCUMENT, + ElementTypes::TYPE_OBJECT, + ]; + } +} diff --git a/src/Patcher/Adapter/LockAdapter.php b/src/Patcher/Adapter/LockAdapter.php new file mode 100644 index 000000000..17c1d8dba --- /dev/null +++ b/src/Patcher/Adapter/LockAdapter.php @@ -0,0 +1,56 @@ +getIndexKey(), $data)) { + return; + } + + $element->setLocked($data[$this->getIndexKey()]); + } + + public function getIndexKey(): string + { + return self::INDEX_KEY; + } + + public function supportedElementTypes(): array + { + return [ + ElementTypes::TYPE_ASSET, + ElementTypes::TYPE_DOCUMENT, + ElementTypes::TYPE_OBJECT, + ]; + } +} diff --git a/src/Patcher/Adapter/ParentIdAdapter.php b/src/Patcher/Adapter/ParentIdAdapter.php index d570f3e97..0c597a6ce 100644 --- a/src/Patcher/Adapter/ParentIdAdapter.php +++ b/src/Patcher/Adapter/ParentIdAdapter.php @@ -17,6 +17,7 @@ namespace Pimcore\Bundle\StudioBackendBundle\Patcher\Adapter; use Pimcore\Bundle\StudioBackendBundle\Patcher\Service\Loader\PatchAdapterInterface; +use Pimcore\Bundle\StudioBackendBundle\Patcher\Service\Loader\TaggedIteratorAdapter; use Pimcore\Bundle\StudioBackendBundle\Util\Constants\ElementTypes; use Pimcore\Model\Element\ElementInterface; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; @@ -25,7 +26,7 @@ /** * @internal */ -#[AutoconfigureTag('pimcore.studio_backend.patch_adapter')] +#[AutoconfigureTag(TaggedIteratorAdapter::ADAPTER_TAG)] final readonly class ParentIdAdapter implements PatchAdapterInterface { private const INDEX_KEY = 'parentId'; diff --git a/src/Patcher/Service/PatchService.php b/src/Patcher/Service/PatchService.php index 59b0b3a47..32623f478 100644 --- a/src/Patcher/Service/PatchService.php +++ b/src/Patcher/Service/PatchService.php @@ -17,50 +17,101 @@ namespace Pimcore\Bundle\StudioBackendBundle\Patcher\Service; use Exception; -use Pimcore\Bundle\StaticResolverBundle\Models\Element\ServiceResolver; +use Pimcore\Bundle\GenericExecutionEngineBundle\Agent\JobExecutionAgentInterface; +use Pimcore\Bundle\GenericExecutionEngineBundle\Model\Job; +use Pimcore\Bundle\GenericExecutionEngineBundle\Model\JobStep; +use Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\Util\JobSteps; +use Pimcore\Bundle\StudioBackendBundle\Element\ExecutionEngine\AutomationAction\Messenger\Messages\PatchMessage; +use Pimcore\Bundle\StudioBackendBundle\Element\Service\ElementServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\AccessDeniedException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementSavingFailedException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; -use Pimcore\Bundle\StudioBackendBundle\Util\Traits\ElementProviderTrait; +use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\Config; +use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\Jobs; +use Pimcore\Model\Element\ElementDescriptor; +use Pimcore\Model\Element\ElementInterface; +use Pimcore\Model\UserInterface; +use function count; /** * @internal */ final class PatchService implements PatchServiceInterface { - use ElementProviderTrait; - public function __construct( private readonly AdapterLoaderInterface $adapterLoader, - private readonly ServiceResolver $serviceResolver + private readonly ElementServiceInterface $elementService, + private readonly JobExecutionAgentInterface $jobExecutionAgent, ) { } /** - * @throws ElementSavingFailedException|NotFoundException + * @throws AccessDeniedException|ElementSavingFailedException|NotFoundException */ - public function patch(string $elementType, array $patchData): array - { - $adapters = $this->adapterLoader->loadAdapters($elementType); - - $error = []; + public function patch( + string $elementType, + array $patchData, + UserInterface $user, + ): ?int { + if (count($patchData) > 1) { + return $this->patchAsynchronously($elementType, $patchData, $user); + } - foreach ($patchData as $data) { - try { - $element = $this->getElement($this->serviceResolver, $elementType, $data['id']); - foreach ($adapters as $adapter) { - $adapter->patch($element, $data); - } + $element = $this->elementService->getAllowedElementById($elementType, $patchData[0]['id'], $user); + $this->patchElement($element, $elementType, $patchData[0]); - $element->save(); + return null; + } - } catch (Exception $exception) { - $error[] = [ - 'id' => $data['id'], - 'message' => $exception->getMessage(), - ]; + /** + * @throws ElementSavingFailedException + */ + public function patchElement( + ElementInterface $element, + string $elementType, + array $elementPatchData + ): void { + try { + $adapters = $this->adapterLoader->loadAdapters($elementType); + foreach ($adapters as $adapter) { + $adapter->patch($element, $elementPatchData); } + + $element->save(); + } catch (Exception $exception) { + throw new ElementSavingFailedException( + $element->getId(), + $exception->getMessage() + ); } + } + + private function patchAsynchronously( + string $elementType, + array $patchData, + UserInterface $user, + ): int { + $job = new Job( + name: Jobs::PATCH_ELEMENTS->value, + steps: [ + new JobStep(JobSteps::ELEMENT_PATCHING->value, PatchMessage::class, '', []), + ], + selectedElements: array_map( + static fn (array $data) => new ElementDescriptor( + $elementType, + $data['id'] + ), + $patchData + ), + environmentData: array_column($patchData, null, 'id'), + ); + + $jobRun = $this->jobExecutionAgent->startJobExecution( + $job, + $user->getId(), + Config::CONTEXT_CONTINUE_ON_ERROR->value + ); - return $error; + return $jobRun->getId(); } } diff --git a/src/Patcher/Service/PatchServiceInterface.php b/src/Patcher/Service/PatchServiceInterface.php index 37feeaf04..48fd10592 100644 --- a/src/Patcher/Service/PatchServiceInterface.php +++ b/src/Patcher/Service/PatchServiceInterface.php @@ -16,8 +16,11 @@ namespace Pimcore\Bundle\StudioBackendBundle\Patcher\Service; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\AccessDeniedException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementSavingFailedException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; +use Pimcore\Model\Element\ElementInterface; +use Pimcore\Model\UserInterface; /** * @internal @@ -25,7 +28,20 @@ interface PatchServiceInterface { /** - * @throws ElementSavingFailedException|NotFoundException + * @throws AccessDeniedException|ElementSavingFailedException|NotFoundException */ - public function patch(string $elementType, array $patchData): array; + public function patch( + string $elementType, + array $patchData, + UserInterface $user, + ): ?int; + + /** + * @throws ElementSavingFailedException + */ + public function patchElement( + ElementInterface $element, + string $elementType, + array $elementPatchData + ): void; } diff --git a/translations/studio.en.yaml b/translations/studio.en.yaml index cefefb2be..01e07c4fd 100644 --- a/translations/studio.en.yaml +++ b/translations/studio.en.yaml @@ -7,15 +7,25 @@ studio_ee_element_permission_missing: User with ID %userId% has missing permissi studio_ee_element_delete_failed: 'Element with type %type% with ID %id% could not be deleted: %message%' studio_ee_zip_file_upload_failed: 'Zip file could not be uploaded: %message%' studio_ee_zip_cleanup_failed: 'Zip directory %directory% could not be removed: %message%' +studio_ee_element_patch_failed: 'Element with type %type% with ID %id% could not be updated: %message%' +studio_ee_no_element_data_found: 'No data found' studio_ee_job_create_zip: Create Zip studio_ee_job_clone_assets: Clone Assets studio_ee_job_upload_zip_file: Upload Zip File studio_ee_job_delete_assets: Delete Assets +studio_ee_job_patch_elements: Patch Elements +studio_ee_job_create_csv: Create CSV +studio_ee_job_upload_assets: Upload Assets studio_ee_job_step_zip_collection: Zip Collection studio_ee_job_step_zip_creation: Zip Creation studio_ee_job_step_asset_deletion: Asset Deletion studio_ee_job_step_asset_cloning: Asset Cloning studio_ee_job_step_csv_file_not_found: CSV file not found +studio_ee_jop_step_zip_uploading: Zip Uploading +studio_ee_job_step_asset_uploading: Asset Uploading +studio_ee_job_step_csv_creation: CSV Creation +studio_ee_job_step_csv_collection: CSV Collection +studio_ee_job_step_element_patching: Element Patching studio_ee_element_folder_collection_not_supported: 'Collection for folders is not supported. ID: %folderId%' studio_ee_file_not_found_for_job_run: 'File %type% not found for job run. ID: %jobRunId%' studio_ee_no_assets_found_for_job_run: 'No assets found for job run. ID: %jobRunId%'