Skip to main content
Version: 4.0

Frontend

If you have a user facing frontend, you can use a Symfony Controller and javascript to start the job, display the status and trigger something like a download when the Job is finished. (Assuming your job creates a downloadable file.)

This is an example Controller for how you can control it from the frontend:

<?php

declare(strict_types=1);

namespace App\Controller;

use CoreShop\Bundle\BatchMessengerBundle\Creator\ZipAssetJobCreator;
use CoreShop\Bundle\BatchMessengerBundle\Model\BatchStatus;
use CoreShop\Bundle\BatchMessengerBundle\Model\TaskInterface;
use CoreShop\Bundle\BatchMessengerBundle\Model\TaskItemInterface;
use CoreShop\Bundle\BatchMessengerBundle\Task\ZipAssetItemTask;
use CoreShop\Component\Resource\Repository\RepositoryInterface;
use League\Flysystem\FilesystemOperator;
use Pimcore\Controller\FrontendController;
use Pimcore\Model\Asset;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;

class DownloadController extends FrontendController
{
public function __construct(
protected TranslatorInterface $translator,
protected RepositoryInterface $batchTaskRepository,
protected FilesystemOperator $batchZipStorage,
) {
}

/**
* Prepare mass download as zip archive.
*
* @Route("/{_locale}/batch-messenger", name="batch_messenger_mass_downloader", methods={"GET"})
*/
public function downloadsAction()
{
$images = new Asset\Listing();
$images->setCondition('type = "image"');

$json = array_map(static fn(Asset $asset) => [
'id' => $asset->getId(),
'quality' => 'original',
], $images->getAssets());

return $this->renderTemplate('batch_messenger/download.html.twig', [
'files' => $json
]);
}

/**
* Prepare mass download as zip archive.
*
* @Route("/api/prepare_download", name="coreshop_batch_messenger_prepare_download", methods={"GET"})
*/
public function prepareMassDownloadAction(
Request $request,
ZipAssetJobCreator $zipAssetJobCreator,
): Response {
$items = json_decode($request->get('items', ''), true);
$allowedQualities = ['low', 'medium', 'original'];

if (!$items) {
throw new NotFoundHttpException('No downloadable items given');
}

$files = [];
$assetCache = [];

foreach ($items as $item) {
if (array_key_exists($item['id'], $assetCache)) {
$asset = $assetCache[$item['id']];
} else {
$asset = Asset::getById($item['id']);

if (!$asset) {
continue;
}

$assetCache[$item['id']] = $asset;
}

if (!$asset instanceof Asset) {
continue;
}

$quality = in_array($item['quality'], $allowedQualities, true) ? $item['quality'] : 'original';

if ($asset instanceof Asset\Image && 'original' !== $quality) {
$files[] = new ZipAssetItemTask(
$asset->getId(),
sprintf('asset_%s', $quality),
sprintf('%s/%s', $quality, $asset->getFilename()),
);
} else {
$files[] = new ZipAssetItemTask(
$asset->getId(),
null,
sprintf('%s/%s', $quality, $asset->getFilename()),
);
}
}

$task = $zipAssetJobCreator->createTask(
1,
true,
...$files,
);

return $this->json([
'success' => true,
'task' => $task->getId(),
]);
}

/**
* Check status of mass download task.
*
* @Route("/api/check_download", name="coreshop_batch_messenger_check_download", methods={"GET"})
*/
public function checkMassDownloadAction(
Request $request,
): Response {
$taskId = $request->get('task');

if (!$taskId) {
throw new NotFoundHttpException('No task id given');
}

/**
* @var TaskInterface $task
*/
$task = $this->batchTaskRepository->find($taskId);

if (!$task instanceof TaskInterface) {
throw new NotFoundHttpException(sprintf('The task with id "%s" has not been found', $taskId));
}

$finishedItems = count(
array_filter($task->getTaskItems(), static function (TaskItemInterface $taskItem) {
return BatchStatus::FINISHED === $taskItem->getStatus();
}),
);

return $this->json([
'success' => true,
'status' => $task->getStatus(),
'progress' => $finishedItems / count($task->getTaskItems()),
]);
}

/**
* Download zip.
*
* @Route("/api/download", name="coreshop_batch_messenger_download", methods={"GET"})
*/
public function massDownloadAction(
Request $request,
): Response {
$taskId = $request->get('task');

if (!$taskId) {
throw new NotFoundHttpException('No task id given');
}

/**
* @var TaskInterface $task
*/
$task = $this->batchTaskRepository->find($taskId);

if (!$task instanceof TaskInterface) {
throw new NotFoundHttpException(sprintf('The task with id "%s" has not been found', $taskId));
}

$fileName = $task->getTaskDataEntry(ZipAssetItemTask::CONTEXT_ZIP_FILE_NAME);

try {
$fileStream = $this->batchZipStorage->readStream($fileName);
} catch (\Exception $e) {
throw new NotFoundHttpException('The file could not be downloaded');
}

$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
sprintf('%s.zip', uniqid('download-', true)),
);

return new StreamedResponse(
static function () use ($fileStream) {
fpassthru($fileStream);
},
Response::HTTP_OK,
[
'Content-Transfer-Encoding',
'binary',
'Content-Type' => 'application/x-zip',
'Content-Disposition' => $disposition,
'Content-Length' => fstat($fileStream)['size'],
],
);
}
}

As very simple example, we here have a twig template that shows a download button and triggers the job. You can see that the javascript comes with the bundle.

{% extends '@CoreShopFrontend/layout-full.html.twig' %}

{% block center %}
<script src="/bundles/coreshopbatchmessenger/js/downloader.js"></script>
<style type="text/css">
.btn {
display: inline-block;
font-weight: 400;
line-height: 1.55;
color: #2A2A2A;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
background-color: transparent;
border: 2px solid transparent;
padding: 0.65rem 2rem;
font-size: 1rem;
border-radius: 0;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}

.btn[data-download] {
position: relative;
}

.btn[data-download][data-in-progress] {
cursor: default;
}

.btn[data-download][data-in-progress] .spinner-border {
display: inline-block !important;
}

.btn[data-download][data-in-progress]:after {
content: '';
position: absolute;
left: -2px;
bottom: -2px;
height: 6px;
width: 100%;
transition: all 0.2s cubic-bezier(0.3, 0.09, 0, 0.99);
-webkit-transform-origin: left center;
transform-origin: left center;
-webkit-transform: scaleX(calc(var(--sz-progress, 0) * 100%));
transform: scaleX(calc(var(--sz-progress, 0) * 100%));
background-color: #56dd76;
}


.btn-primary {
color: #fff;
background-color: var(--primary) !important;
border-color: var(--primary) !important;
}
</style>
<div class="my-items-container">
<button data-prepare-url="{{ path('coreshop_batch_messenger_prepare_download') }}"
data-assets-ids='{{ files|json_encode }}'
data-check="{{ path('coreshop_batch_messenger_check_download') }}"
data-download="{{ path('coreshop_batch_messenger_download') }}"
class="btn btn-primary downloader__download">
<div role="status"></div>
{{ 'Download' | trans }}
</button>
</div>
{% endblock %}

We also use a seperate Flysystem Storage for the finished ZIP File. Per default, this uses the local filesystem under '%kernel.project_dir%/var/coreshop/zip'. When you have a distributed setup like with Kubernetes, you need to change this configuration. This cna be done with this config under config/config.yaml (or any other symfony config file):

flysystem:
storages:
coreshop_batch_messenger.zip.storage:
adapter: 'local'
visibility: public
options:
directory: '%kernel.project_dir%/var/coreshop/zip'