skip to content

Reading & Writing

fopen modes, fgets and fwrite, the file_get_contents and file_put_contents shortcuts, and flock for concurrent writers. Interviewers ask how you read a huge log without running out of memory.

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

explore

questions

5

In PHP, when do you use file_get_contents() and file_put_contents() instead of fopen() with fgets() and fwrite(), and what does each approach cost?

level: juniorimportance: must knowfreq 65%

answer

  1. whole file versus one piece at a time
  2. string|false on failure, plus E_WARNING
  3. memory_limit is 128M by default
  4. FILE_APPEND and LOCK_EX flags
  5. compare the result with === false

basics

~20 s

file_get_contents() and file_put_contents() read or write a whole file in one call, so the entire content sits in memory. fopen() returns a handle for fgets(), fread() and fwrite(), keeping memory flat for large or streamed files.

solid answer

~50 s

`file_get_contents($path)` returns the whole file as one string, or `false` plus an `E_WARNING` when it cannot open it. `file_put_contents($path, $data)` opens, writes and closes in one call and returns the number of bytes written or `false`; by default it truncates, `FILE_APPEND` makes it append and `LOCK_EX` takes an exclusive lock for the write. They are the right tool for small files: a template, a JSON document, a cache entry. The cost is memory, because the whole content becomes one string, and a file bigger than what is left of `memory_limit` (128M by default) ends in a fatal "Allowed memory size exhausted" error. `fopen()` returns a stream handle that you read with `fgets()`/`fread()` and write with `fwrite()`, so only one line or chunk is in memory at a time; the price is more code and an `fclose()`.

code

php · 11 lines
php
<?php
declare(strict_types=1);

$config = file_get_contents(__DIR__ . '/config.json');
if ($config === false) {
    throw new RuntimeException('config.json is missing or unreadable');
}
$settings = json_decode($config, true, flags: JSON_THROW_ON_ERROR);

// Overwrite a small cache file; LOCK_EX serialises concurrent writers.
file_put_contents(__DIR__ . '/cache/settings.php', '<?php return ' . var_export($settings, true) . ';', LOCK_EX);

go deeper

for a junior

Recall that the shortcuts load or write the whole file in one call, return false on failure, and that FILE_APPEND switches file_put_contents() from truncate to append.

for a middle

Explain the memory trade-off against memory_limit, why failure is tested with === false, and what the $offset, $length and LOCK_EX options add.

for a senior

Show you pick the handle API for unbounded input, close handles in long-running scripts, and turn the false-plus-warning result into an exception at the boundary.

for a principal

Frame it as a policy question: which code paths may assume bounded file sizes, and where a streaming abstraction is mandatory so an import never depends on memory_limit.

## Two styles of file I/O in PHP PHP gives you two ways to work with a file's contents: - **The one-call shortcuts** — `file_get_contents()`, `file_put_contents()` and `file()` — open the file, do the whole job and close it before they return. - **The handle API** — `fopen()` returns a **stream resource** (a handle), and you then call `fgets()`, `fread()`, `fwrite()`, `fseek()` and finally `fclose()` on it. Both end up in the same stream layer, so both accept local paths and the stream wrappers PHP supports. What differs is **how much of the file lives in PHP memory at once** and how much control you keep between operations. ## The shortcuts and what they return | Function | Does | Returns on success | Returns on failure | |---|---|---|---| | `file_get_contents($path)` | reads the whole file into one string | `string` | `false` + `E_WARNING` | | `file($path, $flags)` | reads the whole file into an array of lines | `array` | `false` + `E_WARNING` | | `file_put_contents($path, $data, $flags)` | writes `$data`, then closes | bytes written (`int`) | `false` | Details worth knowing: - `file_put_contents()` **truncates** an existing file unless you pass `FILE_APPEND`. Passing `LOCK_EX` takes an exclusive `flock()` on the file for the duration of the write; the flags combine with `|`. - `$data` may be a string, an array (its elements are written one after another with no separator) or a stream resource, whose remaining contents are copied. - `file_get_contents()` also takes an `$offset` and a `$length`, so `file_get_contents($path, false, null, 100, 50)` reads 50 bytes starting at byte 100. A negative offset counts from the end of the file. - `file()` keeps the line ending on every element unless you pass `FILE_IGNORE_NEW_LINES`. ## The handle API ```php <?php declare(strict_types=1); $in = fopen('/var/data/input.txt', 'r'); if ($in === false) { throw new RuntimeException('cannot open input'); } while (($line = fgets($in)) !== false) { // one line in memory at a time } fclose($in); ``` - `fopen($path, $mode)` returns a resource or `false`; the mode string (`'r'`, `'w'`, `'a'`, `'x'`, `'c'`, with an optional `'+'`) decides what happens to existing content. - `fgets($h)` returns the next line including its newline, or `false` when nothing is left. - `fread($h, $n)` returns up to `$n` bytes; `fwrite($h, $s)` returns the number of bytes written or `false`. - `fclose($h)` releases the handle. PHP also closes every handle at the end of the request, but a long script or worker that opens files in a loop without closing them can run into the operating system's open-file limit. ## Memory is the deciding cost A shortcut turns the whole file into a PHP string, and `file()` turns it into an array of strings, each with its own header and an array slot, so its footprint grows beyond the file size. Every script runs under `memory_limit` (128M in the built-in default and in both shipped php.ini files). A read that needs more stops the script with the fatal error *Allowed memory size of N bytes exhausted*. With a handle, memory stays near the size of one line or one chunk, no matter how large the file is. ## Checking for failure correctly Neither style throws on a missing or unreadable file: both return `false` and emit an `E_WARNING`. Always compare with `=== false`, because an empty file returns `""` and a file containing `0` returns `"0"`, and both are falsy: 1. Call the function and keep the result. 2. Test `$result === false`, never `!$result`. 3. Turn the failure into an exception or a clear error of your own, since the warning alone does not stop the script. ## Choosing - **Small, bounded files** (config, templates, a JSON payload, a cache file): the shortcuts — one line, closed for you. - **Large or unbounded files, logs, imports, exports**: `fopen()` plus `fgets()`/`fread()`/`fwrite()`, or `SplFileObject`, so memory stays flat. - **Several operations on the same open file** (lock, read, rewrite, unlock): a handle, because the shortcuts close the file before you can do the next step.

  • What does file_put_contents() do when you pass it an array instead of a string?
    It writes each element's string value one after another with no separator, so `['a', 'b']` produces `ab`. If you want lines, add the newlines yourself, for example with `implode("\n", $lines) . "\n"`. It also accepts a stream resource as `$data` and copies whatever remains in that stream into the file.
  • How do you read only part of a file with file_get_contents()?
    Pass the fourth and fifth arguments: `file_get_contents($path, false, null, $offset, $length)`. The offset may be negative to count from the end of the file, which is handy for the last few kilobytes of a log. A negative `$length` throws a `ValueError`. Seeking with an offset is not supported reliably on remote streams.
  • Why might a script that reads files in a loop fail even though each file is small?
    If it uses `fopen()` and forgets `fclose()`, each iteration keeps another descriptor open until the request ends, and a long CLI job or worker can hit the operating system's per-process open-file limit, after which `fopen()` returns `false`. The shortcuts close the file themselves, and a handle is also released when its last reference goes away.

saying these in an interview costs you the question

  • file_get_contents() streams the file, so it is safe for files of any size.
  • Checking if (!$data) is enough to detect that the read failed.
  • file_put_contents() appends to an existing file by default.
  • fopen() throws an exception when the file does not exist.
  • An unclosed fopen() handle stays open for later requests to reuse.
open as a page

In PHP, how do you process a multi-gigabyte log file line by line without hitting memory_limit, and which loop mistakes still break it?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Open the file with fopen() and loop while fgets() does not return false, or iterate an SplFileObject, so only one line is in memory. file() and file_get_contents() load everything, and collecting results in an array grows memory again.

open as a page

In PHP's fopen(), what do the modes 'r', 'w', 'a', 'x' and 'c' do, and what does adding '+' change?

level: juniorimportance: should knowfreq 50%

basics

~20 s

'r' reads an existing file; 'w' creates or truncates for writing; 'a' creates or opens and always writes at the end; 'x' creates only when the file is absent; 'c' creates or opens without truncating. '+' adds reading or writing.

open as a page

In PHP, why is fopen($path, 'w') followed by flock() the wrong way to update a shared file, and what is the correct sequence?

level: middleimportance: should knowfreq 35%

basics

~20 s

Mode 'w' truncates the file inside fopen(), before flock() runs, so another process can read an empty file or lose an update. Open with 'c+', take LOCK_EX, read, ftruncate() and rewind(), write, fflush(), then release with LOCK_UN.

open as a page

In PHP, many concurrent requests append records to one audit log file; why can records interleave or tear, and how do you make each append safe?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Records tear when one record is written in several fwrite() calls, when writers seek to the end themselves, or when some writers skip the lock. Build the whole record first, then file_put_contents($log, $record, FILE_APPEND | LOCK_EX), or fopen() 'a', flock() LOCK_EX, one fwrite(), release.

open as a page