Download the complete example ZIP

A CSV download is a practical first PHP endpoint: it turns a small table into a file that another program can read. The important part is preserving each field when it contains punctuation or a line break. Concatenating values with commas looks plausible until a note such as Notebook, blue becomes two columns.

This example exports three fictional rows using PHP's CSV writer. It needs PHP 8.1 or later and no database. The fixed rows make the output reproducible, so you can inspect quoting before connecting a real data source. It is a download-format example, not a complete customer-reporting system.

Create and run export.php

Put the file in its own folder. Start a local server with php -S 127.0.0.1:8000 and open http://127.0.0.1:8000/export.php. The response is named demo-export.csv. A browser normally offers it as a download because the response declares an attachment. Use a local development server only for this exercise.

export.php

<?php
declare(strict_types=1);

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'GET') {
    http_response_code(405);
    header('Allow: GET');
    header('Content-Type: text/plain; charset=utf-8');
    exit('Use GET to download this read-only example.');
}

// Fixed fictional rows; no database, authentication or real customer data.
$rows = [
    ['id', 'name', 'note'],
    ['demo-1', 'Asha', 'Notebook, blue'],
    ['demo-2', 'Mira', 'Said "hello"'],
    ['demo-3', 'Dev', "First line\nSecond line"],
];

header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="demo-export.csv"');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
$output = fopen('php://output', 'wb');
if ($output === false) {
    http_response_code(500);
    exit;
}
foreach ($rows as $row) {
    // Explicit empty escape uses doubled quotes rather than PHP's legacy escape.
    if (fputcsv($output, $row, ',', '"', '', "\r\n") === false) {
        fclose($output);
        exit;
    }
}
fclose($output);

Understand the download response

The endpoint only accepts GET because exporting this fixed table is a read-only operation. A POST receives HTTP 405 and an Allow header identifying the supported method. The response announces CSV with UTF-8 encoding and uses a fixed filename rather than constructing a header from a user-supplied name. The no-store directive asks caches not to retain this demonstration response.

Headers must be sent before output. Do not leave a byte-order mark, stray HTML or diagnostic print before the opening PHP tag. In a deployed reporting application, log warnings privately: a warning printed into the response can corrupt the CSV even if the endpoint appears to download successfully.

Let the CSV writer handle punctuation

fputcsv receives each row as an array. The example explicitly chooses a comma separator, double-quote enclosure, empty escape string and CRLF record ending. Quoting a field is different from removing its comma or newline: those characters remain part of the original cell. The PHP manual recommends explicitly configuring the escape parameter; see the fputcsv reference.

The third data row contains an embedded newline. Counting physical lines in the downloaded file is therefore not a reliable way to count records. Reopen it with a CSV parser and verify four records including the heading row. The note in the second data record should contain a literal pair of quotation marks, rather than an extra column or a truncated value.

Verify the actual file

These checks ran against PHP 8.2.12: a GET returned the expected CSV content type and attachment header; parsing the downloaded response recovered the comma-containing note, quoted greeting and two-line note exactly. A POST returned 405 with Allow: GET. No successful database integration or large-export benchmark is claimed.

FieldExpected parsed value
First noteNotebook, blue
Second noteSaid "hello"
Third noteFirst line followed by a newline and Second line

Before replacing the fixed rows

For a real report, authorize the requested records on the server and select only the required columns. Use parameterized queries for filters, and keep database passwords outside the public source. Decide how to handle a failure before output begins. Once streaming has started, an error cannot simply turn the partially downloaded bytes into a clean replacement response.

CSV quoting also does not make arbitrary spreadsheet formulas harmless. If users can supply cell contents, define and test a policy for values a spreadsheet may interpret as formulas. This sample's trusted fictional rows do not exercise that problem and should not be described as a formula-injection defense. Some recipients also need a particular character encoding or delimiter; verify those requirements with their actual importer.

Keep the fixed fixture when adding a database. Run the small export first, then test an empty result, a restricted user and a controlled query failure. That separates file-format errors from application authorization and data-access errors, and gives the team a downloadable example that can be reproduced without production credentials.