PHP Directories
Working With Directories
In simple terms: Directories (folders) organize files. PHP can create folders, list what is inside them, search for files by pattern, walk through nested folders, and delete folders — which is needed for upload storage, caches, backups, reports, and file managers.
Technically: PHP offers procedural functions (mkdir(), rmdir(), scandir(), glob(), opendir()/readdir()) and object-oriented iterators from the SPL (DirectoryIterator, FilesystemIterator, RecursiveDirectoryIterator). Iterators are the most flexible choice for traversing directory trees.
Example Folder Used in This Lesson
<?php
$dir = __DIR__ . '/demo-dir';
mkdir("$dir/docs", 0755, true);
mkdir("$dir/images/photos", 0755, true);
file_put_contents("$dir/docs/readme.txt", "Read me now\n"); // 12 bytes
file_put_contents("$dir/docs/guide.md", "# Guide\n"); // 8 bytes
file_put_contents("$dir/images/logo.png", str_repeat('x', 1024));
file_put_contents("$dir/images/photos/beach.jpg", str_repeat('x', 2048));
file_put_contents("$dir/notes.txt", "Notes\n"); // 6 bytes
This creates:
demo-dir
├── docs
│ ├── guide.md
│ └── readme.txt
├── images
│ ├── logo.png
│ └── photos
│ └── beach.jpg
└── notes.txt
Creating Directories: mkdir()
Syntax
mkdir(string $directory, int $permissions = 0777, bool $recursive = false): bool
<?php
$path = __DIR__ . '/storage/uploads/2026/10';
if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) {
throw new RuntimeException("Cannot create directory: $path");
}
echo "Ready: " . (is_dir($path) ? 'yes' : 'no') . "\n";
Output
Ready: yes
How It Works
$recursive = truecreates all missing parent folders (storage,uploads,2026) in one call. Without it,mkdir()fails if a parent does not exist.0755means: owner can read, write, and enter; others can read and enter. The final permissions are also affected by the process'sumask.mkdir()fails with a warning if the folder already exists. The pattern above checksis_dir()first and again after a failedmkdir()— this handles the case where another request created the folder at the same moment.
Organizing uploads by date (uploads/2026/10) keeps the number of files per folder manageable.
Listing a Directory
scandir()
<?php
$dir = __DIR__ . '/demo-dir';
print_r(scandir($dir));
$entries = array_values(array_diff(scandir($dir), ['.', '..']));
echo implode(', ', $entries) . "\n";
Output
Array
(
[0] => .
[1] => ..
[2] => docs
[3] => images
[4] => notes.txt
)
docs, images, notes.txt
scandir()returns all entries, sorted alphabetically, including the special entries.(the folder itself) and..(the parent folder), which must almost always be removed.- It returns names only, not full paths, and does not distinguish files from folders — use
is_file("$dir/$name")oris_dir(...).
glob() — Matching a Pattern
<?php
$dir = __DIR__ . '/demo-dir';
print_r(array_map('basename', glob("$dir/*.txt")));
print_r(array_map('basename', glob("$dir/*", GLOB_ONLYDIR)));
Output
Array
(
[0] => notes.txt
)
Array
(
[0] => docs
[1] => images
)
| Pattern | Matches |
|---|---|
* | Any number of characters |
? | Exactly one character |
[abc] | One of the listed characters |
glob()returns full paths (as built from the pattern), sorted alphabetically, and never includes.or...GLOB_ONLYDIRreturns only directories.glob()searches one level only;*does not descend into subfolders.- It returns an empty array when nothing matches (and
falseon some errors), so useglob(...) ?: []when looping.
FilesystemIterator
<?php
$dir = __DIR__ . '/demo-dir';
foreach (new FilesystemIterator($dir) as $item) {
$type = $item->isDir() ? 'folder' : $item->getSize() . ' bytes';
echo $item->getFilename() . " ($type)\n";
}
Output (order depends on the filesystem)
docs (folder)
images (folder)
notes.txt (6 bytes)
Each entry is an SplFileInfo object with methods such as getFilename(), getPathname(), getExtension(), getSize(), getMTime(), isDir(), and isFile(). . and .. are skipped automatically. Unlike scandir() and glob(), the iteration order is not sorted — sort the results yourself if order matters.
The older opendir() / readdir() / closedir() functions do the same job procedurally; you will see them in legacy code.
Walking Through Nested Directories
RecursiveDirectoryIterator combined with RecursiveIteratorIterator visits every file in a folder and all of its subfolders:
<?php
$dir = __DIR__ . '/demo-dir';
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
);
$totalSize = 0;
$byExtension = [];
foreach ($files as $file) {
if (!$file->isFile()) {
continue;
}
$size = $file->getSize();
$extension = strtolower($file->getExtension());
$totalSize += $size;
$byExtension[$extension] = ($byExtension[$extension] ?? 0) + $size;
}
arsort($byExtension);
echo "Total: $totalSize bytes\n";
foreach ($byExtension as $extension => $size) {
echo str_pad($extension, 5) . "$size bytes\n";
}
Output
Total: 3098 bytes
jpg 2048 bytes
png 1024 bytes
txt 18 bytes
md 8 bytes
How It Works
RecursiveDirectoryIteratorcan look inside subfolders;SKIP_DOTSignores.and...RecursiveIteratorIteratorflattens the tree, so a singleforeachvisits every entry at every depth. By default it yields leaves (files), not folders.- The loop adds up file sizes in total and per extension. The two
.txtfiles (12 + 6 bytes) are combined.
The same pattern is used to calculate storage usage per user, find large or old files, or build a search index.
Practical Example: Printing a Directory Tree
<?php
function printTree(string $path, string $prefix = ''): void
{
$entries = array_values(array_diff(scandir($path), ['.', '..']));
$last = count($entries) - 1;
foreach ($entries as $index => $name) {
$isLast = $index === $last;
echo $prefix . ($isLast ? '└── ' : '├── ') . $name . "\n";
if (is_dir("$path/$name")) {
printTree("$path/$name", $prefix . ($isLast ? ' ' : '│ '));
}
}
}
$dir = __DIR__ . '/demo-dir';
echo basename($dir) . "\n";
printTree($dir);
Output
demo-dir
├── docs
│ ├── guide.md
│ └── readme.txt
├── images
│ ├── logo.png
│ └── photos
│ └── beach.jpg
└── notes.txt
Step-by-Step Explanation
scandir()provides sorted names;.and..are removed.- Each entry is printed with
├──, or└──for the last entry in a folder. - For folders, the function calls itself (recursion — see Recursive Functions) with a longer prefix:
│continues the vertical line if more entries follow at this level, and four spaces end it.
Path Helpers
<?php
$path = '/var/www/shop/storage/reports/sales-2026-10.csv';
echo dirname($path) . "\n";
echo dirname($path, 2) . "\n";
echo basename($path) . "\n";
echo basename($path, '.csv') . "\n";
print_r(pathinfo($path));
Output
/var/www/shop/storage/reports
/var/www/shop/storage
sales-2026-10.csv
sales-2026-10
Array
(
[dirname] => /var/www/shop/storage/reports
[basename] => sales-2026-10.csv
[extension] => csv
[filename] => sales-2026-10
)
| Function | Purpose |
|---|---|
dirname($path, $levels) | Parent directory (optionally several levels up) |
basename($path, $suffix) | Last component, optionally without a suffix |
pathinfo($path) | All parts at once |
realpath($path) | Canonical absolute path; false if it does not exist |
getcwd() / chdir() | Get / change the current working directory |
disk_free_space($dir) | Free space on the filesystem, in bytes |
Deleting Directories
rmdir() removes only empty folders. To delete a folder with its contents, delete the contents first (deepest first), as shown in File Delete / Rename / Copy:
<?php
function deleteDirectory(string $path): void
{
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
}
rmdir($path);
}
deleteDirectory(__DIR__ . '/demo-dir');
var_dump(is_dir(__DIR__ . '/demo-dir'));
Output
bool(false)
CHILD_FIRST makes the iterator return each folder's contents before the folder itself. Double-check the path before calling such a function — the deletion is permanent.
Common Mistakes
Forgetting . and ..
Effect: Loops process the current and parent folders, sometimes causing infinite recursion.
Fix: Use array_diff(), glob(), or FilesystemIterator::SKIP_DOTS.
Using Names Without the Directory
Effect: is_file($name) checks the wrong location, because scandir() returns names, not paths.
Fix: Combine them: "$dir/$name".
Assuming Sorted Iterator Output
Effect: File lists appear in an unpredictable order.
Fix: Collect results and sort them.
Creating Nested Folders Without $recursive
Effect: mkdir() fails because a parent folder does not exist.
Fix: Pass true as the third argument.
Listing Folders From User Input
Effect: Attackers browse the server's filesystem with ../.
Fix: Restrict operations to a fixed base folder and validate paths with realpath().
Best Practices
- Create nested folders with
mkdir($path, 0755, true)and handle existing folders gracefully. - Use
glob()for simple pattern matches and SPL iterators for recursive traversal. - Always exclude
.and.., and work with full paths. - Sort listings explicitly when order matters.
- Restrict directory operations to known base folders and be extremely careful with recursive deletion.
Official reference: https://www.php.net/manual/en/ref.dir.php