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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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, and price_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 example
<?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.