Reading & Writing Files

PHP · Lesson 24 of 50 7 min read Updated

Not every piece of data needs a database. A small shop might keep a log of orders in a text file, a teacher might export marks to a CSV file for Excel, and almost every app writes error logs. PHP has simple functions for all of this.

In this lesson you will write and read whole files, read big files line by line, work with CSV, manage folders, and avoid the mistakes that make file code fail or become unsafe.

Writing and reading a whole file

The two functions you will use most are file_put_contents() and file_get_contents(). The first writes a string to a file (creating it if needed), the second reads the whole file into a string.

PHP
<?php
declare(strict_types=1);

$file = __DIR__ . '/shopping.txt';

file_put_contents($file, "Atta 5 kg\n");
file_put_contents($file, "Toor dal 1 kg\n", FILE_APPEND | LOCK_EX);
$bytes = file_put_contents($file, "Ghee 500 g\n", FILE_APPEND | LOCK_EX);

echo file_get_contents($file);
echo "Last write: $bytes bytes\n";
Output
Atta 5 kg
Toor dal 1 kg
Ghee 500 g
Last write: 11 bytes

A few things to notice:

  • __DIR__ is the folder of the current script. Building paths from it means the code works no matter which folder you run it from.
  • Without flags, file_put_contents() replaces the file. FILE_APPEND adds to the end instead.
  • LOCK_EX locks the file while writing, so two visitors writing at the same moment do not mix their lines.
  • It returns the number of bytes written, or false if writing failed.

Reading line by line

file() reads a file into an array with one element per line. The two flags remove the line breaks and skip blank lines.

PHP
<?php
declare(strict_types=1);

$lines = file(__DIR__ . '/shopping.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

foreach ($lines as $number => $item) {
    echo ($number + 1) . ". $item\n";
}
echo 'Items: ' . count($lines) . "\n";
Output
1. Atta 5 kg
2. Toor dal 1 kg
3. Ghee 500 g
Items: 3

Both file() and file_get_contents() load the whole file into memory. That is fine for small files, but a log file of 2 GB would use up the server's memory. For big files, open the file with fopen() and read one line at a time with fgets():

PHP
<?php
declare(strict_types=1);

$handle = fopen(__DIR__ . '/shopping.txt', 'r');
if ($handle === false) {
    exit("Could not open the file.\n");
}

$count = 0;
while (($line = fgets($handle)) !== false) {
    $count++;
    echo $count . ': ' . strtoupper(trim($line)) . "\n";
}
fclose($handle);
Output
1: ATTA 5 KG
2: TOOR DAL 1 KG
3: GHEE 500 G

fopen() returns a handle, a resource that points to the open file. fgets() returns the next line, or false at the end of the file. Only one line is in memory at a time. The second argument to fopen() is the mode:

ModeMeaning
rRead only. The file must exist.
wWrite only. Creates the file, or empties it if it exists.
aAppend. Creates the file if needed and writes at the end.
xCreate a new file for writing. Fails if the file already exists.
r+Read and write, starting at the beginning.

Working with CSV files

CSV (comma-separated values) is the format spreadsheets use for import and export. fputcsv() writes one row and adds quotes where needed; fgetcsv() reads one row back as an array.

PHP
<?php
declare(strict_types=1);

$rows = [
    ['name', 'city', 'marks'],
    ['Aarav', 'Rohtak', 88],
    ['Meera', 'Kochi', 93],
    ['Rohan', 'Pune', 79],
];

$out = fopen(__DIR__ . '/marks.csv', 'w');
foreach ($rows as $row) {
    fputcsv($out, $row, escape: '');
}
fclose($out);

$in = fopen(__DIR__ . '/marks.csv', 'r');
$header = fgetcsv($in, escape: '');
while (($row = fgetcsv($in, escape: '')) !== false) {
    $student = array_combine($header, $row);
    echo "{$student['name']} from {$student['city']} scored {$student['marks']}\n";
}
fclose($in);
Output
Aarav from Rohtak scored 88
Meera from Kochi scored 93
Rohan from Pune scored 79

array_combine() joins the header row with each data row, so you can use names like $student['city'] instead of numbers like $row[1].

Folders and checking files

Real apps need to create folders, list files and delete old ones.

PHP
<?php
declare(strict_types=1);

$folder = __DIR__ . '/reports/2026';

if (!is_dir($folder)) {
    mkdir($folder, 0775, true);   // true = create parent folders too
}

file_put_contents("$folder/october.txt", "Sales: 1,42,500\n");
file_put_contents("$folder/november.txt", "Sales: 1,58,200\n");

foreach (glob("$folder/*.txt") as $path) {
    echo basename($path) . ' (' . filesize($path) . " bytes)\n";
}

unlink("$folder/october.txt");
echo count(glob("$folder/*.txt")) . " file left\n";
Output
november.txt (16 bytes)
october.txt (16 bytes)
1 file left

glob() returns the paths that match a pattern, basename() takes only the file name from a path, and unlink() deletes a file. To rename or move a file, use rename($from, $to).

If you read a file that does not exist, file_get_contents() shows a warning (Failed to open stream: No such file or directory) and returns false. Check first with is_file(), which is true only for an existing file (not a folder), and is_readable():

PHP
$path = __DIR__ . '/settings.txt';

if (is_file($path) && is_readable($path)) {
    $settings = file_get_contents($path);
} else {
    $settings = 'default settings';
}

Never build file paths from user input

Imagine a page show.php?file=notice.txt that runs readfile(__DIR__ . '/notices/' . $_GET['file']). An attacker can send ?file=../../config/db.php and read your database password. This is called path traversal: the ../ parts climb out of the folder you meant to use.

Safe options, from best to acceptable:

  • Keep an allow-list of the files that may be opened, and look the request up in it.
  • Use an ID from the user to find a file name that you stored, for example in the database.
  • If you must use a name, pass it through basename(), which removes every folder part, then check the file exists in your folder.

Common mistakes

  • Using a relative path such as 'data.txt'. It is relative to the current working directory, which changes between the browser and the command line. Use __DIR__ . '/data.txt'.
  • Forgetting FILE_APPEND. Each write then wipes the previous content.
  • Not checking for false. fopen() and file_put_contents() can fail, for example when the folder is not writable by the web server.
  • Reading huge files with file_get_contents(). Read them line by line instead.

Key points

  • file_put_contents() writes a file; add FILE_APPEND | LOCK_EX to append safely.
  • file_get_contents() and file() read a whole file; use fopen() and fgets() for big files.
  • Build paths with __DIR__ so they do not depend on where the script runs from.
  • Use fputcsv() and fgetcsv() for CSV, and pass escape: ''.
  • mkdir(), glob(), unlink() and rename() manage folders and files.
  • Never put user input straight into a file path; use an allow-list or basename().

Exercise

Write visitors.php that records each visit. Every time it runs, it appends a line with the date and time (use date('Y-m-d H:i:s')) to visits.log in the same folder. Then it prints how many visits have been logged so far and the most recent line.

Show solution
PHP
<?php
declare(strict_types=1);

$log = __DIR__ . '/visits.log';

file_put_contents($log, date('Y-m-d H:i:s') . "\n", FILE_APPEND | LOCK_EX);

$lines = file($log, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

echo 'Total visits: ' . count($lines) . "\n";
echo 'Latest visit: ' . end($lines) . "\n";

FILE_APPEND keeps the old lines and LOCK_EX stops two visits writing at the same moment. end() returns the last element of the array, which is the newest line.

Quick Quiz

1. What happens when you call file_put_contents() on an existing file without any flags?

2. Which approach is best for reading a 2 GB log file?

3. Why is readfile('docs/' . $_GET['name']) dangerous?

4. What does __DIR__ contain?

Learn PHP with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call