Problématique : permettre aux utilisateurs d’envoyer des données au format CSV. Sachant que la plupart utilisent Microsoft Excel.
On utilisera toujours l’encodage UTF-8, mais on veut pouvoir le traiter avec ou sans BOM
Une fois les donnée lue, on voudra les traiter et les enregistrer en Base de donnée.
Le code devra être réutilisable nota ment dans un projet Symfony
Première version rapide
Le PHP possède une fonction fgetcsv qui permet de lire facilement une ligne et nous renverra un tableau de chaîne de caractère que l’on pourra traiter.
$row = 1;
if (($csv= fopen("test.csv", "r"))) {
while (($data = fgetcsv($csv, 1000, ","))) {
$num = count($data);
echo "<p> $num fields in line $row: <br /></p>\n";
$row++;
for ($c=0; $c < $num; $c++) {
echo $data[$c] . "<br />\n";
}
}
fclose($csv);
}
Gérer le BOM UTF-8
Le BOM est une suite d’octet qui peuvent être présents au début du fichier CSV. Il est notamment utilisé par Microsoft Excel pour déterminer l’encodage des CSV. Il sera donc systématiquement présent dans les CSV généré par Excel. On parlera d’encodage UTF-8 BOM. Fâcheusement PHP ne le gère pas nativement
/**
* @throws \Exception
*/
public function checkAndOpenCsv(string $path): mixed
{
if (!file_exists($path)) {
throw new \Exception('Fichier introuvable: '.$path);
}
$csv = fopen($path, 'r');
if (!$csv) {
throw new \Exception('Impossible d'ouvrir le fichier: '.$path);
}
if (!CsvImporter::checkFileUtf8encoded($csv)) {
throw new \Exception('Encodage non UTF-8');
}
// Ignorer le BOM UTF-8 s'il est présent
if ("\xEF" !== fgetc($csv) || "\xBB" !== fgetc($csv) || "\xBF" !== fgetc($csv)) {
rewind($csv); // Revenir au début du fichier si le BOM n'est pas présent
}
return $csv;
}
public static function checkFileUtf8encoded($csv): bool
{
$firstLine = fgets($csv);
rewind($csv);
return mb_check_encoding($firstLine, 'UTF-8');
}
On en profite pour vérifier l’encodage, ici uniquement sur la première ligne.
Gestion des entêtes de colonne
La première ligne du csv est parfois constitué des entêtes de colonne
On va vouloir transformer les ligne en tableau clé valeur dont la clé est l’entête. Voici une version simplifiée
$csv = checkAndOpenCsv($path)
$header = fgetcsv($csv)
$rowNumber = 1;
while ($row = fgetcsv($csv) ) {
$rowNumber ++
$data = array_combine($headers, $row);
foreach (iterable_expression as $key => $value){
echo $rowNumber . ' - ' . $key . ' - ' . $value. "<br />\n";
}
}
fclose($handle);
Séparer le code de traitement du code de lecture
Chaque csv va avoir un code de traitement différent. Ce code sera inclus dans une classe qui implémentera une interface commune.
interface CsvRowImporterInterface
{
// controle des entêtes de colonne
public function checkHeader(array $header): void;
// traitement
public function importRow(array $data);
// enclosure par defaut ""
public function getEnclosure(): string;
// séparateur par defaut ;
public function getSeparator(): string;
// csv avec ou sans entéte
public function isWithHeader(): bool;
}
/**
* @throws \Exception
*/
public function importCsv(
string $path,
CsvRowImporterInterface $rowImporter,
bool $checkOnly,
callable $onAdvance = null,
): array {
$csv = $this->checkAndOpenCsv($path);
if ($rowImporter->isWithHeader()) {
$headers = CsvImporter::getCsvLine($csv, $rowImporter->getSeparator(), $rowImporter->getEnclosure());
if (false === $headers) {
throw new \Exception('Impossible de récupérer les headers');
}
$rowImporter->checkHeader($headers);
}
$line = 0;
$error = [];
$ok = [];
while ($row = CsvImporter::getCsvLine($csv, $rowImporter->getSeparator(), $rowImporter->getEnclosure())) {
++$line;
try {
if ($rowImporter->isWithHeader()) {
$data = array_combine($headers, $row);
} else {
$data = $row;
}
$rowImporter->importRow($data, $checkOnly);
++$ok
} catch (\Exception $e) {
$error[] = 'ligne '.$line.' : '.$e->getMessage();
}
}
fclose($csv);
return [$error, $ok];
}
Ce guide propose une solution robuste et flexible pour gérer l’importation de fichiers CSV en PHP, en tenant compte des problématiques spécifiques liées à l’encodage UTF-8 et à la gestion du BOM. En tirant parti de l’interface CsvRowImporterInterface
, le code est conçu pour être modulaire et réutilisable, facilitant l’intégration dans des projets Symfony.
Cette approche permet de séparer clairement le traitement des données du processus de lecture, offrant ainsi une architecture plus maintenable et adaptable à divers besoins métiers.
Avec ces bases solides, vous êtes prêt à intégrer une gestion avancée des CSV dans vos projets tout en respectant les bonnes pratiques de développement.
good!!!