PHP File Handling
What Is File Handling?
In simple terms: File handling means reading from and writing to files on the server's disk — for example, saving a log of errors, reading a CSV of products, storing uploaded images, or caching generated pages.
Technically: PHP's filesystem functions let you create, read, write, append to, copy, rename, and delete files and directories, and query information such as size and modification time. There are two styles: simple whole-file functions (file_get_contents(), file_put_contents()) and stream functions that work piece by piece through a file handle (fopen(), fgets(), fwrite(), fclose()).
Typical Uses
| Use | Example |
|---|---|
| Logs | Appending error or audit entries |
| Data import/export | Reading and writing CSV or JSON files |
| Uploads | Storing user files (see $_FILES) |
| Caching | Saving generated HTML or API responses |
| Configuration | Reading settings files |
| Reports | Generating text or CSV reports |
For data that many users read and change at the same time (orders, accounts), a database is usually the better choice; files are best for logs, imports/exports, media, and caches.
The Two Approaches
Whole-File Functions
<?php
$file = __DIR__ . '/notes.txt';
$bytes = file_put_contents($file, "First line\n");
echo "Wrote $bytes bytes\n";
$bytes = file_put_contents($file, "Second line\n", FILE_APPEND);
echo "Appended $bytes bytes\n";
echo file_get_contents($file);
print_r(file($file, FILE_IGNORE_NEW_LINES));
echo "Size: " . filesize($file) . " bytes\n";
unlink($file);
var_dump(file_exists($file));
Output
Wrote 11 bytes
Appended 12 bytes
First line
Second line
Array
(
[0] => First line
[1] => Second line
)
Size: 23 bytes
bool(false)
How It Works
file_put_contents()creates the file (or replaces its contents) and returns the number of bytes written.- With the
FILE_APPENDflag, new content is added to the end instead of replacing the file. file_get_contents()reads the entire file into one string.file()reads the file into an array of lines;FILE_IGNORE_NEW_LINESremoves the line endings.filesize()returns the size in bytes, andunlink()deletes the file.
These functions open, process, and close the file in one call — ideal for small files.
Stream Functions
<?php
$file = __DIR__ . '/notes.txt';
$handle = fopen($file, 'w');
fwrite($handle, "Line A\n");
fwrite($handle, "Line B\n");
fclose($handle);
$handle = fopen($file, 'r');
while (($line = fgets($handle)) !== false) {
echo 'Read: ' . rtrim($line) . "\n";
}
fclose($handle);
unlink($file);
Output
Read: Line A
Read: Line B
Stream functions keep a file handle open, so you can read or write in small pieces. This is essential for large files (which should never be loaded into memory at once) and for writing data incrementally. They are covered in detail in PHP File Open/Read and PHP File Create/Write.
Which to Use
| Situation | Use |
|---|---|
| Small files (configuration, JSON, templates) | file_get_contents() / file_put_contents() |
| Large files (logs, big CSV exports) | fopen() + fgets() / fwrite() in a loop |
| Appending a line to a log | file_put_contents($file, $line, FILE_APPEND | LOCK_EX) |
| CSV data | fgetcsv() / fputcsv() (see PHP CSV Files) |
| Object-oriented style | SplFileObject |
Checking Files and Getting Information
<?php
$file = __DIR__ . '/report.txt';
file_put_contents($file, str_repeat('x', 2048));
var_dump(file_exists($file));
var_dump(is_file($file));
var_dump(is_dir(__DIR__));
var_dump(is_readable($file), is_writable($file));
echo "Size: " . filesize($file) . " bytes\n";
echo "Modified: " . date('Y-m-d', filemtime($file)) . "\n";
echo "Extension: " . pathinfo($file, PATHINFO_EXTENSION) . "\n";
unlink($file);
Output (the date is the day you run it)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
Size: 2048 bytes
Modified: 2026-10-15
Extension: txt
| Function | Returns |
|---|---|
file_exists($path) | true if a file or directory exists |
is_file($path) / is_dir($path) | true if it is a regular file / a directory |
is_readable() / is_writable() | Permission checks for the PHP process |
filesize($path) | Size in bytes |
filemtime($path) | Last modification time (Unix timestamp) |
pathinfo($path) | Directory, file name, base name, and extension |
realpath($path) | Absolute, normalized path (or false if it does not exist) |
Note: PHP caches the results of
filesize(),filemtime(), and similar functions during a request. If a file changes during the script and you need fresh information, callclearstatcache()first.
Error Handling
Filesystem functions return false on failure and emit a warning — they do not throw exceptions:
<?php
$content = file_get_contents(__DIR__ . '/does-not-exist.txt');
var_dump($content);
Output
PHP Warning: file_get_contents(/path/does-not-exist.txt): Failed to open stream: No such file or directory in /path/script.php on line 2
bool(false)
Always check results. In application code, it is common to convert failures into exceptions:
<?php
function readFileOrFail(string $path): string
{
if (!is_file($path) || !is_readable($path)) {
throw new RuntimeException("Cannot read file: $path");
}
$content = file_get_contents($path);
if ($content === false) {
throw new RuntimeException("Failed to read file: $path");
}
return $content;
}
Note the strict check === false: an empty file returns '', which is falsy but not an error.
Paths, Permissions, and Security
Build Paths From __DIR__
Relative paths depend on the current working directory, which differs between web requests, cron jobs, and the command line. Build paths from the script's location:
$logFile = __DIR__ . '/../storage/logs/app.log';
Use / as the separator; PHP accepts it on Windows too.
Permissions
The PHP process (for example, the www-data user under Nginx or Apache) needs permission to read or write the files and folders involved. A typical setup gives the web server write access only to specific folders such as storage/ and uploads/, never to the application code.
Never Trust File Names From Users
<?php
function safeStoragePath(string $baseDir, string $fileName): ?string
{
$name = basename($fileName);
if (!preg_match('/^[a-zA-Z0-9_-]+\.(txt|csv|json)$/', $name)) {
return null;
}
return rtrim($baseDir, '/') . '/' . $name;
}
var_dump(safeStoragePath('/var/app/storage', 'report-2026.csv'));
var_dump(safeStoragePath('/var/app/storage', '../../config/secrets.php'));
Output
string(32) "/var/app/storage/report-2026.csv"
NULL
basename()removes any directory part, so../../tricks cannot escape the storage folder (path traversal).- An allow-list pattern restricts names to safe characters and expected extensions.
- Files that users can read or download should be stored outside the public web root and served through PHP after permission checks.
Practical Example: A Simple Application Log
<?php
function writeLog(string $file, string $level, string $message, DateTimeInterface $at): void
{
$line = sprintf("[%s] %s: %s\n", $at->format('Y-m-d H:i:s'), strtoupper($level), $message);
if (file_put_contents($file, $line, FILE_APPEND | LOCK_EX) === false) {
throw new RuntimeException("Cannot write to log file: $file");
}
}
function lastLogLines(string $file, int $count): array
{
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
return $lines === false ? [] : array_slice($lines, -$count);
}
$logFile = __DIR__ . '/app.log';
$time = new DateTimeImmutable('2026-10-15 10:00:00');
writeLog($logFile, 'info', 'User asha logged in', $time);
writeLog($logFile, 'error', 'Payment PAY-889 failed', $time->modify('+2 minutes'));
writeLog($logFile, 'info', 'Order 501 created', $time->modify('+5 minutes'));
echo implode("\n", lastLogLines($logFile, 2)) . "\n";
unlink($logFile);
Output
[2026-10-15 10:02:00] ERROR: Payment PAY-889 failed
[2026-10-15 10:05:00] INFO: Order 501 created
Step-by-Step Explanation
- Each entry is formatted as one line with a timestamp, a level, and a message.
FILE_APPENDadds to the end of the file;LOCK_EXlocks the file during the write, so entries from simultaneous requests are not mixed together.lastLogLines()reads all lines and returns the last few witharray_slice($lines, -$count). (For very large log files, reading from the end with a stream would be more efficient.)- The time is passed in as a parameter, which keeps the function testable.
In production, use an established logging library such as Monolog, which supports log levels, rotation, and sending logs to external services.
Common Mistakes
Not Checking Return Values
Effect: false is treated as file content or a byte count, causing confusing errors later.
Fix: Check with === false.
Loading Huge Files Into Memory
Effect: file_get_contents() or file() on a multi-gigabyte file exceeds the memory limit.
Fix: Read line by line with fgets() or SplFileObject.
Concurrent Writes Without Locking
Effect: Corrupted or interleaved file content.
Fix: Use LOCK_EX or flock().
Using User Input in Paths
Effect: Path traversal and exposure of sensitive files.
Fix: Use basename(), allow-lists, and fixed base directories.
Storing Private Files in the Web Root
Effect: Anyone can download them by URL.
Fix: Store them outside the public folder.
Best Practices
- Use whole-file functions for small files and streams for large ones.
- Build paths from
__DIR__and keep writable folders separate from code. - Check every return value and convert failures into exceptions in application code.
- Lock files during concurrent writes.
- Never build file paths directly from user input.
Official reference: https://www.php.net/manual/en/book.filesystem.php