PHP Streams
last modified October 22, 2026
Streams in PHP provide a unified way to handle input and output: reading from files, writing to network sockets, compressing data, or processing data from external sources. This tutorial covers file streams, in-memory streams, network streams, stream contexts, filters, and custom stream wrappers.
A stream is a resource that can be read from or written to in a linear fashion.
Every stream is identified by a URL-like string of the form
scheme://target, where the scheme selects a wrapper.
Plain file paths use the file:// wrapper; others include
php://, http://, https://,
data:// and compress.zlib://. The same functions,
such as fopen, fread, fgets,
fwrite and file_get_contents, work with all of them.
The stream_get_wrappers function lists the wrappers available in
your installation.
Writing to a file stream
The fopen function opens a stream; fwrite writes to
it and fclose closes it.
<?php
$logFile = fopen("app.log", "a");
if ($logFile === false) {
exit("Failed to open log file\n");
}
$timestamp = date("Y-m-d H:i:s");
fwrite($logFile, "[$timestamp] User logged in\n");
fclose($logFile);
echo "Log entry added\n";
echo "Lines in log: ", count(file("app.log")), "\n";
The file app.log is opened in append mode ("a"), which
creates it if it does not exist and places the write position at its end. The
fopen function returns false on failure, so the
result is checked before use. The file function reads the file
back as an array of lines.
$ php basic_file_stream.php Log entry added Lines in log: 1
Reading from a file stream
The fgets function reads a stream line by line. We read the
following configuration file.
host=localhost port=8080 # comment debug=true
The program parses the key-value pairs.
<?php
$configFile = fopen("config.ini", "r");
if ($configFile === false) {
exit("Failed to open config file\n");
}
$settings = [];
while (($line = fgets($configFile)) !== false) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
continue;
}
[$key, $value] = explode("=", $line, 2);
$settings[$key] = $value;
}
fclose($configFile);
print_r($settings);
fgets returns false at the end of the stream, which
ends the loop. Empty lines, comments, and lines without an equals sign are
skipped. Reading line by line keeps only one line in memory at a time. For real
INI files, PHP also has the parse_ini_file function.
$ php read_file_stream.php
Array
(
[host] => localhost
[port] => 8080
[debug] => true
)
In-memory streams
The php://memory and php://temp streams behave like
files but live in memory. php://temp switches to a temporary file
once the data exceeds 2 MB.
<?php
$stream = fopen("php://memory", "w+");
fwrite($stream, "an old falcon\n");
fwrite($stream, "a blue sky\n");
echo "Position: ", ftell($stream), "\n";
rewind($stream);
while (($line = fgets($stream)) !== false) {
echo "Read: $line";
}
fclose($stream);
After writing, the position is at the end of the data. The rewind
function moves it back to the beginning so that the data can be read. In-memory
streams are useful for tests and for APIs that require a stream rather than a
string.
$ php memory_stream.php Position: 25 Read: an old falcon Read: a blue sky
Standard streams
In the CLI, PHP provides the STDIN, STDOUT and
STDERR constants, which are already opened streams.
<?php
fwrite(STDOUT, "Normal output\n");
fwrite(STDERR, "Error output\n");
$out = fopen("php://output", "w");
fprintf($out, "%s has %d letters\n", "falcon", 6);
fclose($out);
Error messages written to STDERR do not mix with regular output
when the output is redirected to a file or a pipe. The
php://output stream writes to the same output buffer as
echo, and also works in web requests.
$ php std_streams.php Normal output Error output falcon has 6 letters
The data wrapper
The data:// wrapper reads data embedded directly in the URL
(RFC 2397).
<?php
echo file_get_contents("data://text/plain,Hello from a data URI"), "\n";
$encoded = base64_encode("Encoded text");
echo file_get_contents("data://text/plain;base64,$encoded"), "\n";
The data can be plain text or Base64-encoded. This is handy for tests and for passing small values to functions that only accept a stream or a file name.
$ php data_wrapper.php Hello from a data URI Encoded text
Network streams
With the https:// wrapper, which needs the
openssl extension, fopen can read from a remote URL.
<?php
$stream = fopen("https://httpbin.org/json", "r");
if ($stream === false) {
exit("Failed to fetch data\n");
}
$response = stream_get_contents($stream);
fclose($stream);
$data = json_decode($response, true, flags: JSON_THROW_ON_ERROR);
echo "Title: ", $data["slideshow"]["title"], "\n";
echo "Author: ", $data["slideshow"]["author"], "\n";
echo "Slides: ", count($data["slideshow"]["slides"]), "\n";
The stream_get_contents function reads the remaining data of the
stream into a string. The JSON response from httpbin.org, a public
HTTP testing service, is decoded with JSON_THROW_ON_ERROR, so
invalid JSON raises an exception instead of returning null.
Network access must be allowed by the allow_url_fopen
setting.
$ php network_stream.php Title: Sample Slide Show Author: Yours Truly Slides: 2
Stream contexts
Stream contexts configure options for a stream, such as the HTTP method, headers, request body, timeouts, or SSL settings.
<?php
$payload = json_encode(["user_id" => 123, "action" => "login"]);
$context = stream_context_create([
"http" => [
"method" => "POST",
"header" => "Content-Type: application/json\r\n",
"content" => $payload,
"timeout" => 10,
"ignore_errors" => true,
],
]);
$response = file_get_contents("https://httpbin.org/post", false, $context);
if ($response === false) {
exit("Failed to send request\n");
}
$headers = http_get_last_response_headers();
echo "Status: ", $headers[0], "\n";
$data = json_decode($response, true);
echo "Echoed JSON: ", json_encode($data["json"]), "\n";
The context created with stream_context_create turns the request
into a POST request with a JSON body. The ignore_errors option
returns the response body even for 4xx and 5xx status codes. The
http_get_last_response_headers function, added in PHP 8.4, returns
the response headers; the first one is the status line. It replaces the
$http_response_header variable, which is deprecated as of PHP 8.5.
The httpbin.org/post endpoint echoes the received JSON back.
$ php stream_context.php
Status: HTTP/1.1 200 OK
Echoed JSON: {"action":"login","user_id":123}
Stream filters
Stream filters process data as it passes through a stream. PHP has built-in
filters such as string.toupper, string.rot13,
convert.base64-encode, zlib.deflate and
convert.iconv.*.
<?php
$stream = fopen("php://temp", "w+");
fwrite($stream, "name,city\nJane,Bratislava\nJohn,Košice\n");
rewind($stream);
stream_filter_append($stream, "string.toupper");
while (($line = fgets($stream)) !== false) {
echo "Processed: $line";
}
fclose($stream);
The stream_filter_append function attaches the filter to the
stream; every line read afterwards is converted to uppercase. Note that
string.toupper works on bytes and only converts ASCII letters, so
the š in Košice stays lowercase. For multibyte text, use
mb_strtoupper.
$ php stream_filter.php Processed: NAME,CITY Processed: JANE,BRATISLAVA Processed: JOHN,KOšICE
Custom stream filters
A custom filter is a class that extends php_user_filter.
<?php
class MaskDigitsFilter extends php_user_filter
{
public function filter($in, $out, &$consumed, bool $closing): int
{
while ($bucket = stream_bucket_make_writeable($in)) {
$bucket->data = preg_replace('/\d/', '*', $bucket->data);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
stream_filter_register("mask.digits", MaskDigitsFilter::class);
$out = fopen("php://output", "w");
stream_filter_append($out, "mask.digits");
fwrite($out, "Card: 4111 1111 1111 1111\n");
fwrite($out, "PIN: 1234\n");
fclose($out);
The filter method receives the incoming data in buckets. Each
bucket is modified, here by masking all digits, and passed on to the output.
The filter is registered with stream_filter_register and attached
to the output stream, so everything written to it is masked.
In a real filter, a digit sequence may be split between two buckets. This does not matter for masking single digits, but a filter that looks for longer patterns would have to buffer data between calls.
$ php custom_filter.php Card: **** **** **** **** PIN: ****
Custom stream wrappers
A stream wrapper implements a whole scheme. It is a class with methods such as
stream_open, stream_read and
stream_write, registered with stream_wrapper_register.
<?php
class MemoryLogWrapper
{
public $context;
private static array $logs = [];
private string $name;
private int $position = 0;
public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
{
$this->name = parse_url($path, PHP_URL_HOST);
self::$logs[$this->name] ??= '';
return true;
}
public function stream_write(string $data): int
{
self::$logs[$this->name] .= $data;
$this->position += strlen($data);
return strlen($data);
}
public function stream_read(int $count): string
{
$chunk = substr(self::$logs[$this->name], $this->position, $count);
$this->position += strlen($chunk);
return $chunk;
}
public function stream_eof(): bool
{
return $this->position >= strlen(self::$logs[$this->name]);
}
public function stream_tell(): int
{
return $this->position;
}
public function stream_stat(): array
{
return ['size' => strlen(self::$logs[$this->name])];
}
public function stream_seek(int $offset, int $whence): bool
{
$this->position = match ($whence) {
SEEK_SET => $offset,
SEEK_CUR => $this->position + $offset,
SEEK_END => strlen(self::$logs[$this->name]) + $offset,
};
return true;
}
}
stream_wrapper_register("memlog", MemoryLogWrapper::class);
$logger = fopen("memlog://debug", "w+");
fwrite($logger, "Error: Invalid input\n");
fwrite($logger, "Warning: Low disk space\n");
rewind($logger);
echo fread($logger, 1024);
fclose($logger);
file_put_contents("memlog://audit", "User 42 deleted a record\n");
echo file_get_contents("memlog://audit");
The memlog:// wrapper keeps named logs in a static array. The host
part of the URL, such as debug or audit, selects the
log. The stream_tell and stream_seek methods are
needed for rewind, and stream_stat for
file_get_contents, which asks for the size of the stream. The
public $context property is set by PHP; declaring it avoids the
dynamic property deprecation of PHP 8.2.
Once registered, the wrapper works with all stream functions, including
file_put_contents and file_get_contents.
$ php custom_wrapper.php Error: Invalid input Warning: Low disk space User 42 deleted a record
Stream metadata
The stream_get_meta_data function returns information about an open
stream.
<?php
$file = fopen("config.ini", "rb");
$meta = stream_get_meta_data($file);
echo "Wrapper: ", $meta["wrapper_type"], "\n";
echo "Stream type: ", $meta["stream_type"], "\n";
echo "Mode: ", $meta["mode"], "\n";
echo "Seekable: ", var_export($meta["seekable"], true), "\n";
echo "URI: ", $meta["uri"], "\n";
echo "Size: ", fstat($file)["size"], " bytes\n";
fclose($file);
The metadata include the wrapper type, the stream type, the mode, whether the
stream is seekable, and the URI. The size of a file stream is available through
fstat.
$ php stream_metadata.php Wrapper: plainfile Stream type: STDIO Mode: rb Seekable: true URI: config.ini Size: 47 bytes
Compressed streams
The compress.zlib:// wrapper reads and writes gzip files
transparently.
<?php
$text = str_repeat("PHP streams are a unified I/O abstraction.\n", 200);
$archive = fopen("compress.zlib://data.gz", "wb");
fwrite($archive, $text);
fclose($archive);
echo "Original size: ", strlen($text), " bytes\n";
echo "Compressed size: ", filesize("data.gz"), " bytes\n";
$restored = file_get_contents("compress.zlib://data.gz");
echo "Restored correctly: ", var_export($restored === $text, true), "\n";
The data are compressed while they are written and decompressed when the file is
read back. The repetitive text compresses extremely well. The result is a
regular gzip file that can be opened with gunzip.
$ php compress_stream.php Original size: 8600 bytes Compressed size: 112 bytes Restored correctly: true
Streaming large files
Streams can process large files in chunks without loading them into memory.
<?php
$source = fopen("php://temp", "w+");
for ($i = 0; $i < 100_000; $i++) {
fwrite($source, "Line $i: " . str_repeat("x", 90) . "\n");
}
rewind($source);
$dest = fopen("copy.txt", "wb");
$bytes = 0;
while (!feof($source)) {
$chunk = fread($source, 8192);
$bytes += fwrite($dest, $chunk);
}
fclose($dest);
echo "Copied: ", number_format($bytes), " bytes\n";
printf("Peak memory: %.1f MB\n", memory_get_peak_usage() / 1024 / 1024);
rewind($source);
$dest = fopen("copy2.txt", "wb");
$copied = stream_copy_to_stream($source, $dest);
echo "stream_copy_to_stream copied: ", number_format($copied), " bytes\n";
fclose($source);
fclose($dest);
The program creates about 10 MB of data in a php://temp stream and
copies it to a file in 8 KB chunks. Peak memory usage stays low, because only
one chunk is held in memory at a time and php://temp moves its
content to a temporary file after 2 MB. The
stream_copy_to_stream function does the same copying in a single
call.
$ php large_file_stream.php Copied: 10,288,890 bytes Peak memory: 2.5 MB stream_copy_to_stream copied: 10,288,890 bytes
Socket streams
The stream_socket_client function opens a TCP connection.
<?php
$socket = stream_socket_client("tcp://example.com:80", $errno, $errstr, 10);
if ($socket === false) {
exit("Failed to connect: $errstr ($errno)\n");
}
fwrite($socket, "HEAD / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
$statusLine = fgets($socket);
echo "Status: ", trim($statusLine), "\n";
while (($line = fgets($socket)) !== false && trim($line) !== '') {
if (stripos($line, 'Content-Type:') === 0) {
echo trim($line), "\n";
}
}
fclose($socket);
The program sends a raw HTTP HEAD request to
example.com and reads the status line and the headers until the
empty line that separates them from the body. The fourth argument of
stream_socket_client is the connection timeout in seconds.
$ php socket_stream.php Status: HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8
A simple TCP server
The stream_socket_server function creates a listening socket. The
following server reverses the line it receives.
<?php
$server = stream_socket_server("tcp://127.0.0.1:8000", $errno, $errstr);
if ($server === false) {
exit("Failed to start server: $errstr ($errno)\n");
}
echo "Listening on 127.0.0.1:8000\n";
while ($client = stream_socket_accept($server, -1)) {
$line = trim(fgets($client));
fwrite($client, strrev($line) . "\n");
fclose($client);
}
The stream_socket_accept function waits for a client; the timeout
of -1 means it waits indefinitely. Each client is served in turn.
<?php
$client = stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 5);
if ($client === false) {
exit("Failed to connect: $errstr ($errno)\n");
}
fwrite($client, "an old falcon\n");
echo "Server replied: ", fgets($client);
fclose($client);
The client sends a line and prints the reply. We start the server in one terminal and run the client in another.
$ php tcp_server.php Listening on 127.0.0.1:8000
$ php tcp_client.php Server replied: noclaf dlo na
Best practices for streams
- Check for failure:
fopenand similar functions returnfalseon failure; check the result before using the stream. - Close streams: call
fclosewhen done, or keep the stream's scope short so that it is closed automatically. - Process data in chunks: read large files with
fgetsorfreadinstead of loading them whole. - Use contexts for HTTP: set timeouts and headers in a stream context; for complex HTTP work, consider the cURL extension or an HTTP client library.
- Use filters: apply stream filters to transform data on the fly without intermediate copies.
Source
In this tutorial, we explored how to work with streams in PHP, including file handling, in-memory and compressed streams, network communication, stream filters, and custom stream wrappers.
Author
List all PHP tutorials.