PHP Directory Functions: Create, Read, List and Delete

Directory handling looks simple until a script creates a folder in the wrong location or a listing unexpectedly includes . and ... The filesystem has a talent for making small assumptions visible very quickly.

PHP provides built-in directory functions to create folders, inspect their contents, move between directories and remove empty directories. For related file operations such as reading, writing and appending files, see the PHP file handling guide.

Quick answer

The most commonly used PHP directory functions are:

  • is_dir() checks whether a path is a directory.
  • mkdir() creates a directory.
  • scandir() returns directory entries as an array.
  • opendir() opens a directory handle.
  • readdir() reads one entry at a time from an open directory.
  • closedir() closes the directory handle.
  • rmdir() removes an empty directory.
  • getcwd() returns the current working directory.
  • chdir() changes the current working directory.

The official PHP directory functions reference contains the complete function list and signatures.

Which directory listing function should you use?

PHP offers more than one way to list a directory. The right choice depends mainly on the directory size and how you want to process the entries.

Method Best use Important behaviour
scandir() Simple listings of small or medium directories Returns all entries as an array and sorts them by default
opendir() with readdir() Large directories or entry-by-entry processing Reads one entry at a time without loading the full listing into an array
DirectoryIterator Object-oriented directory inspection Provides methods such as isFile(), isDir() and isDot()

For most straightforward directory listings, start with scandir(). Use opendir() and readdir() when you need tighter control or want to process a very large directory one entry at a time.

Check whether a directory exists

Use is_dir() before reading, creating or removing a directory. It returns true only when the supplied path exists and refers to a directory.

<?php

$directoryPath = __DIR__ . '/storage';

if (is_dir($directoryPath)) {
    echo 'The storage directory exists.';
}

Using __DIR__ builds the path from the directory containing the current PHP file. This is usually safer than relying on a relative path whose meaning can change with the script’s working directory.

Create a directory with mkdir()

The mkdir() function creates a directory. At minimum, it needs the path of the new directory.

<?php

$directoryPath = __DIR__ . '/storage';

if (!is_dir($directoryPath)) {
    $created = mkdir($directoryPath);

    if (!$created) {
        throw new RuntimeException('Unable to create the storage directory.');
    }
}

Checking with is_dir() first prevents a warning when the directory already exists. The return value from mkdir() should also be checked instead of assuming the operation succeeded.

Create nested directories

By default, mkdir() cannot create missing parent directories. Pass true as its third argument to enable recursive directory creation.

<?php

$directoryPath = __DIR__ . '/storage/reports/2026';

if (!is_dir($directoryPath)) {
    $created = mkdir($directoryPath, 0755, true);

    if (!$created) {
        throw new RuntimeException('Unable to create the nested directories.');
    }
}

This creates the complete storage/reports/2026 path, including any missing parent directories.

The mkdir() documentation describes all four parameters:

mkdir(
    string $directory,
    int $permissions = 0777,
    bool $recursive = false,
    ?resource $context = null
): bool

Understand the permission argument

Directory permissions are written as an octal number, so the leading zero matters. For example, use 0755, not 755.

The permission value is also affected by the system’s current umask. Therefore, passing 0777 does not guarantee that the directory will receive exactly those permissions. On Windows, the permission argument is ignored.

Avoid using 0777 as a routine fix for permission errors. Grant only the access required by the PHP process and the application.

List directory contents with scandir()

scandir() is the simplest choice when you want all directory entries as an array.

<?php

$directoryPath = __DIR__ . '/storage';
$entries = scandir($directoryPath);

if ($entries === false) {
    throw new RuntimeException('Unable to read the storage directory.');
}

foreach ($entries as $entry) {
    if ($entry === '.' || $entry === '..') {
        continue;
    }

    echo htmlspecialchars($entry, ENT_QUOTES, 'UTF-8') . '<br>';
}

The special entries . and .. represent the current directory and its parent. They are normal filesystem entries, but they are rarely useful in an application listing, so the loop skips them.

scandir() sorts entries in ascending alphabetical order by default. Pass SCANDIR_SORT_DESCENDING as the second argument to reverse the order.

<?php

$directoryPath = __DIR__ . '/storage';
$entries = scandir($directoryPath, SCANDIR_SORT_DESCENDING);

if ($entries === false) {
    throw new RuntimeException('Unable to read the storage directory.');
}

Use SCANDIR_SORT_NONE when sorting is unnecessary. The resulting order then depends on the filesystem.

List only files or only directories

scandir() returns names, but it does not identify which entries are files or directories. Build the complete path and inspect it with is_file() or is_dir().

<?php

$directoryPath = __DIR__ . '/storage';
$entries = scandir($directoryPath);

if ($entries === false) {
    throw new RuntimeException('Unable to read the storage directory.');
}

foreach ($entries as $entry) {
    if ($entry === '.' || $entry === '..') {
        continue;
    }

    $entryPath = $directoryPath . DIRECTORY_SEPARATOR . $entry;

    if (!is_file($entryPath)) {
        continue;
    }

    echo htmlspecialchars($entry, ENT_QUOTES, 'UTF-8') . '<br>';
}

DIRECTORY_SEPARATOR uses the separator expected by the operating system. PHP generally accepts forward slashes across common platforms, but the constant makes path construction explicit.

Escaping the filename before printing it is important. A filename can contain characters that have meaning in HTML, even when the name came from your own server.

Read a directory with opendir() and readdir()

opendir() opens a directory and returns a handle. You can then call readdir() repeatedly to fetch one entry at a time.

<?php

$directoryPath = __DIR__ . '/storage';
$directoryHandle = opendir($directoryPath);

if ($directoryHandle === false) {
    throw new RuntimeException('Unable to open the storage directory.');
}

while (($entry = readdir($directoryHandle)) !== false) {
    if ($entry === '.' || $entry === '..') {
        continue;
    }

    echo htmlspecialchars($entry, ENT_QUOTES, 'UTF-8') . '<br>';
}

closedir($directoryHandle);

The strict comparison with false matters. A valid directory entry can have a value that PHP considers false-like, so using a loose comparison can stop the loop too early.

Call closedir() after processing the entries. PHP will release the handle when the script ends, but closing it explicitly makes the resource lifecycle clear.

When readdir() is a better choice than scandir()

scandir() loads the full directory listing into an array. That is convenient, but it uses more memory as the directory grows.

readdir() processes one entry at a time. It is a better fit when a directory contains many files or when the script can act on each entry immediately without keeping the complete list.

Use DirectoryIterator for object-oriented directory handling

DirectoryIterator provides an object-oriented alternative to the procedural directory functions. Each entry exposes methods for checking its type and reading metadata.

<?php

$directoryPath = __DIR__ . '/storage';

try {
    $iterator = new DirectoryIterator($directoryPath);

    foreach ($iterator as $entry) {
        if ($entry->isDot()) {
            continue;
        }

        if (!$entry->isFile()) {
            continue;
        }

        echo htmlspecialchars(
            $entry->getFilename(),
            ENT_QUOTES,
            'UTF-8'
        ) . '<br>';
    }
} catch (UnexpectedValueException $exception) {
    throw new RuntimeException(
        'Unable to read the storage directory.',
        0,
        $exception
    );
}

The isDot() method is a convenient replacement for manually checking . and ... Other useful methods include isDir(), getSize(), getExtension() and getMTime().

Use DirectoryIterator when you need more than filenames and prefer working with entry objects. The DirectoryIterator documentation lists all available methods.

Get and change the current working directory

getcwd() returns the script’s current working directory.

<?php

$currentDirectory = getcwd();

if ($currentDirectory === false) {
    throw new RuntimeException('Unable to determine the working directory.');
}

echo htmlspecialchars($currentDirectory, ENT_QUOTES, 'UTF-8');

The working directory is not always the same as the directory containing the current PHP file. It can depend on how the script was started, the web server configuration or an earlier call to chdir().

That difference is why __DIR__ is usually more predictable when building paths to application files.

Change the working directory with chdir()

chdir() changes the working directory for the current script.

<?php

$targetDirectory = __DIR__ . '/storage';

if (!chdir($targetDirectory)) {
    throw new RuntimeException('Unable to change the working directory.');
}

$currentDirectory = getcwd();

if ($currentDirectory === false) {
    throw new RuntimeException('Unable to determine the working directory.');
}

echo htmlspecialchars($currentDirectory, ENT_QUOTES, 'UTF-8');

Use chdir() carefully in larger applications. It changes how later relative paths are resolved, which can make unrelated code behave differently. Absolute paths based on __DIR__ are usually easier to follow and debug.

Remove an empty directory with rmdir()

The rmdir() function removes a directory only when it is empty.

<?php

$directoryPath = __DIR__ . '/storage/archive';

if (is_dir($directoryPath)) {
    $removed = rmdir($directoryPath);

    if (!$removed) {
        throw new RuntimeException('Unable to remove the archive directory.');
    }
}

If the directory still contains files or subdirectories, rmdir() fails. This behaviour is intentional. PHP does not silently delete a complete directory tree.

Remove a non-empty directory safely

To delete a directory and everything inside it, process the child entries first. Files must be deleted with unlink(), while nested directories must be emptied before calling rmdir().

<?php

function removeDirectory(string $directoryPath): void
{
    if (!is_dir($directoryPath)) {
        return;
    }

    $entries = scandir($directoryPath);

    if ($entries === false) {
        throw new RuntimeException(
            'Unable to read directory: ' . $directoryPath
        );
    }

    foreach ($entries as $entry) {
        if ($entry === '.' || $entry === '..') {
            continue;
        }

        $entryPath = $directoryPath . DIRECTORY_SEPARATOR . $entry;

        if (is_dir($entryPath) && !is_link($entryPath)) {
            removeDirectory($entryPath);
            continue;
        }

        if (!unlink($entryPath)) {
            throw new RuntimeException(
                'Unable to delete file: ' . $entryPath
            );
        }
    }

    if (!rmdir($directoryPath)) {
        throw new RuntimeException(
            'Unable to remove directory: ' . $directoryPath
        );
    }
}

removeDirectory(__DIR__ . '/storage/archive');

The is_link() check is important. A symbolic link that points to a directory should be deleted as a link, not followed recursively into another location.

Recursive deletion is destructive, so never pass an unchecked request value directly to this function. Confirm that the resolved path is inside an application-controlled directory before deleting anything.

The difference between removing a file and removing a directory is covered in more detail in the PHP unlink() and unset() comparison.

Work with paths safely

Directory operations become risky when a path comes from user input. A value such as ../../ can escape the intended base directory if it is simply appended to a filesystem path.

A safer pattern is to resolve the target with realpath() and confirm that it remains inside an allowed base directory.

<?php

$baseDirectory = realpath(__DIR__ . '/storage');

if ($baseDirectory === false) {
    throw new RuntimeException('The storage directory does not exist.');
}

$requestedName = 'reports';
$targetDirectory = realpath(
    $baseDirectory . DIRECTORY_SEPARATOR . $requestedName
);

if ($targetDirectory === false) {
    throw new RuntimeException('The requested directory does not exist.');
}

$allowedPrefix = $baseDirectory . DIRECTORY_SEPARATOR;

if (
    $targetDirectory !== $baseDirectory
    && !str_starts_with($targetDirectory, $allowedPrefix)
) {
    throw new RuntimeException('The requested directory is not allowed.');
}

realpath() returns the canonical absolute path and resolves symbolic links. It returns false when the path does not exist, so it is best suited to validating existing files and directories.

For a new directory that has not yet been created, validate the parent directory with realpath() and restrict the new name to an expected format.

<?php

$baseDirectory = realpath(__DIR__ . '/storage');
$directoryName = 'report-2026';

if ($baseDirectory === false) {
    throw new RuntimeException('The storage directory does not exist.');
}

if (!preg_match('/\A[a-z0-9_-]+\z/i', $directoryName)) {
    throw new InvalidArgumentException('Invalid directory name.');
}

$newDirectoryPath = $baseDirectory
    . DIRECTORY_SEPARATOR
    . $directoryName;

Common directory errors and fixes

Permission denied

The PHP process must have permission to access the parent directory and perform the requested operation. Check ownership and permissions instead of automatically changing everything to 0777.

No such file or directory

This usually means the path is wrong or a parent directory does not exist. Print or log the resolved path during development, and use recursive mkdir() when missing parent directories should be created.

Directory not empty

rmdir() removes only empty directories. Delete the contained files and subdirectories first, or reject the operation when the directory is expected to remain non-empty.

Relative path points to an unexpected location

Relative paths depend on the current working directory. Build application paths with __DIR__ or another known absolute base path.

Changes are not detected immediately

PHP caches information returned by some filesystem functions during a request. Call clearstatcache() when a script changes the filesystem and must immediately perform another metadata check on the same path.

<?php

$directoryPath = __DIR__ . '/storage/cache';

if (!is_dir($directoryPath)) {
    mkdir($directoryPath, 0755, true);
}

clearstatcache(true, $directoryPath);

if (!is_dir($directoryPath)) {
    throw new RuntimeException('The directory was not created.');
}

PHP directory functions FAQ

What is the easiest way to list files in a directory?

Use scandir() for a simple directory listing. It returns all entries as an array. Remember to skip . and .. and use is_file() when you want files only.

What is the difference between scandir() and readdir()?

scandir() reads the complete directory into an array and sorts it by default. readdir() reads one entry at a time from a handle opened with opendir(). The second approach is more suitable for very large directories.

Can mkdir() create parent directories?

Yes. Pass true as the third argument.

<?php

mkdir(__DIR__ . '/storage/reports/2026', 0755, true);

Why does rmdir() fail?

rmdir() fails when the directory does not exist, is not empty or cannot be removed because of filesystem permissions. Check the return value and handle the failure instead of suppressing the warning.

Should I use @ to hide directory function warnings?

No. The error-control operator hides useful diagnostic information and makes filesystem problems harder to investigate. Check return values and throw or log a meaningful error instead.

How can I get the full path of a directory entry?

Join the parent directory and entry name with DIRECTORY_SEPARATOR.

<?php

$entryPath = $directoryPath
    . DIRECTORY_SEPARATOR
    . $entry;

Conclusion

PHP has straightforward functions for common directory operations. Use mkdir() to create directories, scandir() for simple listings, and opendir() with readdir() when entries should be processed one at a time.

Build paths from a known absolute base, check every filesystem operation, and treat user-supplied directory names as untrusted input. Those habits prevent most directory-handling bugs before they become production surprises.

Photo of Vincy, PHP developer
Written by Vincy Last updated: July 28, 2026
I'm a PHP developer with 20+ years of experience and a Master's degree in Computer Science. I build and improve production PHP systems for eCommerce, payments, webhooks, and integrations, including legacy upgrades (PHP 5/7 to PHP 8.x).

Continue Learning

These related tutorials may help you continue learning.

Leave a Reply

Your email address will not be published. Required fields are marked *

Explore topics
Need PHP help?