feat(shared) : add reusable XLSX spreadsheet exporter
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat d'export d'une feuille de calcul tabulaire vers un binaire XLSX.
|
||||
*
|
||||
* Service GENERIQUE et reutilisable : il ne connait aucune entite metier. Le
|
||||
* module appelant decide QUOI exporter (en-tetes + lignes deja mappees) ; cette
|
||||
* interface decrit seulement COMMENT produire le fichier. Aucun module n'est
|
||||
* couple a une implementation concrete : on depend de ce contrat (dans Shared),
|
||||
* jamais l'inverse (regle ABSOLUE n°1).
|
||||
*
|
||||
* Implementee par App\Shared\Infrastructure\Export\PhpSpreadsheetExporter (on
|
||||
* ne la reference pas via @see pour ne pas creer un import Domain -> Infra).
|
||||
*/
|
||||
interface SpreadsheetExporterInterface
|
||||
{
|
||||
/**
|
||||
* Genere un classeur XLSX a une feuille et retourne son contenu binaire.
|
||||
*
|
||||
* @param string $sheetTitle titre de l'onglet (assaini / tronque par l'implementation si besoin)
|
||||
* @param list<string> $headers libelles de la ligne d'en-tete (ligne 1)
|
||||
* @param iterable<list<null|scalar>> $rows lignes de donnees ; chaque ligne est une liste de cellules alignee sur $headers
|
||||
*
|
||||
* @return string contenu binaire du fichier XLSX
|
||||
*/
|
||||
public function export(string $sheetTitle, array $headers, iterable $rows): string;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Infrastructure\Export;
|
||||
|
||||
use App\Shared\Domain\Contract\SpreadsheetExporterInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Implementation XLSX du contrat d'export via la librairie PhpSpreadsheet.
|
||||
*
|
||||
* Strictement technique : ecrit la ligne d'en-tete puis les lignes de donnees
|
||||
* dans l'unique feuille du classeur, et retourne le binaire. Aucune logique
|
||||
* metier, aucune reference a une entite d'un module — le mapping des colonnes
|
||||
* est de la responsabilite de l'appelant.
|
||||
*/
|
||||
final class PhpSpreadsheetExporter implements SpreadsheetExporterInterface
|
||||
{
|
||||
// Excel limite le titre d'un onglet a 31 caracteres et interdit certains
|
||||
// caracteres ; on assainit pour ne jamais faire echouer setTitle().
|
||||
private const int MAX_SHEET_TITLE_LENGTH = 31;
|
||||
private const string INVALID_TITLE_CHARS = '*:/\?[]';
|
||||
|
||||
public function export(string $sheetTitle, array $headers, iterable $rows): string
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle($this->sanitizeSheetTitle($sheetTitle));
|
||||
|
||||
// Ligne 1 : en-tete.
|
||||
$sheet->fromArray($headers, null, 'A1');
|
||||
|
||||
// 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);
|
||||
++$rowNumber;
|
||||
}
|
||||
|
||||
return $this->toBinary($spreadsheet);
|
||||
}
|
||||
|
||||
private function toBinary(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
|
||||
// Le writer ecrit vers un chemin de fichier : on passe par un fichier
|
||||
// temporaire puis on lit son contenu binaire.
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'xlsx_export_');
|
||||
if (false === $tmpFile) {
|
||||
throw new RuntimeException('Impossible de creer un fichier temporaire pour l\'export XLSX.');
|
||||
}
|
||||
|
||||
try {
|
||||
$writer->save($tmpFile);
|
||||
$binary = file_get_contents($tmpFile);
|
||||
if (false === $binary) {
|
||||
throw new RuntimeException('Lecture du fichier XLSX temporaire impossible.');
|
||||
}
|
||||
|
||||
return $binary;
|
||||
} finally {
|
||||
// Libere les references internes de PhpSpreadsheet puis supprime le
|
||||
// fichier temporaire, meme en cas d'exception.
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
@unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire les caracteres interdits et tronque a 31 caracteres ; renvoie un
|
||||
* titre par defaut si la chaine resultante est vide.
|
||||
*/
|
||||
private function sanitizeSheetTitle(string $title): string
|
||||
{
|
||||
$clean = str_replace(str_split(self::INVALID_TITLE_CHARS), '', $title);
|
||||
$clean = mb_substr($clean, 0, self::MAX_SHEET_TITLE_LENGTH);
|
||||
|
||||
return '' === $clean ? 'Export' : $clean;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user