Reading & Writing Files
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
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";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_APPENDadds to the end instead. LOCK_EXlocks 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
falseif 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
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";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
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);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:
| Mode | Meaning |
|---|---|
r | Read only. The file must exist. |
w | Write only. Creates the file, or empties it if it exists. |
a | Append. Creates the file if needed and writes at the end. |
x | Create 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
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);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
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";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():
$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()andfile_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; addFILE_APPEND | LOCK_EXto append safely.file_get_contents()andfile()read a whole file; usefopen()andfgets()for big files.- Build paths with
__DIR__so they do not depend on where the script runs from. - Use
fputcsv()andfgetcsv()for CSV, and passescape: ''. mkdir(),glob(),unlink()andrename()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
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?
Without FILE_APPEND, the old content is overwritten.
2. Which approach is best for reading a 2 GB log file?
Reading one line at a time keeps only a small part of the file in memory.
3. Why is readfile('docs/' . $_GET['name']) dangerous?
This is path traversal; use an allow-list or basename() instead of raw input.
4. What does __DIR__ contain?
__DIR__ is the absolute path of the folder that holds the script, so paths built from it are reliable.
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
