PHP File Read Write

Every real-world PHP application eventually needs to touch the filesystem — writing a log entry, saving an uploaded file, generating a CSV export, or reading a configuration file. PHP treats files as streams: sequences of bytes you can open, read from, write to, and close, using a small set of built-in functions. Understanding file I/O properly means understanding not just which function to call, but how file pointers, modes, and buffering actually behave.

Overview: How PHP File I/O Works

When you open a file in PHP, the engine doesn’t hand you the file itself — it hands you a resource that represents an open connection to the underlying operating system file descriptor. This resource is often called a file handle or stream. Every stream has an internal file pointer: an offset that tracks where the next read or write operation will occur. Functions like fread(), fwrite(), and fgets() operate relative to that pointer and advance it automatically as data is consumed or produced.

PHP’s file functions actually work through its streams layer, which is why the same functions (fopen(), fread(), fwrite()) can transparently work not just with local files, but also with php://memory, php://stdin, HTTP URLs, and even compressed archives — anywhere a stream wrapper is registered. For plain file access, though, you only need to remember two things: the mode you open a file in determines where the pointer starts and what operations are allowed, and every handle you open should eventually be closed to flush buffered data and release the OS descriptor.

PHP also offers a higher-level, convenience API — file_get_contents() and file_put_contents() — that opens, reads or writes, and closes a file in a single call. Internally these still go through the same streams layer; they are just wrappers that save you from writing the open/loop/close boilerplate for the common case of wanting the whole file at once.

Syntax

The two most common ways to work with files are the low-level handle-based functions and the high-level whole-file functions.

<?php
// Low-level: explicit handle
$handle = fopen(string $filename, string $mode);
fwrite($handle, string $data);
$data = fread($handle, int $length);
fclose($handle);

// High-level: whole file at once
file_put_contents(string $filename, string $data);
$data = file_get_contents(string $filename);
Mode Meaning
r Read only. Pointer starts at the beginning. File must exist.
r+ Read and write. Pointer starts at the beginning. File must exist.
w Write only. Truncates the file to zero length (or creates it) the moment it opens.
w+ Read and write. Truncates the file first.
a Write only, appending. Pointer starts at the end; existing content is preserved.
a+ Read and append. Reads from the start, writes always go to the end.
x Write only, but fails if the file already exists (creates it otherwise).
b Binary flag, appended to any mode above (e.g. rb) — safe to include on every platform.
Function Purpose
fopen() Opens a file and returns a stream resource, or false on failure.
fread($h, $len) Reads up to $len bytes from the current pointer position.
fwrite($h, $data) Writes a string to the stream at the current pointer position.
fgets($h) Reads a single line, including its trailing newline.
feof($h) Returns true once the pointer has reached the end of the file.
fclose($h) Flushes buffered writes and closes the handle.
fgetcsv() / fputcsv() Reads or writes one CSV row at a time, handling quoting automatically.
file_get_contents() Reads an entire file into a string in one call.
file_put_contents() Writes a string to a file in one call (accepts a FILE_APPEND flag).
flock() Requests an advisory OS-level lock to coordinate concurrent access.

Examples

Example 1: Writing and Reading a Whole File

For small files where you just need the entire contents, file_put_contents() and file_get_contents() are the simplest option — each is a single function call that handles opening and closing internally.

<?php
$filePath = __DIR__ . '/notes.txt';

$content = "PHP is a popular scripting language.\nIt is widely used for web development.\n";

file_put_contents($filePath, $content);

$readBack = file_get_contents($filePath);

echo $readBack;
echo "File size: " . filesize($filePath) . " bytes\n";

Output:

PHP is a popular scripting language.
It is widely used for web development.
File size: 76 bytes

The first call creates notes.txt (or overwrites it if it already exists) and writes the two-line string in one step. file_get_contents() then reads the whole file back into a single PHP string, which echo prints exactly as stored, newlines included. filesize() reports the file’s size in bytes on disk, which matches the length of the string we wrote.

Example 2: Reading a File Line by Line

When you need more control — or you’re dealing with a file too large to comfortably hold in memory all at once — open a handle explicitly and step through it with fgets().

<?php
$filePath = __DIR__ . '/log.txt';

$handle = fopen($filePath, 'w');
if ($handle === false) {
    die('Could not open file for writing.');
}

fwrite($handle, "Line one\n");
fwrite($handle, "Line two\n");
fwrite($handle, "Line three\n");
fclose($handle);

$handle = fopen($filePath, 'r');
if ($handle === false) {
    die('Could not open file for reading.');
}

$lineNumber = 1;
while (!feof($handle)) {
    $line = fgets($handle);
    if ($line === false) {
        break;
    }
    echo $lineNumber . ": " . $line;
    $lineNumber++;
}

fclose($handle);

Output:

1: Line one
2: Line two
3: Line three

This example opens the file twice: once in 'w' mode to write three lines with fwrite(), and once in 'r' mode to read it back. The while (!feof($handle)) loop calls fgets() repeatedly; each call returns one line, including its trailing newline, until the pointer reaches the end of the file. Because fgets() already includes the newline character, the echo statement doesn’t add one of its own.

Example 3: Working with CSV Data

Building comma-separated text by hand with implode(',') breaks as soon as a field contains a comma or a quote. fputcsv() and fgetcsv() handle that quoting correctly.

<?php
$filePath = __DIR__ . '/users.csv';

$header = ['id', 'name', 'email'];
$rows = [
    [1, 'Ada Lovelace', 'ada@example.com'],
    [2, 'Grace Hopper', 'grace@example.com'],
];

$handle = fopen($filePath, 'w');
fputcsv($handle, $header);
foreach ($rows as $row) {
    fputcsv($handle, $row);
}
fclose($handle);

$handle = fopen($filePath, 'r');
while (($data = fgetcsv($handle)) !== false) {
    echo implode(' | ', $data) . "\n";
}
fclose($handle);

Output:

id | name | email
1 | Ada Lovelace | ada@example.com
2 | Grace Hopper | grace@example.com

fputcsv() takes an array and writes it as a properly formatted, comma-separated line, quoting fields that need it automatically. fgetcsv() does the reverse: it reads one line and parses it back into an array, so the loop condition ($data = fgetcsv($handle)) !== false both reads a row and detects end-of-file in a single expression.

How PHP Handles Files Step by Step (Under the Hood)

It helps to think of every file operation as a sequence of distinct steps happening between PHP and the operating system:

  1. fopen() asks the OS to open (or create) the file and returns a stream resource wrapping an OS file descriptor plus PHP’s own internal buffer.
  2. The mode string sets the initial pointer position and the permitted operations — 'a' seeks to the end immediately, while 'w' truncates the file to zero bytes right away, before you’ve written anything at all.
  3. fwrite() copies your string into PHP’s internal write buffer and advances the pointer; the operating system may not receive the bytes immediately, since PHP buffers writes for performance.
  4. fread() and fgets() copy bytes from the OS (refilling PHP’s read buffer as needed) into a PHP string and advance the pointer by however many bytes were consumed.
  5. fclose() flushes any buffered writes to disk and releases the OS file descriptor. If a script never calls it, PHP closes the handle automatically at script end — but until then, writes may sit unflushed.
  6. file_get_contents() and file_put_contents() perform all of the steps above internally in one call, using an efficient single read or write rather than a manual loop.

Common Mistakes

Mistake 1: Not Checking fopen() and Never Closing the Handle

fopen() returns false on failure rather than throwing an exception, so code that assumes it always succeeds will fail confusingly at the next line instead. Forgetting fclose() also wastes file descriptors and can leave writes unflushed.

<?php
$handle = fopen('/tmp/data.txt', 'r');
$content = fread($handle, filesize('/tmp/data.txt'));
echo $content;
// Handle is never closed and the return value of fopen() was never checked.
<?php
$filePath = '/tmp/data.txt';
$handle = fopen($filePath, 'r');

if ($handle === false) {
    throw new RuntimeException("Unable to open $filePath");
}

$content = fread($handle, filesize($filePath));
fclose($handle);

echo $content;

The corrected version checks the result of fopen() before using it and closes the handle as soon as it’s done, so the file descriptor and any buffered data are released promptly.

Mistake 2: Opening in ‘w’ Mode When You Meant to Append

'w' mode truncates a file to zero length the instant fopen() succeeds, even if fwrite() is never called. This is one of the most common ways developers accidentally destroy a log or data file — meaning to add a new line but wiping out everything that was there before.

<?php
function appendLogEntry(string $message): void {
    $handle = fopen(__DIR__ . '/app.log', 'w');
    fwrite($handle, date('Y-m-d H:i:s') . " - $message\n");
    fclose($handle);
}

appendLogEntry('User logged in');
appendLogEntry('User logged out');
// Every call reopens the file in 'w' mode, so the previous entry is erased each time.
<?php
function appendLogEntry(string $message): void {
    $handle = fopen(__DIR__ . '/app.log', 'a');
    fwrite($handle, date('Y-m-d H:i:s') . " - $message\n");
    fclose($handle);
}

appendLogEntry('User logged in');
appendLogEntry('User logged out');

Switching to 'a' mode makes every write land at the end of the file instead of truncating it first, so both log entries survive.

A related but less obvious mistake is calling file_get_contents() on files that may be very large, such as multi-gigabyte log files or big CSV exports. Because it loads the entire file into a single PHP string, this can exhaust the script’s memory_limit and crash. For large files, stream through them with fopen() and fgets() (or fread() in fixed-size chunks), as shown in Example 2, so only a small piece of the file is held in memory at any time.

Best Practices

  • Use file_get_contents() / file_put_contents() for small, whole files; switch to fopen() with fgets() or fread() for large files or when data needs to be processed as it streams in.
  • Always check whether fopen() returned false before using the handle.
  • Always call fclose() as soon as you’re done with a handle, so buffered writes are flushed and the OS descriptor is freed.
  • Double-check your mode string — 'w' truncates, 'a' does not; a single wrong letter can silently delete data.
  • Use file_put_contents($path, $data, FILE_APPEND | LOCK_EX) instead of a manual open/write/close sequence when you also need locking, since it does both in one call.
  • Use flock() around writes when multiple requests or processes might write to the same file concurrently, to avoid interleaved or corrupted output.
  • Never build a file path directly from user input without validating it against a known safe directory — doing so opens the door to path traversal attacks.
  • Prefer fgetcsv() / fputcsv() over manual comma-splitting for CSV data, since they correctly handle quoted fields, embedded commas, and embedded newlines.
  • Check file_exists(), is_readable(), or is_writable() before an operation if the code needs to behave differently when a path is missing or inaccessible, rather than relying solely on fopen() failing.

Practice Exercises

  1. Write a script that creates a file called visitors.txt and appends a new timestamped line to it every time the script runs, without erasing previous visits. Which fopen() mode do you need?
  2. Write a function readLastLine(string $path): string|false that opens a text file and returns only its last line, using fgets() in a loop rather than file_get_contents() plus explode().
  3. Write a script that reads a CSV file of products (columns: name, price, quantity) with fgetcsv(), calculates the total value (price × quantity summed across all rows), and prints the result formatted as currency.

Summary

  • PHP files are accessed as streams through a resource, or file handle, returned by fopen().
  • The mode string ('r', 'w', 'a', etc.) controls where the pointer starts and whether the file is truncated, created, or preserved.
  • fread(), fwrite(), and fgets() operate relative to an internal pointer that advances automatically as you read or write.
  • file_get_contents() / file_put_contents() are convenient one-call wrappers best suited to small files; large or streaming data should use the handle-based functions.
  • Always check fopen()‘s return value and always close handles with fclose() to flush buffers and free OS resources.
  • fgetcsv() / fputcsv() handle CSV quoting correctly and should be preferred over manual string splitting.
  • flock() and the FILE_APPEND | LOCK_EX flags help prevent data corruption when multiple processes write to the same file.