PHP Directories

Su Subhalakshmi Chandran Updated 05 Nov 2026
7 min read ·Lesson 247 of 248

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 = true creates all missing parent folders (storage, uploads, 2026) in one call. Without it, mkdir() fails if a parent does not exist.
  • 0755 means: owner can read, write, and enter; others can read and enter. The final permissions are also affected by the process's umask.
  • mkdir() fails with a warning if the folder already exists. The pattern above checks is_dir() first and again after a failed mkdir() — 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") or is_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
)
PatternMatches
*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_ONLYDIR returns only directories.
  • glob() searches one level only; * does not descend into subfolders.
  • It returns an empty array when nothing matches (and false on some errors), so use glob(...) ?: [] 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

  1. RecursiveDirectoryIterator can look inside subfolders; SKIP_DOTS ignores . and ...
  2. RecursiveIteratorIterator flattens the tree, so a single foreach visits every entry at every depth. By default it yields leaves (files), not folders.
  3. The loop adds up file sizes in total and per extension. The two .txt files (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

  1. scandir() provides sorted names; . and .. are removed.
  2. Each entry is printed with ├──, or └── for the last entry in a folder.
  3. 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
)
FunctionPurpose
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

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings Function Arguments PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Transactions PDO Error Handling PHP XML Parsers PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader PHP XMLWriter AJAX Intro PHP Class Constants Encryption with Sodium Recursive Functions Variable Functions Anonymous Functions Closures Arrow Functions First-class Callable Syntax PHP Arrays Indexed Arrays Associative Arrays Create Arrays Variables Scope Static Variables Variable Variables PHP Echo / Print PHP printf / sprintf PHP Data Types PHP Booleans PHP NULL PHP Strings String Functions array_map / array_filter / array_reduce Array Functions PHP Superglobals $GLOBALS What Is `$_SERVER` ? $_REQUEST $_POST $_GET $_FILES $_COOKIE PHP Type Juggling MySQL Order By MySQL Delete Data MySQL Update Data MySQL Limit Data MySQL Joins with PHP Pagination with PHP PDO Introduction PDO Connect PDO Prepared Statements PDO Fetch Modes PSR-4 Autoloading Installing Extensions (PECL / PIE) OPcache & Performance PHP-FPM Project: Contact Form with Email Project: Login & Registration System Project: CRUD App with MySQL Project: Shopping Cart with Sessions Project: REST API with PHP DateTime Class Timezones PHP Include Include vs Require PHP File Handling PHP File Open/Read PHP File Create/Write File Delete / Rename / Copy PHP Directories PHP CSV Files
Ask about this post
AI Ask about this post

Ask questions about PHP Directories and get answers drawn from it.

Signed-in readers only.