skip to content

Wrappers & Contexts

Every PHP file function goes through a stream wrapper, so php://memory, data://, compress.zlib:// and http:// URLs work like paths. Interviewers probe contexts, filters and allow_url_fopen.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In PHP, what is the difference between php://memory and php://temp, and how would you use one to buffer a generated report before sending it?

level: middleimportance: must knowfreq 40%

answer

  1. one stays in RAM, one spills to disk
  2. 2 MB default threshold
  3. /maxmemory:NN in bytes
  4. rewind() before reading back
  5. fpassthru() or stream_copy_to_stream()

basics

~20 s

php://memory keeps all data in RAM, limited only by memory_limit; php://temp keeps up to 2 MB in memory, then moves to a temporary file. Write the report into php://temp, rewind(), then stream it out with fpassthru() or stream_copy_to_stream().

solid answer

~40 s

Both are read-write streams you open with `fopen('php://memory', 'r+')` or `fopen('php://temp', 'r+')` and use like a file. `php://memory` holds everything in RAM, so a large report counts fully against `memory_limit`. `php://temp` holds data in memory until it passes a threshold, 2 MB by default or `php://temp/maxmemory:NN` in bytes, then continues in a temporary file in the same directory `sys_get_temp_dir()` reports. For a report of unknown size I write rows into `php://temp`, read the size from `fstat($h)['size']` if I need a Content-Length, call `rewind($h)`, and send it with `fpassthru($h)` or `stream_copy_to_stream($h, $out)`. The classic bugs are forgetting `rewind()`, which makes `stream_get_contents()` return an empty string, and turning the whole buffer back into one string, which throws away the memory benefit.

go deeper

for a junior

Recall that php://memory is RAM only while php://temp spills to a temp file after 2 MB, and that you must rewind() before reading back.

for a middle

Explain the maxmemory parameter, why stream_get_contents() returns an empty string without rewind(), and why each fopen() creates a new stream.

for a senior

Show you buffer large exports in php://temp, send them with fpassthru() or stream_copy_to_stream(), and never turn the buffer back into one giant string.

for a principal

Decide when to buffer at all: complete-before-send guarantees cost memory, disk and latency, while streaming straight out is cheaper but cannot retract a half-sent response.

## Two in-memory stream wrappers PHP's `php://` wrapper includes two streams that behave like a file but have no name on disk: - **`php://memory`** stores its data in memory, always. - **`php://temp`** stores its data in memory until it exceeds a limit, then switches to a temporary file. The default limit is **2 MB**; `php://temp/maxmemory:NN` sets it to `NN` bytes. The file goes in the same directory `sys_get_temp_dir()` returns. Both are opened with `fopen()`, usually in a read-write mode such as `'r+'`, both support `fwrite()`, `fread()`, `fseek()`, `rewind()` and `fstat()`, and both vanish when closed. | | `php://memory` | `php://temp` | |---|---|---| | Where data lives | RAM only | RAM up to the limit, then a temp file | | Memory cost for a 200 MB report | about 200 MB, subject to `memory_limit` | about the limit (2 MB by default) | | Speed | fastest for small data | same as memory until it spills, then disk I/O | | Typical use | small buffers, tests, faking a file handle | output of unknown or large size | ## The report pattern A controller builds an export, needs to know when it is complete (for example to compute a checksum or a size, or to fail cleanly before any byte is sent), and only then sends it: ```php <?php declare(strict_types=1); $buf = fopen('php://temp/maxmemory:' . (4 * 1024 * 1024), 'r+'); foreach ($rows as $row) { fwrite($buf, json_encode($row, JSON_THROW_ON_ERROR) . "\n"); } $size = fstat($buf)['size']; rewind($buf); header('Content-Type: application/x-ndjson'); header('Content-Length: ' . $size); fpassthru($buf); fclose($buf); ``` Step by step: 1. **Open** `php://temp` with a limit that suits the host: small reports never touch disk, large ones cost only the limit in RAM. 2. **Write** rows as they are produced. If generation fails half-way, nothing has been sent yet, so the response can still be an error page. 3. **Measure** with `fstat()`, which both wrappers support. 4. **`rewind()`**: after writing, the pointer is at the end, and reading from there returns nothing. 5. **Send** with `fpassthru($buf)`, which copies the rest of the stream to the output, or `stream_copy_to_stream($buf, fopen('php://output', 'w'))`. `php://output` is a write-only stream that goes through the same output-buffering layer as `echo`. ## Reading it back as a string `stream_get_contents($stream, ?int $length = null, int $offset = -1)` returns the remaining content as one string. With the default offset `-1` it starts at the **current position**, which is why calling it right after writing returns `""`. Pass offset `0` or call `rewind()` first. Keep in mind that turning a 200 MB buffer into a string needs 200 MB, which cancels what `php://temp` saved. ## Pitfalls - **Not reusable.** Each `fopen('php://memory', …)` creates a new, empty stream. `file_put_contents('php://memory', 'x')` followed by `file_get_contents('php://memory')` returns nothing, because the second call opened a different stream. - **`php://memory` is not free.** Its content counts toward `memory_limit`, so it only moves the problem when the data is large. - **The limit is in bytes.** `maxmemory:5` means five bytes, not five megabytes. - **Temp files and disk space.** After spilling, `php://temp` writes to the temp directory, which may be a small `tmpfs`; a huge export can fill it. - **Casting to a C `FILE*`.** The manual notes that some extensions need a standard I/O stream and may fail to cast a memory stream, notably on Windows. ## Choosing the threshold The `maxmemory` value is a trade between RAM and disk per concurrent request: - Multiply it by the number of workers that can build reports at the same time; that is the worst-case memory the buffers add on top of normal usage. - Keep it well below `memory_limit`, because the rows, the encoder and the framework also need memory. - A few megabytes is usually enough to keep typical reports in RAM and push only the rare large export to disk. - If the temp directory is a RAM-backed `tmpfs`, spilling saves PHP memory but not machine memory. ## When to skip the buffer If the report does not need a size, checksum or all-or-nothing behaviour, write rows straight to `php://output` as they are generated. The buffer is worth it when the response must be complete and valid before its first byte leaves.

  • Why does stream_get_contents($buf) return an empty string right after the rows were written?
    Writing leaves the pointer at the end of the stream, and `stream_get_contents()` with its default offset of `-1` reads from the current position. There is nothing after the end, so it returns `""`. Call `rewind($buf)` first, or pass an offset: `stream_get_contents($buf, null, 0)`.
  • When would you still pick php://memory over php://temp?
    When the data is small and bounded and you want no chance of touching disk: a unit test that needs a real stream handle, building a small attachment, or feeding a function that expects a resource. For output whose size depends on user data, `php://temp` caps the memory cost at its threshold.
  • How do you check that a php://temp buffer really spilled to disk?
    Write past the threshold and compare `memory_get_usage()` before and after; with `php://temp` it stays roughly flat beyond the limit, while `php://memory` grows with the data. The temporary file is created in the directory `sys_get_temp_dir()` reports, so watching that directory during a large export also shows it.

php://memory is a whiteboard: fast, but when it is full you are out of space. php://temp is a whiteboard with a filing cabinet next to it: once the board fills, the rest goes into the cabinet, and you read it back the same way.

saying these in an interview costs you the question

  • php://memory writes to disk automatically once memory_limit is near.
  • php://temp/maxmemory:5 keeps five megabytes in memory.
  • Opening php://memory twice returns the same shared buffer.
  • stream_get_contents() always reads from the start of the stream.
  • php://temp never uses the disk, it is just a faster php://memory.
open as a page

In PHP, what happens when file_get_contents() is given an https:// URL, and what does the allow_url_fopen setting control?

level: juniorimportance: should knowfreq 40%

basics

~20 s

The https:// prefix selects PHP's HTTP stream wrapper, which sends a GET request and returns the body. allow_url_fopen, On by default and changeable only in system configuration, decides whether URL wrappers such as http://, https://, ftp:// and data:// may be opened.

open as a page

In PHP, how do you send a JSON POST with a timeout through file_get_contents() and stream_context_create(), and read the response status?

level: middleimportance: should knowfreq 35%

basics

~20 s

Build a context with stream_context_create(['http' => ['method' => 'POST', 'header' => …, 'content' => $json, 'timeout' => 5.0, 'ignore_errors' => true]]), pass it to file_get_contents(), then read the status line from http_get_last_response_headers(), which replaces the deprecated $http_response_header.

open as a page

A PHP endpoint passes a request parameter straight to file_get_contents(); besides path traversal, what can an attacker reach through stream wrappers, and how do you close it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A value that starts with a scheme picks the wrapper: it can make the server fetch internal URLs, return attacker-supplied data: bytes, or read files through php://filter, compress.zlib:// or phar://. Map IDs to paths or prefix a fixed directory, and disable unused wrappers.

open as a page

In PHP, how do stream filters and the compress.zlib:// wrapper let you gzip or base64-encode a large file while streaming it?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

stream_filter_append() attaches a filter such as convert.base64-encode or zlib.deflate to a handle's read or write chain, so data is transformed chunk by chunk. compress.zlib:// opens a gzip file as a stream. Either way stream_copy_to_stream() moves the data without loading it whole.

open as a page