File src/include/lib/Garradin/CSV.php artifact 1313a7b1ea part of check-in 4f00f554dc


<?php

namespace Garradin;

class CSV
{
	static public function readAsArray(string $path)
	{
		if (!file_exists($path) || !is_readable($path))
		{
			throw new \RuntimeException('Fichier inconnu : '.$path);
		}

		$fp = self::open($path);

		if (!$fp)
		{
			return false;
		}

		$delim = self::findDelimiter($fp);
		self::skipBOM($fp);

		$line = 0;
		$out = [];
		$nb_columns = null;

		while (!feof($fp))
		{
			$row = fgetcsv($fp, 4096, $delim);
			$line++;

			if (empty($row))
			{
				continue;
			}

			if (null === $nb_columns)
			{
				$nb_columns = count($row);
			}

			if (count($row) != $nb_columns)
			{
				throw new UserException('Erreur sur la ligne ' . $line . ' : incohérence dans le nombre de colonnes avec la première ligne.');
			}

			$out[$line] = $row;
		}

		fclose($fp);

		return $out;
	}

    static public function open(string $file)
    {
        ini_set('auto_detect_line_endings', true);
        return fopen($file, 'r');
    }

    static public function findDelimiter(&$fp)
    {
        $line = '';

        while ($line === '' && !feof($fp))
        {
            $line = fgets($fp, 4096);
        }

        if (strlen($line) >= 4095) {
            throw new UserException('Fichier CSV illisible : la première ligne est trop longue.');
        }

        // Delete the columns content
        $line = preg_replace('/".*?"/', '', $line);

        $delims = [
            ';' => substr_count($line, ';'),
            ',' => substr_count($line, ','),
            "\t"=> substr_count($line, "\t")
        ];

        arsort($delims);
        reset($delims);

        rewind($fp);

        return key($delims);
    }

    static public function skipBOM(&$fp)
    {
        // Skip BOM
        if (fgets($fp, 4) !== chr(0xEF) . chr(0xBB) . chr(0xBF))
        {
            fseek($fp, 0);
        }
    }

    static public function row($row): string
    {
        $row = (array) $row;

        array_walk($row, function (&$field) {
            $field = strtr($field, ['"' => '""', "\r\n" => "\n"]);
        });

        return sprintf("\"%s\"\r\n", implode('","', $row));
    }

    static public function export(string $format, string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null): void
    {
        if ('csv' == $format) {
            self::toCSV(... array_slice(func_get_args(), 1));
        }
        else {
            self::toODS(... array_slice(func_get_args(), 1));
        }
    }

    static public function toCSV(string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null): void
    {
        header('Content-type: application/csv');
        header(sprintf('Content-Disposition: attachment; filename="%s.csv"', $name));

        $fp = fopen('php://output', 'w');

        if ($header)
        {
            fputs($fp, self::row($header));
        }

        foreach ($iterator as $row)
        {
            if (is_object($row) && $row instanceof Entity) {
                $row = $row->asArray();
            }
            elseif (is_object($row)) {
                $row = (array) $row;
            }

            if (!$header)
            {
                fputs($fp, self::row(array_keys($row)));
                $header = true;
            }

            if (null !== $row_map_callback) {
                $row = call_user_func($row_map_callback, $row);
            }

            fputs($fp, self::row($row));
        }

        fclose($fp);
    }

    static public function toODS(string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null): void
    {
        header('Content-type: application/vnd.oasis.opendocument.spreadsheet');
        header(sprintf('Content-Disposition: attachment; filename="%s.ods"', $name));

        $ods = new ODSWriter;
        $ods->table_name = $name;

        if ($header)
        {
            $ods->add((array) $header);
        }

        foreach ($iterator as $row)
        {
            if (is_object($row) && $row instanceof Entity) {
                $row = $row->asArray();
            }
            elseif (is_object($row)) {
                $row = (array) $row;
            }

            if (!$header)
            {
                $ods->add(array_keys($row));
                $header = true;
            }

            if (null !== $row_map_callback) {
                $row = call_user_func($row_map_callback, $row);
            }

            $ods->add((array) $row);
        }

        $ods->output();
    }

    static public function import(array $file, array $expected_columns): \Generator
    {
        if (empty($file['size']) || empty($file['tmp_name'])) {
            throw new UserException('Fichier invalide');
        }

        $fp = fopen($file['tmp_name'], 'r');

        if (!$fp) {
            throw new UserException('Le fichier ne peut être ouvert');
        }

        // Find the delimiter
        $delim = self::findDelimiter($fp);
        self::skipBOM($fp);

        $line = 1;

        $columns = fgetcsv($fp, 4096, $delim);
        $columns = array_map('trim', $columns);

        // Check for required columns
        foreach ($expected_columns as $column) {
            if (!in_array($column, $columns, true)) {
                throw new UserException(sprintf('La colonne "%s" est absente du fichier importé', $column));
            }
        }

        while (!feof($fp))
        {
            $row = fgetcsv($fp, 4096, $delim);
            $line++;

            // Empty line, skip
            if (empty($row)) {
                continue;
            }

            if (count($row) != count($columns))
            {
                $db->rollback();
                throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.');
            }

            $row = array_combine($columns, $row);

            yield $line => $row;
        }

        fclose($fp);
    }
}