Initialisation du package
This commit is contained in:
commit
f4a4a8b1e0
|
@ -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
|
||||
}
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace App\Service\Import\Interfaces;
|
||||
interface ColumnSpecificationInterface
|
||||
{
|
||||
public function isSatisfiedBy(string $value): bool;
|
||||
|
||||
public function getErrorMessage(): string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
namespace App\Service\Import\Interfaces;
|
||||
interface DataProcessorInterface
|
||||
|
||||
{
|
||||
public function process(array $validdata): void;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace App\Service\Import\Interfaces;
|
||||
interface ErrorHandlerInterface
|
||||
{
|
||||
public function handle(array $errors): void;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
namespace App\Service\Import\Interfaces;
|
||||
interface FileProcessorInterface
|
||||
{
|
||||
public function processFile(string $Path): array;
|
||||
public function getHeader(string $path): array;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue