Skip to content

Instantly share code, notes, and snippets.

@nrctkno
Created May 24, 2025 13:51
Show Gist options
  • Save nrctkno/1ee321946a6ef412a668b82743b6c357 to your computer and use it in GitHub Desktop.
Save nrctkno/1ee321946a6ef412a668b82743b6c357 to your computer and use it in GitHub Desktop.
Example of CSV handler for PHP
<?php
declare(strict_types=1);
final class CSVFile
{
const HEADERS_NONE = 0;
const HEADERS_VERBATIM = 1;
const HEADERS_NORMALIZED = 2;
private SplFileObject $file;
private function __construct(SplFileObject $file)
{
$this->file = $file;
}
public static function fromRequestData($data): static {
if (empty($data)) {
throw new BadRequestException('File upload failed, try again.');
}
if ($data['error'] !== 0) {
$reason = match ($data['error']) {
UPLOAD_ERR_INI_SIZE => 'File too large',
UPLOAD_ERR_FORM_SIZE => 'File too large',
UPLOAD_ERR_PARTIAL => 'File upload incomplete',
UPLOAD_ERR_NO_FILE => 'No file uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'No temporary directory',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'File upload stopped by extension',
default => 'Unknown error occurred'
};
throw new BadRequestException('File upload failed: '. $reason);
}
$allowed_mime = [
'text/x-comma-separated-values',
'text/comma-separated-values',
'application/octet-stream',
'application/vnd.ms-excel',
'application/x-csv',
'text/x-csv',
'text/csv',
'application/csv',
'application/excel',
'application/vnd.msexcel',
'text/plain'
];
if (empty($data['type']) || !in_array($data['type'], $allowed_mime)) {
throw new BadRequestException('Invalid file type ('.$data['type'].'), expecting CSV file.');
}
if (empty($data['tmp_name']) || !is_uploaded_file($data['tmp_name'])) {
throw new BadRequestException('Session upload failed, please try again.');
}
if ($data['error'] !== \UPLOAD_ERR_OK) {
throw new BadRequestException(
"Error uploading session file (error code {$data['error']}), is file corrupted?"
);
}
try {
$file = new SplFileObject($data['tmp_name'], 'r');
} catch (\RuntimeException $e) {
throw new BadRequestException('Could not open file for processing: ' . $e->getMessage());
}
return new self($file);
}
public function getIterator(
int $headersStyle = self::HEADERS_NONE,
string $separator = ',',
string $enclosure = '"',
string $escape = '\\'
): Generator {
$this->file->setFlags(
SplFileObject::READ_CSV | // Parse CSV
SplFileObject::SKIP_EMPTY | // Skip empty lines
SplFileObject::READ_AHEAD | // Read ahead optimization
SplFileObject::DROP_NEW_LINE // Drop newlines
);
$this->file->setCsvControl($separator, $enclosure, $escape);
if ($headersStyle > self::HEADERS_NONE && !$this->file->eof()) {
$headers = $this->file->fgetcsv();
if ($headersStyle === self::HEADERS_NORMALIZED) {
// Normalize headers by removing whitespace and converting to lowercase
$headers = array_map(fn($header) => str_replace(' ', '_', strtolower(trim($header))), $headers);
}
}
while (!$this->file->eof()) {
$row = $this->file->fgetcsv();
// Skip invalid or empty rows
if ($row === false) {
continue;
}
// Skip rows that are empty or contain only empty values
if (empty($row) || (count($row) === 1 && empty(trim($row[0])))) {
continue;
}
// If using headers, map the row values to the headers
if ($headersStyle > self::HEADERS_NONE) {
$row = array_combine($headers, $row);
if ($row === false) {
throw new RuntimeException('Invalid CSV format: inconsistent number of columns');
}
}
yield $row;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment