Files are still useful
Most application data belongs in a database, but files are everywhere: configuration, logs, exported reports, imported price lists, cached data. PHP makes simple file work very short. Every example here works in the current folder; in real code, build paths from __DIR__ as in include, require and splitting code into files.
Reading and writing a whole file
file_put_contents() writes a string to a file, creating it if needed and replacing what was there. It returns the number of bytes written, or false on failure. file_get_contents() reads the whole file back as a string:
<?php
$bytes = file_put_contents('notes.txt', "Restock keyboards\nCall the courier\n");
echo "Wrote $bytes bytes\n";
$text = file_get_contents('notes.txt');
echo $text;
echo strlen($text) . " characters\n";Wrote 35 bytes
Restock keyboards
Call the courier
35 charactersTo add to the end of a file instead of replacing it, pass FILE_APPEND. LOCK_EX locks the file during the write, so two visitors writing at the same moment do not mix their lines. This is a simple log:
<?php
function logLine(string $message): void
{
$line = date('Y-m-d H:i:s', 1717405200) . " $message" . PHP_EOL;
file_put_contents('app.log', $line, FILE_APPEND | LOCK_EX);
}
logLine("Order 1042 placed");
logLine("Order 1042 paid");
echo file_get_contents('app.log');2024-06-03 09:00:00 Order 1042 placed
2024-06-03 09:00:00 Order 1042 paid(The time is fixed here so the output is repeatable; a real log would use date('Y-m-d H:i:s').)
Reading line by line
file() reads a file into an array of lines. The flags remove line endings and skip blank lines:
<?php
file_put_contents('emails.txt', "asha@example.com\n\nliam@example.org\nkavya@example.in\n");
$emails = file('emails.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
echo count($emails) . " addresses\n";
foreach ($emails as $number => $email) {
echo ($number + 1) . ": $email\n";
}3 addresses
1: asha@example.com
2: liam@example.org
3: kavya@example.inBoth file() and file_get_contents() load the whole file into memory, which is fine for files up to a few megabytes. For large files, such as a log with millions of lines, open the file and read one line at a time with fopen() and fgets(). Memory use then stays small whatever the file's size:
<?php
file_put_contents('access.log', "200 /\n404 /old-page\n200 /tools/\n500 /checkout\n404 /missing\n");
$handle = fopen('access.log', 'r');
$errors = 0;
while (($line = fgets($handle)) !== false) {
[$status, $path] = explode(' ', trim($line), 2);
if ($status >= 400) {
$errors++;
echo "Problem: $status $path\n";
}
}
fclose($handle);
echo "$errors problems found\n";Problem: 404 /old-page
Problem: 500 /checkout
Problem: 404 /missing
3 problems foundfopen() takes a mode: 'r' read, 'w' write (empties the file first), 'a' append, 'x' create a new file and fail if it exists, and 'r+' read and write. fgets() returns false at the end of the file, which ends the loop. Always fclose() what you open.
CSV files
CSV (comma-separated values) is how spreadsheets exchange data. Do not split CSV lines with explode(): fields can contain commas, quotes and even line breaks inside quotes. fputcsv() and fgetcsv() handle all of that:
<?php
$rows = [
['sku', 'name', 'price'],
['KBD-01', 'Mechanical Keyboard', '89.50'],
['SP-02', 'Salt, Pepper "Classic" Set', '9.99'],
];
$out = fopen('products.csv', 'w');
foreach ($rows as $row) {
fputcsv($out, $row);
}
fclose($out);
echo file_get_contents('products.csv');sku,name,price
KBD-01,"Mechanical Keyboard",89.50
SP-02,"Salt, Pepper ""Classic"" Set",9.99fputcsv() added quotes where a field contains spaces, commas or quotes, and doubled the quotes inside. Reading it back gives the original values, and the header row can become the keys of each record:
<?php
file_put_contents('products.csv', "sku,name,price\nKBD-01,\"Mechanical Keyboard\",89.50\nSP-02,\"Salt, Pepper \"\"Classic\"\" Set\",9.99\n");
$in = fopen('products.csv', 'r');
$header = fgetcsv($in);
$products = [];
while (($row = fgetcsv($in)) !== false) {
$products[] = array_combine($header, $row);
}
fclose($in);
foreach ($products as $p) {
echo "{$p['sku']}: {$p['name']} at {$p['price']}\n";
}KBD-01: Mechanical Keyboard at 89.50
SP-02: Salt, Pepper "Classic" Set at 9.99When a CSV export is opened in Excel, accented characters can look wrong unless the file starts with a UTF-8 "byte order mark". Writing "\xEF\xBB\xBF" at the start of the file fixes that.
Checking files and folders
Reading a file that does not exist gives a warning and false. Check first, and check the result of writes:
<?php
var_dump(is_file('notes.txt'));
var_dump(is_file('missing.txt'));
$text = @file_get_contents('missing.txt');
if ($text === false) {
echo "Could not read missing.txt\n";
}
if (!is_dir('exports')) {
mkdir('exports', 0775, true);
}
var_dump(is_dir('exports'));
echo filesize('notes.txt') . " bytes\n";
print_r(array_values(array_diff(scandir('.'), ['.', '..'])));bool(true)
bool(false)
Could not read missing.txt
bool(true)
35 bytes
Array
(
[0] => access.log
[1] => app.log
[2] => emails.txt
[3] => exports
[4] => notes.txt
[5] => products.csv
[6] => script.php
)notes.txt from the first example is still there, while missing.txt never existed. scandir() lists the folder, including . and .. (the folder itself and its parent), which array_diff() removes; script.php is the example being run. Other useful functions: is_readable(), is_writable(), unlink() (delete a file), rename(), copy(), glob('*.csv') (list matching files) and pathinfo() (split a path into folder, name and extension).
The @ in front of file_get_contents() hides the warning, because the code handles the failure itself. Use @ only like this, never to hide problems you have not dealt with.
Security with paths
Never build a file path directly from user input. A request for ?file=../../.env could read your secrets. If visitors may choose a file, choose it from a list you control, or strip the input down with basename() and check it against an allowed pattern. And keep files that should not be public outside the web root.
Common mistakes
- Forgetting FILE_APPEND, so each log line replaces the whole log.
- Splitting CSV with explode(), which breaks on commas inside fields.
- Loading huge files with file() and running out of memory. Read line by line.
- Building paths from user input, which allows reading any file on the server.
Exercise
Write a script that creates scores.csv with the rows name,score, Asha,82, Liam,67 and Kavya,91, then reads it back with fgetcsv() and prints the average score and the name of the top scorer.
Show one possible answer
<?php
$out = fopen('scores.csv', 'w');
foreach ([['name', 'score'], ['Asha', 82], ['Liam', 67], ['Kavya', 91]] as $row) {
fputcsv($out, $row);
}
fclose($out);
$in = fopen('scores.csv', 'r');
fgetcsv($in); // skip the header
$scores = [];
while (($row = fgetcsv($in)) !== false) {
$scores[$row[0]] = (int) $row[1];
}
fclose($in);
echo "Average: " . round(array_sum($scores) / count($scores), 1) . "\n";
arsort($scores);
echo "Top: " . array_key_first($scores) . "\n";Average: 80
Top: KavyaKey points
- file_get_contents() reads a whole file into a string; file_put_contents() writes one, and FILE_APPEND adds to the end.
- file() returns the lines as an array; fopen() with fgets() reads line by line, which suits large files.
- fgetcsv() and fputcsv() read and write CSV correctly, including quotes and commas inside fields.
- Check that a file exists (is_file, is_readable) and that a write succeeded, and build paths with __DIR__.