data types and standard library
CSV
CSV is still common for uploads, exports, finance reports, stock lists, product catalogues, and admin tools. It looks simple, but commas inside quoted values, missing columns, encodings, and spreadsheet behaviour make it easy to parse incorrectly.
Use PHP's CSV functions instead of splitting lines with explode(','). A comma is not always a separator.
Read CSV with fgetcsv()
Use a stream, read one row at a time, and let PHP handle quoting rules.
<?php
declare(strict_types=1);
$handle = fopen('php://temp', 'r+');
fputcsv($handle, ['name', 'email']);
fputcsv($handle, ['Nia Stone', 'NIA@EXAMPLE.COM']);
fputcsv($handle, ['Lee Wong', 'lee@example.com']);
rewind($handle);
while (($row = fgetcsv($handle)) !== false) {
echo implode(' | ', $row) . PHP_EOL;
}
fclose($handle);
// Prints:
// name | email
// Nia Stone | NIA@EXAMPLE.COM
// Lee Wong | lee@example.com
This example uses php://temp so it can run without a separate file. In an upload handler, the same loop would read the uploaded file path.
Quoted commas are valid data
This is why manual splitting is unsafe.
<?php
declare(strict_types=1);
$row = str_getcsv('"Large notebook, lined",1299');
echo $row[0] . PHP_EOL;
echo $row[1] . PHP_EOL;
// Prints:
// Large notebook, lined
// 1299
explode(',', $line) would incorrectly split the product name into two columns.
Treat the header as a contract
For imports, validate the header before processing rows.
<?php
declare(strict_types=1);
function assertHeader(array $header): void
{
$expected = ['sku', 'name', 'price_pennies'];
if ($header !== $expected) {
throw new InvalidArgumentException('CSV header must be: ' . implode(', ', $expected));
}
}
assertHeader(['sku', 'name', 'price_pennies']);
echo 'Header accepted' . PHP_EOL;
// Prints:
// Header accepted
Without this check, a supplier can reorder columns and your importer may write valid-looking but wrong data.
Combine headers with rows
Once the header is trusted, combine it with each row to get named fields.
<?php
declare(strict_types=1);
$header = ['sku', 'name', 'price_pennies'];
$row = ['KB-101', 'Keyboard', '2499'];
if (count($row) !== count($header)) {
throw new InvalidArgumentException('Row has the wrong number of columns.');
}
$record = array_combine($header, $row);
echo $record['sku'] . ' costs ' . $record['price_pennies'] . ' pennies' . PHP_EOL;
// Prints:
// KB-101 costs 2499 pennies
Named fields are easier to validate and review than numeric indexes such as $row[2].
Validate row values
CSV gives you strings. Convert and validate before the data reaches the database.
<?php
declare(strict_types=1);
function normaliseProductRow(array $record, int $lineNumber): array
{
$sku = trim((string) $record['sku']);
$name = trim((string) $record['name']);
$price = filter_var($record['price_pennies'], FILTER_VALIDATE_INT);
if ($sku === '' || $name === '' || $price === false || $price < 0) {
throw new InvalidArgumentException('Line ' . $lineNumber . ' contains invalid product data.');
}
return [
'sku' => $sku,
'name' => $name,
'pricePennies' => $price,
];
}
$product = normaliseProductRow([
'sku' => ' KB-101 ',
'name' => ' Keyboard ',
'price_pennies' => '2499',
], 2);
echo $product['sku'] . ': ' . $product['pricePennies'] . PHP_EOL;
// Prints:
// KB-101: 2499
Line numbers matter here too. A useful import error points the user to the row they need to fix.
Write CSV with fputcsv()
Use fputcsv() for exports so quoting is handled correctly.
<?php
declare(strict_types=1);
$handle = fopen('php://temp', 'r+');
fputcsv($handle, ['sku', 'name', 'price_pennies']);
fputcsv($handle, ['NB-200', 'Notebook, lined', '399']);
rewind($handle);
echo stream_get_contents($handle);
fclose($handle);
// Prints:
// sku,name,price_pennies
// NB-200,"Notebook, lined",399
The comma in Notebook, lined is data, so the field is quoted in the output.
Protect spreadsheet exports
If exported CSV is opened in spreadsheet software, cells beginning with characters such as =, +, -, or @ may be interpreted as formulas. Prefix user-controlled text when the CSV is intended for spreadsheets.
<?php
declare(strict_types=1);
function safeSpreadsheetCell(string $value): string
{
if ($value !== '' && str_contains('=+-@', $value[0])) {
return "'" . $value;
}
return $value;
}
echo safeSpreadsheetCell('=IMPORTXML("https://example.com")') . PHP_EOL;
// Prints:
// '=IMPORTXML("https://example.com")
This is not needed for every internal machine-to-machine CSV, but it is important for exports that humans open in spreadsheet tools.
What to remember
Use fgetcsv() and fputcsv(), validate headers before rows, convert string fields into the types your application expects, keep line numbers in errors, and think about spreadsheet-specific risks for exported user data.
Practice
Task: Import products from CSV
Write a small CSV importer for product rows.
Requirements
- Use
declare(strict_types=1);. - Use an in-memory stream with
php://temp. - Write a header row of
sku,name, andprice_pennies. - Write at least two data rows.
- Read the CSV back with
fgetcsv(). - Validate that the header matches the expected columns.
- Convert each data row into an associative array.
- Validate that SKU and name are not empty and price is a non-negative integer.
- Print the imported products.
- Include the expected output as comments in the same PHP code block.
Do not split CSV rows manually. The practice should use PHP's CSV parser.
Show solution
<?php
declare(strict_types=1);
function normaliseProduct(array $record, int $lineNumber): array
{
$sku = trim((string) $record['sku']);
$name = trim((string) $record['name']);
$price = filter_var($record['price_pennies'], FILTER_VALIDATE_INT);
if ($sku === '' || $name === '' || $price === false || $price < 0) {
throw new InvalidArgumentException('Line ' . $lineNumber . ' contains invalid product data.');
}
return [
'sku' => $sku,
'name' => $name,
'pricePennies' => $price,
];
}
$handle = fopen('php://temp', 'r+');
fputcsv($handle, ['sku', 'name', 'price_pennies']);
fputcsv($handle, ['KB-101', 'Keyboard', '2499']);
fputcsv($handle, ['NB-200', 'Notebook, lined', '399']);
rewind($handle);
$header = fgetcsv($handle);
if ($header !== ['sku', 'name', 'price_pennies']) {
throw new InvalidArgumentException('Unexpected CSV header.');
}
$products = [];
$lineNumber = 1;
while (($row = fgetcsv($handle)) !== false) {
$lineNumber++;
if (count($row) !== count($header)) {
throw new InvalidArgumentException('Line ' . $lineNumber . ' has the wrong number of columns.');
}
$products[] = normaliseProduct(array_combine($header, $row), $lineNumber);
}
fclose($handle);
foreach ($products as $product) {
echo $product['sku'] . ': ' . $product['name'] . ' - ' . $product['pricePennies'] . PHP_EOL;
}
// Prints:
// KB-101: Keyboard - 2499
// NB-200: Notebook, lined - 399
The solution validates the header before trusting row positions, keeps line numbers available for useful errors, and converts the CSV strings into the application shape before anything would be saved.