CSV files look like simple comma-separated text, but a quoted name can contain commas and a quoted note can span several lines. Splitting each line with explode() loses that structure. PHP's fgetcsv() and fputcsv() handle the CSV grammar so you can focus on column names, validation, and business rules. This lesson imports a small stock list, checks its shape, and explains safe export design. You will distinguish a field's text from its meaning, preserve identifiers with leading zeros, and avoid loading an entire large import into memory before discovering the first error.
Understand CSV before reading it
A CSV record contains fields separated by a delimiter, often a comma. Quotes enclose fields containing delimiters or line breaks, and doubled quotes represent a quote inside a field. A logical record can therefore span multiple physical lines. fgetcsv() reads one parsed record from an open stream; str_getcsv() parses CSV already held in a string. The header is a convention rather than a universal requirement, so decide whether your importer requires one. Record the delimiter and expected character encoding for each supplier. A semicolon-separated European export will not parse correctly under a comma policy. PHP does not automatically convert every encoding to UTF-8. Convert supported encodings deliberately and reject unsupported files instead of silently corrupting customer names.
Set CSV controls explicitly
For contemporary PHP, call fgetcsv($handle, null, ',', '"', '') to choose comma, double-quote enclosure, and an empty proprietary escape character. Depending on the default escape argument is deprecated from PHP 8.4, so explicitly selecting it avoids a version-sensitive warning. The empty escape setting uses the common doubled-quote behavior without PHP's backslash escape convention. Use compatible settings for fputcsv() when you export data. Check fopen() and close the handle in finally. A blank line may produce a single null field rather than a normal complete record. Decide whether your import skips such records or reports them as invalid. Do not assume that end of file, an empty record, and a row full of empty strings are identical cases.
Validate the header and row shape
Read the header separately, then compare it with the required column names and permitted ordering. A supplier can misspell quantity, duplicate a column, or add an unsupported column. If you support arbitrary order, build a checked column-index map. Otherwise, requiring an exact header makes the contract clear. Strip a UTF-8 byte-order mark from the first header cell if your policy permits it. Before array_combine(), check that the record has the same number of fields as the header. Report a record number and field name for errors, without dumping sensitive rows into a public response. fgetcsv() returns strings or null for fields, not application-ready numbers. Parsing CSV grammar successfully does not establish that the row is valid.
Convert only values your rules permit
Treat product codes and postal codes as strings even when they contain only digits; integer conversion would discard leading zeros. For stock, validate an integer range and compare the failure result strictly with false so zero is accepted. For prices, agree on a decimal representation and currency rather than relying on local spreadsheet formatting. A value like 1,200 may be text with a thousands separator, a decimal value under another locale, or an incorrectly quoted field. Define the accepted format in the import instructions. Validate dates using a known format with round-trip checks. Normalize whitespace only for fields where it is irrelevant. Keep the original record available for a private review report while writing only validated, normalized values to the database.
Design an import that can recover
For a small import, validate every row before beginning database changes, then insert accepted rows in one transaction. For a large import, stream into a staging table and validate in batches so memory use stays bounded. Specify whether one bad row cancels the whole import or only that row. Neither policy is automatically correct; invoices usually need stronger all-or-nothing rules than a supplier's optional product descriptions. A stable supplier key helps make reruns idempotent rather than creating duplicates. Store a job identifier and counts for accepted, rejected, and skipped records. Do not report success after silently ignoring a database exception. Separate upload validation from CSV validation; a text-looking file can still contain malformed records or excessive data.
Export for the destination that will open it
Use fputcsv() rather than joining fields by commas. If users will open the export in a spreadsheet, consider formula injection: cells beginning with characters such as = can be interpreted as formulas. Quoting a CSV field does not necessarily stop spreadsheet formula evaluation. Apply an export policy appropriate to the supported spreadsheet tools, often prefixing risky text with an apostrophe, and explain that this changes the exported representation. Preserve machine-to-machine data separately if exact text matters. For browser downloads send a controlled filename, text/csv content type, and attachment disposition before writing bytes. Stream results instead of building a giant array. Output encoding, delimiter, line endings, and formula policy are parts of the export contract.
Practice task and worked solution
Import a customer file with email, name, and joined_on columns. Write a header check, reject wrong field counts, validate email syntax, and parse the date under one documented format. The solution collects record-specific errors and inserts only after the chosen acceptance policy is satisfied. Add a rerun rule using a unique customer key. Test a quoted comma in a name, a doubled quote, a blank record, a duplicate header, and a malformed date. Then export rejected records with a spreadsheet-safe text policy so the error report itself does not execute submitted formulas. Keep database credentials and confidential rows out of browser error messages.
Continue learning
Review file handling, arrays, and form validation for streams and value checks.
The fgetcsv manual documents explicit escape settings and the PHP 8.4 deprecation.
CSV Processing Examples
Each example shows the code, the output it produces and what each part does.
Validate a small stock CSV
This CLI example constructs a temporary stream, so no external file is required. The header and every record are checked before use.
Expected output is 0012: 0 followed by 0040: 7. The code retains leading zeros because the product code remains a string, while quantity becomes a validated integer.
<?php
$handle = fopen('php://temp', 'r+');
fwrite($handle, "code,quantity\n0012,0\n0040,7\n");
rewind($handle);
try {
$header = fgetcsv($handle, null, ',', '"', '');
if ($header !== ['code', 'quantity']) throw new RuntimeException('Bad header');
while (($row = fgetcsv($handle, null, ',', '"', '')) !== false) {
if (count($row) !== 2) throw new RuntimeException('Bad record');
$quantity = filter_var($row[1], FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0],
]);
if ($quantity === false) throw new RuntimeException('Bad quantity');
echo $row[0] . ': ' . $quantity . PHP_EOL;
}
} finally {
fclose($handle);
}0012: 0
0040: 7CSV Processing FAQ
Why not explode(',')?
It ignores quoted delimiters and multiline fields. A value such as "Smith, Jane" contains a comma inside quotes and must stay one field, and quoted fields can even contain line breaks. fgetcsv() and str_getcsv() follow the CSV quoting rules, so use them instead.
Does fgetcsv convert numbers?
No. Every field comes back as a string, so "12.50" is text and "" might mean a missing value. Apply field-specific validation afterwards, for example filter_var($price, FILTER_VALIDATE_FLOAT), and decide how empty cells should be treated.
Must every CSV use commas?
No. Many spreadsheet programs in countries that use a decimal comma export with semicolons, and tab-separated files are common too. Agree on a delimiter with whoever produces the file and pass it to fgetcsv(), rather than guessing from the content.
Does quoting stop spreadsheet formulas?
Not reliably. A cell starting with =, +, - or @ may be run as a formula when the CSV is opened in a spreadsheet, even when it's quoted. Use a deliberate export policy, such as prefixing such values with an apostrophe, when exporting user-supplied text for spreadsheet users.
CSV Processing Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A quoted name contains a comma. Which reader preserves it?
CSV Processing Quiz
2 questions to check what you learned.
Why retain product code 0012 as text?
- CSV cannot contain numbers
- Its leading zeros are part of the identifier
- PHP cannot compare text
- fputcsv requires integers
What should precede array_combine?
- Deleting the header
- Converting every field to float
- A check that header and record field counts match
- Trusting any row length