skip to content

Files & Streams

PHP reads and writes files through one stream layer: fopen handles, whole-file helpers, directory functions and wrappers like php://memory. Interviewers probe locking, memory and paths.

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

explore

questions

15

In PHP, what goes wrong with mkdir($dir, 755, true), and how do you create a directory tree with the permissions you intend?

level: juniorimportance: must knowfreq 45%

answer

  1. decimal versus octal literal
  2. 0755 or 0o755 (8.1+)
  3. $recursive creates every missing parent
  4. the umask still trims the mode
  5. false plus E_WARNING when it already exists

basics

~20 s

755 without a leading zero is a decimal number, octal 1363, so the directory gets scrambled permissions. Write 0755 (or 0o755 since PHP 8.1), pass true to create parents, expect the umask to trim the mode, and treat an existing directory as a false return.

solid answer

~50 s

`mkdir(string $directory, int $permissions = 0777, bool $recursive = false)` takes the mode as an integer, and PHP reads `755` as decimal, which is octal `1363`: the directory gets a sticky bit and nonsense permission bits, often ones where the owner cannot even list it. Mode literals must be octal, `0755`, or `0o755` with the explicit prefix PHP 8.1 added. `$recursive = true` creates every missing parent with the same mode. The requested mode is then reduced by the process umask, so `0777` usually yields `0755`; if you need an exact mode, call `chmod()` afterwards rather than changing the umask, which the manual warns against in multithreaded servers. `mkdir()` returns `false` with an `E_WARNING` when the directory already exists, so the race-safe idiom is `if (!is_dir($d) && !mkdir($d, 0775, true) && !is_dir($d)) { throw … }`. Finally, remember who runs it: under a web server the process user, not you, needs write access to the parent.

go deeper

for a junior

Recall the signature, that modes are octal literals like 0755, and that mkdir() returns false with a warning instead of throwing.

for a middle

Explain why 755 becomes octal 1363, how the umask trims the requested mode, and why chmod() afterwards gives an exact mode.

for a senior

Show the race-safe is_dir/mkdir/is_dir idiom and diagnose failures by the process user and is_writable(), not by your own shell account.

for a principal

Set a convention for where the application may create directories, with which modes and owners, so deploys and workers never fight over permissions.

## The signature ```php mkdir(string $directory, int $permissions = 0777, bool $recursive = false, $context = null): bool ``` - **`$permissions`** is an integer holding Unix mode bits. It is ignored on Windows. - **`$recursive`** creates every missing parent directory, each with the same `$permissions`. - The return value is `true` on success, or `false` plus an `E_WARNING` (such as *File exists* or *Permission denied*) on failure. It does not throw. ## The octal trap Permission modes are written in octal, and PHP only treats an integer literal as octal when it starts with `0` (or, since PHP 8.1, with the explicit `0o` prefix). So: | Literal | Value PHP passes | Result | |---|---|---| | `0755` | octal 755 | `rwxr-xr-x` before the umask | | `0o755` | octal 755 (PHP 8.1+) | same as `0755`, clearer to read | | `755` | decimal 755 = octal **1363** | sticky bit set and scrambled bits; with a umask of 022 the owner ends up with `-wx`, unable to list the directory | | `'0755'` | a string; under `strict_types` a `TypeError`, otherwise coerced as decimal 755 | same bug as `755`, or a crash | The same trap applies to `chmod($file, 644)`, which sets a mode nobody wanted. Mode values read from configuration arrive as strings; convert them with `octdec('755')` instead of casting. ## What the umask does to the mode The operating system clears from the requested mode every bit set in the process **umask**. PHP exposes it through `umask(?int $mask = null): int`, which returns the current value when called without an argument. With the common umask `022`, `mkdir($d, 0777)` creates `0755` and `mkdir($d, 0775)` creates `0755` too. If a directory must have an exact mode, such as group-writable `0775` for a shared cache: 1. Create it with `mkdir($d, 0775, true)`. 2. Call `chmod($d, 0775)` on it afterwards; `chmod()` is not filtered by the umask. The manual advises against calling `umask()` in multithreaded web servers, because every thread shares one process umask. As a server module PHP restores it after each request, but concurrent requests can still see each other's value. `chmod()` after creation has no such side effect. Note that with `$recursive`, only the final directory gets your explicit `chmod()` unless you walk the parents too. ## Existing directories and races `mkdir()` treats an existing target as an error: it returns `false` and warns, even with `$recursive = true` (existing *parents* are fine; only the last component counts). Two workers creating the same cache directory at the same moment will therefore see one success and one warning. The idiom that survives the race: ```php <?php declare(strict_types=1); $dir = __DIR__ . '/var/invoices/2026/09'; if (!is_dir($dir) && !mkdir($dir, 0o775, true) && !is_dir($dir)) { throw new RuntimeException("Cannot create {$dir}"); } ``` Read it as: if it is missing, try to create it; if that failed, check once more, because another process may have created it in between; only then is it a real error. ## Who is creating it Permissions are checked against the **user the PHP process runs as**. A script that works from your shell can fail under PHP-FPM, whose pool runs as a different user. Useful checks: - `is_writable($parent)` tells you whether this process may create entries there. - `fileperms($path)` returns the full `st_mode`, including file-type bits; `substr(sprintf('%o', fileperms($path)), -4)` shows it as `0755`. - `is_dir($path)` distinguishes a directory from a regular file with the same name, a common reason for a *File exists* warning. ## Reading the warning `mkdir()` reports the operating system's reason in its `E_WARNING`, and each one points to a different fix: | Warning text | Usual cause | Fix | |---|---|---| | *File exists* | the final directory, or a regular file with that name, is already there | treat as success after `is_dir()`, or pick another name | | *No such file or directory* | a parent is missing and `$recursive` was `false` | pass `true` as the third argument | | *Permission denied* | the process user cannot write to the parent | fix ownership or group of the parent, not `0777` everywhere | ## Checklist - Always write modes as `0755` or `0o755`, never `755`. - Pass `true` as the third argument when parents may be missing. - Expect the umask to reduce the mode; use `chmod()` for exact modes. - Handle the "already exists" case with the double `is_dir()` idiom. - Check the result; a `false` from `mkdir()` means the next write into that directory fails too.

  • How do you convert a permission string such as "0755" from a config file into a mode for chmod()?
    Use `octdec('0755')` or `octdec('755')`, which interpret the digits as octal and return 493. A plain `(int) '0755'` cast reads the string as decimal 755, which is octal 1363, and reproduces the literal bug. Validate the string first so only digits 0-7 are accepted.
  • Why can mkdir() succeed from the command line but fail when the same code runs in a web request?
    Permissions are checked against the process user. From a shell that is you; under PHP-FPM it is the pool's user, which may not own or have write access to the parent directory. `is_writable($parent)` inside the web request shows what that process can do. open_basedir, if set, can also refuse paths outside its list.
  • What does mkdir($d, 0775, true) do when $d already exists?
    It returns `false` and emits an `E_WARNING` such as *File exists*. The recursive flag only tolerates parents that already exist; an existing final directory is still treated as an error. Check with `is_dir()` before and after the call to make the operation idempotent and race-safe.

saying these in an interview costs you the question

  • mkdir($dir, 755) sets rwxr-xr-x because PHP reads mode numbers as octal.
  • mkdir() with the recursive flag returns true when the directory already exists.
  • The mode passed to mkdir() is applied exactly, ignoring the umask.
  • Calling umask(0) per request is the safe fix in a threaded server.
  • If mkdir() works from the shell it will work under the web server.
open as a page

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%

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.

open as a page

In PHP, why can is_file() or filesize() keep returning an old result inside one script, and when is clearstatcache() actually needed?

level: middleimportance: must knowfreq 40%

basics

~20 s

PHP caches the last stat() result, and is_file(), filesize(), filemtime() and similar functions reuse it for the same path. If another process changes that file, repeated checks in one script can see old data until clearstatcache() is called.

open as a page

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%

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().

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, 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 tmpfile(), tempnam() and sys_get_temp_dir() differ, and why is a temp name built from time() or uniqid() unsafe?

level: middleimportance: should knowfreq 30%

basics

~20 s

tmpfile() returns an open handle to a file deleted when closed; tempnam() atomically creates a uniquely named 0600 file and returns its path, which you must delete; sys_get_temp_dir() only names the directory. Guessable names can collide or be pre-created by another user.

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, 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

In PHP, how do you replace a file so that readers never see a half-written version, and when does rename() stop being atomic?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Write the new content to a temporary file in the destination's directory, fix its mode, then rename() it over the old file; on one file system that swap is atomic. Across file systems PHP falls back to copy-and-delete, which readers can catch half-written.

open as a page

In PHP, a nightly job must delete generated PDF invoices older than 90 days from a directory with a million files; how do you write it safely?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Iterate the directory with DirectoryIterator instead of glob() or scandir(), skip dot entries, symlinks and non-PDF names, compare getMTime() with a cutoff, and check unlink()'s return. Files still being written should live under temp names so the job never sees them.

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

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