Lire un CSV en PHP

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];
    }

A suivre …

Un commentaire

  1. good!!!

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *