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
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
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
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
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
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
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
.txtreport files usingLOCK_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
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.