From b986081ce3f71126aea9fc736eec37a2523dcea4 Mon Sep 17 00:00:00 2001 From: mcop1 <89011527+mcop1@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:59:33 +0100 Subject: [PATCH] [Improvement] Reports export csv (#709) * Added csv export messages, handler and controller * Refactor csv export service part I * Refactor csv export service part I * Apply php-cs-fixer changes * Refactor csv export service part 2 * Apply php-cs-fixer changes * Added custom report csv endpoint * Apply php-cs-fixer changes * Deleted unnecessary class * Changed endpoint to post * Apply php-cs-fixer changes * Moved download/delete csv endpoint to export tag * Apply php-cs-fixer changes * Fix tests * Apply php-cs-fixer changes * Permissions * Apply php-cs-fixer changes * Inject interface instead of service, added api tag --- config/assets.yaml | 2 - config/custom_reports.yaml | 18 ++- config/export.yaml | 33 +++++ config/pimcore/execution_engine.yaml | 5 +- .../Download/DeleteZipController.php | 2 +- .../Download/DownloadZipController.php | 2 +- .../Handler/CsvAssetDataCollectionHandler.php | 2 +- .../CsvFolderDataCollectionHandler.php | 2 +- src/Asset/ExecutionEngine/Util/JobSteps.php | 1 - src/Asset/Service/DownloadService.php | 95 ------------- .../Service/DownloadServiceInterface.php | 24 ---- .../Service/ExecutionEngine/CsvService.php | 77 +---------- .../ExecutionEngine/CsvServiceInterface.php | 21 --- .../Request/CsvExportRequestBody.php | 75 ++++++++++ .../Controller/Chart/GetController.php | 6 +- .../Export/Csv/ExportController.php | 75 ++++++++++ .../Handler/CsvCollectionHandler.php | 101 ++++++++++++++ .../Messages/CsvCollectionMessage.php | 26 ++++ .../ExecutionEngine/Util/JobSteps.php | 21 +++ ...tDataParameter.php => ExportParameter.php} | 42 +++++- .../Repository/CustomReportRepository.php | 2 +- .../CustomReportRepositoryInterface.php | 2 +- src/CustomReport/Service/AdapterService.php | 6 +- .../Service/AdapterServiceInterface.php | 4 +- src/CustomReport/Service/CsvService.php | 81 +++++++++++ .../Service/CsvServiceInterface.php | 27 ++++ .../Service/CustomReportService.php | 31 ++++- .../Service/CustomReportServiceInterface.php | 10 +- .../PimcoreStudioBackendExtension.php | 4 +- src/ExecutionEngine/Util/StepConfig.php | 8 +- .../Download/DeleteCsvController.php | 27 ++-- .../Download/DownloadCsvController.php | 31 ++--- src/Export/Csv/CsvExportService.php | 129 ++++++++++++++++++ .../Csv}/CsvCreationSubscriber.php | 19 +-- .../Messenger/Handler/CsvCreationHandler.php | 38 +++--- .../Messenger/Messages/CsvCreationMessage.php | 2 +- src/Export/ExecutionEngine/Util/JobSteps.php | 21 +++ src/Export/ExportServiceInterface.php | 36 +++++ src/Export/Service/DownloadService.php | 129 ++++++++++++++++++ .../Service/DownloadServiceInterface.php | 49 +++++++ .../Parameter/Query/BoolParameter.php | 40 ++++++ .../Parameter/Query/FilterParameter.php | 4 +- src/OpenApi/Config/Tags.php | 5 + translations/studio.en.yaml | 2 +- translations/studio_api_docs.en.yaml | 26 +++- 45 files changed, 1046 insertions(+), 317 deletions(-) create mode 100644 config/export.yaml create mode 100644 src/CustomReport/Attribute/Request/CsvExportRequestBody.php create mode 100644 src/CustomReport/Controller/Export/Csv/ExportController.php create mode 100644 src/CustomReport/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCollectionHandler.php create mode 100644 src/CustomReport/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCollectionMessage.php create mode 100644 src/CustomReport/ExecutionEngine/Util/JobSteps.php rename src/CustomReport/MappedParameter/{ChartDataParameter.php => ExportParameter.php} (59%) create mode 100644 src/CustomReport/Service/CsvService.php create mode 100644 src/CustomReport/Service/CsvServiceInterface.php rename src/{Asset => Export}/Controller/Download/DeleteCsvController.php (69%) rename src/{Asset => Export}/Controller/Download/DownloadCsvController.php (71%) create mode 100644 src/Export/Csv/CsvExportService.php rename src/{Asset/EventSubscriber => Export/EventSubscriber/Csv}/CsvCreationSubscriber.php (73%) rename src/{Asset => Export}/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php (69%) rename src/{Asset => Export}/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCreationMessage.php (86%) create mode 100644 src/Export/ExecutionEngine/Util/JobSteps.php create mode 100644 src/Export/ExportServiceInterface.php create mode 100644 src/Export/Service/DownloadService.php create mode 100644 src/Export/Service/DownloadServiceInterface.php create mode 100644 src/OpenApi/Attribute/Parameter/Query/BoolParameter.php diff --git a/config/assets.yaml b/config/assets.yaml index 8cb85b579..375559793 100644 --- a/config/assets.yaml +++ b/config/assets.yaml @@ -85,7 +85,6 @@ services: Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Handler\AssetCloneHandler: ~ Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Handler\AssetUploadHandler: ~ - Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Handler\CsvCreationHandler: ~ Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Handler\CsvAssetDataCollectionHandler: ~ Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Handler\CsvFolderDataCollectionHandler: ~ Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Handler\ZipDownloadHandler: ~ @@ -96,7 +95,6 @@ services: # Pimcore\Bundle\StudioBackendBundle\Asset\EventSubscriber\CloneSubscriber: ~ - Pimcore\Bundle\StudioBackendBundle\Asset\EventSubscriber\CsvCreationSubscriber: ~ Pimcore\Bundle\StudioBackendBundle\Asset\EventSubscriber\UploadSubscriber: ~ Pimcore\Bundle\StudioBackendBundle\Asset\EventSubscriber\ZipDownloadSubscriber: ~ Pimcore\Bundle\StudioBackendBundle\Asset\EventSubscriber\ZipUploadSubscriber: ~ diff --git a/config/custom_reports.yaml b/config/custom_reports.yaml index 65163a048..0fabba62c 100644 --- a/config/custom_reports.yaml +++ b/config/custom_reports.yaml @@ -30,10 +30,26 @@ services: # Services # + Pimcore\Bundle\StudioBackendBundle\CustomReport\Service\CsvServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\CustomReport\Service\CsvService + Pimcore\Bundle\StudioBackendBundle\CustomReport\Service\CustomReportServiceInterface: class: Pimcore\Bundle\StudioBackendBundle\CustomReport\Service\CustomReportService Pimcore\Bundle\StudioBackendBundle\CustomReport\Service\AdapterServiceInterface: class: Pimcore\Bundle\StudioBackendBundle\CustomReport\Service\AdapterService arguments: - $adapters: '@pimcore.custom_report.adapter.factories' \ No newline at end of file + $adapters: '@pimcore.custom_report.adapter.factories' + + # + # Messages + # + + Pimcore\Bundle\StudioBackendBundle\CustomReport\ExecutionEngine\AutomationAction\Messenger\Messages\CsvCollectionMessage: ~ + + # + # Handler + # + + Pimcore\Bundle\StudioBackendBundle\CustomReport\ExecutionEngine\AutomationAction\Messenger\Handler\CsvCollectionHandler: ~ + diff --git a/config/export.yaml b/config/export.yaml new file mode 100644 index 000000000..3ab52404e --- /dev/null +++ b/config/export.yaml @@ -0,0 +1,33 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + # controllers are imported separately to make sure they're public + # and have a tag that allows actions to type-hint services + Pimcore\Bundle\StudioBackendBundle\Export\Controller\: + resource: '../src/Export/Controller' + public: true + tags: [ 'controller.service_arguments' ] + + # + # Handler + # + + Pimcore\Bundle\StudioBackendBundle\Export\ExecutionEngine\AutomationAction\Messenger\Handler\CsvCreationHandler: ~ + + # + # Services + # + + Pimcore\Bundle\StudioBackendBundle\Export\Csv\CsvExportService: ~ + Pimcore\Bundle\StudioBackendBundle\Export\ExportServiceInterface: '@Pimcore\Bundle\StudioBackendBundle\Export\Csv\CsvExportService' + Pimcore\Bundle\StudioBackendBundle\Export\Service\DownloadServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\Export\Service\DownloadService + + # + # Event Subscriber + # + + Pimcore\Bundle\StudioBackendBundle\Export\EventSubscriber\Csv\CsvCreationSubscriber: ~ \ No newline at end of file diff --git a/config/pimcore/execution_engine.yaml b/config/pimcore/execution_engine.yaml index b2140025f..77185cd68 100644 --- a/config/pimcore/execution_engine.yaml +++ b/config/pimcore/execution_engine.yaml @@ -13,7 +13,6 @@ framework: routing: Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\CsvAssetCollectionMessage: pimcore_generic_execution_engine Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\CsvFolderCollectionMessage: pimcore_generic_execution_engine - Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\CsvCreationMessage: pimcore_generic_execution_engine Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\AssetCloneMessage: 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\ZipDownloadMessage: pimcore_generic_execution_engine @@ -23,5 +22,7 @@ framework: Pimcore\Bundle\StudioBackendBundle\Element\ExecutionEngine\AutomationAction\Messenger\Messages\PatchMessage: pimcore_generic_execution_engine Pimcore\Bundle\StudioBackendBundle\Element\ExecutionEngine\AutomationAction\Messenger\Messages\PatchFolderMessage: pimcore_generic_execution_engine Pimcore\Bundle\StudioBackendBundle\Element\ExecutionEngine\AutomationAction\Messenger\Messages\RewriteRefMessage: pimcore_generic_execution_engine + Pimcore\Bundle\StudioBackendBundle\Export\ExecutionEngine\AutomationAction\Messenger\Messages\CsvCreationMessage: pimcore_generic_execution_engine Pimcore\Bundle\StudioBackendBundle\DataObject\ExecutionEngine\AutomationAction\Messenger\Messages\CloneMessage: pimcore_generic_execution_engine - Pimcore\Bundle\StudioBackendBundle\Tag\ExecutionEngine\AutomationAction\Messenger\Messages\BatchTagOperationMessage: pimcore_generic_execution_engine \ No newline at end of file + Pimcore\Bundle\StudioBackendBundle\Tag\ExecutionEngine\AutomationAction\Messenger\Messages\BatchTagOperationMessage: pimcore_generic_execution_engine + Pimcore\Bundle\StudioBackendBundle\CustomReport\ExecutionEngine\AutomationAction\Messenger\Messages\CsvCollectionMessage: pimcore_generic_execution_engine \ No newline at end of file diff --git a/src/Asset/Controller/Download/DeleteZipController.php b/src/Asset/Controller/Download/DeleteZipController.php index 7c7282091..4f0f8ee7f 100644 --- a/src/Asset/Controller/Download/DeleteZipController.php +++ b/src/Asset/Controller/Download/DeleteZipController.php @@ -17,11 +17,11 @@ namespace Pimcore\Bundle\StudioBackendBundle\Asset\Controller\Download; use OpenApi\Attributes\Delete; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\DownloadServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine\ZipServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; +use Pimcore\Bundle\StudioBackendBundle\Export\Service\DownloadServiceInterface; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Path\IdParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\SuccessResponse; diff --git a/src/Asset/Controller/Download/DownloadZipController.php b/src/Asset/Controller/Download/DownloadZipController.php index 2986f5f87..ffe746725 100644 --- a/src/Asset/Controller/Download/DownloadZipController.php +++ b/src/Asset/Controller/Download/DownloadZipController.php @@ -18,13 +18,13 @@ use OpenApi\Attributes\Get; use Pimcore\Bundle\StudioBackendBundle\Asset\Attribute\Response\Header\ContentDisposition; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\DownloadServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine\ZipServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\EnvironmentException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\StreamResourceNotFoundException; +use Pimcore\Bundle\StudioBackendBundle\Export\Service\DownloadServiceInterface; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Path\IdParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; diff --git a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvAssetDataCollectionHandler.php b/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvAssetDataCollectionHandler.php index 64feb6a97..be09c6d05 100644 --- a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvAssetDataCollectionHandler.php +++ b/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvAssetDataCollectionHandler.php @@ -98,7 +98,7 @@ public function __invoke(CsvAssetCollectionMessage $message): void ), ]; - $this->updateContextArrayValues($jobRun, StepConfig::ASSET_EXPORT_DATA->value, $assetData); + $this->updateContextArrayValues($jobRun, StepConfig::CSV_EXPORT_DATA->value, $assetData); } catch (Exception $e) { $this->abort($this->getAbortData( Config::CSV_DATA_COLLECTION_FAILED_MESSAGE->value, diff --git a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvFolderDataCollectionHandler.php b/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvFolderDataCollectionHandler.php index 2347c3d84..5c170fa8b 100644 --- a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvFolderDataCollectionHandler.php +++ b/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvFolderDataCollectionHandler.php @@ -107,7 +107,7 @@ public function __invoke(CsvFolderCollectionMessage $message): void ), ]; - $this->updateContextArrayValues($jobRun, StepConfig::ASSET_EXPORT_DATA->value, $assetData); + $this->updateContextArrayValues($jobRun, StepConfig::CSV_EXPORT_DATA->value, $assetData); } catch (Exception $e) { $this->abort($this->getAbortData( Config::CSV_DATA_COLLECTION_FAILED_MESSAGE->value, diff --git a/src/Asset/ExecutionEngine/Util/JobSteps.php b/src/Asset/ExecutionEngine/Util/JobSteps.php index 0c8c5d9b8..7ca4345b8 100644 --- a/src/Asset/ExecutionEngine/Util/JobSteps.php +++ b/src/Asset/ExecutionEngine/Util/JobSteps.php @@ -23,5 +23,4 @@ enum JobSteps: string case ASSET_CLONING = 'studio_ee_job_step_asset_cloning'; 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'; } diff --git a/src/Asset/Service/DownloadService.php b/src/Asset/Service/DownloadService.php index 4b023e191..495f8c361 100644 --- a/src/Asset/Service/DownloadService.php +++ b/src/Asset/Service/DownloadService.php @@ -17,20 +17,12 @@ namespace Pimcore\Bundle\StudioBackendBundle\Asset\Service; use Exception; -use League\Flysystem\FilesystemException; -use League\Flysystem\FilesystemOperator; use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\ImageDownloadConfigParameter; -use Pimcore\Bundle\StudioBackendBundle\Element\Service\StorageServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementStreamResourceNotFoundException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\EnvironmentException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidAssetFormatTypeException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidElementTypeException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidThumbnailException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\StreamResourceNotFoundException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ThumbnailResizingFailedException; -use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Service\ExecutionEngineServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\Asset\FormatTypes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseHeaders; use Pimcore\Bundle\StudioBackendBundle\Util\Trait\StreamedResponseTrait; @@ -41,7 +33,6 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\StreamedResponse; use function in_array; -use function sprintf; /** * @internal @@ -52,8 +43,6 @@ use TempFilePathTrait; public function __construct( - private ExecutionEngineServiceInterface $executionEngineService, - private StorageServiceInterface $storageService, private ThumbnailServiceInterface $thumbnailService, private array $defaultFormats, ) { @@ -145,88 +134,4 @@ public function downloadImageByThumbnail( false ); } - - /** - * @throws EnvironmentException|ForbiddenException|NotFoundException|StreamResourceNotFoundException - */ - public function downloadResourceByJobRunId( - int $jobRunId, - string $tempFileName, - string $tempFolderName, - string $mimeType, - string $downloadName, - ): StreamedResponse { - $this->executionEngineService->validateJobRun($jobRunId); - $fileName = $this->getTempFileName($jobRunId, $tempFileName); - $folderName = $this->getTempFileName($jobRunId, $tempFolderName); - $filePath = $folderName . '/' . $fileName; - - $streamedResponse = $this->getFileStreamedResponse( - $filePath, - $mimeType, - $downloadName, - $this->validateStorage($filePath, $jobRunId) - ); - - try { - $this->storageService->cleanUpFolder($folderName); - } catch (FilesystemException) { - throw new EnvironmentException( - sprintf( - 'Failed to clean up temporary folder %s', - $folderName - ) - ); - } - - return $streamedResponse; - } - - /** - * @throws EnvironmentException|NotFoundException - */ - public function cleanupDataByJobRunId( - int $jobRunId, - string $folderName, - string $fileName - ): void { - $this->executionEngineService->validateJobRun($jobRunId); - $this->validateStorage($this->getTempFilePath($jobRunId, $folderName . '/' . $fileName), $jobRunId); - - try { - $this->storageService->cleanUpFolder( - $this->getTempFileName( - $jobRunId, - $folderName - ), - true - ); - } catch (FilesystemException $e) { - throw new EnvironmentException( - sprintf( - 'Failed to delete file based on jobRunId %d: %s', - $jobRunId, - $e->getMessage() - ), - ); - } - } - - /** - * @throws EnvironmentException - */ - private function validateStorage(string $filePath, int $jobRunId): FilesystemOperator - { - $storage = $this->storageService->getTempStorage(); - if (!$this->storageService->tempFileExists($filePath)) { - throw new EnvironmentException( - sprintf( - 'Resource not found for jobRun with Id %d', - $jobRunId - ) - ); - } - - return $storage; - } } diff --git a/src/Asset/Service/DownloadServiceInterface.php b/src/Asset/Service/DownloadServiceInterface.php index e9af47e4d..28cafaae5 100644 --- a/src/Asset/Service/DownloadServiceInterface.php +++ b/src/Asset/Service/DownloadServiceInterface.php @@ -18,12 +18,8 @@ use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\ImageDownloadConfigParameter; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementStreamResourceNotFoundException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\EnvironmentException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidAssetFormatTypeException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidElementTypeException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\StreamResourceNotFoundException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ThumbnailResizingFailedException; use Pimcore\Model\Asset; use Symfony\Component\HttpFoundation\BinaryFileResponse; @@ -64,24 +60,4 @@ public function downloadImageByThumbnail( Asset $image, string $thumbnailName ): BinaryFileResponse; - - /** - * @throws EnvironmentException|ForbiddenException|NotFoundException|StreamResourceNotFoundException - */ - public function downloadResourceByJobRunId( - int $jobRunId, - string $tempFileName, - string $tempFolderName, - string $mimeType, - string $downloadName, - ): StreamedResponse; - - /** - * @throws EnvironmentException|NotFoundException - */ - public function cleanupDataByJobRunId( - int $jobRunId, - string $folderName, - string $fileName - ): void; } diff --git a/src/Asset/Service/ExecutionEngine/CsvService.php b/src/Asset/Service/ExecutionEngine/CsvService.php index 0a917fba8..3c0efb7c2 100644 --- a/src/Asset/Service/ExecutionEngine/CsvService.php +++ b/src/Asset/Service/ExecutionEngine/CsvService.php @@ -16,25 +16,20 @@ namespace Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine; -use League\Flysystem\FilesystemException; -use League\Flysystem\FilesystemOperator; use Pimcore\Bundle\GenericExecutionEngineBundle\Agent\JobExecutionAgentInterface; use Pimcore\Bundle\GenericExecutionEngineBundle\Model\Job; use Pimcore\Bundle\GenericExecutionEngineBundle\Model\JobStep; use Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\CsvAssetCollectionMessage; -use Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\CsvCreationMessage; use Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\CsvFolderCollectionMessage; use Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\Util\JobSteps; use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\ExportAssetParameter; use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\ExportFolderParameter; -use Pimcore\Bundle\StudioBackendBundle\Element\Service\StorageServiceInterface; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\Config; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\Jobs; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\StepConfig; -use Pimcore\Bundle\StudioBackendBundle\Grid\Service\GridServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Grid\Util\Collection\ColumnCollection; +use Pimcore\Bundle\StudioBackendBundle\Export\ExecutionEngine\AutomationAction\Messenger\Messages\CsvCreationMessage; +use Pimcore\Bundle\StudioBackendBundle\Export\ExecutionEngine\Util\JobSteps as ExportJobSteps; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Util\Trait\TempFilePathTrait; use Pimcore\Model\Element\ElementDescriptor; /** @@ -42,14 +37,9 @@ */ final readonly class CsvService implements CsvServiceInterface { - use TempFilePathTrait; - public function __construct( private JobExecutionAgentInterface $jobExecutionAgent, - private SecurityServiceInterface $securityService, - private StorageServiceInterface $storageService, - private GridServiceInterface $gridService, - private string $defaultDelimiter, + private SecurityServiceInterface $securityService ) { } @@ -93,32 +83,6 @@ public function generateCsvFileForFolders(ExportFolderParameter $exportFolderPar ); } - /** - * @throws FilesystemException - */ - public function createCsvFile( - int $id, - ColumnCollection $columnCollection, - array $settings, - array $assetData, - ?string $delimiter = null, - ): void { - $storage = $this->storageService->getTempStorage(); - $headers = $this->getHeaders($columnCollection, $settings); - if ($delimiter === null) { - $delimiter = $this->defaultDelimiter; - } - $data[] = implode($delimiter, $headers) . StepConfig::NEW_LINE->value; - foreach ($assetData as $row) { - $data[] = implode($delimiter, array_map([$this, 'encodeFunc'], $row)) . StepConfig::NEW_LINE->value; - } - - $storage->write( - $this->getCsvFilePath($id, $storage), - implode($data) - ); - } - private function generateCsvFileJob( array $elements, array $collectionSettings, @@ -141,39 +105,6 @@ private function generateCsvFileJob( return $jobRun->getId(); } - /** - * @throws FilesystemException - */ - private function getCsvFilePath(int $id, FilesystemOperator $storage): string - { - $folderName = $this->getTempFileName($id, self::CSV_FOLDER_NAME); - $file = $this->getTempFileName($id, self::CSV_FILE_NAME); - $storage->createDirectory($folderName); - - return $folderName . '/' . $file; - } - - private function encodeFunc(?string $value): string - { - $value = str_replace('"', '""', $value ?? ''); - - //force wrap value in quotes and return - return '"' . $value . '"'; - } - - private function getHeaders(ColumnCollection $columnCollection, array $settings): array - { - $header = $settings[StepConfig::SETTINGS_HEADER->value] ?? StepConfig::SETTINGS_HEADER_NO_HEADER->value; - if ($header === StepConfig::SETTINGS_HEADER_NO_HEADER->value) { - return []; - } - - return $this->gridService->getColumnKeys( - $columnCollection, - $header === StepConfig::SETTINGS_HEADER_NAME->value - ); - } - private function mapJobSteps( array $elements, array $collectionSettings, @@ -194,7 +125,7 @@ private function mapJobSteps( private function getCsvCreationStep(array $settings): JobStep { return new JobStep( - JobSteps::CSV_CREATION->value, + ExportJobSteps::CSV_CREATION->value, CsvCreationMessage::class, '', $settings diff --git a/src/Asset/Service/ExecutionEngine/CsvServiceInterface.php b/src/Asset/Service/ExecutionEngine/CsvServiceInterface.php index 52f94b830..13ef2d7a8 100644 --- a/src/Asset/Service/ExecutionEngine/CsvServiceInterface.php +++ b/src/Asset/Service/ExecutionEngine/CsvServiceInterface.php @@ -16,36 +16,15 @@ namespace Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine; -use League\Flysystem\FilesystemException; use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\ExportAssetParameter; use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\ExportFolderParameter; -use Pimcore\Bundle\StudioBackendBundle\Grid\Util\Collection\ColumnCollection; /** * @internal */ interface CsvServiceInterface { - public const CSV_FILE_NAME = 'download-csv-{id}.csv'; - - public const CSV_FOLDER_NAME = 'download-csv-{id}'; - public function generateCsvFileForAssets(ExportAssetParameter $exportAssetParameter): int; public function generateCsvFileForFolders(ExportFolderParameter $exportFolderParameter): int; - - /** - * @throws FilesystemException - */ - public function createCsvFile( - int $id, - ColumnCollection $columnCollection, - array $settings, - array $assetData, - ?string $delimiter = null, - ): void; - - public function getTempFileName(int $id, string $path): string; - - public function getTempFilePath(int $id, string $path): string; } diff --git a/src/CustomReport/Attribute/Request/CsvExportRequestBody.php b/src/CustomReport/Attribute/Request/CsvExportRequestBody.php new file mode 100644 index 000000000..e54854f93 --- /dev/null +++ b/src/CustomReport/Attribute/Request/CsvExportRequestBody.php @@ -0,0 +1,75 @@ +jsonResponse( $this->customReportService->getChartData($name, $chartDataParameter) diff --git a/src/CustomReport/Controller/Export/Csv/ExportController.php b/src/CustomReport/Controller/Export/Csv/ExportController.php new file mode 100644 index 000000000..d225d3e6a --- /dev/null +++ b/src/CustomReport/Controller/Export/Csv/ExportController.php @@ -0,0 +1,75 @@ +value)] + #[Post( + path: self::PREFIX . '/custom-report/export/csv', + operationId: 'custom_report_export_csv', + description: 'custom_report_export_csv_description', + summary: 'custom_report_export_csv_summary', + tags: [Tags::CustomReports->value] + )] + #[CsvExportRequestBody] + #[CreatedResponse( + description: 'custom_report_export_csv_created_response', + content: new IdJson('ID of created jobRun', 'jobRunId') + )] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::NOT_FOUND, + ])] + public function exportCsv( + #[MapRequestPayload] ExportParameter $exportParameter + ): Response { + return $this->jsonResponse( + $this->csvService->generateCsvFile( + $exportParameter + ) + ); + } +} diff --git a/src/CustomReport/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCollectionHandler.php b/src/CustomReport/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCollectionHandler.php new file mode 100644 index 000000000..ae34029cd --- /dev/null +++ b/src/CustomReport/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCollectionHandler.php @@ -0,0 +1,101 @@ +getJobRun($message); + if (!$this->shouldBeExecuted($jobRun)) { + return; + } + $name = ''; + + try { + $exportParameter = ExportParameter::fromArray( + $this->extractConfigFieldFromJobStepConfig($message, StepConfig::CUSTOM_REPORT_CONFIG->value) + ); + $name = $exportParameter->getName(); + $reportConfig = $this->customReportService->getCustomReportByName($name); + $exportFields = $this->customReportService->getFieldsForExport($reportConfig); + $reportData = $this->customReportAdapterService->getData( + $reportConfig, + $exportParameter + ); + $csvData = $this->customReportService->generateCsvData( + $reportData, + $exportFields, + $exportParameter->getIncludeHeaders() + ); + + $this->updateContextArrayValues( + $this->getJobRun($message), + StepConfig::CSV_EXPORT_DATA->value, + $csvData + ); + } catch (Exception $e) { + $this->abort($this->getAbortData( + Config::CSV_DATA_COLLECTION_FAILED_MESSAGE->value, + [ + 'id' => $name, + 'message' => $e->getMessage(), + ] + )); + } + + $this->updateProgress($this->publishService, $jobRun, $this->getJobStep($message)->getName()); + } + + protected function configureStep(): void + { + $this->stepConfiguration->setRequired(StepConfig::CUSTOM_REPORT_CONFIG->value); + $this->stepConfiguration->setAllowedTypes( + StepConfig::CUSTOM_REPORT_CONFIG->value, + StepConfig::CONFIG_TYPE_ARRAY->value + ); + } +} diff --git a/src/CustomReport/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCollectionMessage.php b/src/CustomReport/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCollectionMessage.php new file mode 100644 index 000000000..1252036a5 --- /dev/null +++ b/src/CustomReport/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCollectionMessage.php @@ -0,0 +1,26 @@ +validate(); } @@ -40,6 +44,22 @@ public function getSortOrder(): ?string return $this->sortOrder; } + public static function fromArray(array $data): self + { + return new self( + name: $data['name'], + filters: $data['filters'] ?? null, + drillDownFilters: $data['drillDownFilters'] ?? null, + sortOrder: $data['sortOrder'] ?? null, + sortBy: $data['sortBy'] ?? null, + reportOffset: $data['reportOffset'] ?? null, + reportLimit: $data['reportLimit'] ?? null, + fields: $data['fields'] ?? null, + includeHeaders: $data['includeHeaders'] ?? false, + defaultDelimiter: $data['defaultDelimiter'] ?? null + ); + } + public function getSortBy(): ?string { return $this->sortBy; @@ -71,4 +91,24 @@ public function getDrillDownFilters(): ?array { return $this->drillDownFilters; } + + public function getFields(): ?array + { + return $this->fields; + } + + public function getIncludeHeaders(): bool + { + return $this->includeHeaders; + } + + public function getDefaultDelimiter(): ?string + { + return $this->defaultDelimiter; + } + + public function getName(): string + { + return $this->name; + } } diff --git a/src/CustomReport/Repository/CustomReportRepository.php b/src/CustomReport/Repository/CustomReportRepository.php index 7fd34b2d2..1193bf24c 100644 --- a/src/CustomReport/Repository/CustomReportRepository.php +++ b/src/CustomReport/Repository/CustomReportRepository.php @@ -56,7 +56,7 @@ public function loadForCurrentUser(): array ); } - public function loadByName(string $name): ?Config + public function loadByName(string $name): Config { $report = null; $exception = null; diff --git a/src/CustomReport/Repository/CustomReportRepositoryInterface.php b/src/CustomReport/Repository/CustomReportRepositoryInterface.php index 76ade9647..b6c4dca57 100644 --- a/src/CustomReport/Repository/CustomReportRepositoryInterface.php +++ b/src/CustomReport/Repository/CustomReportRepositoryInterface.php @@ -32,5 +32,5 @@ public function loadForCurrentUser(): array; /** * @throws NotFoundException */ - public function loadByName(string $name): ?Config; + public function loadByName(string $name): Config; } diff --git a/src/CustomReport/Service/AdapterService.php b/src/CustomReport/Service/AdapterService.php index 4d5e6b49b..5be201ed1 100644 --- a/src/CustomReport/Service/AdapterService.php +++ b/src/CustomReport/Service/AdapterService.php @@ -19,7 +19,7 @@ use Pimcore\Bundle\CustomReportsBundle\Tool\Adapter\CustomReportAdapterFactoryInterface; use Pimcore\Bundle\CustomReportsBundle\Tool\Adapter\CustomReportAdapterInterface; use Pimcore\Bundle\CustomReportsBundle\Tool\Config; -use Pimcore\Bundle\StudioBackendBundle\CustomReport\MappedParameter\ChartDataParameter; +use Pimcore\Bundle\StudioBackendBundle\CustomReport\MappedParameter\ExportParameter; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use stdClass; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -34,7 +34,7 @@ public function __construct( ) { } - public function getData(Config $report, ChartDataParameter $chartDataParameter): array + public function getData(Config $report, ExportParameter $chartDataParameter): array { return $this->getAdapter($report)->getData( $chartDataParameter->getFilters(), @@ -42,7 +42,7 @@ public function getData(Config $report, ChartDataParameter $chartDataParameter): $chartDataParameter->getSortOrder(), $chartDataParameter->getReportOffset(), $chartDataParameter->getReportLimit(), - null, + $chartDataParameter->getFields(), $chartDataParameter->getDrillDownFilters() ); } diff --git a/src/CustomReport/Service/AdapterServiceInterface.php b/src/CustomReport/Service/AdapterServiceInterface.php index 3973c1c2b..875594a31 100644 --- a/src/CustomReport/Service/AdapterServiceInterface.php +++ b/src/CustomReport/Service/AdapterServiceInterface.php @@ -17,7 +17,7 @@ namespace Pimcore\Bundle\StudioBackendBundle\CustomReport\Service; use Pimcore\Bundle\CustomReportsBundle\Tool\Config; -use Pimcore\Bundle\StudioBackendBundle\CustomReport\MappedParameter\ChartDataParameter; +use Pimcore\Bundle\StudioBackendBundle\CustomReport\MappedParameter\ExportParameter; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; /** @@ -28,5 +28,5 @@ interface AdapterServiceInterface /** * @throws NotFoundException */ - public function getData(Config $report, ChartDataParameter $chartDataParameter): array; + public function getData(Config $report, ExportParameter $chartDataParameter): array; } diff --git a/src/CustomReport/Service/CsvService.php b/src/CustomReport/Service/CsvService.php new file mode 100644 index 000000000..d2ed44219 --- /dev/null +++ b/src/CustomReport/Service/CsvService.php @@ -0,0 +1,81 @@ +value => $exportParameter, + ]; + + return $this->generateCsvFileJob( + $collectionSettings, + ); + } + + private function generateCsvFileJob( + array $collectionSettings + ): int { + + $jobSteps = [ + new JobStep( + CustomReportJobSteps::CUSTOM_REPORT_CSV_COLLECTION->value, + CsvCollectionMessage::class, + '', + $collectionSettings + ), + new JobStep( + JobSteps::CSV_CREATION->value, + CsvCreationMessage::class, + '', + [] + ), + ]; + + $jobRun = $this->jobExecutionAgent->startJobExecution( + new Job(Jobs::CREATE_CSV->value, $jobSteps), + $this->securityService->getCurrentUser()->getId(), + Config::CONTEXT_STOP_ON_ERROR->value + ); + + return $jobRun->getId(); + } +} diff --git a/src/CustomReport/Service/CsvServiceInterface.php b/src/CustomReport/Service/CsvServiceInterface.php new file mode 100644 index 000000000..d05ea9eed --- /dev/null +++ b/src/CustomReport/Service/CsvServiceInterface.php @@ -0,0 +1,27 @@ +customReportRepository->loadByName($reportName); } - public function getChartData(string $reportName, ChartDataParameter $chartDataParameter): CustomReportChartData + public function getChartData(string $reportName, ExportParameter $chartDataParameter): CustomReportChartData { $reportConfig = $this->getCustomReportByName($reportName); $data = $this->adapterService->getData($reportConfig, $chartDataParameter); @@ -110,4 +110,31 @@ public function getCustomReportDetails(string $reportName): CustomReportDetails return $reportDetails; } + + public function getFieldsForExport(Config $reportConfig): array + { + $columns = $reportConfig->getColumnConfiguration(); + $fields = []; + foreach ($columns as $column) { + if ($column['export']) { + $fields[] = $column['name']; + } + } + + return $fields; + } + + public function generateCsvData(array $reportData, array $exportFields, bool $includeHeaders): array + { + $csvData = []; + if ($includeHeaders) { + $csvData[] = $exportFields; + } + + foreach ($reportData['data'] ?? [] as $row) { + $csvData[] = array_values($row); + } + + return $csvData; + } } diff --git a/src/CustomReport/Service/CustomReportServiceInterface.php b/src/CustomReport/Service/CustomReportServiceInterface.php index 5f11c0c38..07db0b4fe 100644 --- a/src/CustomReport/Service/CustomReportServiceInterface.php +++ b/src/CustomReport/Service/CustomReportServiceInterface.php @@ -17,7 +17,7 @@ namespace Pimcore\Bundle\StudioBackendBundle\CustomReport\Service; use Pimcore\Bundle\CustomReportsBundle\Tool\Config; -use Pimcore\Bundle\StudioBackendBundle\CustomReport\MappedParameter\ChartDataParameter; +use Pimcore\Bundle\StudioBackendBundle\CustomReport\MappedParameter\ExportParameter; use Pimcore\Bundle\StudioBackendBundle\CustomReport\Schema\CustomReportChartData; use Pimcore\Bundle\StudioBackendBundle\CustomReport\Schema\CustomReportDetails; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; @@ -34,15 +34,19 @@ public function getCustomReportConfigTree(): array; /** * @throws NotFoundException */ - public function getCustomReportByName(string $reportName): ?Config; + public function getCustomReportByName(string $reportName): Config; /** * @throws NotFoundException */ - public function getChartData(string $reportName, ChartDataParameter $chartDataParameter): CustomReportChartData; + public function getChartData(string $reportName, ExportParameter $chartDataParameter): CustomReportChartData; /** * @throws NotFoundException */ public function getCustomReportDetails(string $reportName): CustomReportDetails; + + public function getFieldsForExport(Config $reportConfig): array; + + public function generateCsvData(array $reportData, array $exportFields, bool $includeHeaders): array; } diff --git a/src/DependencyInjection/PimcoreStudioBackendExtension.php b/src/DependencyInjection/PimcoreStudioBackendExtension.php index 4ccf80d02..b7a0bfab4 100644 --- a/src/DependencyInjection/PimcoreStudioBackendExtension.php +++ b/src/DependencyInjection/PimcoreStudioBackendExtension.php @@ -19,7 +19,6 @@ use Exception; use Pimcore\Bundle\CoreBundle\DependencyInjection\ConfigurationHelper; use Pimcore\Bundle\StudioBackendBundle\Asset\Service\DownloadServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine\CsvServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine\ZipServiceInterface; use Pimcore\Bundle\StudioBackendBundle\DataObject\Service\DataAdapterServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Element\Service\ElementDeleteServiceInterface; @@ -27,6 +26,7 @@ use Pimcore\Bundle\StudioBackendBundle\Exception\InvalidHostException; use Pimcore\Bundle\StudioBackendBundle\Exception\InvalidPathException; use Pimcore\Bundle\StudioBackendBundle\Exception\InvalidUrlPrefixException; +use Pimcore\Bundle\StudioBackendBundle\Export\Csv\CsvExportService; use Pimcore\Bundle\StudioBackendBundle\Grid\Service\ConfigurationServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Mercure\Service\HubServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Note\Service\NoteServiceInterface; @@ -97,7 +97,7 @@ public function load(array $configs, ContainerBuilder $container): void $definition = $container->getDefinition(ZipServiceInterface::class); $definition->setArgument('$downloadLimits', $config['asset_download_settings']); - $definition = $container->getDefinition(CsvServiceInterface::class); + $definition = $container->getDefinition(CsvExportService::class); $definition->setArgument('$defaultDelimiter', $config['csv_settings']['default_delimiter']); $definition = $container->getDefinition(ConfigurationServiceInterface::class); diff --git a/src/ExecutionEngine/Util/StepConfig.php b/src/ExecutionEngine/Util/StepConfig.php index 526a8255d..c55f4fd08 100644 --- a/src/ExecutionEngine/Util/StepConfig.php +++ b/src/ExecutionEngine/Util/StepConfig.php @@ -22,9 +22,12 @@ enum StepConfig: string { use EnumToValueArrayTrait; + case ID = 'id'; + case CUSTOM_REPORT_CONFIG = 'custom_report_config'; + case CUSTOM_REPORT_TO_EXPORT = 'custom_report_to_export'; case ASSET_TO_EXPORT = 'asset_to_export'; case FOLDER_TO_EXPORT = 'folder_to_export'; - case ASSET_EXPORT_DATA = 'asset_export_data'; + case CSV_EXPORT_DATA = 'csv_export_data'; case CONFIG_CONFIGURATION = 'config'; case CONFIG_COLUMNS = 'columns'; case CONFIG_FILTERS = 'filters'; @@ -35,4 +38,7 @@ enum StepConfig: string case SETTINGS_HEADER_NAME = 'name'; case NEW_LINE = "\r\n"; case CONFIG_TYPE_ARRAY = 'array'; + case CONFIG_TYPE_INT = 'int'; + case CONFIG_TYPE_STRING = 'string'; + case CONFIG_TYPE_BOOL = 'bool'; } diff --git a/src/Asset/Controller/Download/DeleteCsvController.php b/src/Export/Controller/Download/DeleteCsvController.php similarity index 69% rename from src/Asset/Controller/Download/DeleteCsvController.php rename to src/Export/Controller/Download/DeleteCsvController.php index 36712936b..efde25c40 100644 --- a/src/Asset/Controller/Download/DeleteCsvController.php +++ b/src/Export/Controller/Download/DeleteCsvController.php @@ -14,24 +14,22 @@ * @license http://www.pimcore.org/license GPLv3 and PCL */ -namespace Pimcore\Bundle\StudioBackendBundle\Asset\Controller\Download; +namespace Pimcore\Bundle\StudioBackendBundle\Export\Controller\Download; use OpenApi\Attributes\Delete; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\DownloadServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine\CsvServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\EnvironmentException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; +use Pimcore\Bundle\StudioBackendBundle\Export\Csv\CsvExportService; +use Pimcore\Bundle\StudioBackendBundle\Export\Service\DownloadServiceInterface; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Path\IdParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\SuccessResponse; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Config\Tags; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; -use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; /** @@ -49,14 +47,13 @@ public function __construct( /** * @throws EnvironmentException|ForbiddenException|NotFoundException */ - #[Route('/assets/download/csv/{jobRunId}', name: 'pimcore_studio_api_csv_delete', methods: ['DELETE'])] - #[IsGranted(UserPermissions::ASSETS->value)] + #[Route('/export/download/csv/{jobRunId}', name: 'pimcore_studio_api_export_delete_csv', methods: ['DELETE'])] #[Delete( - path: self::PREFIX . '/assets/download/csv/{jobRunId}', - operationId: 'asset_delete_csv', - description: 'asset_delete_csv_description', - summary: 'asset_delete_csv_summary', - tags: [Tags::Assets->name] + path: self::PREFIX . '/export/download/csv/{jobRunId}', + operationId: 'export_delete_csv', + description: 'export_delete_csv_description', + summary: 'export_delete_csv_summary', + tags: [Tags::Export->value] )] #[IdParameter(type: 'JobRun', name: 'jobRunId')] #[SuccessResponse] @@ -65,12 +62,12 @@ public function __construct( HttpResponseCodes::FORBIDDEN, HttpResponseCodes::NOT_FOUND, ])] - public function deleteAssetsCsv(int $jobRunId): Response + public function deleteCsv(int $jobRunId): Response { $this->downloadService->cleanupDataByJobRunId( $jobRunId, - CsvServiceInterface::CSV_FOLDER_NAME, - CsvServiceInterface::CSV_FILE_NAME + CsvExportService::CSV_FOLDER_NAME, + CsvExportService::CSV_FILE_NAME ); return new Response(); diff --git a/src/Asset/Controller/Download/DownloadCsvController.php b/src/Export/Controller/Download/DownloadCsvController.php similarity index 71% rename from src/Asset/Controller/Download/DownloadCsvController.php rename to src/Export/Controller/Download/DownloadCsvController.php index 6fab2ea15..6a2971fc0 100644 --- a/src/Asset/Controller/Download/DownloadCsvController.php +++ b/src/Export/Controller/Download/DownloadCsvController.php @@ -14,17 +14,17 @@ * @license http://www.pimcore.org/license GPLv3 and PCL */ -namespace Pimcore\Bundle\StudioBackendBundle\Asset\Controller\Download; +namespace Pimcore\Bundle\StudioBackendBundle\Export\Controller\Download; use OpenApi\Attributes\Get; use Pimcore\Bundle\StudioBackendBundle\Asset\Attribute\Response\Header\ContentDisposition; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\DownloadServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine\CsvServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\EnvironmentException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\StreamResourceNotFoundException; +use Pimcore\Bundle\StudioBackendBundle\Export\Csv\CsvExportService; +use Pimcore\Bundle\StudioBackendBundle\Export\Service\DownloadServiceInterface; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Path\IdParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; @@ -32,10 +32,8 @@ use Pimcore\Bundle\StudioBackendBundle\OpenApi\Config\Tags; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\Asset\MimeTypes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; -use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; /** @@ -53,18 +51,17 @@ public function __construct( /** * @throws EnvironmentException|ForbiddenException|NotFoundException|StreamResourceNotFoundException */ - #[Route('/assets/download/csv/{jobRunId}', name: 'pimcore_studio_api_csv_download_asset', methods: ['GET'])] - #[IsGranted(UserPermissions::ASSETS->value)] + #[Route('/export/download/csv/{jobRunId}', name: 'pimcore_studio_api_export_download_csv', methods: ['GET'])] #[Get( - path: self::PREFIX . '/assets/download/csv/{jobRunId}', - operationId: 'asset_download_csv', - description: 'asset_download_csv_description', - summary: 'asset_download_csv_summary', - tags: [Tags::Assets->name] + path: self::PREFIX . '/export/download/csv/{jobRunId}', + operationId: 'export_download_csv', + description: 'export_download_csv_description', + summary: 'export_download_csv_summary', + tags: [Tags::Export->value] )] #[IdParameter(type: 'JobRun', name: 'jobRunId')] #[SuccessResponse( - description: 'asset_download_csv_success_response', + description: 'export_download_csv_success_response', content: [new MediaType('application/csv')], headers: [new ContentDisposition()] )] @@ -73,14 +70,14 @@ public function __construct( HttpResponseCodes::FORBIDDEN, HttpResponseCodes::NOT_FOUND, ])] - public function assetDownloadCsv(int $jobRunId): StreamedResponse + public function downloadCsv(int $jobRunId): StreamedResponse { return $this->downloadService->downloadResourceByJobRunId( $jobRunId, - CsvServiceInterface::CSV_FILE_NAME, - CsvServiceInterface::CSV_FOLDER_NAME, + CsvExportService::CSV_FILE_NAME, + CsvExportService::CSV_FOLDER_NAME, MimeTypes::CSV->value, - 'assets.csv' + 'export.csv' ); } } diff --git a/src/Export/Csv/CsvExportService.php b/src/Export/Csv/CsvExportService.php new file mode 100644 index 000000000..89b005295 --- /dev/null +++ b/src/Export/Csv/CsvExportService.php @@ -0,0 +1,129 @@ +storageService->getTempStorage(); + if ($withHeaders) { + $headers = $this->getHeaders($columns, $withGroup); + } + if ($delimiter === null) { + $delimiter = $this->defaultDelimiter; + } + + $data[] = implode($delimiter, $headers) . StepConfig::NEW_LINE->value; + foreach ($csvData as $row) { + $data[] = implode($delimiter, array_map([$this, 'encodeFunc'], $row)) . StepConfig::NEW_LINE->value; + } + + $storage->write( + $this->getCsvFilePath($id, $storage), + implode($data) + ); + } + + /** + * @throws FilesystemException + */ + public function cleanUpFileSystem(int $jobRunId): void + { + $this->storageService->cleanUpFlysystemFile( + $this->getTempFilePath( + $jobRunId, + self::CSV_FOLDER_NAME . '/' . self::CSV_FILE_NAME + ) + ); + + $this->storageService->cleanUpFolder( + $this->getTempFilePath($jobRunId, self::CSV_FOLDER_NAME) + ); + } + + private function encodeFunc(?string $value): string + { + $value = str_replace('"', '""', $value ?? ''); + + //force wrap value in quotes and return + return '"' . $value . '"'; + } + + private function getHeaders(array $columns, bool $withGroup): array + { + if (empty($columns)) { + return []; + } + + $columnCollection = $this->gridService->getConfigurationFromArray( + $columns, + true + ); + + return $this->gridService->getColumnKeys( + $columnCollection, + $withGroup + ); + } + + /** + * @throws FilesystemException + */ + private function getCsvFilePath(int $id, FilesystemOperator $storage): string + { + $folderName = $this->getTempFileName($id, self::CSV_FOLDER_NAME); + $file = $this->getTempFileName($id, self::CSV_FILE_NAME); + $storage->createDirectory($folderName); + + return $folderName . '/' . $file; + } +} diff --git a/src/Asset/EventSubscriber/CsvCreationSubscriber.php b/src/Export/EventSubscriber/Csv/CsvCreationSubscriber.php similarity index 73% rename from src/Asset/EventSubscriber/CsvCreationSubscriber.php rename to src/Export/EventSubscriber/Csv/CsvCreationSubscriber.php index b825e95c0..bfdf2c7ca 100644 --- a/src/Asset/EventSubscriber/CsvCreationSubscriber.php +++ b/src/Export/EventSubscriber/Csv/CsvCreationSubscriber.php @@ -14,16 +14,15 @@ * @license http://www.pimcore.org/license GPLv3 and PCL */ -namespace Pimcore\Bundle\StudioBackendBundle\Asset\EventSubscriber; +namespace Pimcore\Bundle\StudioBackendBundle\Export\EventSubscriber\Csv; use League\Flysystem\FilesystemException; use Pimcore\Bundle\GenericExecutionEngineBundle\Event\JobRunStateChangedEvent; use Pimcore\Bundle\GenericExecutionEngineBundle\Model\JobRunStates; use Pimcore\Bundle\StudioBackendBundle\Asset\Mercure\Events; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine\CsvServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Element\Service\StorageServiceInterface; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Service\EventSubscriberServiceInterface; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\Jobs; +use Pimcore\Bundle\StudioBackendBundle\Export\ExportServiceInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -33,8 +32,7 @@ { public function __construct( private EventSubscriberServiceInterface $eventSubscriberService, - private CsvServiceInterface $csvService, - private StorageServiceInterface $storageService, + private ExportServiceInterface $csvService ) { } @@ -70,15 +68,6 @@ public function onStateChanged(JobRunStateChangedEvent $event): void */ private function cleanupOnFail(int $jobRunId): void { - $this->storageService->cleanUpFlysystemFile( - $this->csvService->getTempFilePath( - $jobRunId, - CsvServiceInterface::CSV_FOLDER_NAME . '/' . CsvServiceInterface::CSV_FILE_NAME - ) - ); - - $this->storageService->cleanUpFolder( - $this->csvService->getTempFilePath($jobRunId, CsvServiceInterface::CSV_FOLDER_NAME) - ); + $this->csvService->cleanupFileSystem($jobRunId); } } diff --git a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php b/src/Export/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php similarity index 69% rename from src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php rename to src/Export/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php index 2bf8cda3b..51d17909a 100644 --- a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php +++ b/src/Export/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCreationHandler.php @@ -14,17 +14,16 @@ * @license http://www.pimcore.org/license GPLv3 and PCL */ -namespace Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Handler; +namespace Pimcore\Bundle\StudioBackendBundle\Export\ExecutionEngine\AutomationAction\Messenger\Handler; use Exception; use League\Flysystem\FilesystemException; -use Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages\CsvCreationMessage; -use Pimcore\Bundle\StudioBackendBundle\Asset\Service\ExecutionEngine\CsvServiceInterface; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\AutomationAction\AbstractHandler; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\Config; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\StepConfig; use Pimcore\Bundle\StudioBackendBundle\ExecutionEngine\Util\Trait\HandlerProgressTrait; -use Pimcore\Bundle\StudioBackendBundle\Grid\Service\GridServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\Export\ExecutionEngine\AutomationAction\Messenger\Messages\CsvCreationMessage; +use Pimcore\Bundle\StudioBackendBundle\Export\ExportServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Mercure\Service\PublishServiceInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -38,8 +37,7 @@ final class CsvCreationHandler extends AbstractHandler public function __construct( private readonly PublishServiceInterface $publishService, - private readonly CsvServiceInterface $csvService, - private readonly GridServiceInterface $gridService + private readonly ExportServiceInterface $csvService ) { parent::__construct(); } @@ -55,27 +53,26 @@ public function __invoke(CsvCreationMessage $message): void } $columns = $this->extractConfigFieldFromJobStepConfig($message, StepConfig::CONFIG_COLUMNS->value); - $settings = $this->extractConfigFieldFromJobStepConfig($message, StepConfig::CONFIG_CONFIGURATION->value); - $columnCollection = $this->gridService->getConfigurationFromArray( - $columns, - true - ); + $headers = $settings[StepConfig::SETTINGS_HEADER->value] ?? StepConfig::SETTINGS_HEADER_NO_HEADER->value; + $delimiter = $settings[StepConfig::SETTINGS_DELIMITER->value] ?? null; - if (!isset($jobRun->getContext()[StepConfig::ASSET_EXPORT_DATA->value])) { + if (!isset($jobRun->getContext()[StepConfig::CSV_EXPORT_DATA->value])) { $this->abort($this->getAbortData( Config::CSV_CREATION_FAILED_MESSAGE->value, - ['message' => 'Asset export data not found in job run context'] + ['message' => 'Csv export data not found in job run context'] )); } - $assetData = $jobRun->getContext()[StepConfig::ASSET_EXPORT_DATA->value]; + $csvData = $jobRun->getContext()[StepConfig::CSV_EXPORT_DATA->value]; try { - $this->csvService->createCsvFile( + $this->csvService->createExportFile( $jobRun->getId(), - $columnCollection, - $settings, - $assetData, + $columns, + $csvData, + $headers !== StepConfig::SETTINGS_HEADER_NO_HEADER->value, + $headers === StepConfig::SETTINGS_HEADER_NAME, + $delimiter ); } catch (Exception|FilesystemException $e) { $this->abort($this->getAbortData( @@ -99,5 +96,10 @@ protected function configureStep(): void StepConfig::CONFIG_COLUMNS->value, StepConfig::CONFIG_TYPE_ARRAY->value ); + + $this->stepConfiguration->setDefaults([ + StepConfig::CONFIG_COLUMNS->value => [], + StepConfig::CONFIG_CONFIGURATION->value => [], + ]); } } diff --git a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCreationMessage.php b/src/Export/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCreationMessage.php similarity index 86% rename from src/Asset/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCreationMessage.php rename to src/Export/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCreationMessage.php index 666887b74..3b003f696 100644 --- a/src/Asset/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCreationMessage.php +++ b/src/Export/ExecutionEngine/AutomationAction/Messenger/Messages/CsvCreationMessage.php @@ -14,7 +14,7 @@ * @license http://www.pimcore.org/license GPLv3 and PCL */ -namespace Pimcore\Bundle\StudioBackendBundle\Asset\ExecutionEngine\AutomationAction\Messenger\Messages; +namespace Pimcore\Bundle\StudioBackendBundle\Export\ExecutionEngine\AutomationAction\Messenger\Messages; use Pimcore\Bundle\GenericExecutionEngineBundle\Messenger\Messages\AbstractExecutionEngineMessage; diff --git a/src/Export/ExecutionEngine/Util/JobSteps.php b/src/Export/ExecutionEngine/Util/JobSteps.php new file mode 100644 index 000000000..5e27003f4 --- /dev/null +++ b/src/Export/ExecutionEngine/Util/JobSteps.php @@ -0,0 +1,21 @@ +executionEngineService->validateJobRun($jobRunId); + $fileName = $this->getTempFileName($jobRunId, $tempFileName); + $folderName = $this->getTempFileName($jobRunId, $tempFolderName); + $filePath = $folderName . '/' . $fileName; + + $streamedResponse = $this->getFileStreamedResponse( + $filePath, + $mimeType, + $downloadName, + $this->validateStorage($filePath, $jobRunId) + ); + + try { + $this->storageService->cleanUpFolder($folderName); + } catch (FilesystemException) { + throw new EnvironmentException( + sprintf( + 'Failed to clean up temporary folder %s', + $folderName + ) + ); + } + + return $streamedResponse; + } + + /** + * @throws EnvironmentException|NotFoundException + */ + public function cleanupDataByJobRunId( + int $jobRunId, + string $folderName, + string $fileName + ): void { + $this->executionEngineService->validateJobRun($jobRunId); + $this->validateStorage($this->getTempFilePath($jobRunId, $folderName . '/' . $fileName), $jobRunId); + + try { + $this->storageService->cleanUpFolder( + $this->getTempFileName( + $jobRunId, + $folderName + ), + true + ); + } catch (FilesystemException $e) { + throw new EnvironmentException( + sprintf( + 'Failed to delete file based on jobRunId %d: %s', + $jobRunId, + $e->getMessage() + ), + ); + } + } + + /** + * @throws EnvironmentException + */ + private function validateStorage(string $filePath, int $jobRunId): FilesystemOperator + { + $storage = $this->storageService->getTempStorage(); + if (!$this->storageService->tempFileExists($filePath)) { + throw new EnvironmentException( + sprintf( + 'Resource not found for jobRun with Id %d', + $jobRunId + ) + ); + } + + return $storage; + } +} diff --git a/src/Export/Service/DownloadServiceInterface.php b/src/Export/Service/DownloadServiceInterface.php new file mode 100644 index 000000000..4c38fe5cc --- /dev/null +++ b/src/Export/Service/DownloadServiceInterface.php @@ -0,0 +1,49 @@ +value, description: 'tag_emails_description' )] +#[Tag( + name: Tags::Export->value, + description: 'tag_export_description' +)] #[Tag( name: Tags::Mercure->value, description: 'tag_mercure_description' @@ -152,6 +156,7 @@ enum Tags: string case Elements = 'Elements'; case ExecutionEngine = 'Execution Engine'; case Emails = 'E-Mails'; + case Export = 'Export'; case Mercure = 'Mercure'; case Metadata = 'Metadata'; case Notes = 'Notes'; diff --git a/translations/studio.en.yaml b/translations/studio.en.yaml index b6e0e67c4..865d91fa9 100644 --- a/translations/studio.en.yaml +++ b/translations/studio.en.yaml @@ -9,7 +9,7 @@ studio_ee_zip_file_copy_failed: 'Zip file could not be copied: %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_csv_creation_failed: 'CSV Export failed: %message%' -studio_ee_csv_data_collection_failed: 'Data for CSV export could not be extracted for asset %id%: %message%' +studio_ee_csv_data_collection_failed: 'Data for CSV export could not be extracted for %id%: %message%' studio_ee_element_patch_failed: 'Element with type %type% with ID %id% could not be updated: %message%' studio_ee_element_rewrite_references_failed: 'Element references with type %type% with ID %id% could not be rewritten: %message%' studio_ee_element_tag_operation_failed: 'Could not %operation% tags for element type %type% and ID %id%: %message%' diff --git a/translations/studio_api_docs.en.yaml b/translations/studio_api_docs.en.yaml index a9f254fb5..74b1cad0c 100644 --- a/translations/studio_api_docs.en.yaml +++ b/translations/studio_api_docs.en.yaml @@ -46,9 +46,9 @@ asset_custom_settings_get_by_id_description: | Retrieves custom settings based on the given {id}.
The {id} must be an ID of existing asset asset_custom_settings_get_by_id_success_response: Successfully retrieved custom settings as JSON asset_custom_settings_get_by_id_summary: Get custom settings of an asset by ID -asset_delete_csv_description: | +export_delete_csv_description: | Delete the CSV file with given {jobRunId} returned in the response of the create csv endpoint -asset_delete_csv_summary: Delete asset CSV file based on jobRunId +export_delete_csv_summary: Delete asset CSV file based on jobRunId asset_delete_zip_description: | Delete the ZIP file with given {jobRunId} returned in the response of the create zip endpoint asset_delete_zip_summary: Delete asset ZIP file based on jobRunId @@ -60,10 +60,10 @@ asset_download_by_id_description: | Download the original asset stream based on the provided {id} asset_download_by_id_success_response: Original asset binary file asset_download_by_id_summary: Download asset by ID -asset_download_csv_description: | +export_download_csv_description: | Download the CSV file with given {jobRunId} returned in the response of the create csv endpoint -asset_download_csv_success_response: CSV File as attachment -asset_download_csv_summary: Download CSV file for assets +export_download_csv_success_response: CSV File as attachment +export_download_csv_summary: Download CSV file for job run id asset_download_zip_description: | Download the ZIP archive with assets based on the given {jobRunId} returned in the response of the create zip endpoint asset_download_zip_success_response: ZIP archive as attachment @@ -679,8 +679,22 @@ custom_reports_chart_success_response: Chart data as JSON. The actual data depen custom_reports_chart_sort_by_parameter: Sort by column parameter custom_reports_chart_report_limit_parameter: Limit of the report data custom_reports_chart_report_offset_parameter: Offset of the report data +custom_report_export_csv: CSV export +custom_report_export_csv_summary: Export report data as CSV +custom_report_export_csv_created_response: Successfully created jobRun for csv export +custom_report_export_csv_description: | + Creating the CSV file for custom reports.
Parameters are: Delimiter can be set to anything, but the default is a semicolon.
+ Download has to be triggered separately via the csv download route with the {jobRunId} returned in the response data_object_format_path_description: | Format the path of the data given by the {targets} option. data_object_format_path_summary: Format the path of the data data_object_format_path_success_response: Formatted path of the objects - +tag_export_description: Export