Files, Streams and I/O
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05. PHP’s file functions are thin wrappers around a single
abstraction, the stream: the same fopen(), fgets() and file_get_contents() work on local files, memory
buffers, compressed files, HTTP URLs and standard input, depending on the wrapper named in the path. Upload handling
in web applications is covered in File Uploads and Images.
Whole files: file_get_contents and file_put_contents
For files that fit in memory, two functions do everything:
<?php
declare(strict_types=1);
$dir = sys_get_temp_dir() . '/library';
@mkdir($dir, 0775, true);
$log = "{$dir}/loans.log";
file_put_contents($log, "978-0-00-000001-1 lent\n"); // create or truncate
file_put_contents($log, "978-0-00-000003-5 lent\n", FILE_APPEND | LOCK_EX); // append, with an exclusive lock
echo file_get_contents($log); // whole file as a string
print_r(file($log, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); // lines as an array
// Writing atomically: write to a temporary file, then rename over the target
$tmp = tempnam($dir, 'cat');
file_put_contents($tmp, json_encode(['titles' => ['Kindred']], JSON_THROW_ON_ERROR));
rename($tmp, "{$dir}/catalogue.json"); // readers see old or new, never half
Both return false on failure and emit a warning; check the result (=== false) or convert warnings to exceptions
(see Errors and Exceptions). rename() within the same
filesystem is atomic, which makes "write temp then rename" the standard way to replace files that other processes
read.
Source: file_get_contents and file_put_contents
Streams with fopen
For large files or incremental processing, open a stream with fopen($path, $mode):
| Mode | Meaning |
|---|---|
|
read / read and write; the file must exist |
|
write / read and write; truncates or creates |
|
append (writes always go to the end) / append and read |
|
create exclusively; fails if the file exists (safe for lock files and unique names) |
|
open for writing without truncating, creating if needed (useful with |
|
binary flag (only meaningful on Windows); |
<?php
declare(strict_types=1);
$path = sys_get_temp_dir() . '/returns.txt';
file_put_contents($path, "Kindred\nSolaris\n\nDawn\n");
$handle = fopen($path, 'r');
if ($handle === false) {
throw new RuntimeException("Cannot open {$path}");
}
try {
$n = 0;
while (($line = fgets($handle)) !== false) { // !== false: a line "0" is falsy
$line = rtrim($line, "\r\n");
if ($line !== '') {
echo ++$n, ": {$line}\n";
}
}
} finally {
fclose($handle); // always release the handle
}
fread($h, $length) reads bytes, fwrite() writes, fseek()/ftell()/rewind() move the position, and feof()
reports the end. SplFileObject offers the same as an iterable object.
CSV
fgetcsv() and fputcsv() read and write CSV records, handling quoting and embedded delimiters correctly;
str_getcsv() parses one line from a string. Their escape parameter (a backslash by default) implements a
non-standard escaping that breaks RFC 4180 files; since PHP 8.4 relying on the default is deprecated, and the
recommendation is to pass escape: '' to disable it.
<?php
declare(strict_types=1);
$csv = fopen('php://temp', 'w+'); // an in-memory stream
fputcsv($csv, ['isbn', 'title', 'year'], escape: '');
fputcsv($csv, ['978-0-00-000001-1', 'Kindred, a "novel"', 1979], escape: '');
rewind($csv);
echo stream_get_contents($csv);
rewind($csv);
$header = fgetcsv($csv, escape: '');
while (($row = fgetcsv($csv, escape: '')) !== false) {
print_r(array_combine($header, $row));
}
isbn,title,year
978-0-00-000001-1,"Kindred, a ""novel""",1979
Array
(
[isbn] => 978-0-00-000001-1
[title] => Kindred, a "novel"
[year] => 1979
)
For very large files, wrap fgetcsv() in a generator (see
Iterators and Generators). Values always come back as
strings; convert and validate them explicitly. When exporting data users will open in a spreadsheet, prefix cells that
start with =, +, - or @ to prevent CSV formula injection.
Source: fgetcsv, fputcsv and PHP 8.4 deprecated features
Locking with flock
flock() takes an advisory lock on an open file: LOCK_SH (shared, for readers), LOCK_EX (exclusive, for
writers), LOCK_UN (release), combined with LOCK_NB to fail immediately instead of waiting. Advisory locks only
protect against processes that also use flock(), and they do not work reliably on some network filesystems.
<?php
declare(strict_types=1);
$counterFile = sys_get_temp_dir() . '/loan-counter.txt';
$h = fopen($counterFile, 'c+'); // do not truncate before locking
if (flock($h, LOCK_EX)) { // wait for the exclusive lock
$count = (int) stream_get_contents($h);
ftruncate($h, 0);
rewind($h);
fwrite($h, (string) ($count + 1));
fflush($h);
flock($h, LOCK_UN);
}
fclose($h);
echo file_get_contents($counterFile) >= 1 ? "counter incremented\n" : '';
For counters and coordination between servers, use a database or Redis (atomic increments, locks) instead of files.
Source: flock
Directories and paths
<?php
declare(strict_types=1);
$root = sys_get_temp_dir() . '/covers';
@mkdir("{$root}/sf", 0775, true);
touch("{$root}/kindred.jpg");
touch("{$root}/sf/solaris.png");
print_r(array_values(array_diff(scandir($root), ['.', '..']))); // ["kindred.jpg", "sf"]
print_r(array_map(basename(...), glob("{$root}/*.jpg"))); // ["kindred.jpg"]
foreach (new FilesystemIterator($root, FilesystemIterator::SKIP_DOTS) as $entry) {
echo $entry->getFilename(), $entry->isDir() ? '/' : '', "\n";
}
print_r(pathinfo('/srv/covers/kindred.cover.jpg')); // dirname, basename, extension, filename
echo basename('/srv/covers/kindred.jpg', '.jpg'), ' ', dirname('/srv/covers/sf/x.jpg', 2), "\n"; // kindred /srv/covers
var_dump(realpath("{$root}/sf/../kindred.jpg")); // canonical absolute path, or false
realpath() resolves .. and symlinks and returns false for missing paths, which makes it a building block for
checking that a user-supplied path stays inside a base directory (str_starts_with(realpath($p), $base . '/')).
Recursive traversal with RecursiveDirectoryIterator is shown in
Iterators and Generators. mkdir($p, 0775, true)
creates parents; rmdir() removes empty directories; copy(), rename() and unlink() work on files.
Temporary files and content types
tmpfile() returns a handle to a file deleted automatically when closed; tempnam($dir, $prefix) creates a uniquely
named file and returns its path; sys_get_temp_dir() gives the system temporary directory. php://temp and
php://memory are in-memory streams (php://temp spills to disk above 2 MB by default).
To identify a file’s real type, inspect its content with finfo (the fileinfo extension), never trust the extension or
a client-declared type:
<?php
declare(strict_types=1);
$finfo = new finfo(FILEINFO_MIME_TYPE);
var_dump($finfo->buffer("GIF89a\x01\x00\x01\x00\x80\x00\x00")); // string(9) "image/gif"
var_dump($finfo->buffer('<?php echo 1;')); // string(10) "text/x-php"
$tmp = tmpfile();
fwrite($tmp, "%PDF-1.7\n");
var_dump($finfo->file(stream_get_meta_data($tmp)['uri'])); // string(15) "application/pdf"
fclose($tmp); // deleted
Since PHP 8.1 finfo_open() returns a finfo object, and since 8.5 finfo_close() is deprecated: objects are freed
automatically.
Streams, wrappers, contexts and filters
A wrapper is chosen by the scheme of the path. The built-in ones (stream_get_wrappers() lists them):
| Wrapper | Purpose |
|---|---|
|
local files |
|
the process’s standard streams |
|
the raw request body (web SAPIs), readable once per stream |
|
the output buffer, like |
|
in-memory streams |
|
apply filters when reading or writing another stream |
|
inline data (RFC 2397): |
|
remote resources (subject to |
|
transparently (de)compress |
|
files inside a PHAR archive |
|
directory listings matching a pattern |
A stream context passes options to a wrapper (HTTP method, headers, timeouts, TLS settings). A filter transforms
data as it flows: string.toupper, string.rot13, convert.base64-encode, zlib.deflate, dechunk, or your own
php_user_filter subclass.
<?php
declare(strict_types=1);
echo file_get_contents('data://text/plain;base64,S2luZHJlZA=='), "\n"; // Kindred
$gz = sys_get_temp_dir() . '/catalogue-backup.gz';
file_put_contents("compress.zlib://{$gz}", str_repeat("Kindred\n", 1000));
printf("compressed to %d bytes, %d bytes uncompressed\n", filesize($gz), strlen(file_get_contents("compress.zlib://{$gz}")));
$h = fopen('php://memory', 'w+');
stream_filter_append($h, 'string.toupper', STREAM_FILTER_WRITE);
fwrite($h, 'solaris');
rewind($h);
echo stream_get_contents($h), "\n"; // SOLARIS
// A context for an HTTP request: method, headers, timeout, and no exception on 4xx/5xx bodies
$context = stream_context_create(['http' => [
'method' => 'GET',
'header' => "Accept: application/json\r\n",
'timeout' => 5,
'ignore_errors' => true,
]]);
// $json = file_get_contents('https://api.example.org/books', false, $context);
// $headers = http_get_last_response_headers(); // 8.4: replaces $http_response_header
var_dump(is_resource($context));
For HTTP beyond simple GETs, a real client (cURL, Guzzle, Symfony HttpClient) is preferable; see HTTP Clients, APIs and Email.
STDIN, STDOUT and STDERR
The CLI defines the constants STDIN, STDOUT and STDERR as open streams. Write diagnostics to STDERR, so that
STDOUT stays clean for data that may be piped to another program:
<?php
declare(strict_types=1);
fwrite(STDERR, "reading titles from standard input...\n");
$titles = [];
while (($line = fgets(STDIN)) !== false) {
$titles[] = trim($line);
}
fprintf(STDOUT, "%d titles\n", count($titles));
$ printf 'Kindred\nSolaris\n' | php count-titles.php 2>/dev/null
2 titles
Source: Input/output streams
open_basedir and allow_url_fopen
Two php.ini directives restrict what file functions can reach:
-
open_basedirlimits filesystem access to a list of directories (/srv/app:/tmp); outside them, functions fail with a warning. It is a defence-in-depth measure, not a sandbox: it does not restrict shell commands or extensions. -
allow_url_fopen(defaultOn) lets file functions open URLs;allow_url_include(defaultOff, keep it off) would letincludeload remote code. Disableallow_url_fopenon servers that do not need it, and use an HTTP client for outgoing requests.
Never pass user input directly to file functions: path traversal (../../etc/passwd) and wrapper injection
(php://filter/..., phar://) are classic vulnerabilities; see
Web Security Fundamentals.
Source: open_basedir and Filesystem runtime configuration
Where older books and tutorials are outdated
-
CSV escaping: relying on the default
escapeoffgetcsv()/fputcsv()/str_getcsv()is deprecated since 8.4. -
fgetss()(Kromann ch. 9-10) was removed in 8.0; usestrip_tags()on the read data, or better, escape on output. -
$http_response_headeris superseded byhttp_get_last_response_headers()(8.4). -
finfo_close()is deprecated (8.5);finforesources became objects in 8.1. -
each()/list()loops overfile()— useforeach, or a generator overfgets()for large files.