data types and standard library

Files and Directories

PHP applications use the filesystem for uploads, exports, generated reports, caches, logs, temporary files, local development fixtures, and command-line tools.

Filesystem code crosses a boundary outside PHP's memory. That means you need to handle missing directories, permissions, partial writes, unsafe filenames, cleanup, and the difference between paths controlled by your application and paths supplied by users.

Build paths deliberately

Use one trusted base directory and append known-safe names to it.

PHP example
<?php

declare(strict_types=1);

$directory = sys_get_temp_dir() . '/php-course-files';
$path = $directory . DIRECTORY_SEPARATOR . 'note.txt';

echo str_ends_with($path, 'note.txt') ? 'path ready' : 'bad path';
echo PHP_EOL;

// Prints:
// path ready

DIRECTORY_SEPARATOR keeps path construction portable. In many web apps, paths are built from project configuration rather than directly from user input.

Create directories before writing

Check whether the directory exists and create it recursively when needed.

PHP example
<?php

declare(strict_types=1);

$directory = sys_get_temp_dir() . '/php-course-files';

if (!is_dir($directory)) {
    mkdir($directory, 0775, true);
}

echo is_dir($directory) ? 'directory exists' : 'missing';
echo PHP_EOL;

rmdir($directory);

// Prints:
// directory exists

In production code, handle failures from mkdir() if permissions or parent paths are uncertain.

Write and read files

For small files, file_put_contents() and file_get_contents() are straightforward.

PHP example
<?php

declare(strict_types=1);

$directory = sys_get_temp_dir() . '/php-course-files';
$path = $directory . '/note.txt';

if (!is_dir($directory)) {
    mkdir($directory, 0775, true);
}

file_put_contents($path, "Saved note\n", LOCK_EX);

echo file_get_contents($path);

unlink($path);
rmdir($directory);

// Prints:
// Saved note

LOCK_EX asks PHP to take an exclusive lock while writing. It is a useful habit for small local writes that may be touched by more than one process.

Use atomic replacement for important files

When replacing a file that other code may read, write a temporary file first and rename it into place.

PHP example
<?php

declare(strict_types=1);

function writeFileAtomically(string $path, string $contents): void
{
    $directory = dirname($path);

    if (!is_dir($directory)) {
        mkdir($directory, 0775, true);
    }

    $temporaryPath = tempnam($directory, 'tmp_');
    file_put_contents($temporaryPath, $contents, LOCK_EX);
    rename($temporaryPath, $path);
}

$directory = sys_get_temp_dir() . '/php-course-atomic';
$path = $directory . '/settings.txt';

writeFileAtomically($path, "enabled=true\n");

echo file_get_contents($path);

unlink($path);
rmdir($directory);

// Prints:
// enabled=true

This reduces the chance that a reader sees a half-written file.

List directory contents

Use directory APIs and skip . and ...

PHP example
<?php

declare(strict_types=1);

$directory = sys_get_temp_dir() . '/php-course-list';

if (!is_dir($directory)) {
    mkdir($directory, 0775, true);
}

file_put_contents($directory . '/a.txt', 'A');
file_put_contents($directory . '/b.txt', 'B');

$files = [];

foreach (new DirectoryIterator($directory) as $entry) {
    if ($entry->isFile()) {
        $files[] = $entry->getFilename();
    }
}

sort($files);

echo implode(', ', $files) . PHP_EOL;

unlink($directory . '/a.txt');
unlink($directory . '/b.txt');
rmdir($directory);

// Prints:
// a.txt, b.txt

Directory listing is useful for imports, cleanup jobs, file browsers, and generated exports.

Guard against path traversal

Never concatenate a user-supplied path directly into a storage path. A value such as ../../config.php can escape the intended directory.

PHP example
<?php

declare(strict_types=1);

function safeFilename(string $filename): string
{
    $basename = basename($filename);

    if ($basename !== $filename || $basename === '' || str_contains($basename, "\0")) {
        throw new InvalidArgumentException('Invalid filename.');
    }

    return $basename;
}

try {
    safeFilename('../config.php');
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// Invalid filename.

For user uploads, a better approach is usually to generate your own random storage name and store the original name only as metadata.

What to remember

Filesystem code should be explicit about its base directory, directory creation, locking or atomic replacement, cleanup, and filename safety. Treat paths from users as untrusted input and keep uploads or generated files away from places where PHP code can execute.

Practice

Task: Save and list generated reports

Write a small report storage example using the filesystem.

Requirements

  • Use declare(strict_types=1);.
  • Create a temporary report directory if it does not exist.
  • Write two .txt report files using LOCK_EX.
  • List only files in the report directory.
  • Sort the filenames before printing.
  • Read and print the contents of one report.
  • Clean up the files and directory at the end.
  • Include the expected output as comments in the same PHP code block.

The example should show safe directory creation, writing, reading, listing, and cleanup.

Show solution
PHP example
<?php

declare(strict_types=1);

$directory = sys_get_temp_dir() . '/php-course-reports';

if (!is_dir($directory)) {
    mkdir($directory, 0775, true);
}

$dailyPath = $directory . '/daily.txt';
$weeklyPath = $directory . '/weekly.txt';

file_put_contents($dailyPath, "Daily total: 120\n", LOCK_EX);
file_put_contents($weeklyPath, "Weekly total: 560\n", LOCK_EX);

$filenames = [];

foreach (new DirectoryIterator($directory) as $entry) {
    if ($entry->isFile()) {
        $filenames[] = $entry->getFilename();
    }
}

sort($filenames);

echo 'Reports: ' . implode(', ', $filenames) . PHP_EOL;
echo file_get_contents($dailyPath);

unlink($dailyPath);
unlink($weeklyPath);
rmdir($directory);

// Prints:
// Reports: daily.txt, weekly.txt
// Daily total: 120

The solution creates its own directory, writes files with an exclusive lock, lists real files rather than directory markers, reads a known path, and removes everything it created. That is the minimum discipline expected before this pattern is used for exports, caches, or uploaded files.