📂 Lesson 13: File Handling & Includes
PHP can read and write files on the server — logs, configuration files, user uploads, CSV exports, cached data. It can also include other PHP files to organize your code into reusable pieces. This lesson covers both: file I/O for data, and includes for structure.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Read entire files into strings or arrays with
file_get_contents()andfile() - Write and append to files with
file_put_contents() - Use the handle-based API (
fopen,fread,fwrite,fclose) for fine-grained control - Check file existence, size, and permissions before operating on files
- Organize code with
include,require,include_once, andrequire_once - Use
__DIR__and__FILE__for reliable file paths
Estimated Time: 45 minutes
Prerequisites: Lessons 11–12 (forms, validation)
📑 In This Lesson
Reading Files
PHP provides several functions for reading files, each suited to different situations.
file_get_contents() — Read Entire File as a String
This is the simplest and most common way to read a file. It slurps the entire file into a single string:
<?php
// Read entire file into a string
$content = file_get_contents("data/notes.txt");
echo $content;
// Read a file from an absolute path
$config = file_get_contents("/var/www/config/settings.ini");
// Read from a URL (if allow_url_fopen is enabled)
$html = file_get_contents("https://example.com");
// Read with error handling
$content = @file_get_contents("maybe_missing.txt");
if ($content === false) {
echo "Could not read the file.";
} else {
echo "File contains " . strlen($content) . " bytes.";
}
⚠️ Memory Warning
file_get_contents() loads the entire file into memory. For a 10MB log file, that's 10MB of RAM. For very large files, use the handle-based API (fopen/fread) to read in chunks instead.
file() — Read File into an Array of Lines
<?php
// Each line becomes an array element (newlines included)
$lines = file("data/names.txt");
// ["Alice\n", "Bob\n", "Carol\n", "Dave\n"]
// Strip newlines with FILE_IGNORE_NEW_LINES
$lines = file("data/names.txt", FILE_IGNORE_NEW_LINES);
// ["Alice", "Bob", "Carol", "Dave"]
// Also skip empty lines
$lines = file("data/names.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// Process each line
$lines = file("data/scores.txt", FILE_IGNORE_NEW_LINES);
foreach ($lines as $lineNum => $line) {
echo "Line " . ($lineNum + 1) . ": $line\n";
}
// Count lines in a file
$lineCount = count(file("access.log"));
echo "Log has $lineCount entries.";
readfile() — Output File Directly
<?php
// Reads a file and sends it directly to the output buffer
// Useful for serving file downloads — no variable needed
header("Content-Type: application/pdf");
header("Content-Disposition: attachment; filename=\"report.pdf\"");
readfile("files/report.pdf");
exit;
Choosing the Right Read Function
| Function | Returns | Best For |
|---|---|---|
file_get_contents() |
Single string | Reading config files, JSON, HTML, small text files |
file() |
Array of lines | Line-by-line processing, counting lines, CSV-like data |
readfile() |
Outputs directly | Serving file downloads, streaming to browser |
fopen() + fread() |
Chunks at a time | Large files, binary files, fine-grained control |
Writing Files
file_put_contents() — Write String to File
The counterpart to file_get_contents(). Writes a string to a file, creating it if it doesn't exist:
<?php
// Write a string to a file (creates file if needed, overwrites if exists)
file_put_contents("data/output.txt", "Hello, World!\n");
// Write an array of lines
$lines = ["Line 1", "Line 2", "Line 3"];
file_put_contents("data/output.txt", implode("\n", $lines));
// Append to a file (don't overwrite!)
file_put_contents("data/log.txt", "New entry\n", FILE_APPEND);
// Append with a lock (prevents concurrent writes from corrupting data)
file_put_contents("data/log.txt", "Safe entry\n", FILE_APPEND | LOCK_EX);
// Returns number of bytes written, or false on failure
$bytes = file_put_contents("data/output.txt", $content);
if ($bytes === false) {
echo "Failed to write file!";
} else {
echo "Wrote $bytes bytes.";
}
Practical Example: Simple Log Function
<?php
function log_message(string $level, string $message): void {
$timestamp = date("Y-m-d H:i:s");
$entry = "[$timestamp] [$level] $message\n";
file_put_contents("logs/app.log", $entry, FILE_APPEND | LOCK_EX);
}
// Usage
log_message("INFO", "User alice logged in");
log_message("ERROR", "Failed to connect to database");
log_message("WARNING", "Disk space below 10%");
// Log file looks like:
// [2026-04-17 14:30:00] [INFO] User alice logged in
// [2026-04-17 14:30:01] [ERROR] Failed to connect to database
// [2026-04-17 14:30:02] [WARNING] Disk space below 10%
Practical Example: JSON Data Store
<?php
// Save data as JSON
function save_data(string $file, array $data): bool {
$json = json_encode($data, JSON_PRETTY_PRINT);
return file_put_contents($file, $json, LOCK_EX) !== false;
}
// Load data from JSON
function load_data(string $file): array {
if (!file_exists($file)) {
return [];
}
$json = file_get_contents($file);
return json_decode($json, true) ?? [];
}
// Usage: a simple "database" using JSON files
$todos = load_data("data/todos.json");
$todos[] = [
"task" => "Learn PHP file handling",
"done" => false,
"created" => date("Y-m-d H:i:s"),
];
save_data("data/todos.json", $todos);
✅ file_put_contents Covers Most Cases
For writing small to medium files (config, logs, JSON, text), file_put_contents() is all you need. Use the FILE_APPEND flag for logs, and LOCK_EX when multiple processes might write simultaneously.
The Handle-Based API
For more control — reading large files in chunks, writing to specific positions, or working with binary data — use the traditional open/read/write/close pattern.
fopen() Modes
| Mode | Description | Creates? | Truncates? |
|---|---|---|---|
"r" |
Read only (pointer at start) | No | No |
"w" |
Write only (erases existing content!) | Yes | Yes |
"a" |
Append (write at end) | Yes | No |
"r+" |
Read and write (pointer at start) | No | No |
"w+" |
Read and write (erases existing!) | Yes | Yes |
"a+" |
Read and append | Yes | No |
⚠️ "w" Mode Erases Everything!
fopen("file.txt", "w") immediately empties the file, even before you write anything. If you want to add to an existing file, use "a" (append). This is one of the most common mistakes in file handling.
Reading with fopen/fgets/fclose
<?php
// Open the file
$handle = fopen("data/large_log.txt", "r");
if ($handle === false) {
die("Cannot open file!");
}
// Read line by line (memory efficient for large files)
$lineNumber = 0;
while (($line = fgets($handle)) !== false) {
$lineNumber++;
$line = trim($line); // Remove trailing newline
echo "Line $lineNumber: $line\n";
}
// Close the file (important!)
fclose($handle);
Reading in Chunks with fread
<?php
// Read a specific number of bytes at a time
$handle = fopen("data/bigfile.bin", "r");
while (!feof($handle)) { // feof = "end of file?"
$chunk = fread($handle, 8192); // Read 8KB at a time
// Process $chunk...
echo "Read " . strlen($chunk) . " bytes\n";
}
fclose($handle);
Writing with fopen/fwrite/fclose
<?php
// Write to a new file
$handle = fopen("data/report.txt", "w");
fwrite($handle, "Sales Report\n");
fwrite($handle, str_repeat("=", 30) . "\n\n");
$products = [
["Widget", 150, 12.99],
["Gadget", 75, 24.99],
["Doohickey", 200, 8.50],
];
foreach ($products as [$name, $qty, $price]) {
$line = sprintf("%-15s %5d $%8.2f\n", $name, $qty, $price);
fwrite($handle, $line);
}
fwrite($handle, "\nGenerated: " . date("Y-m-d H:i:s") . "\n");
fclose($handle);
// Append to an existing log
$log = fopen("data/access.log", "a");
$entry = date("Y-m-d H:i:s") . " | " . $_SERVER["REMOTE_ADDR"] . " | " . $_SERVER["REQUEST_URI"] . "\n";
fwrite($log, $entry);
fclose($log);
The fgetcsv/fputcsv Shortcut
PHP has built-in CSV parsing functions that work with file handles. We'll cover these in detail in the next section.
✅ Always Close Your Files
Every fopen() must be paired with an fclose(). Open file handles consume system resources, and leaving them open can lead to resource exhaustion, file locking issues, and data loss (unflushed write buffers). PHP closes handles at script end, but explicit closing is a best practice.
File Information & Checks
Before reading or writing, check that the file exists and that you have permission to access it.
Essential Check Functions
<?php
$file = "data/config.json";
// Does it exist?
if (file_exists($file)) {
echo "File exists!\n";
}
// Is it a regular file (not a directory)?
if (is_file($file)) {
echo "It's a file!\n";
}
// Is it a directory?
if (is_dir("data/")) {
echo "It's a directory!\n";
}
// Can we read it?
if (is_readable($file)) {
echo "We can read it!\n";
}
// Can we write to it?
if (is_writable($file)) {
echo "We can write to it!\n";
}
// File size in bytes
$size = filesize($file);
echo "Size: $size bytes\n";
// Last modified time (Unix timestamp)
$modified = filemtime($file);
echo "Last modified: " . date("Y-m-d H:i:s", $modified) . "\n";
Safe File Operations Pattern
<?php
function safe_read(string $path): string|false {
if (!file_exists($path)) {
error_log("File not found: $path");
return false;
}
if (!is_readable($path)) {
error_log("File not readable: $path");
return false;
}
return file_get_contents($path);
}
function safe_write(string $path, string $content): bool {
// Ensure the directory exists
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0755, true); // Create directory recursively
}
$bytes = file_put_contents($path, $content, LOCK_EX);
if ($bytes === false) {
error_log("Failed to write: $path");
return false;
}
return true;
}
pathinfo() — Dissect File Paths
<?php
$path = "/var/www/html/uploads/photo_2026.jpg";
$info = pathinfo($path);
echo $info["dirname"]; // "/var/www/html/uploads"
echo $info["basename"]; // "photo_2026.jpg"
echo $info["filename"]; // "photo_2026"
echo $info["extension"]; // "jpg"
// Or get a specific component
echo pathinfo($path, PATHINFO_EXTENSION); // "jpg"
// basename() gets just the filename from a path
echo basename($path); // "photo_2026.jpg"
echo basename($path, ".jpg"); // "photo_2026" (strip extension)
// dirname() gets the directory portion
echo dirname($path); // "/var/www/html/uploads"
echo dirname($path, 2); // "/var/www/html" (go up 2 levels)
Other Useful File Functions
<?php
// Delete a file
unlink("data/temp.txt");
// Rename/move a file
rename("old_name.txt", "new_name.txt");
rename("uploads/temp.txt", "archive/document.txt"); // Also moves
// Copy a file
copy("template.html", "pages/new_page.html");
// Create a directory
mkdir("data/exports", 0755); // Single directory
mkdir("data/exports/2026/04", 0755, true); // Recursive (create all levels)
// Delete an empty directory
rmdir("data/old_exports");
// List files in a directory
$files = scandir("uploads/");
// [".", "..", "file1.txt", "file2.jpg", "file3.pdf"]
// Cleaner — filter out . and ..
$files = array_diff(scandir("uploads/"), [".", ".."]);
// glob() — find files matching a pattern
$phpFiles = glob("*.php"); // All PHP files in current dir
$images = glob("uploads/*.{jpg,png,gif}", GLOB_BRACE); // Multiple extensions
$allLogs = glob("logs/**/*.log"); // (no recursive by default)
Working with CSV Files
CSV (Comma-Separated Values) is one of the most common data exchange formats. PHP has dedicated functions for reading and writing CSV data.
Reading CSV with fgetcsv()
<?php
// Sample CSV file (data/products.csv):
// name,price,stock,category
// Widget,12.99,150,Tools
// Gadget,49.99,30,Electronics
// Doohickey,8.50,200,Tools
$handle = fopen("data/products.csv", "r");
if ($handle === false) {
die("Cannot open CSV file!");
}
// Read the header row
$headers = fgetcsv($handle);
// ["name", "price", "stock", "category"]
// Read data rows
$products = [];
while (($row = fgetcsv($handle)) !== false) {
// Combine headers with values to get associative array
$products[] = array_combine($headers, $row);
}
fclose($handle);
// Now $products is:
// [
// ["name" => "Widget", "price" => "12.99", "stock" => "150", "category" => "Tools"],
// ["name" => "Gadget", "price" => "49.99", "stock" => "30", "category" => "Electronics"],
// ...
// ]
foreach ($products as $p) {
echo sprintf("%-15s $%6.2f (%s in stock)\n",
$p["name"], (float)$p["price"], $p["stock"]);
}
Writing CSV with fputcsv()
<?php
$orders = [
["order_id" => 1001, "customer" => "Alice", "total" => 89.97, "date" => "2026-04-15"],
["order_id" => 1002, "customer" => "Bob", "total" => 45.50, "date" => "2026-04-16"],
["order_id" => 1003, "customer" => "Carol", "total" => 123.00, "date" => "2026-04-17"],
];
$handle = fopen("data/orders_export.csv", "w");
// Write header row
fputcsv($handle, ["Order ID", "Customer", "Total", "Date"]);
// Write data rows
foreach ($orders as $order) {
fputcsv($handle, [
$order["order_id"],
$order["customer"],
number_format($order["total"], 2),
$order["date"],
]);
}
fclose($handle);
echo "Exported " . count($orders) . " orders to CSV.";
Complete CSV Helper Functions
<?php
/**
* Read a CSV file into an array of associative arrays.
* First row is treated as headers.
*/
function read_csv(string $file): array {
if (!file_exists($file)) return [];
$handle = fopen($file, "r");
if ($handle === false) return [];
$headers = fgetcsv($handle);
if ($headers === false) {
fclose($handle);
return [];
}
$data = [];
while (($row = fgetcsv($handle)) !== false) {
if (count($row) === count($headers)) {
$data[] = array_combine($headers, $row);
}
}
fclose($handle);
return $data;
}
/**
* Write an array of associative arrays to a CSV file.
* Headers are taken from the keys of the first row.
*/
function write_csv(string $file, array $data): bool {
if (empty($data)) return false;
$handle = fopen($file, "w");
if ($handle === false) return false;
// Write headers from first row's keys
fputcsv($handle, array_keys($data[0]));
// Write data rows
foreach ($data as $row) {
fputcsv($handle, array_values($row));
}
fclose($handle);
return true;
}
// Usage
$products = read_csv("data/products.csv");
// Filter and export
$tools = array_filter($products, fn($p) => $p["category"] === "Tools");
write_csv("data/tools_only.csv", array_values($tools));
include & require
As your projects grow, you'll want to split code into multiple files — reusable functions in one file, HTML headers in another, configuration in a third. PHP's include and require statements bring those files together.
📖 How It Works
When PHP encounters include "file.php", it pauses, reads and executes that file's code, then continues. It's as if the included file's code were pasted right at that spot.
include vs. require
| Statement | If File Not Found | Use When |
|---|---|---|
include |
Warning — script continues | Non-critical files (sidebar, ad block) |
require |
Fatal error — script stops | Essential files (config, database connection) |
include_once |
Warning — skips if already included | Helper functions (prevent redeclaration) |
require_once |
Fatal error — skips if already required | Class definitions, core libraries |
<?php
// include — warning if missing, script continues
include "sidebar.php"; // If missing: Warning, but page still loads
// require — fatal error if missing, script stops
require "config.php"; // If missing: Fatal error, nothing works
// include_once — only includes the file once, even if called multiple times
include_once "helpers/format.php";
include_once "helpers/format.php"; // Ignored — already included
// require_once — same as require, but only once
require_once "classes/Database.php";
require_once "classes/Database.php"; // Ignored — already required
✅ When to Use Which
require_once is the most common choice for function libraries and class definitions — it prevents redeclaration errors and fails loudly if the file is missing.
require for config files and database connections — if these are missing, the app can't function.
include for optional content like sidebars or widgets — the page should still render if a widget file is missing.
__DIR__ and __FILE__ — Reliable Paths
One of the trickiest parts of includes is getting the file path right. If your file structure looks like this:
project/
├── index.php
├── config.php
├── includes/
│ ├── header.php
│ ├── footer.php
│ └── functions.php
├── pages/
│ ├── about.php ← includes header.php
│ └── contact.php ← includes header.php
└── admin/
└── dashboard.php ← also includes header.php
If about.php uses include "../includes/header.php", it works. But if you visit the page from a different working directory, it breaks. The solution: use __DIR__.
<?php
// __DIR__ = absolute path to the directory containing THIS file
// __FILE__ = absolute path to THIS file
// In pages/about.php:
echo __DIR__; // "/var/www/project/pages"
echo __FILE__; // "/var/www/project/pages/about.php"
// BAD — relative path depends on working directory
include "../includes/header.php"; // Might break!
// GOOD — absolute path using __DIR__
include __DIR__ . "/../includes/header.php"; // Always works!
// BETTER — define a project root constant
define("ROOT_PATH", dirname(__DIR__)); // Goes up one level from pages/
include ROOT_PATH . "/includes/header.php";
include ROOT_PATH . "/includes/footer.php";
⚠️ Always Use __DIR__ for Include Paths
Relative paths like "../includes/file.php" are relative to the current working directory, which can change depending on how the script is called (web server, CLI, cron job). __DIR__ is always relative to the file containing the statement, so it works consistently everywhere.
Variables and Scope in Included Files
<?php
// config.php
$dbHost = "localhost";
$dbName = "myapp";
$siteName = "My Awesome Site";
// index.php
require "config.php";
// $dbHost, $dbName, $siteName are now available here
echo $siteName; // "My Awesome Site"
// If you include inside a function, the included file
// only has access to that function's scope:
function loadConfig() {
include "config.php";
echo $siteName; // Works — it's in this function's scope
}
// echo $siteName; // Error — not available outside the function!
Building Reusable Components
The most common use of includes is building a page layout system — a shared header, footer, and navigation that every page uses.
Project Structure
project/
├── config.php ← Site-wide settings
├── includes/
│ ├── header.php ← HTML head, nav, opening tags
│ ├── footer.php ← Footer, closing tags, scripts
│ └── functions.php ← Reusable helper functions
├── index.php ← Home page
├── about.php ← About page
└── contact.php ← Contact page
config.php
<?php
// Site configuration
define("SITE_NAME", "PHP Foundations Demo");
define("SITE_URL", "http://localhost/project");
define("ROOT_PATH", __DIR__);
// Database config (for later lessons)
define("DB_HOST", "localhost");
define("DB_NAME", "myapp");
define("DB_USER", "root");
define("DB_PASS", "");
includes/functions.php
<?php
/**
* Escape output for safe HTML display.
*/
function e(string $text): string {
return htmlspecialchars($text, ENT_QUOTES, "UTF-8");
}
/**
* Check if the current page matches a given path (for active nav highlighting).
*/
function is_active(string $page): string {
$current = basename($_SERVER["PHP_SELF"]);
return $current === $page ? 'class="active"' : '';
}
/**
* Redirect to a different page.
*/
function redirect(string $url): never {
header("Location: $url");
exit;
}
/**
* Display a formatted error or success message.
*/
function flash_message(string $type, string $message): string {
return "<div class=\"alert alert-$type\">" . e($message) . "</div>";
}
includes/header.php
<?php
require_once __DIR__ . "/../config.php";
require_once __DIR__ . "/functions.php";
// $pageTitle should be set before including header.php
$pageTitle = ($pageTitle ?? "Home") . " — " . SITE_NAME;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e($pageTitle) ?></title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav>
<a href="index.php" <?= is_active("index.php") ?>>Home</a>
<a href="about.php" <?= is_active("about.php") ?>>About</a>
<a href="contact.php" <?= is_active("contact.php") ?>>Contact</a>
</nav>
<main>
includes/footer.php
</main>
<footer>
<p>© <?= date("Y") ?> <?= e(SITE_NAME) ?>. All rights reserved.</p>
</footer>
<script src="scripts.js"></script>
</body>
</html>
index.php — Using the Layout
<?php
$pageTitle = "Home";
include __DIR__ . "/includes/header.php";
?>
<h1>Welcome to <?= e(SITE_NAME) ?></h1>
<p>This is the home page content.</p>
<?php include __DIR__ . "/includes/footer.php"; ?>
about.php — Same Layout, Different Content
<?php
$pageTitle = "About Us";
include __DIR__ . "/includes/header.php";
?>
<h1>About Us</h1>
<p>We're learning PHP!</p>
<?php include __DIR__ . "/includes/footer.php"; ?>
✅ This Is How Real PHP Sites Work
Every PHP framework (Laravel, Symfony, WordPress) uses this same basic concept — shared layout files with page-specific content injected in the middle. Frameworks add more sophistication (template engines, layout inheritance), but the core idea is the same: split repetitive HTML into included files.
Hands-On Exercises
🏋️ Exercise 1: Guestbook
Objective: Build a guestbook that stores entries in a text file.
Instructions:
- Create a self-processing form with Name, Email, and Message fields
- On valid submission, append the entry to
data/guestbook.txt(one JSON object per line) - Display all previous entries below the form (newest first)
- Format each entry with the name, date, and message
- Handle the case where the file doesn't exist yet
💡 Hint
Store each entry as a single line of JSON: json_encode($entry) . "\n". Read all lines with file(), decode each, and reverse the array to show newest first.
✅ Solution
<?php
$dataFile = __DIR__ . "/data/guestbook.txt";
$name = "";
$email = "";
$message = "";
$errors = [];
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");
$message = trim($_POST["message"] ?? "");
if ($name === "") $errors[] = "Name is required.";
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = "Valid email is required.";
if ($message === "") $errors[] = "Message is required.";
if (empty($errors)) {
$entry = json_encode([
"name" => $name,
"email" => $email,
"message" => $message,
"date" => date("Y-m-d H:i:s"),
]);
// Ensure directory exists
if (!is_dir(dirname($dataFile))) {
mkdir(dirname($dataFile), 0755, true);
}
file_put_contents($dataFile, $entry . "\n", FILE_APPEND | LOCK_EX);
header("Location: guestbook.php?success=1");
exit;
}
}
// Load existing entries
$entries = [];
if (file_exists($dataFile)) {
$lines = file($dataFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$decoded = json_decode($line, true);
if ($decoded) $entries[] = $decoded;
}
$entries = array_reverse($entries); // Newest first
}
?>
<h2>Guestbook</h2>
<?php if (isset($_GET["success"])): ?>
<p class="success">Thank you for signing the guestbook!</p>
<?php endif; ?>
<form method="post" action="">
<label>Name: <input type="text" name="name" value="<?= htmlspecialchars($name) ?>"></label><br>
<label>Email: <input type="email" name="email" value="<?= htmlspecialchars($email) ?>"></label><br>
<label>Message:<br><textarea name="message"><?= htmlspecialchars($message) ?></textarea></label><br>
<button type="submit">Sign Guestbook</button>
</form>
<h3><?= count($entries) ?> Entries</h3>
<?php foreach ($entries as $e): ?>
<div class="entry">
<strong><?= htmlspecialchars($e["name"]) ?></strong>
<small>(<?= htmlspecialchars($e["date"]) ?>)</small>
<p><?= nl2br(htmlspecialchars($e["message"])) ?></p>
</div>
<?php endforeach; ?>
🏋️ Exercise 2: CSV Product Manager
Objective: Build a page that reads products from a CSV file, displays them in a table, and lets you add new products.
Instructions:
- Create
data/products.csvwith headers: name, price, stock, category - Read and display all products in an HTML table
- Add a form below the table to add a new product
- Validate: name required, price must be positive number, stock must be non-negative integer
- On valid submission, append to the CSV file and redirect
💡 Hint
Use the read_csv() helper to load products. Open the file in append mode ("a") with fopen/fputcsv to add a new row without overwriting existing data.
✅ Solution
<?php
$csvFile = __DIR__ . "/data/products.csv";
$errors = [];
// Initialize CSV if it doesn't exist
if (!file_exists($csvFile)) {
if (!is_dir(dirname($csvFile))) mkdir(dirname($csvFile), 0755, true);
file_put_contents($csvFile, "name,price,stock,category\n");
}
// Handle form submission
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = trim($_POST["name"] ?? "");
$price = $_POST["price"] ?? "";
$stock = $_POST["stock"] ?? "";
$category = trim($_POST["category"] ?? "");
if ($name === "") $errors[] = "Product name is required.";
if (!is_numeric($price) || (float)$price <= 0) $errors[] = "Price must be a positive number.";
$stockInt = filter_var($stock, FILTER_VALIDATE_INT, ["options" => ["min_range" => 0]]);
if ($stockInt === false) $errors[] = "Stock must be a non-negative whole number.";
if (empty($errors)) {
$handle = fopen($csvFile, "a");
fputcsv($handle, [$name, number_format((float)$price, 2, ".", ""), $stockInt, $category]);
fclose($handle);
header("Location: products.php?added=1");
exit;
}
}
// Read products
$products = [];
$handle = fopen($csvFile, "r");
$headers = fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
if (count($row) === count($headers)) {
$products[] = array_combine($headers, $row);
}
}
fclose($handle);
?>
<h2>Products (<?= count($products) ?>)</h2>
<table border="1">
<tr><th>Name</th><th>Price</th><th>Stock</th><th>Category</th></tr>
<?php foreach ($products as $p): ?>
<tr>
<td><?= htmlspecialchars($p["name"]) ?></td>
<td>$<?= htmlspecialchars($p["price"]) ?></td>
<td><?= htmlspecialchars($p["stock"]) ?></td>
<td><?= htmlspecialchars($p["category"]) ?></td>
</tr>
<?php endforeach; ?>
</table>
<h3>Add Product</h3>
<form method="post">
<input type="text" name="name" placeholder="Product name" required>
<input type="number" name="price" step="0.01" placeholder="Price" required>
<input type="number" name="stock" min="0" placeholder="Stock" required>
<input type="text" name="category" placeholder="Category">
<button type="submit">Add</button>
</form>
🎯 Quick Quiz
Question 1: What's the difference between include and require?
Question 2: What does fopen("data.txt", "w") do if the file already has content?
Question 3: Why should you use __DIR__ in include paths?
Question 4: What flag should you add to file_put_contents() to append data instead of overwriting?
Question 5: When should you use require_once instead of require?
Summary
🎉 Key Takeaways
file_get_contents()reads an entire file into a string — the go-to for small/medium filesfile()reads a file into an array of lines — great for line-by-line processingfile_put_contents()writes a string to a file; useFILE_APPENDto add instead of overwrite,LOCK_EXfor safe concurrent writesfopen()/fread()/fwrite()/fclose()give fine-grained control for large files, binary data, and CSV operations- Always check before operating:
file_exists(),is_readable(),is_writable() fgetcsv()/fputcsv()handle CSV reading and writing with proper quoting and escapinginclude= warning if missing;require= fatal error if missing_oncevariants prevent duplicate inclusion — use for function libraries and class files- Use
__DIR__for reliable include paths — never rely on relative paths - Shared layouts — header.php + footer.php + page content is the standard PHP pattern
📚 Additional Resources
- PHP Manual: Filesystem Functions
- PHP Manual: file_get_contents()
- PHP Manual: file_put_contents()
- PHP Manual: include
- PHP Manual: Magic Constants (__DIR__, __FILE__, etc.)
🚀 What's Next?
With file handling and includes under your belt, Module 4 is complete! In Lesson 14: Superglobals, we kick off Module 5 by exploring all of PHP's superglobal arrays — $_SERVER, $_REQUEST, $_FILES, $_ENV, and more — to understand the full range of data PHP makes available to you.
🎉 Module 4 Complete!
You've finished the Web Fundamentals module! You can now build forms, validate input, handle files, and organize code with includes. You're building real web applications!