Generate export using denormalization

This commit is contained in:
2025-02-23 23:16:30 +01:00
parent 1f1d38acef
commit 2c812fc5fe
7 changed files with 457 additions and 328 deletions

View File

@@ -11,42 +11,248 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityManagerInterface;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Doctrine\ORM\QueryBuilder;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Generate a single export.
*/
final readonly class ExportGenerator
{
public function __construct(
private ExportManager $exportManager,
private StoredObjectManagerInterface $storedObjectManager,
private EntityManagerInterface $entityManager,
private ExportFormHelper $exportFormHelper,
private ExportConfigNormalizer $configNormalizer,
private LoggerInterface $logger,
) {}
public function generate(ExportGeneration $exportGeneration, User $user): void
public function generate(string $exportAlias, array $configuration, ?User $byUser = null): FormattedExportGeneration
{
$this->entityManager->wrapInTransaction(function () use ($exportGeneration) {
$object = $exportGeneration->getStoredObject();
$this->entityManager->refresh($exportGeneration, LockMode::PESSIMISTIC_WRITE);
$this->entityManager->refresh($object, LockMode::PESSIMISTIC_WRITE);
$data = $this->configNormalizer->denormalizeConfig($exportAlias, $configuration);
$centers = $data['centers'];
if (StoredObject::STATUS_PENDING !== $object->getStatus()) {
return;
}
$export = $this->exportManager->getExport($exportAlias);
$context = new ExportGenerationContext($byUser);
$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,
if ($export instanceof DirectExportInterface) {
$generatedExport = $export->generate(
$this->buildCenterReachableScopes($centers),
$data['export'],
$context,
);
$this->storedObjectManager->write($exportGeneration->getStoredObject(), $generation->content, $generation->contentType);
});
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),
$data['export'],
);
if ($query instanceof \Doctrine\ORM\NativeQuery) {
// throw an error if the export require other modifier, which is
// not allowed when the export return a `NativeQuery`
if (\count($export->supportsModifiers()) > 0) {
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
}
} elseif ($query instanceof QueryBuilder) {
// handle filters
$this->handleFilters($query, $data[ExportType::FILTER_KEY], $context);
// handle aggregators
$this->handleAggregators($query, $data[ExportType::AGGREGATOR_KEY], $context);
$this->logger->notice('[export] will execute this qb in export', [
'dql' => $query->getDQL(),
]);
} else {
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
}
$result = $export->getResult($query, $data['export'], $context);
if (!is_iterable($result)) {
throw new \UnexpectedValueException(sprintf('The result of the export should be an iterable, %s given', \gettype($result)));
}
$formatter = $this->exportManager->getFormatter($data['pick_formatter']);
$filtersData = [];
$aggregatorsData = [];
if ($query instanceof QueryBuilder) {
foreach ($this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]) as $alias => $aggregator) {
$aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form'];
}
foreach ($this->retrieveUsedFilters($data[ExportType::FILTER_KEY]) as $alias => $filter) {
$filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form'];
}
}
if (method_exists($formatter, 'generate')) {
return $formatter->generate(
$result,
$data['formatter']['form'],
$exportAlias,
$data['export'],
$filtersData,
$aggregatorsData,
);
}
trigger_deprecation('chill-project/chill-bundles', '3.10', '%s should implements the "generate" method', FormatterInterface::class);
$generatedExport = $formatter->getResponse(
$result,
$data['formatter'],
$exportAlias,
$data['export']['form'],
$filtersData,
$aggregatorsData,
);
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('content-type'));
}
/**
* parse the data to retrieve the used filters and aggregators.
*
* @return list<string>
*/
private function retrieveUsedModifiers(mixed $data): array
{
if (null === $data) {
return [];
}
$usedTypes = array_merge(
$this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]),
$this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY]),
);
return array_values(array_unique($usedTypes));
}
/**
* Retrieve the filter used in this export.
*
* @return list<string> an array with types
*/
private function retrieveUsedFiltersType(mixed $data): array
{
if (null === $data) {
return [];
}
$usedTypes = [];
foreach ($this->retrieveUsedFilters($data) as $filter) {
if (!\in_array($filter->applyOn(), $usedTypes, true)) {
$usedTypes[] = $filter->applyOn();
}
}
return $usedTypes;
}
/**
* @return string[]
*/
private function retrieveUsedAggregatorsType(mixed $data): array
{
if (null === $data) {
return [];
}
$usedTypes = [];
foreach ($this->retrieveUsedAggregators($data) as $alias => $aggregator) {
if (!\in_array($aggregator->applyOn(), $usedTypes, true)) {
$usedTypes[] = $aggregator->applyOn();
}
}
return $usedTypes;
}
/**
* @return iterable<string, AggregatorInterface>
*/
private function retrieveUsedAggregators(mixed $data): iterable
{
if (null === $data) {
return [];
}
foreach ($data as $alias => $aggregatorData) {
if (true === $aggregatorData['enabled']) {
yield $alias => $this->exportManager->getAggregator($alias);
}
}
}
/**
* @return iterable<string, FilterInterface>
*/
private function retrieveUsedFilters(mixed $data): iterable
{
if (null === $data) {
return [];
}
foreach ($data as $alias => $filterData) {
if (true === $filterData['enabled']) {
yield $alias => $this->exportManager->getFilter($alias);
}
}
}
/**
* Alter the query with selected aggregators.
*/
private function handleAggregators(
QueryBuilder $qb,
array $data,
ExportGenerationContext $context,
): void {
foreach ($this->retrieveUsedAggregators($data) as $alias => $aggregator) {
$formData = $data[$alias];
$aggregator->alterQuery($qb, $formData['form'], $context);
}
}
/**
* alter the query with selected filters.
*/
private function handleFilters(
QueryBuilder $qb,
mixed $data,
ExportGenerationContext $context,
): void {
foreach ($this->retrieveUsedFilters($data) as $alias => $filter) {
$formData = $data[$alias];
$filter->alterQuery($qb, $formData['form'], $context);
}
}
/**
* build the array required for defining centers and circles in the initiate
* queries of ExportElementsInterfaces.
*
* @param list<Center>
*/
private function buildCenterReachableScopes(array $centers)
{
return array_map(static fn (Center $center) => ['center' => $center, 'circles' => []], $centers);
}
}