CSV extension (ext/csv) proposal
Under discussion; not accepted. The RFC page (version 1.0, dated 2026-10-08) was created as a Draft at 17:06 UTC on 8 October, the same day the author asked for RFC karma and got it. After an edit at 06:16 UTC on 9 October the status line reads Under Discussion. At 06:19 UTC the author opened the discussion on internals, calling it "an invitation for discussion, not a call for votes". The official RFC index collected at 10:01 UTC on 9 October lists it as discussion. At 19:43 UTC on 9 October the author published version 1.1 (dated 2026-10-09). Its changelog says it was updated after a review of the implementation (see below). An edit at 11:30 UTC on 11 October made it version 1.2 (dated 2026-10-11), still Under Discussion. The page's discussion and voting thread fields still read "tbd". The author is Damian Jóźwiak. The proposed version is the "next minor PHP version (the release after PHP 8.6)", recorded here as 8.7.
The proposal is based on Gina Peter Banyard's girgias/csv extension (BSD-3-Clause). The RFC
says Gina has not reviewed it and that it is not her endorsement. The announcement adds that parts
of the implementation were developed with LLM assistance and reviewed by the author.
Motivation given by the RFC
The RFC argues that the existing CSV functions (fgetcsv(), fputcsv(), str_getcsv() and the
SplFileObject CSV methods) cannot follow RFC 4180 without breaking compatibility. It names
the proprietary $escape mechanism, locale-dependent parsing, single-byte delimiters and
enclosures, empty lines parsing as [null], and the lack of an inverse of str_getcsv(). It
also says PHP 8.6 deprecated SplFileObject::fgetcsv(), fputcsv(), setCsvControl() and
getCsvControl() with no replacement. The
PHP 8.6 deprecations RFC does list "Deprecate
SplFileObject CSV methods", but the result of that vote was not checked here.
What is proposed
A new, always-enabled ext/csv with six functions and one class in the Csv\ namespace:
Csv\array_to_row()andCsv\row_to_array()format and parse one row.Csv\collection_to_buffer(),Csv\buffer_to_collection()andCsv\buffer_to_collection_lax()convert between a string and rows. The strict variants throwValueErrorwhen rows have different numbers of fields. The_laxvariant allows it.Csv\collection_to_file()writes any iterable of arrays through the streams layer one row at a time.final class Csv\LazyLaxCollection(IteratorAggregate) reads rows lazily, withcreateFromBuffer()andcreateFromFile()factories. The RFC says memory use depends on the longest row, not the file size, and that iteration can restart if the stream is seekable.
Every function takes $delimiter = ',', $enclosure = '"' and $eolSequence = "\r\n". All three
may be multibyte. Parsing is described as binary-safe and locale-independent, with no escape
character. Invalid dialects and malformed input throw ValueError, non-array rows throw
TypeError, I/O failures throw Error, and no function returns false.
Version 1.1 adds a "Strict parsing" section. Whatever the dialect, input that does not follow the
RFC 4180 record grammar throws ValueError instead of being guessed. This covers an enclosure inside
an unenclosed field (including whitespace or a BOM before the opening enclosure), data between a
closing enclosure and the next delimiter or line ending ("a" ,b), an enclosure that is never closed,
and CR or LF outside an enclosed field that is not part of the EOL sequence. As a result, an LF-only
file read with the default "\r\n" is an error, not one long row; it is read by passing "\n".
row_to_array() now accepts exactly one row, with an optional trailing EOL sequence.
Version 1.1 also describes stream handling in more detail. $file accepts anything the streams layer
accepts (paths, php://, compress.zlib://, user wrappers) and is opened with the default stream
context. A collection from createFromFile() owns its stream like SplFileObject does: fclose()
refuses to close it, and it is closed when the object is destroyed. Iteration state belongs to each
iterator, so nested loops over a buffer-backed collection are independent. A second concurrent loop over
a file-backed collection throws Error.
The RFC lists differences from the legacy functions: no escape character, no [null] for empty
lines, an enclosure inside an unenclosed field, data after a closing enclosure and an unterminated
enclosure are errors, whitespace before an enclosure is not silently dropped, and the default line
ending is "\r\n". (This page's earlier summary of version 1.0 said whitespace before an enclosure is kept;
under 1.1's strict parsing it is an error. The 1.0 wording could not be re-read, because the wiki's
revision history requires a login.) Existing functions keep their behavior. The only
backward-compatibility impact named is that the top-level Csv\ namespace becomes reserved.
Version 1.1 listed as an open issue that dialects in which one token is a prefix of another (for example
delimiter :: with EOL :) are ambiguous and could be rejected during argument validation. Version 1.2
replaces that with a "Multibyte and overlapping dialect tokens" section in the proposal. Overlapping tokens
are allowed where rows still round-trip: writers also enclose a field when a token could form across the
boundary with a neighbouring delimiter or EOL sequence (the RFC's example: delimiter --, enclosure aa).
Combinations that cannot represent every row unambiguously throw ValueError in readers and writers, for
example delimiter xy with enclosure xyx. The remaining open issues are whether the extension can be
disabled at build time (the implementation supports --disable-csv; the RFC proposes always enabled), the
name LazyLaxCollection, and whether a strict lazy variant should ship in the first version.
Version 1.2 (whether these parts are new since 1.1 was not checked) names two gaps left by the PHP 8.6 SplFileObject deprecation, raised during that
discussion: SplFileObject::fputcsv() has no drop-in replacement because fputcsv() needs a stream that
SplFileObject does not expose, and SplFileObject::READ_CSV was not deprecated although
setCsvControl(), the only way to configure its dialect, was. Its "Unaffected PHP Functionality" section
says the RFC neither changes nor deprecates fgetcsv(), fputcsv(), str_getcsv() or the SplFileObject
CSV methods. The future scope includes a deprecation timeline for str_getcsv(), fputcsv() and fgetcsv()
("deliberately not part of this RFC"), a separate
RFC to deprecate SplFileObject::READ_CSV, and header/encoding handling. As checked on 9 and 11 October, it also lists a possible
object-oriented Csv\Reader/Csv\Writer API with capability interfaces for a later RFC. None of
these is part of this RFC. The planned vote is one question needing a 2/3 majority.
Discussion so far
The first reply, from Lynn on 9 October (message 132845), asked for support for already-open streams/resources, for reading and writing, and what "Lax" means. The author replied at 08:59 UTC:
- "Lax" means rows may have different numbers of fields, as in
buffer_to_collection_lax(). The name comes fromgirgias/csv. The author added a test for it and suggested aLazyCollectionwith configurable strictness as a possible alternative name. - The author is "considering" two new entry points,
Csv\collection_to_stream($stream, iterable $collection, ...)andCsv\LazyLaxCollection::createFromStream($stream, ...), separate from the$fileparameter. The stream parameter would be untyped (documented asresource) and checked at runtime with aTypeError. The proposed semantics: parsing starts at the current position, the stream is borrowed and never closed, repeated iteration seeks back (non-seekable streams iterate once), and because of read-ahead the stream position after a partial iteration is not defined.
The author said the RFC will be updated after the API discussion. Neither version 1.1 nor version 1.2
(checked at 16:05 UTC on 11 October) contains collection_to_stream() or createFromStream(), so
they are not part of the proposal as written.
On 10 October at 10:37 UTC Artem Ukrainskiy (GitHub user ArtUkrainskiy, whose review the RFC cites) argued for a different path: improving the existing ext/standard CSV functions incrementally instead of adding a second parser. They went through the RFC's motivation list:
$escapeis, in their words, already addressed by the PHP 8.4 deprecation, with the default changing to""in PHP 9.0.- For locale dependence, their php-src PR #24207
("Skip mbrlen() for ASCII bytes in php_mblen()", opened 8 October, open and not merged on 10 October)
removes the per-byte libc lookup for ASCII characters in ASCII-compatible locales. They said the
remaining
isspace()check could become an explicit space/tab check. They also argued thatmbrlen()is useful for CJK locales: a Shift_JIS character can contain a|trail byte that is not a delimiter, and a locale-independent byte parser would split it. - Multibyte delimiters and enclosures currently throw
ValueError, so they could be added without changing valid calls. A newstr_putcsv()could be the inverse ofstr_getcsv(). [null]for empty lines and whitespace before an enclosure could be handled by an opt-in strict mode. They said enclosing fields that contain spaces on output is allowed by RFC 4180 section 2.5.
Their main concern is the long-term cost of maintaining two CSV parsers, given the reluctance in the
June and August discussions to remove existing APIs without a migration path. They said this is not an
objection to the implementation: the issues they reported were fixed quickly and the parser is now faster
than fgetcsv() in their benchmarks. They published their benchmarks and test scripts in a public GitHub
repository; those benchmarks were not checked here.
The author replied at 11:50 UTC on 11 October, after the version 1.2 edit:
- They agreed that the RFC's statement that the existing behaviour "cannot be fixed without breaking compatibility" is too broad, since several improvements could be added as new functions or opt-in behaviour. They also agreed that enclosing fields with spaces is allowed by RFC 4180 and that locale-independent byte parsing has different implications for encodings such as Shift_JIS. The version 1.2 page still contains the "cannot be fixed" sentence.
- They cited the new DOM API alongside
DOMDocumentand the URI extension alongsideparse_url()as precedents for a modern API next to an established one, while saying these do not by themselves justify a second CSV parser. - They proposed three directions: (A) improve the existing ext/standard API, (B) continue with ext/csv,
adding entry points for already-open streams, or (C) rework it as an object-oriented API in
Csv\with file and stream factories. They still prefer a dedicated API but are open to the other routes. - They asked whether a directional vote between these approaches would be useful if discussion does not settle it. Such a vote would pick a direction for a revised proposal, not approve an API. No such vote has been announced.
- They said the current RFC does not propose deprecating the existing CSV functions, and that any later consolidation would need its own discussion and a migration path.
Implementation
The implementation is php-src PR #24199, "[RFC] Add
CSV extension (ext/csv)", opened on 8 October 2026 at 17:03 UTC. On 11 October (GitHub API, about
16:05 UTC) it was still open, not a draft and not merged. The RFC calls it a port of girgias/csv 0.6.0.
Version 1.1 says a review of the PR by GitHub user ArtUkrainskiy found a use-after-free (also present
in girgias/csv 0.6.0), a quadratic rescan of long rows in createFromFile() and smaller issues. The RFC says
all of them are fixed, covered by tests and checked with AddressSanitizer and a differential fuzzer. It also
claims six girgias/csv 0.6.0 bugs fixed during the port and 90 phpt tests. In the 11 October message the
author said a remaining writer issue with overlapping tokens was fixed in a later PR commit and that all 92
phpt tests pass locally (the RFC page still says 90). On the reviewer's benchmark it reports
createFromFile() reading rows slightly faster than fgetcsv() (363–1,539 ns against 402–1,742 ns per row,
with the php_mblen() fast path of php-src PR #24207) and writing 10–20% slower than fputcsv(). These
are the author's statements; the review reports, fixes and benchmarks were not checked here.
Limitations
This page summarises a proposal whose list discussion began on 9 October. An alternative of extending the
existing functions has been proposed on the list (10 October), and the author has since asked whether the
direction (A, B or C above) should be settled first (11 October). The full raw page was read on 12 October:
its changelog entry for 1.2 (dated 2026-10-11) matches the overlapping-token policy above, says the
ambiguous-dialect open issue is resolved and links php-src commit b7866156 for the implementation. Its
"Rejected Features" section lists an object-oriented Reader/Writer API in this RFC (left to future work) and
compatibility with the legacy fgetcsv() dialect (escape character, [null] rows, locale dependence). The names, behavior and
target version can change, and nothing here is in released PHP. The API description comes from the
RFC text and was not tested against the PR. girgias/csv can already be installed as a
third-party extension (see PIE 1.5).