Skip to main content

📂 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() and file()
  • 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, and require_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>&copy; <?= 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"; ?>
flowchart TD A["index.php"] --> B["includes/header.php"] A --> C["Page-specific content"] A --> D["includes/footer.php"] B --> E["config.php"] B --> F["functions.php"] style A fill:#dbeafe,stroke:#3b82f6 style C fill:#dcfce7,stroke:#22c55e

✅ 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:

  1. Create a self-processing form with Name, Email, and Message fields
  2. On valid submission, append the entry to data/guestbook.txt (one JSON object per line)
  3. Display all previous entries below the form (newest first)
  4. Format each entry with the name, date, and message
  5. 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:

  1. Create data/products.csv with headers: name, price, stock, category
  2. Read and display all products in an HTML table
  3. Add a form below the table to add a new product
  4. Validate: name required, price must be positive number, stock must be non-negative integer
  5. 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 files
  • file() reads a file into an array of lines — great for line-by-line processing
  • file_put_contents() writes a string to a file; use FILE_APPEND to add instead of overwrite, LOCK_EX for safe concurrent writes
  • fopen()/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 escaping
  • include = warning if missing; require = fatal error if missing
  • _once variants 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

🚀 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!