mkdir
Creates a directory, optionally creating every missing parent directory along the path.
mkdir(string $directory, int $permissions = 0777, bool $recursive = false, ?resource $context = null): bool- Added in PHP 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$directory | string | Path of the directory to create. |
$permissionsoptional | int= 0777 | Octal mode for the new directory, masked by the process umask. Ignored on Windows. |
$recursiveoptional | bool= false | When true, missing parent directories are created as well. |
$contextoptional | ?resource= null | A stream context resource created with stream_context_create(). |
Return value
booltrue on success, false on failure (including when the directory already exists).
Errors
Emits an E_WARNING when the directory already exists, when a parent is missing and recursive is false, or when permissions deny the operation.
mkdir() creates one directory. With $recursive set to true it behaves like mkdir -p and creates every missing segment of the path.
When to reach for it
Typical uses are date-partitioned storage (uploads/2026/08/), per-tenant or per-user folders, and cache trees built from a hash prefix. In all of those, pass true for $recursive so a fresh install works on the first request.
What trips people up
Two things. First, an existing directory is a failure: mkdir() returns false and emits an E_WARNING. Checking is_dir() first narrows the window but does not close it, because a parallel request can win the race between the check and the call. The robust idiom is to suppress the warning and re-check afterwards:
if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
throw new RuntimeException("Cannot create $dir");
}Second, the mode is masked by the umask. With a typical umask of 022, requesting 0777 yields 0755 on disk. Write the mode in octal with a leading zero — 755 without it is the decimal number 755, which is mode 01363. And with $recursive, the mode applies to every directory created, not only the last one.
Example
When to use it
- Create uploads/YYYY/MM before moving an uploaded file into date-partitioned storage.
- Build a two-level cache tree from a hash prefix, like cache/a3/f9/, on first write.
- Provision a per-tenant working directory when an account is created.
Frequently asked questions
Why does mkdir return false when the directory already exists?
PHP treats "already there" as a failure and emits an E_WARNING, matching the underlying mkdir(2) EEXIST error. Guard the call with is_dir(), and re-check is_dir() after a suppressed failure so concurrent requests do not throw.
Why did mkdir with 0777 produce a 0755 directory?
The requested mode is masked by the process umask, which is typically 022. The permissions argument is a ceiling, not a guarantee; set the umask or call chmod afterwards if you truly need the wider mode.
How do I create nested directories in PHP?
Pass true as the third argument: mkdir('a/b/c', 0775, true) creates every missing level, like mkdir -p. Without it, mkdir() fails if any parent is missing.
More examples
Race-safe directory creation
The output is "ready" and the second call produces no warning. The final is_dir() check after a suppressed failure is what makes this safe when two requests create the same directory at once.
<?php
function ensureDir(string $dir, int $mode = 0775): void {
if (!is_dir($dir) && !@mkdir($dir, $mode, true) && !is_dir($dir)) {
throw new RuntimeException("Cannot create directory: $dir");
}
}
ensureDir('uploads/2026/08');
ensureDir('uploads/2026/08'); // second call is a no-op, no warning
echo is_dir('uploads/2026/08') ? "ready\n" : "failed\n";
rmdir('uploads/2026/08'); rmdir('uploads/2026'); rmdir('uploads');Octal mode and the umask
The first line usually prints 755, not 777, because the process umask strips the group and other write bits. The last line shows why the leading zero matters: decimal 755 is mode 1363, not the 0755 you meant.
<?php
mkdir('open-dir', 0777);
mkdir('tight-dir', 0700);
printf("0777 requested -> %o\n", fileperms('open-dir') & 0777);
printf("0700 requested -> %o\n", fileperms('tight-dir') & 0777);
printf("755 without a leading zero is decimal %d = mode %o\n", 755, 755);
rmdir('open-dir'); rmdir('tight-dir');A hashed cache tree
Splitting the hash into two-character segments keeps any single directory from holding millions of entries. Recursive mkdir() creates both levels in one call.
<?php
$key = 'user:42:profile';
$hash = md5($key);
$dir = 'cache/' . substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
if (!is_dir($dir)) { mkdir($dir, 0775, true); }
file_put_contents("$dir/$hash.txt", 'cached payload');
echo "$dir/$hash.txt\n";
echo file_get_contents("$dir/$hash.txt"), "\n";
unlink("$dir/$hash.txt");
rmdir($dir); rmdir(dirname($dir)); rmdir('cache');
Discussion