fix(catalog) : M7 — durcissement stockages (états JSONB séquentiels + Assert\Unique, neutralisation injection formules XLSX partagée, parité listing/export via StorageListFilters, streaming export)

- Storage.setStates() renormalise en liste séquentielle (array_values) : un states posté en objet JSON ne peut plus être persisté en JSONB objet (jsonb_array_length → 500). Doublons rejetés en 422 via Assert\Unique.
- PhpSpreadsheetExporter écrit les cellules chaîne en TYPE_STRING explicite : neutralise l'injection de formules/DDE sur toutes les valeurs saisies (corrige aussi Produit/Client/Logistique/Supplier/Provider/Carrier).
- StorageListFilters : source unique de parsing des filtres (?search, ?siteId[], ?storageTypeId, ?state), consommée par le provider ET l'export → fin des divergences (numéro « 0 » coercé à null, param tableau en 400, id non positif).
- Export en streaming (toIterable + clear par lot) au lieu de getResult() : mémoire bornée.
- Tests : doublon/objet states, normalisation trim RG-7.06, 422 relations nulles, absence de deletedAt, soft-delete liste discriminant, neutralisation formule, parité ?search=0, robustesse param tableau ; garde-fou Assert\Unique enregistré.
This commit is contained in:
2026-06-29 18:01:54 +02:00
parent caa558f582
commit 7075f0f95d
10 changed files with 392 additions and 157 deletions
@@ -5,10 +5,15 @@ declare(strict_types=1);
namespace App\Shared\Infrastructure\Export;
use App\Shared\Domain\Contract\SpreadsheetExporterInterface;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use RuntimeException;
use function is_string;
/**
* Implementation XLSX du contrat d'export via la librairie PhpSpreadsheet.
*
@@ -31,19 +36,45 @@ final class PhpSpreadsheetExporter implements SpreadsheetExporterInterface
$sheet->setTitle($this->sanitizeSheetTitle($sheetTitle));
// Ligne 1 : en-tete.
$sheet->fromArray($headers, null, 'A1');
$this->writeRow($sheet, $headers, 1);
// Lignes 2..n : donnees. Iteration manuelle pour supporter un iterable
// paresseux (generator) sans tout materialiser en memoire.
$rowNumber = 2;
foreach ($rows as $row) {
$sheet->fromArray($row, null, 'A'.$rowNumber);
$this->writeRow($sheet, $row, $rowNumber);
++$rowNumber;
}
return $this->toBinary($spreadsheet);
}
/**
* Ecrit une ligne cellule par cellule. Toute valeur CHAINE est ecrite en type
* STRING explicite (jamais interpretee comme formule), ce qui neutralise
* l'injection de formules / DDE (« CSV / Formula injection ») : une cellule dont
* la valeur commence par `=` `+` `-` `@` (saisie utilisateur, ex. un numero) n'est
* pas evaluee a l'ouverture du fichier, et ce SANS apostrophe visible. Les valeurs
* non-chaines (int / float / null) gardent leur type naturel.
*
* @param list<null|scalar> $row
*/
private function writeRow(Worksheet $sheet, array $row, int $rowNumber): void
{
$column = 1;
foreach ($row as $value) {
$coordinate = Coordinate::stringFromColumnIndex($column).$rowNumber;
if (is_string($value)) {
$sheet->setCellValueExplicit($coordinate, $value, DataType::TYPE_STRING);
} else {
$sheet->setCellValue($coordinate, $value);
}
++$column;
}
}
private function toBinary(Spreadsheet $spreadsheet): string
{
$writer = new Xlsx($spreadsheet);