Reading and Writing Files in PHP
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
Files are the simplest persistence layer: logs, configuration, exports, caches. PHP offers everything from a one-line read to a streaming handle.
One-shot read and write
// read the whole file
$content = file_get_contents("data.txt");
// write (overwrites)
file_put_contents("out.txt", "hello\n");
// append
file_put_contents("log.txt", "another line\n", FILE_APPEND);
file_put_contents is equivalent to fopen + fwrite + fclose and is enough for simple cases. It returns the number of bytes written, or false on failure.
A one-shot read loads the entire file into memory — a few hundred megabytes will hit the memory limit. Stream those instead.
Handling read failures
$content = @file_get_contents("missing.txt"); // @ suppresses the warning — avoid
if ($content === false) {
// handle it
}
Clearer: check first.
$path = "data.txt";
if (!is_file($path) || !is_readable($path)) {
throw new RuntimeException("File is not readable: {$path}");
}
$content = file_get_contents($path);
Avoid @: it hides the problem and makes debugging harder.
Reading line by line
file() returns an array of lines (with their trailing newlines unless you strip them):
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
echo $line . PHP_EOL;
}
$names = array_values(array_filter(array_map("trim", $lines)));
Streaming: for large files
$handle = fopen("big.log", "r");
if ($handle === false) {
throw new RuntimeException("Cannot open file");
}
while (($line = fgets($handle)) !== false) {
// one line at a time, constant memory
if (str_contains($line, "ERROR")) {
echo $line;
}
}
fclose($handle);
fgets reads one line; false means end of file. It keeps the trailing newline, so trim before comparing.
Writing and locking
Concurrent writers overwrite each other, so lock:
file_put_contents("counter.txt", "1", LOCK_EX);
LOCK_EX is an exclusive lock: other processes wait (it works across processes via flock).
For a read-modify-write cycle, lock the handle explicitly:
$handle = fopen("counter.txt", "c+"); // read/write, create if missing, no truncation
flock($handle, LOCK_EX);
$current = (int) stream_get_contents($handle);
$current++;
rewind($handle);
ftruncate($handle, 0);
fwrite($handle, (string) $current);
flock($handle, LOCK_UN);
fclose($handle);
Directories
// create, including parents
if (!is_dir("storage/logs")) {
mkdir("storage/logs", 0755, true);
}
// list entries
foreach (scandir("storage") as $entry) {
if ($entry === "." || $entry === "..") {
continue;
}
echo $entry . PHP_EOL;
}
For recursion, the SPL iterators beat hand-rolled recursion:
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator("src", FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === "php") {
echo $file->getPathname() . PHP_EOL;
}
}
FilesystemIterator::SKIP_DOTS removes . and ...
Paths
__DIR__; // absolute directory of the current file
dirname(__FILE__); // same, older style
basename("/a/b/c.txt"); // "c.txt"
pathinfo("/a/b/c.txt"); // dirname, basename, extension, filename
Build paths from __DIR__ rather than relying on relative paths or the working directory:
$logFile = __DIR__ . "/storage/app.log";
Under PHP-FPM the working directory differs from the CLI’s, which is exactly how “works on the command line, 404 in the browser” file bugs happen.
File information
$path = __DIR__ . "/data.txt";
file_exists($path); // file or directory exists
is_file($path); // regular file
is_dir($path); // directory
is_readable($path); // readable
is_writable($path); // writable
filesize($path); // bytes, false on failure
filemtime($path); // last modified, Unix timestamp
Deleting, renaming, copying:
unlink("old.txt"); // delete
rename("a.txt", "b.txt"); // rename or move
copy("a.txt", "backup/a.txt"); // copy
Worked example: a small logger
<?php
declare(strict_types=1);
function appLog(string $level, string $message, string $dir): void
{
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$file = $dir . "/" . date("Y-m-d") . ".log";
$line = sprintf(
"[%s] %s: %s%s",
date("H:i:s"),
strtoupper($level),
$message,
PHP_EOL
);
file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
}
appLog("error", "Database connection failed", __DIR__ . "/storage/logs");
Resulting storage/logs/2026-11-03.log:
[14:32:10] ERROR: Database connection failed
Common mistakes
1. Relative paths
The working directory differs between CLI and web. Always start from __DIR__.
2. file_get_contents on a huge file
Memory exhaustion. Stream with fopen + fgets.
3. Concurrent writes without a lock
Writes clobber each other. Use LOCK_EX or flock.
4. Using @ instead of checking
Errors disappear but the code keeps running in a broken state. Check is_file / is_readable first.
5. Forgetting fclose
Long-running scripts leak descriptors until they hit the limit.
6. Overly permissive permissions
0777 is a security problem. Start at 0755 for directories and 0644 for files, and grant the minimum needed.
Summary
- Small files:
file_get_contents/file_put_contents. Large files:fopen+fgets. - Append with
FILE_APPEND, lock concurrent writes withLOCK_EX. - Build paths from
__DIR__, never from the current working directory. - Check
is_file/is_readable/is_writableinstead of suppressing errors. - Close what you open.
Next: errors and exceptions — what to catch, what to fix, and what to show users.