parsecsv/php-parsecsv
ParseCsv is a lightweight PHP library for reading, parsing, and writing CSV files with a simple API. It supports custom delimiters, enclosures, encodings, header mapping, filtering, sorting, and large-file handling, making CSV import/export straightforward in PHP and Laravel projects.
Install with Composer; there is no Laravel service provider or facade to register:
composer require parsecsv/php-parsecsv
The package is framework-agnostic. The class you will use nearly every time is ParseCsv\Csv. Start by reading src/Csv.php and focusing on:
parse(), parseString(), and auto()$csv->data, $csv->titles, and $csv->rowcount$delimiter, $enclosure, $heading, $offset, $limit, $conditions, $sort_by, and $sort_reverseThe quickest useful pattern is importing an uploaded CSV into an array:
use ParseCsv\Csv;
$csv = new Csv();
$csv->auto(Storage::path('imports/users.csv'));
$headers = $csv->titles; // first row when heading = true
$rows = $csv->data; // associative rows keyed by header
For an uploaded file, avoid writing to disk when possible:
$csv = new Csv();
$csv->parseString($request->file('csv')->get());
foreach ($csv->data as $row) {
// $row['email'], $row['name'], etc.
}
Use auto() when you do not control the file source. It attempts to detect encoding and delimiter. Use parse() or parseString() when you know the format and want deterministic behavior. A good first production use case is a small admin import: read a CSV, validate each row with Laravel’s validator, transform it into a DTO/array, and dispatch inserts in a transaction or queue.
Wrap the parser in an application-level importer. The library is stateful and exposes public properties, so keep it behind a small class rather than scattering property assignments through controllers:
final class CsvUserImporter
{
public function handle(string $path): array
{
$csv = new Csv();
$csv->auto($path);
$valid = [];
$errors = [];
foreach ($csv->data as $index => $row) {
$line = $index + 2; // account for header row
$validator = Validator::make($row, [
'name' => 'required|string|max:255',
'email' => 'required|email',
]);
if ($validator->fails()) {
$errors[$line] = $validator->errors()->all();
continue;
}
$valid[] = $validator->validated();
}
return compact('valid', 'errors');
}
}
Normalize headers before trusting them. Real CSVs have headers like E-mail Address, email, or duplicate names. When heading = true, the parser keys $csv->data by the header row. If you need canonical keys, either map rows manually or parse without headers and handle the first row yourself:
$csv = new Csv();
$csv->heading = false;
$csv->parseString($contents);
$rawHeaders = array_shift($csv->data);
$headers = array_map(fn ($h) => Str::slug(trim($h), '_'), $rawHeaders);
$rows = array_map(fn ($row) => array_combine($headers, $row), $csv->data);
Use built-in filtering and sorting for simple reports. Conditions and sorting are applied during parsing, which is convenient for small files:
$csv = new Csv();
$csv->conditions = [
['field' => 'status', 'value' => 'active'],
['field' => 'created_at', 'value' => '2024-01-01', 'operator' => '>='],
];
$csv->sort_by = 'last_name';
$csv->sort_reverse = false;
$csv->parse($path);
Batch large imports with offset and limit. The parser loads the whole file into $csv->data, so memory is the limiting factor. For moderately large files, process windows:
$offset = 0;
$batchSize = 500;
do {
$csv = new Csv();
$csv->offset = $offset;
$csv->limit = $batchSize;
$csv->parse($path);
foreach ($csv->data as $row) {
// process one batch
}
$offset += $batchSize;
} while (count($csv->data) === $batchSize);
This still re-reads the file for each batch in most cases. If files are very large, use fgetcsv() or league/csv with generators instead.
Export by setting titles and data, then call save(). This is useful for generated reports:
$csv = new Csv();
$csv->titles = ['Name', 'Email', 'Created'];
$csv->data = $users->map(fn ($user) => [
'name' => $user->name,
'email' => $user->email,
'created' => $user->created_at->toDateTimeString(),
])->all();
$csv->save(storage_path('app/exports/users.csv'));
For an HTTP download in Laravel, prefer streamDownload() over the package’s output helper so Laravel controls headers:
return response()->streamDownload(function () use ($csv) {
$csv->save('php://output');
}, 'users.csv', ['Content-Type' => 'text/csv']);
Use it as a parsing step, not as your domain layer. A good workflow is: parse CSV → normalize headers → validate rows → map to DTOs → persist. Keep the Csv object local to the parsing method so its mutable state cannot leak between requests or queued jobs.
In queues, pass the file path, not the parsed data. Serializing $csv->data for large files bloats jobs. Store the file in storage/app/imports, pass an ID/path to the job, and let the job parse it.
composer.json, and isolate it behind an adapter if the import/export flow is important. For complex CSV requirements, compare against league/csv or Laravel Excel before committing.$csv->conditions, $offset, $limit, $sort_by, and related properties persist on the instance. Reusing one instance for multiple files can produce surprising results. Create a new Csv per parse.auto() is a heuristic. It guesses delimiter and encoding from the file. Semicolon-separated exports, tab-delimited files, or malformed first rows can confuse it. If you control the format, set $csv->delimiter = ';' explicitly and call parse().auto() generally copes better than raw parsing, but if the first header looks like \xEF\xBB\xBFemail, strip the BOM or normalize the first title. For Windows-1252 files, convert to UTF-8 before or during parsing using the encoding-related properties or mb_convert_encoding().$csv->titles and the keys in $csv->data are exactly what the file contains. Duplicate, empty, or whitespace-padded headers cause missing or overwritten columns. Validate headers up front and fail fast with a clear error.count($row) or map positionally when consistency matters.$csv->data is a full in-memory array. A 100 MB CSV can easily become hundreds of MB as PHP arrays. Use batching only for modest files; switch to a streaming parser for large uploads.=, +, -, or @ can execute in spreadsheet software. Sanitize before saving:$value = preg_match('/^[=+\-@]/', (string) $value) ? "'" . $value : $value;
\r\n, \n, quoted commas, and embedded newlines.save() and output buffering can conflict in Laravel. Avoid calling a method that emits headers after Laravel has started the response. Use response()->streamDownload() and write to php://output, or generate the file and return a Storage::download() response.str_getcsv() or fgetcsv() internals, suppress or patch carefully, and run your test suite on the PHP version used in production.ParseCsv\Csv or, more maintainably, wrap it in your own CsvReader/CsvWriter adapter so a future replacement does not ripple through the codebase.parseString(). It avoids filesystem fixtures and makes importer tests fast and deterministic:$csv = new Csv();
$csv->parseString("name,email\nJane,[email protected]\n");
$this->assertSame('Jane', $csv->data[0]['name']);
How can I help you explore Laravel packages today?