File Handling
By the end of this lesson you'll be able to read, write, and append files; stream large files line by line; handle CSV and JSON; work with directories; and do it all safely — the foundation of logging, exports, config, and data persistence in PHP.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ The Quick Pair: file_put_contents & file_get_contents
The fastest way to work with a file is the quick pair : two functions that each do a whole job in one line. file_put_contents($file, $text) creates the file (or overwrites it) and writes your string; file_get_contents($file) reads the whole file back as one string. Pass the flag FILE_APPEND to add to the end instead of wiping the file first. These open and close the file for you, so they're perfect for small files and simple jobs.
2️⃣ Handles & Modes: fopen, fwrite, fclose
When you need to do several operations on one file, open a handle — a live connection to the file you keep open. fopen($file, $mode) returns the handle; fwrite() writes to it; and you must call fclose() when finished. The mode is the most important choice you'll make: "r" reads (the file must exist), "w" writes but truncates the file to empty first, and "a" appends to the end and never erases.
3️⃣ Reading Line by Line: fgets, feof & file()
For a large file, reading the whole thing at once can use a lot of memory. Instead, loop with fgets($handle) , which returns the next line each call and false when there are no more — so while (($line = fgets($handle)) !== false) walks the file one line at a time. feof() ("end of file") tells you when you've read past the last line. If the file is small and you just want every line, file($file) returns them as an array in one call.
4️⃣ Check Before You Touch: file_exists & is_readable
Reading a file that isn't there throws a warning and can break your page. Defend against it: file_exists($file) asks "is anything at this path?", is_readable($file) asks "am I allowed to read it?", and is_writable($file) asks the same about writing. Checking first turns a crash into a message you control. You can also inspect a path with filesize() , pathinfo() , and basename() .
5️⃣ CSV Files: fgetcsv & fputcsv
CSV (comma-separated values) is the format spreadsheets and exports speak. Don't build CSV by gluing strings with commas — a value that contains a comma would break the columns. Instead, fputcsv($handle, $array) writes one array as a correctly-quoted CSV line, and fgetcsv($handle) reads one line back into an array, respecting quotes. Notice in the output that Bob's comma-containing email stays a single field.
Now you try. This script should build a log that grows — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
6️⃣ JSON Files: json_encode & json_decode
JSON is how APIs and config files store structured data. json_encode($array) turns an array into a JSON string you can save (add JSON_PRETTY_PRINT to indent it for humans), and json_decode($string, true) turns it back into an associative array. Pair them with the quick read/write functions and you have a tiny database in a file.
One more guided exercise. Number every line of a file by reading it one line at a time. Replace each ___ using the hints.
7️⃣ Directories: scandir & mkdir
Folders have their own small toolkit. mkdir($dir, 0755, true) creates a directory — the true also creates any missing parent folders, and 0755 sets permissions. is_dir() checks a folder exists. scandir($dir) lists everything inside — but it always includes "." (this folder) and ".." (the parent), so filter those out before you use the list.
Safety: never trust a user-supplied path
If you build a file path from user input — say a ?file= value in the URL — an attacker can send something like ../../etc/passwd to climb out of your folder and read system files. This is a path traversal attack, and it's one of the most common file-handling bugs.
Defend in three layers: strip the directory part with basename($input) so only a filename survives; keep everything inside a fixed base folder; and validate the name against an allow-list before opening it.
Even though the name "passwd" survives, it's now safely pinned inside your uploads folder — the dangerous ../ parts are stripped, so it can never reach the real system file.
Common Errors (and the fix)
- Your file is suddenly empty after you "added" to it — you opened it with mode "w" , which truncates the file to empty the instant it opens. To add without erasing, use "a" (append) or file_put_contents($f, $text, FILE_APPEND) .
- "failed to open stream: No such file or directory" — the path is wrong or the folder doesn't exist yet. Check for a typo, and create the directory with mkdir($dir, 0755, true) before writing into it. Guard reads with file_exists() first.
- "failed to open stream: Permission denied" — the account PHP runs as isn't allowed to read or write there. Confirm with is_writable($dir) , and make the target folder writable by the server (or write to sys_get_temp_dir() , which always is).
- Writes seem to vanish, or the file is locked by another process — you forgot fclose($handle) , so buffered data was never flushed and the handle stayed open. Always close a handle when you're done; the quick functions close for you.
- A user reads files they shouldn't — you built the path from raw input, allowing ../ path traversal. Run the name through basename() and an allow-list, and keep it inside a fixed base folder.
Pro Tips
- 💡 For log files, append with a lock: file_put_contents($f, $msg . PHP_EOL, FILE_APPEND | LOCK_EX) — LOCK_EX stops two simultaneous writes from corrupting each other.
- 💡 Use PHP_EOL for line endings instead of hard-coding \n when a file may be read on Windows — it picks the right newline for the platform.
- 💡 Small files only? Prefer the quick pair; reach for fopen() handles when files get large or you need to stream them line by line.
📋 Quick Reference — File Handling
Function / Mode
Example
What It Does
file_put_contents()
file_put_contents($f, $s)
Write a string (overwrites)
FILE_APPEND
..., $s, FILE_APPEND)
Add to the end, don't erase
file_get_contents()
$s = file_get_contents($f)
Read whole file as a string
file()
$lines = file($f)
Read file into an array of lines
fopen() / fclose()
$h = fopen($f, "r")
Open / close a file handle
fgets() / fwrite()
fgets($h)
Read / write one line at a time
"r" / "w" / "a"
fopen($f, "a")
Read / overwrite / append mode
fgetcsv() / fputcsv()
$row = fgetcsv($h)
Read / write one CSV line
json_encode/decode()
json_decode($s, true)
Array ⇄ JSON string
file_exists()
file_exists($f)
Does the path exist?
scandir() / mkdir()
scandir($dir)
List / create directories
Frequently Asked Questions
Mini-Challenge: A High-Score Table in JSON
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This save-then-load loop is exactly how real apps persist data between runs.
🎉 Lesson Complete!
- ✅ The quick pair — file_put_contents() and file_get_contents() — write and read a whole file in one line; add FILE_APPEND to grow it
- ✅ A fopen() handle + fwrite() / fgets() let you stream, and you always finish with fclose()
- ✅ Modes matter: "r" reads, "w" overwrites, "a" appends
- ✅ Read line by line with fgets() / feof() , or all at once with file()
- ✅ Use fgetcsv() / fputcsv() for CSV and json_encode() / json_decode() for JSON
- ✅ Guard with file_exists() / is_readable() , and never trust a user-supplied path — sanitise with basename()
- ✅ Next lesson: Forms & User Input — receive data from the browser and validate it safely
Practice quiz
What does file_put_contents($file, $text) do if the file already exists?
- Appends to the end
- Throws an error
- Overwrites the whole file
- Does nothing
Answer: Overwrites the whole file. file_put_contents creates the file or overwrites it. Pass the FILE_APPEND flag to add to the end instead.
Which flag makes file_put_contents add to the end instead of overwriting?
- FILE_APPEND
- FILE_ADD
- FILE_END
- APPEND_MODE
Answer: FILE_APPEND. Pass FILE_APPEND as the third argument to write to the end of the file without erasing it first.
What happens to a file the moment you open it with fopen($file, 'w')?
- Nothing until you write
- It is locked but kept
- It is deleted
- It is truncated (emptied) immediately
Answer: It is truncated (emptied) immediately. Mode 'w' truncates the file to empty the instant it opens. Use 'a' (append) if you want to keep existing content.
Which fopen mode appends to the end and never erases existing content?
- 'r'
- 'a'
- 'w'
- 'x'
Answer: 'a'. Mode 'a' (append) sends every write to the end of the file and never erases. 'r' reads, 'w' overwrites.
What does fgets($handle) return when there are no more lines?
- false
- An empty string
- null
- 0
Answer: false. fgets returns the next line each call and false at the end, which is why while (($line = fgets($h)) !== false) walks the file.
What does file($file) return?
- The whole file as one string
- A file handle
- An array with one element per line
- The number of bytes
Answer: An array with one element per line. file() reads the whole file into an array, one line per element (use FILE_IGNORE_NEW_LINES to drop trailing newlines).
Why use fgetcsv()/fputcsv() instead of splitting on commas yourself?
- They are faster
- They correctly handle commas and quotes inside a field
- They compress the file
- They sort the rows
Answer: They correctly handle commas and quotes inside a field. fputcsv/fgetcsv handle quoting and escaping, so a value that contains a comma (like an email) stays a single field.
When reading config back with json_decode(file_get_contents($f), true), what does the true do?
- Validates the JSON
- Pretty-prints it
- Throws on error
- Returns an associative array instead of an object
Answer: Returns an associative array instead of an object. The true second argument makes json_decode return an associative array rather than a stdClass object.
After scandir($dir), which two entries should you filter out?
- '.git' and '.env'
- '.' and '..'
- 'tmp' and 'log'
- Hidden files only
Answer: '.' and '..'. scandir always includes '.' (this folder) and '..' (the parent), so filter those out before using the list.
How do you defend against a path-traversal value like '../../etc/passwd'?
- Trust the browser MIME type
- Use file_get_contents directly
- Strip the directory part with basename() and validate against an allow-list
- Add FILE_APPEND
Answer: Strip the directory part with basename() and validate against an allow-list. basename() throws away any directory part so the .. climbing is gone; keep files in a fixed base folder and allow-list the name.
Continue this course
- Previous: Arrays & Collections
- Next: Forms & User Input