Remove unused CSVFormatter and improve translations handling

CSVFormatter was deprecated and is no longer in use, so it was removed. Additionally, translation handling was enhanced across multiple formatters, supporting `TranslatableInterface` to ensure better localization compatibility.
This commit is contained in:
Julien Fastré 2025-04-08 10:38:07 +02:00
parent b2d3d806b6
commit d9251239f7
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
6 changed files with 40 additions and 475 deletions

View File

@ -1,452 +0,0 @@
<?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\Formatter;
use Chill\MainBundle\Export\ExportManagerAwareInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
/**
* Command to get the report with curl:
* curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff.
*
* @deprecated this formatter is not used any more
*/
class CSVFormatter implements FormatterInterface, ExportManagerAwareInterface
{
use ExportManagerAwareTrait;
protected $aggregators;
protected $aggregatorsData;
protected $export;
protected $exportData;
protected $filtersData;
protected $formatterData;
protected $labels;
protected $result;
public function __construct(
protected TranslatorInterface $translator,
) {}
/**
* @uses appendAggregatorForm
*/
public function buildForm(FormBuilderInterface $builder, $exportAlias, array $aggregatorAliases): void
{
$aggregators = $this->getExportManager()->getAggregators($aggregatorAliases);
$nb = \count($aggregatorAliases);
foreach ($aggregators as $alias => $aggregator) {
$builderAggregator = $builder->create($alias, FormType::class, [
'label' => $aggregator->getTitle(),
'block_name' => '_aggregator_placement_csv_formatter',
]);
$this->appendAggregatorForm($builderAggregator, $nb);
$builder->add($builderAggregator);
}
}
public function getNormalizationVersion(): int
{
return 1;
}
public function normalizeFormData(array $formData): array
{
return [];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return [];
}
public function getFormDefaultData(array $aggregatorAliases): array
{
return [];
}
public function gatherFiltersDescriptions()
{
$descriptions = [];
foreach ($this->filtersData as $key => $filterData) {
$statement = $this->getExportManager()
->getFilter($key)
->describeAction($filterData);
if (null === $statement) {
continue;
}
if (\is_array($statement)) {
$descriptions[] = $this->translator->trans(
$statement[0],
$statement[1],
$statement[2] ?? null,
$statement[3] ?? null
);
} else {
$descriptions[] = $statement;
}
}
return $descriptions;
}
public function getName(): string|\Symfony\Contracts\Translation\TranslatableInterface
{
return 'Comma separated values (CSV)';
}
public function getResponse(
$result,
$formatterData,
$exportAlias,
array $exportData,
array $filtersData,
array $aggregatorsData,
) {
$this->result = $result;
$this->orderingHeaders($formatterData);
$this->export = $this->getExportManager()->getExport($exportAlias);
$this->aggregators = iterator_to_array($this->getExportManager()
->getAggregators(array_keys($aggregatorsData)));
$this->exportData = $exportData;
$this->aggregatorsData = $aggregatorsData;
$this->labels = $this->gatherLabels();
$this->filtersData = $filtersData;
$response = new Response();
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
// $response->headers->set('Content-Disposition','attachment; filename="export.csv"');
$response->setContent($this->generateContent());
return $response;
}
public function getType(): string
{
return 'tabular';
}
protected function gatherLabels()
{
return array_merge(
$this->gatherLabelsFromAggregators(),
$this->gatherLabelsFromExport()
);
}
protected function gatherLabelsFromAggregators()
{
$labels = [];
/* @var $aggretator \Chill\MainBundle\Export\AggregatorInterface */
foreach ($this->aggregators as $alias => $aggregator) {
$keys = $aggregator->getQueryKeys($this->aggregatorsData[$alias]);
// gather data in an array
foreach ($keys as $key) {
$values = array_map(static function ($row) use ($key, $alias) {
if (!\array_key_exists($key, $row)) {
throw new \LogicException("the key '".$key."' is declared by the aggregator with alias '".$alias."' but is not ".'present in results');
}
return $row[$key];
}, $this->result);
$labels[$key] = $aggregator->getLabels(
$key,
array_unique($values),
$this->aggregatorsData[$alias]
);
}
}
return $labels;
}
protected function gatherLabelsFromExport()
{
$labels = [];
$export = $this->export;
$keys = $this->export->getQueryKeys($this->exportData);
foreach ($keys as $key) {
$values = array_map(static function ($row) use ($key, $export) {
if (!\array_key_exists($key, $row)) {
throw new \LogicException("the key '".$key."' is declared by the export with title '".$export->getTitle()."' but is not ".'present in results');
}
return $row[$key];
}, $this->result);
$labels[$key] = $this->export->getLabels(
$key,
array_unique($values),
$this->exportData
);
}
return $labels;
}
protected function generateContent()
{
$line = null;
$rowKeysNb = \count($this->getRowHeaders());
$columnKeysNb = \count($this->getColumnHeaders());
$resultsKeysNb = \count($this->export->getQueryKeys($this->exportData));
$results = $this->getOrderedResults();
/** @var string[] $columnHeaders the column headers associations */
$columnHeaders = [];
/** @var string[] $data the data of the csv file */
$contentData = [];
$content = [];
// create a file pointer connected to the output stream
$output = fopen('php://output', 'wb');
// title
fputcsv($output, [$this->translator->trans($this->export->getTitle())]);
// blank line
fputcsv($output, ['']);
// add filtering description
foreach ($this->gatherFiltersDescriptions() as $desc) {
fputcsv($output, [$desc]);
}
// blank line
fputcsv($output, ['']);
// iterate on result to : 1. populate row headers, 2. populate column headers, 3. add result
foreach ($results as $row) {
$rowHeaders = \array_slice($row, 0, $rowKeysNb);
// first line : we create line and adding them row headers
if (!isset($line)) {
$line = \array_slice($row, 0, $rowKeysNb);
}
// do we have to create a new line ? if the rows are equals, continue on the line, else create a next line
if (\array_slice($line, 0, $rowKeysNb) !== $rowHeaders) {
$contentData[] = $line;
$line = \array_slice($row, 0, $rowKeysNb);
}
// add the column headers
/** @var string[] $columns the column for this row */
$columns = \array_slice($row, $rowKeysNb, $columnKeysNb);
$columnPosition = $this->findColumnPosition($columnHeaders, $columns);
// fill with blank at the position given by the columnPosition + nbRowHeaders
for ($i = 0; $i < $columnPosition; ++$i) {
if (!isset($line[$rowKeysNb + $i])) {
$line[$rowKeysNb + $i] = '';
}
}
$resultData = \array_slice($row, $resultsKeysNb * -1);
foreach ($resultData as $data) {
$line[] = $data;
}
}
// we add the last line
$contentData[] = $line;
// column title headers
for ($i = 0; $i < $columnKeysNb; ++$i) {
$line = array_fill(0, $rowKeysNb, '');
foreach ($columnHeaders as $set) {
$line[] = $set[$i];
}
$content[] = $line;
}
// row title headers
$headerLine = [];
foreach ($this->getRowHeaders() as $headerKey) {
$headerLine[] = \array_key_exists('_header', $this->labels[$headerKey]) ?
$this->labels[$headerKey]['_header'] : '';
}
foreach ($this->export->getQueryKeys($this->exportData) as $key) {
$headerLine[] = \array_key_exists('_header', $this->labels[$key]) ?
$this->labels[$key]['_header'] : '';
}
fputcsv($output, $headerLine);
unset($headerLine); // free memory
// generate CSV
foreach ($content as $line) {
fputcsv($output, $line);
}
foreach ($contentData as $line) {
fputcsv($output, $line);
}
$text = stream_get_contents($output);
fclose($output);
return $text;
}
protected function getColumnHeaders()
{
return $this->getPositionnalHeaders('c');
}
protected function getRowHeaders()
{
return $this->getPositionnalHeaders('r');
}
/**
* ordering aggregators, preserving key association.
*
* This function do not mind about position.
*
* If two aggregators have the same order, the second given will be placed
* after. This is not significant for the first ordering.
*/
protected function orderingHeaders(array $formatterData)
{
$this->formatterData = $formatterData;
uasort(
$this->formatterData,
static fn (array $a, array $b): int => ($a['order'] <= $b['order'] ? -1 : 1)
);
}
/**
* append a form line by aggregator on the formatter form.
*
* This form allow to choose the aggregator position (row or column) and
* the ordering
*
* @param string $nbAggregators
*/
private function appendAggregatorForm(FormBuilderInterface $builder, $nbAggregators)
{
$builder->add('order', ChoiceType::class, [
'choices' => array_combine(
range(1, $nbAggregators),
range(1, $nbAggregators)
),
'multiple' => false,
'expanded' => false,
]);
$builder->add('position', ChoiceType::class, [
'choices' => [
'row' => 'r',
'column' => 'c',
],
'multiple' => false,
'expanded' => false,
]);
}
private function findColumnPosition(&$columnHeaders, $columnToFind): int
{
$i = 0;
foreach ($columnHeaders as $set) {
if ($set === $columnToFind) {
return $i;
}
++$i;
}
// we didn't find it, adding the column
$columnHeaders[] = $columnToFind;
return $i++;
}
private function getOrderedResults()
{
$r = [];
$results = $this->result;
$labels = $this->labels;
$rowKeys = $this->getRowHeaders();
$columnKeys = $this->getColumnHeaders();
$resultsKeys = $this->export->getQueryKeys($this->exportData);
$headers = array_merge($rowKeys, $columnKeys);
foreach ($results as $row) {
$line = [];
foreach ($headers as $key) {
$line[] = \call_user_func($labels[$key], $row[$key]);
}
// append result
foreach ($resultsKeys as $key) {
$line[] = \call_user_func($labels[$key], $row[$key]);
}
$r[] = $line;
}
array_multisort($r);
return $r;
}
/**
* @param string $position may be 'c' (column) or 'r' (row)
*
* @return string[]
*
* @throws \RuntimeException
*/
private function getPositionnalHeaders($position)
{
$headers = [];
foreach ($this->formatterData as $alias => $data) {
if (!\array_key_exists($alias, $this->aggregatorsData)) {
throw new \RuntimeException('the formatter wants to use the '."aggregator with alias {$alias}, but the export do not ".'contains data about it');
}
$aggregator = $this->aggregators[$alias];
if ($data['position'] === $position) {
$headers = array_merge($headers, $aggregator->getQueryKeys($this->aggregatorsData[$alias]));
}
}
return $headers;
}
}

View File

@ -17,6 +17,7 @@ use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use function count; use function count;
@ -174,8 +175,6 @@ class CSVListFormatter implements FormatterInterface, ExportManagerAwareInterfac
* @param string $key * @param string $key
* @param string $value * @param string $value
* *
* @return string
*
* @throws \LogicException if the label is not found * @throws \LogicException if the label is not found
*/ */
protected function getLabel($key, $value) protected function getLabel($key, $value)
@ -225,9 +224,12 @@ class CSVListFormatter implements FormatterInterface, ExportManagerAwareInterfac
} }
foreach ($first_row as $key => $value) { foreach ($first_row as $key => $value) {
$header_line[] = $this->translator->trans( $content = $this->getLabel($key, '_header');
$this->getLabel($key, '_header') if ($content instanceof TranslatableInterface) {
); $header_line[] = $content->trans($this->translator, $this->translator->getLocale());
} else {
$header_line[] = $this->translator->trans($this->getLabel($key, '_header'));
}
} }
if (\count($header_line) > 0) { if (\count($header_line) > 0) {

View File

@ -176,8 +176,6 @@ class CSVPivotedListFormatter implements FormatterInterface, ExportManagerAwareI
* @param string $key * @param string $key
* @param string $value * @param string $value
* *
* @return string
*
* @throws \LogicException if the label is not found * @throws \LogicException if the label is not found
*/ */
protected function getLabel($key, $value) protected function getLabel($key, $value)

View File

@ -19,6 +19,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInterface class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInterface
@ -71,7 +72,7 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
* *
* replaced when `getResponse` is called. * replaced when `getResponse` is called.
* *
* @var type * @var array
*/ */
protected $result; protected $result;
@ -248,14 +249,15 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
foreach ($this->filtersData as $alias => $data) { foreach ($this->filtersData as $alias => $data) {
$filter = $this->getExportManager()->getFilter($alias); $filter = $this->getExportManager()->getFilter($alias);
$description = $filter->describeAction($data, 'string'); $description = $filter->describeAction($data);
if (\is_array($description)) { if (\is_array($description)) {
$description = $this->translator $description = $this->translator
->trans( ->trans(
$description[0], $description[0],
$description[1] ?? [] $description[1] ?? [],
); );
} elseif ($description instanceof TranslatableInterface) {
$description = $description->trans($this->translator, $this->translator->getLocale());
} }
$worksheet->setCellValue('A'.$line, $description); $worksheet->setCellValue('A'.$line, $description);
@ -284,9 +286,13 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
$displayables = []; $displayables = [];
foreach ($globalKeys as $key) { foreach ($globalKeys as $key) {
$displayables[] = $this->translator->trans( $displayable = $this->getDisplayableResult($key, '_header');
$this->getDisplayableResult($key, '_header')
); if ($displayable instanceof TranslatableInterface) {
$displayables[] = $displayable->trans($this->translator, $this->translator->getLocale());
} else {
$displayables[] = $this->translator->trans($this->getDisplayableResult($key, '_header'));
}
} }
// add headers on worksheet // add headers on worksheet
@ -418,8 +424,6 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
* Get the displayable result. * Get the displayable result.
* *
* @param string $key * @param string $key
*
* @return string
*/ */
protected function getDisplayableResult($key, mixed $value) protected function getDisplayableResult($key, mixed $value)
{ {
@ -432,15 +436,15 @@ class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInte
return \call_user_func($this->cacheDisplayableResult[$key], $value); return \call_user_func($this->cacheDisplayableResult[$key], $value);
} }
protected function getTitle() protected function getTitle(): string
{ {
$title = $this->translator->trans($this->export->getTitle()); $original = $this->export->getTitle();
if (30 < strlen($title)) { if ($original instanceof TranslatableInterface) {
return substr($title, 0, 30).'…'; return $original->trans($this->translator, $this->translator->getLocale());
} }
return $title; return $this->translator->trans($original);
} }
protected function initializeCache($key) protected function initializeCache($key)

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export; namespace Chill\MainBundle\Export;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class SortExportElement final readonly class SortExportElement
@ -19,12 +20,21 @@ final readonly class SortExportElement
private TranslatorInterface $translator, private TranslatorInterface $translator,
) {} ) {}
private function trans(string|TranslatableInterface $message): string
{
if ($message instanceof TranslatableInterface) {
return $message->trans($this->translator, $this->translator->getLocale());
}
return $this->translator->trans($message);
}
/** /**
* @param array<int|string, FilterInterface> $elements * @param array<int|string, FilterInterface> $elements
*/ */
public function sortFilters(array &$elements): void public function sortFilters(array &$elements): void
{ {
uasort($elements, fn (FilterInterface $a, FilterInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle())); uasort($elements, fn (FilterInterface $a, FilterInterface $b) => $this->trans($a->getTitle()) <=> $this->trans($b->getTitle()));
} }
/** /**
@ -32,6 +42,6 @@ final readonly class SortExportElement
*/ */
public function sortAggregators(array &$elements): void public function sortAggregators(array &$elements): void
{ {
uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle())); uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->trans($a->getTitle()) <=> $this->trans($b->getTitle()));
} }
} }

View File

@ -33,6 +33,9 @@ use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @template-implements ListInterface<array>
*/
class ReportList implements ExportElementValidatedInterface, ListInterface class ReportList implements ExportElementValidatedInterface, ListInterface
{ {
use \Chill\MainBundle\Export\ExportDataNormalizerTrait; use \Chill\MainBundle\Export\ExportDataNormalizerTrait;