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.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
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.
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.
📋 Quick Reference — File Handling
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.
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.