Initialisation du package

This commit is contained in:
Dorian Contal 2025-04-23 08:49:08 +02:00
commit f4a4a8b1e0
21 changed files with 599 additions and 0 deletions

27
composer.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "sudalys/import-service",
"description": "Service Symfony pour l'importation et la validation de fichiers CSV et Excel (XLS/XLSX), avec transformation automatisée.",
"keywords": ["import","csv","excel","xls","xlsx","validation","sudalys"],
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Dorian Contal",
"email": "d.contal@sudalys.fr"
}
],
"require": {
"php": "^8.1",
"league/csv": "^9.11",
"phpoffice/phpspreadsheet": "^1.18",
"box/spout": "^3.3",
"psr/log": "^3.0"
},
"autoload": {
"psr-4": {
"App\\Service\\Import\\": "src/Service/Import/"
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Service\Import;
use App\Service\Import\CsvValidator;
use App\Service\Import\Processor\BasicCsvFileProcessor;
use App\Service\Import\Processor\MainProcessor;
use App\Service\Import\ErrorHandler\LoggingErrorHandler;
use App\Service\Import\Specification\ExactHeaderSpecification;
use App\Service\Import\Specification\RegexColumnSpecification;
use App\Service\Import\Specification\NumericSpecification;
use App\Service\Import\Specification\RequiredColumnSpecification;
use App\Service\Import\Result\ImportResult;
use Psr\Log\LoggerInterface;
class CsvImportService
{
private CsvImporter $importer;
private array $header;
private array $regexListe;
public function __construct()
{}
public function import(string $filePath, array $header, array $regexListe, array $requiredColumns, LoggerInterface $logger): array
{
$this->header = $header;
$this->regexListe = $regexListe;
$headerSpec = new ExactHeaderSpecification($this->header);
$columnSpecs = [];
foreach ($header as $col) {
// Si regex définie → priorité
if (isset($regexListe[$col])) {
$columnSpecs[$col] = new RegexColumnSpecification(
$regexListe[$col],
"Le format de la colonne '$col' est invalide."
);
}
elseif (in_array($col, $requiredColumns)) {
$columnSpecs[$col] = new RequiredColumnSpecification("La colonne '$col' est obligatoire.");
}
}
$fileProcessor = new BasicCsvFileProcessor();
$validator = new CsvValidator($headerSpec, $columnSpecs);
$errorHandler = new LoggingErrorHandler($logger);
$mainProcessor = new MainProcessor();
$this->importer = new CsvImporter($fileProcessor, $validator,$errorHandler,
$mainProcessor,
);
$result = $this->importer->import($filePath);
return [
'success' => $result->isSuccess(),
'errors' => $result->getErrors(),
'processedCount' => $result->getProcessedCount(),
'message' => $result->getMessage(),
];
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Service\Import;
use App\Service\Import\Interfaces\FileProcessorInterface;
use App\Service\Import\Interfaces\ValidatorInterface;
use App\Service\Import\Interfaces\ErrorHandlerInterface;
use App\Service\Import\Interfaces\DataProcessorInterface;
use App\Service\Import\Result\ImportResult;
use function Symfony\Component\DependencyInjection\Loader\Configurator\iterator;
class CsvImporter
{
private FileProcessorInterface $fileProcessor;
private ValidatorInterface $validator;
private ErrorHandlerInterface $errorHandler;
private DataProcessorInterface $dataProcessor;
public function __construct(
FileProcessorInterface $fileProcessor,
ValidatorInterface $validator,
ErrorHandlerInterface $errorHandler,
DataProcessorInterface $dataProcessor
) {
$this->fileProcessor = $fileProcessor;
$this->validator = $validator;
$this->errorHandler = $errorHandler;
$this->dataProcessor = $dataProcessor;
}
public function import(string $filePath): ImportResult
{
$rows = $this->fileProcessor->processfile($filePath);
$header = $this->fileProcessor->getHeader($filePath);
$validationResult = $this->validator->validate($header, $rows);
if ($validationResult->hasErrors()) {
$this->errorHandler->handle($validationResult->getErrors());
}
$this->dataProcessor->process($rows);
return new ImportResult(
$validationResult->getErrors(),
!$validationResult->hasErrors(),
$validationResult->hasErrors() ? 'Import terminé avec erreurs.' : ' Import réussi.',
count($rows)
);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Service\Import;
use App\Service\Import\Interfaces\HeaderSpecificationInterface;
use App\Service\Import\Result\ValidationResult;
use App\Service\Import\Interfaces\ValidatorInterface;
class CsvValidator implements ValidatorInterface
{
public function __construct(private HeaderSpecificationInterface $headerSpecification, private $columnSpecification){}
public function validate(array $header, iterable $records): ValidationResult
{
$errors = [];
$validData = [];
if (!$this->headerSpecification->isSatisfiedBy($header)) {
$errors[] = $this->headerSpecification->getErrorMessage();
$errors = array_merge($errors, $this->headerSpecification->getErrorsHeader($header));
}
$rowIndex = 2;
foreach ($records as $row){
$rowErrors = [];
foreach ($this->columnSpecification as $column => $specification) {
if (!isset($row[$column]) || !$specification->isSatisfiedBy($row[$column])) {
$rowErrors[] = "Ligne $rowIndex: " . $specification->getErrorMessage();
}
}
if (!empty($rowErrors)) {
$errors = array_merge($errors, $rowErrors);
} else {
$validData[] = $row;
}
$rowIndex++;
}
return new ValidationResult( $errors, $validData);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Service\Import\ErrorHandler;
use App\Service\Import\Interfaces\ErrorHandlerInterface;
use Psr\Log\LoggerInterface;
class LoggingErrorHandler implements ErrorHandlerInterface
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function handle(array $errors): void
{
foreach ($errors as $error) {
$this->logger->error($error);
}
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace App\Service\Import;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
class ExcelFileTransformer
{
public function transform(string $filePath, string $type = 'csv', int $headerPosition = 3): string
{
if (!file_exists($filePath)) {
throw new \RuntimeException("Le fichier n'existe pas.");
}
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if ($extension === 'xls') {
$filePath = $this->convertXlsToXlsx($filePath);
} elseif ($extension === 'xlsx') {
$filePath = $this->purgeXlsx($filePath);
} else {
throw new \RuntimeException("Format non pris en charge : .$extension");
}
$reader = ReaderEntityFactory::createXLSXReader();
$reader->open($filePath);
$tmpCsvPath = tempnam(sys_get_temp_dir(), 'converted_') . '.' . $type;
$handle = fopen($tmpCsvPath, 'w');
$rowIndex = 1;
foreach ($reader->getSheetIterator() as $sheet) {
if ($sheet->getName() !== '__ACTIVE_SHEET__') {
continue;
}
foreach ($sheet->getRowIterator() as $row) {
if ($rowIndex++ < $headerPosition) {
continue;
}
$cells = array_map(function($cell) {
$value = $cell->getValue();
if ($value instanceof \DateTime) {
return $value->format('Y-m-d H:i');
}
return $value;
}, $row->getCells());
fputcsv($handle, $cells);
}
break;
}
$reader->close();
fclose($handle);
return $tmpCsvPath;
}
private function convertXlsToXlsx(string $xlsFilePath): string
{
if (!file_exists($xlsFilePath)) {
throw new \RuntimeException("Le fichier XLS n'existe pas : $xlsFilePath");
}
$reader = IOFactory::createReader('Xls');
$reader->setReadDataOnly(false);
$spreadsheet = $reader->load($xlsFilePath);
$activeSheet = $spreadsheet->getSheet($spreadsheet->getActiveSheetIndex());
$originalTitle = $activeSheet->getTitle();
$activeSheet->setTitle('__ACTIVE_SHEET__');
$xlsxFilePath = tempnam(sys_get_temp_dir(), 'converted_') . '.xlsx';
$writer = new Xlsx($spreadsheet);
$writer->setPreCalculateFormulas(false);
$writer->save($xlsxFilePath);
$activeSheet->setTitle($originalTitle);
return $xlsxFilePath;
}
private function purgeXlsx(string $filePath): string
{
$reader = IOFactory::createReader('Xlsx');
$reader->setReadDataOnly(false);
$spreadsheet = $reader->load($filePath);
$activeSheet = $spreadsheet->getSheet($spreadsheet->getActiveSheetIndex());
$originalTitle = $activeSheet->getTitle();
$activeSheet->setTitle('__ACTIVE_SHEET__');
$purgedPath = tempnam(sys_get_temp_dir(), 'purged_') . '.xlsx';
$writer = new Xlsx($spreadsheet);
$writer->setPreCalculateFormulas(false);
$writer->save($purgedPath);
// Optionally restore original title in memory
$activeSheet->setTitle($originalTitle);
return $purgedPath;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Service\Import\Interfaces;
interface ColumnSpecificationInterface
{
public function isSatisfiedBy(string $value): bool;
public function getErrorMessage(): string;
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Service\Import\Interfaces;
interface DataProcessorInterface
{
public function process(array $validdata): void;
}

View File

@ -0,0 +1,6 @@
<?php
namespace App\Service\Import\Interfaces;
interface ErrorHandlerInterface
{
public function handle(array $errors): void;
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Service\Import\Interfaces;
interface FileProcessorInterface
{
public function processFile(string $Path): array;
public function getHeader(string $path): array;
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Service\Import\Interfaces;
interface HeaderSpecificationInterface
{
public function isSatisfiedBy(array $header): bool;
public function getErrorMessage(): string;
public function getErrorsHeader(array $header): array;
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Service\Import\Interfaces;
use App\Service\Import\Result\ValidationResult;
interface ValidatorInterface
{
public function validate(array $header, iterable $records ): ValidationResult;
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Service\Import\Processor;
use App\Service\Import\Interfaces\FileProcessorInterface;
use League\Csv\Reader;
class BasicCsvFileProcessor implements FileProcessorInterface
{
public function processFile(string $path): array
{
$csv = Reader::createFromPath($path, 'r');
$csv->setHeaderOffset(0);
return iterator_to_array($csv->getRecords(), false);
}
public function getHeader(string $path): array
{
$csv = Reader::createFromPath($path, 'r');
$csv->setHeaderOffset(0);
return $csv->getHeader();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Service\Import\Processor;
use App\Service\Import\Interfaces\DataProcessorInterface;
class DatabaseDataProcessor implements DataProcessorInterface
{
public function __construct()
{
}
public function process(array $validdata): void
{
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Service\Import\Processor;
use App\Service\Import\Interfaces\DataProcessorInterface;
//use App\Service\Import\Interfaces\AutreProcessorInterface; // Ajoute d'autres interfaces de traitement si nécessaire
class MainProcessor implements DataProcessorInterface
{
/**
* @var DataProcessorInterface[]
*/
private array $processors = [];
public function __construct()
{
// BasicCsvFileProcessor pas nécessaire ici car déjà géré dans le service d'importation en amont
$this->processors[] = new DatabaseDataProcessor();
// $this->processors[] = new AutreProcessor(); // Ajoute d'autres ici au besoin
}
public function process(array $row): void
{
foreach ($this->processors as $processor) {
$processor->process($row);
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Service\Import\Result;
class ImportResult
{
private array $errors;
private bool $success;
private string $message;
private int $processedCount;
public function __construct(array $errors, bool $success, string $message, int $processedCount)
{
$this->errors = $errors;
$this->success = $success;
$this->message = $message;
$this->processedCount = $processedCount;
}
public function getErrors(): array
{
return $this->errors;
}
public function isSuccess(): bool
{
return $this->success;
}
public function getMessage(): string
{
return $this->message;
}
public function getProcessedCount(): int
{
return $this->processedCount;
}
public function hasErrors(): bool
{
return !empty($this->errors);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Service\Import\Result;
class ValidationResult
{
private array $errors;
private array $validData;
public function __construct(array $errors, array $validData)
{
$this->errors = $errors;
$this->validData = $validData;
}
public function getErrors(): array
{
return $this->errors;
}
public function getValidData(): array
{
return $this->validData;
}
public function hasErrors(): bool
{
return !empty($this->errors);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Service\Import\Specification;
use App\Service\Import\Interfaces\HeaderSpecificationInterface;
class ExactHeaderSpecification implements HeaderSpecificationInterface
{
private array $expectedHeader;
private string $errorMessage = 'L\'en-tête ne correspond pas';
public function __construct(array $expectedHeader)
{
$this->expectedHeader = $expectedHeader;
}
public function isSatisfiedBy(array $header): bool
{
return $header === $this->expectedHeader;
}
public function getErrorMessage(): string
{
return $this->errorMessage;
}
public function getErrorsHeader(array $header): array
{
$errorsHeader = [];
foreach ($this->expectedHeader as $key => $value) {
if (!isset($header[$key]) || $header[$key] !== $value) {
$errorsHeader[] = "La colonne $value est manquante ou ne correspond pas à la valeur attendue [ $header[$key] ]";
}
}
return $errorsHeader;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Service\Import\Specification;
use App\Service\Import\Interfaces\ColumnSpecificationInterface;
class NumericSpecification implements ColumnSpecificationInterface
{
private string $errorMessage ;
private string $pattern;
public function __construct(string $pattern, string $errorMessage)
{
$this->pattern = $pattern;
$this->errorMessage = $errorMessage;
}
public function isSatisfiedBy(string $value): bool
{
return preg_match($this->pattern, $value) === 1;
}
public function getErrorMessage(): string
{
return $this->errorMessage;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Service\Import\Specification;
use App\Service\Import\Interfaces\ColumnSpecificationInterface;
class RegexColumnSpecification implements ColumnSpecificationInterface
{
private string $pattern;
private string $errorMessage;
public function __construct(string $pattern, string $errorMessage)
{
$this->pattern = $pattern;
$this->errorMessage = $errorMessage;
}
public function isSatisfiedBy(string $value): bool
{
return preg_match($this->pattern, $value) === 1;
}
public function getErrorMessage(): string
{
return $this->errorMessage;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Service\Import\Specification;
use App\Service\Import\Interfaces\ColumnSpecificationInterface;
class RequiredColumnSpecification implements ColumnSpecificationInterface
{
private string $errorMessage;
public function __construct(string $errorMessage = 'Ce champ est requis.')
{
$this->errorMessage = $errorMessage;
}
public function isSatisfiedBy(mixed $value): bool
{
return !empty($value) && trim($value) !== '';
}
public function getErrorMessage(): string
{
return $this->errorMessage;
}
}