skip to content

Directories & Permissions

Creating, moving and inspecting paths with mkdir, rename, unlink, chmod and realpath, plus directory iteration and the stat cache. Interviewers probe stale is_file results and safe temp files.

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

explore

questions

5

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