First step to async generation [WIP]

This commit is contained in:
Julien Fastré 2025-02-20 14:33:50 +01:00
parent 732b7dc8f7
commit 057c34610d
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
20 changed files with 489 additions and 41 deletions

View File

@ -5,6 +5,10 @@ framework:
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
failure_transport: failed
buses:
messenger.bus.default:
middleware:
- 'Chill\MainBundle\Messenger\Middleware\AuthenticationMiddleware'
transports:
# those transports are added by chill-bundles recipes
@ -19,7 +23,9 @@ framework:
async: ~
auto_setup: true
priority: '%env(MESSENGER_TRANSPORT_DSN)%/priority'
priority:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%/priority'
# end of transports added by chill-bundles recipes
# https://symfony.com/doc/current/messenger.html#transport-configuration
failed: 'doctrine://default?queue_name=failed'
@ -61,6 +67,7 @@ framework:
'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority
'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
# end of routes added by chill-bundles recipes
# Route your messages to the transports
# 'App\Message\YourMessage': async

View File

@ -0,0 +1,84 @@
@startuml
'https://plantuml.com/sequence-diagram
autonumber
User -> ExportController: configure export using form
activate ExportController
ExportController -> ExportForm: build form
activate ExportForm
loop for every ExportElement (Filter, Aggregator)
ExportForm -> ExportElement: `buildForm`
activate ExportElement
ExportElement -> ExportForm: add form to builders
deactivate ExportElement
end
ExportForm -> ExportController
deactivate ExportForm
ExportController -> User: show form
deactivate ExportController
note left of User: Configure the export:\ncheck filters, aggregators, …
User -> ExportController: post configuration of the export
activate ExportController
ExportController -> ExportForm: `getData`
activate ExportForm
ExportForm -> ExportController: return data: list of entities, etc.
deactivate ExportForm
loop for every ExportElement (Filter, Aggregator)
ExportController -> ExportElement: serializeData (data)
activate ExportElement
ExportElement -> ExportController: return serializedData (simple array with string, int, …)
deactivate ExportElement
end
ExportController -> Database: `INSERT INTO RequestGeneration_table` (insert new entity)
ExportController -> MessageQueue: warn about a new request
activate MessageQueue
ExportController -> User: "ok, generation is in process"
deactivate ExportController
note left of User: The user see a waiting screen
MessageQueue -> MessengerConsumer: forward the message to the MessengerConsumer
deactivate MessageQueue
activate MessengerConsumer
MessengerConsumer -> Database: `SELECT * FROM RequestGeneration_table WHERE id = %s`
activate Database
Database -> MessengerConsumer: return RequestGeneration with serializedData
deactivate Database
loop for every ExportElement (Filter, Aggregator)
MessengerConsumer -> ExportElement: deserializeData
activate ExportElement
ExportElement -> MessengerConsumer: return data (list of entities, etc.) from the serialized array
deactivate ExportElement
MessengerConsumer -> ExportElement: alter the sql query (`ExportElement::alterQuery`)
activate ExportElement
ExportElement -> MessengerConsumer: return the query with WHERE and GROUP BY clauses
deactivate ExportElement
end
MessengerConsumer -> MessengerConsumer: prepare the export
MessengerConsumer -> MessengerConsumer: save the export as a stored object
MessengerConsumer -> Database: `UPDATE RequestGeneration_table SET ready = true`
deactivate MessengerConsumer
User -> ExportController: pull every 5s to know if the export is generated
activate ExportController
ExportController -> User: warn the export is generated
deactivate ExportController
User -> ExportController: download the export from object storage
@enduml

View File

@ -11,6 +11,7 @@
"@hotwired/stimulus": "^3.0.0",
"@luminateone/eslint-baseline": "^1.0.9",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5",

View File

@ -11,22 +11,26 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\DirectExportInterface;
use Chill\MainBundle\Export\ExportFormHelper;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
use Chill\MainBundle\Form\SavedExportType;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Form\Type\Export\FormatterType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
use Chill\MainBundle\Messenger\Stamp\AuthenticationStamp;
use Chill\MainBundle\Redis\ChillRedis;
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
@ -37,6 +41,8 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -61,6 +67,8 @@ class ExportController extends AbstractController
private readonly SavedExportRepositoryInterface $savedExportRepository,
private readonly Security $security,
ParameterBagInterface $parameterBag,
private readonly MessageBusInterface $messageBus,
private readonly ClockInterface $clock,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
@ -128,22 +136,21 @@ class ExportController extends AbstractController
* @throws \RedisException
*/
#[Route(path: '/{_locale}/exports/generate-from-saved/{id}', name: 'chill_main_export_generate_from_saved')]
public function generateFromSavedExport(SavedExport $savedExport): RedirectResponse
public function generateFromSavedExport(SavedExport $savedExport): Response
{
$this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport);
$key = md5(uniqid((string) random_int(0, mt_getrandmax()), false));
$exportGeneration = ExportGeneration::fromSavedExport($savedExport, $this->clock->now()->add(new \DateInterval('P3M')));
$this->entityManager->persist($exportGeneration);
$this->entityManager->flush();
$this->redis->setEx($key, 3600, \serialize($savedExport->getOptions()));
$this->messageBus->dispatch(
new Envelope(
new ExportRequestGenerationMessage($exportGeneration),
[new AuthenticationStamp($this->security->getUser())]
));
return $this->redirectToRoute(
'chill_main_export_download',
[
'alias' => $savedExport->getExportAlias(),
'key' => $key, 'prevent_save' => true,
'returnPath' => $this->generateUrl('chill_main_export_saved_list_my'),
]
);
return new Response('Ok: '.$exportGeneration->getId()->toString());
}
/**

View File

@ -28,8 +28,10 @@ interface DirectExportInterface extends ExportElementInterface
/**
* Generate the export.
*
* @return FormattedExportGeneration
*/
public function generate(array $acl, array $data = []): Response;
public function generate(array $acl, array $data = []): Response|FormattedExportGeneration;
/**
* get a description, which will be used in UI (and translated).

View File

@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Form\Type\Export\FilterType;
use Chill\MainBundle\Form\Type\Export\FormatterType;
@ -91,7 +91,7 @@ final readonly class ExportFormHelper
}
public function savedExportDataToFormData(
SavedExport $savedExport,
ExportGeneration $savedExport,
string $step,
array $formOptions = [],
): array {
@ -104,7 +104,7 @@ final readonly class ExportFormHelper
}
private function savedExportDataToFormDataStepCenter(
SavedExport $savedExport,
ExportGeneration $savedExport,
): array {
$builder = $this->formFactory
->createBuilder(
@ -125,7 +125,7 @@ final readonly class ExportFormHelper
}
private function savedExportDataToFormDataStepExport(
SavedExport $savedExport,
ExportGeneration $savedExport,
array $formOptions,
): array {
$builder = $this->formFactory
@ -147,7 +147,7 @@ final readonly class ExportFormHelper
}
private function savedExportDataToFormDataStepFormatter(
SavedExport $savedExport,
ExportGeneration $savedExport,
array $formOptions,
): array {
$builder = $this->formFactory

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Export;
use Chill\MainBundle\Entity\User;
class ExportGenerationContext
{
public function __construct(
public readonly User $byUser,
) {}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Export;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\User;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityManagerInterface;
final readonly class ExportGenerator
{
public function __construct(
private ExportManager $exportManager,
private StoredObjectManagerInterface $storedObjectManager,
private EntityManagerInterface $entityManager,
private ExportFormHelper $exportFormHelper,
) {}
public function generate(ExportGeneration $exportGeneration, User $user): void
{
$this->entityManager->wrapInTransaction(function () use ($exportGeneration) {
$object = $exportGeneration->getStoredObject();
$this->entityManager->refresh($exportGeneration, LockMode::PESSIMISTIC_WRITE);
$this->entityManager->refresh($object, LockMode::PESSIMISTIC_WRITE);
if (StoredObject::STATUS_PENDING !== $object->getStatus()) {
return;
}
$generation = $this->exportManager->generateExport(
$exportGeneration->getExportAlias(),
$centers = $this->exportFormHelper->savedExportDataToFormData($exportGeneration, 'centers'),
$this->exportFormHelper->savedExportDataToFormData($exportGeneration, 'export', ['picked_centers' => $centers]),
$this->exportFormHelper->savedExportDataToFormData($exportGeneration, 'formatter', ['picked_centers' => $centers]),
$user,
);
$this->storedObjectManager->write($exportGeneration->getStoredObject(), $generation->content, $generation->contentType);
});
}
}

View File

@ -165,25 +165,31 @@ class ExportManager
$this->formatters[$alias] = $formatter;
}
/**
* Generate a response which contains the requested data.
*/
public function generate(string $exportAlias, array $pickedCentersData, array $data, array $formatterData): Response
public function generateExport(string $exportAlias, array $pickedCentersData, array $data, array $formatterData, User $byUser): FormattedExportGeneration
{
$export = $this->getExport($exportAlias);
$centers = $this->getPickedCenters($pickedCentersData);
$context = new ExportGenerationContext($byUser);
if ($export instanceof DirectExportInterface) {
return $export->generate(
$generatedExport = $export->generate(
$this->buildCenterReachableScopes($centers, $export),
$data[ExportType::EXPORT_KEY]
$data[ExportType::EXPORT_KEY],
);
if ($generatedExport instanceof Response) {
trigger_deprecation('chill-project/chill-bundles', '3.10', 'DirectExportInterface should not return a %s instance, but a %s instance', Response::class, FormattedExportGeneration::class);
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('Content-Type'));
}
return $generatedExport;
}
$query = $export->initiateQuery(
$this->retrieveUsedModifiers($data),
$this->buildCenterReachableScopes($centers, $export),
$data[ExportType::EXPORT_KEY]
$export->denormalizeFormData($data[ExportType::EXPORT_KEY], $context),
);
if ($query instanceof \Doctrine\ORM\NativeQuery) {
@ -194,10 +200,10 @@ class ExportManager
}
} elseif ($query instanceof QueryBuilder) {
// handle filters
$this->handleFilters($export, $query, $data[ExportType::FILTER_KEY], $centers);
$this->handleFilters($export, $query, $data[ExportType::FILTER_KEY], $centers, $context);
// handle aggregators
$this->handleAggregators($export, $query, $data[ExportType::AGGREGATOR_KEY], $centers);
$this->handleAggregators($export, $query, $data[ExportType::AGGREGATOR_KEY], $centers, $context);
$this->logger->notice('[export] will execute this qb in export', [
'dql' => $query->getDQL(),
@ -206,7 +212,7 @@ class ExportManager
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
}
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);
$result = $export->getResult($query, $export->denormalizeFormData($data[ExportType::EXPORT_KEY], $context));
if (!is_iterable($result)) {
throw new \UnexpectedValueException(sprintf('The result of the export should be an iterable, %s given', \gettype($result)));
@ -231,14 +237,44 @@ class ExportManager
$filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form'];
}
return $formatter->getResponse(
if (method_exists($formatter, 'generate')) {
return $formatter->generate(
$result,
$formatterData,
$exportAlias,
$data[ExportType::EXPORT_KEY],
$filtersData,
$aggregatorsData,
);
}
trigger_deprecation('chill-project/chill-bundles', '3.10', '%s should implements the "generate" method', FormatterInterface::class);
$generatedExport = $formatter->getResponse(
$result,
$formatterData,
$exportAlias,
$data[ExportType::EXPORT_KEY],
$filtersData,
$aggregatorsData
$aggregatorsData,
);
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('content-type'));
}
/**
* Generate a response which contains the requested data.
*/
public function generate(string $exportAlias, array $pickedCentersData, array $data, array $formatterData): Response
{
$generated = $this->generateExport(
$exportAlias,
$pickedCentersData,
$data,
$formatterData,
);
return new Response($generated->content, headers: ['Content-Type' => $generated->contentType]);
}
/**
@ -453,6 +489,7 @@ class ExportManager
DirectExportInterface|ExportInterface|null $export = null,
?array $centers = null,
): bool {
dump(__METHOD__, $this->tokenStorage->getToken()->getUser());
if ($element instanceof ExportInterface || $element instanceof DirectExportInterface) {
$role = $element->requiredRole();
} else {
@ -473,7 +510,7 @@ class ExportManager
$role
);
}
dump($centers);
foreach ($centers as $center) {
if (false === $this->authorizationChecker->isGranted($role, $center)) {
// debugging
@ -534,16 +571,13 @@ class ExportManager
QueryBuilder $qb,
array $data,
array $center,
ExportGenerationContext $context,
) {
$aggregators = $this->retrieveUsedAggregators($data);
foreach ($aggregators as $alias => $aggregator) {
if (false === $this->isGrantedForElement($aggregator, $export, $center)) {
throw new UnauthorizedHttpException('You are not authorized to use the aggregator'.$aggregator->getTitle());
}
$formData = $data[$alias];
$aggregator->alterQuery($qb, $formData['form']);
$aggregator->alterQuery($qb, $aggregator->denormalizeFormData($formData['form'], $context));
}
}
@ -561,20 +595,17 @@ class ExportManager
QueryBuilder $qb,
mixed $data,
array $centers,
ExportGenerationContext $context,
) {
$filters = $this->retrieveUsedFilters($data);
foreach ($filters as $alias => $filter) {
if (false === $this->isGrantedForElement($filter, $export, $centers)) {
throw new UnauthorizedHttpException('You are not authorized to use the filter '.$filter->getTitle());
}
$formData = $data[$alias];
$this->logger->debug('alter query by filter '.$alias, [
'class' => self::class, 'function' => __FUNCTION__,
]);
$filter->alterQuery($qb, $formData['form']);
$filter->alterQuery($qb, $filter->denormalizeFormData($formData['form'], $context));
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Export;
final readonly class FormattedExportGeneration
{
public function __construct(
public string $content,
public string $contentType,
) {}
}

View File

@ -13,6 +13,9 @@ namespace Chill\MainBundle\Export;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @method generate($result, $formatterData, string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData): FormattedExportGeneration
*/
interface FormatterInterface
{
public const TYPE_LIST = 'list';

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Export\Messenger;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\User;
use Ramsey\Uuid\UuidInterface;
final readonly class ExportRequestGenerationMessage
{
public UuidInterface $id;
public int $userId;
public function __construct(
ExportGeneration $exportGeneration,
User $user,
) {
$this->id = $exportGeneration->getId();
$this->userId = $user->getId();
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Export\Messenger;
use Chill\MainBundle\Export\ExportGenerator;
use Chill\MainBundle\Repository\ExportGenerationRepository;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
#[AsMessageHandler]
final readonly class ExportRequestGenerationMessageHandler implements MessageHandlerInterface
{
public function __construct(
private ExportGenerationRepository $repository,
private UserRepositoryInterface $userRepository,
private ExportGenerator $exportGenerator,
) {}
public function __invoke(ExportRequestGenerationMessage $exportRequestGenerationMessage)
{
if (null === $exportGeneration = $this->repository->find($exportRequestGenerationMessage->id)) {
throw new \UnexpectedValueException('ExportRequestGenerationMessage not found');
}
if (null === $user = $this->userRepository->find($exportRequestGenerationMessage->userId)) {
throw new \UnexpectedValueException('User not found');
}
$this->exportGenerator->generate($exportGeneration, $user);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Chill\MainBundle\Messenger\Authentication;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\User\UserInterface;
class AuthenticatedMessengerToken extends AbstractToken
{
public function __construct(UserInterface $user, array $roles = [])
{
parent::__construct($roles);
$this->setUser($user);
$this->setAuthenticated(true);
}
public function getCredentials(): null
{
return null;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Messenger\Middleware;
use Chill\MainBundle\Messenger\Authentication\AuthenticatedMessengerToken;
use Chill\MainBundle\Messenger\Stamp\AuthenticationStamp;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
final readonly class AuthenticationMiddleware implements MiddlewareInterface
{
public function __construct(
private TokenStorageInterface $tokenStorage,
private UserProviderInterface $userProvider,
) {}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
dump(__METHOD__);
if (null !== $authenticationStamp = $envelope->last(AuthenticationStamp::class)) {
return;
/** @var AuthenticationStamp $authenticationStamp */
dump("authenticate user", $authenticationStamp->getUserId());
if (null !== $this->tokenStorage->getToken()) {
dump("token already present");
} else {
$user = $this->userProvider->loadUserByUsername($authenticationStamp->getUserId());
$this->tokenStorage->setToken(new AuthenticatedMessengerToken($user, [...$user->getRoles(), 'IS_AUTHENTICATED_FULLY']));
}
}
return $stack->next()->handle($envelope, $stack);
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Messenger\Stamp;
use Symfony\Component\Messenger\Stamp\StampInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Stamp which will add a user as authenticated during the message handling.
*/
final readonly class AuthenticationStamp implements StampInterface
{
private string $userId;
public function __construct(UserInterface $user)
{
$this->userId = $user->getUserIdentifier();
}
public function getUserId(): string
{
return $this->userId;
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\ExportGeneration;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ExportGeneration>
*/
class ExportGenerationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ExportGeneration::class);
}
}

View File

@ -101,6 +101,11 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Messenger\:
resource: '../Messenger/'
autowire: true
autoconfigure: true
Chill\MainBundle\Cron\:
resource: '../Cron'
autowire: true

View File

@ -6,8 +6,12 @@ services:
Chill\MainBundle\Export\Helper\:
resource: '../../Export/Helper'
Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessageHandler: ~
Chill\MainBundle\Export\ExportFormHelper: ~
Chill\MainBundle\Export\ExportGenerator: ~
chill.main.export_element_validator:
class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator
tags:

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Export;
use Chill\MainBundle\Export\AccompanyingCourseExportHelper;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
@ -49,6 +50,16 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface
return [];
}
public function normalizeFormData(array $formData): array
{
return $formData;
}
public function denormalizeFormData(array $formData, ExportGenerationContext $context): array
{
return $formData;
}
public function getAllowedFormattersTypes(): array
{
return [FormatterInterface::TYPE_TABULAR];