rmdir
Removes an empty directory from the filesystem.
rmdir(string $directory, ?resource $context = null): bool- Added in PHP 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$directory | string | Path to the directory to remove. It must exist and be empty. |
$contextoptional | ?resource= null | A stream context resource created with stream_context_create(), used by stream wrappers that support directory removal. |
Return value
boolReturns true on success or false on failure.
Errors
Emits an E_WARNING on failure, for example when the directory does not exist, is not empty, or permissions forbid the operation.
rmdir() deletes a single directory. It is deliberately narrow: the directory must already be empty and the parent directory must be writable by the process. It does not recurse, and it will not delete a file — that is unlink()'s job.
When to reach for it
- Cleaning up a per-job working directory once its files have been removed.
- Removing a lock directory used as a cheap mutex.
- The final step of a recursive delete routine, after the contents are gone.
Recursive deletion
PHP has no built-in recursive remove. Walk the tree bottom-up and call unlink() on files and rmdir() on directories, or use RecursiveIteratorIterator with RecursiveDirectoryIterator::CHILD_FIRST. Always skip the . and .. entries that scandir() returns.
What trips people up
"Empty" means empty to the operating system, including dotfiles. A stray .DS_Store, .gitkeep or editor swap file will make rmdir() fail with a warning even though the directory looks empty in a file browser. glob('dir/*') will not show those; use scandir() instead.
PHP also caches stat results, so is_dir() may still report true right after a successful rmdir() in the same request. Call clearstatcache() before re-checking.
Example
When to use it
- Delete a temporary upload staging folder after moving its files into permanent storage.
- Release a lock implemented as mkdir('/path/job.lock') at the end of a cron script.
- Prune empty date-based directories left behind by a log rotation job.
Frequently asked questions
How do I delete a directory with files in it in PHP?
rmdir() cannot do it alone — delete the contents first. Iterate the directory bottom-up with RecursiveDirectoryIterator::CHILD_FIRST, calling unlink() on files and rmdir() on directories.
Why does rmdir say the directory is not empty when it looks empty?
It almost certainly contains hidden entries such as .DS_Store, .gitkeep or a lock file. List it with scandir() rather than glob(), which skips names starting with a dot.
What is the difference between rmdir and unlink?
rmdir() removes directories and only empty ones; unlink() removes files. On Windows, a symlink pointing at a directory must be removed with rmdir(), not unlink().
More examples
Recursive delete, children first
Directories are removed from the deepest level upward. Calling rmdir('demo_dir') first would fail, because the parent still contains the sub directory.
<?php
mkdir('demo_dir/sub', 0777, true);
file_put_contents('demo_dir/sub/f.txt', 'data');
foreach (['demo_dir/sub/f.txt'] as $f) { unlink($f); }
rmdir('demo_dir/sub');
var_dump(rmdir('demo_dir'));
clearstatcache();
var_dump(file_exists('demo_dir'));Check emptiness including dotfiles
scandir() reports the hidden entry so you know rmdir() would fail, while glob() returns an empty array and would make the directory look ready to delete.
<?php
mkdir('demo_dir');
file_put_contents('demo_dir/.hidden', '');
$entries = array_diff(scandir('demo_dir'), ['.', '..']);
var_dump($entries); // the dotfile is listed
var_dump(glob('demo_dir/*')); // glob misses it
unlink('demo_dir/.hidden');
rmdir('demo_dir');Directory as a lock
mkdir() is atomic, so the second call returns false while the lock is held. rmdir() releases it. This pattern needs no lock file cleanup on read.
<?php
if (@mkdir('job.lock')) {
echo "lock acquired\n";
var_dump(@mkdir('job.lock')); // second attempt fails
rmdir('job.lock');
echo "lock released\n";
}
clearstatcache();
var_dump(is_dir('job.lock'));
Discussion