Practice 10b - PHP: The Storage Class
Introduction
In the previous session every file contained the same boilerplate:
$books = json_decode(file_get_contents("data.json"), true);
// ...modify $books...
file_put_contents("data.json", json_encode($books, JSON_PRETTY_PRINT));
This session refactors the entire book wishlist to use the provided Storage class, which wraps all of that into clean, named methods. Instead of thinking about files and JSON, you just call add, findAll, findById, update, and delete.
1. Including files — require_once and friends
Before we use Storage.php we need to load it. PHP has four ways to pull in another file:
| Statement | File missing → | Included twice → |
|---|---|---|
include | Warning, continues | Included again |
include_once | Warning, continues | Skipped |
require | Fatal error, stops | Included again |
require_once | Fatal error, stops | Skipped |
The rule of thumb:
- Use
require_oncefor files that are essential and might be referenced in multiple places — like a class file or a config file. The script cannot work without them, and including a class definition twice causes a PHP error. - Use
includefor optional template pieces (e.g. a header partial) where a missing file should not crash everything.
For Storage.php we always use require_once:
require_once("Storage.php");
If Storage.php is missing, the script stops immediately with a clear error. If the same file was already loaded earlier in the request (e.g. from another require_once in a chain), PHP skips it silently instead of re-declaring the class.
2. What Storage.php gives you
Include it once at the top of any file and create an instance:
require_once("Storage.php");
$storage = new Storage(new JsonIO("data.json"));
JsonIO("data.json") tells the class to use data.json as the backing file. Storage reads the file when it is constructed and writes it back automatically when the script finishes — you never call a save function yourself.
The automatic save happens in the class’s __destruct method, which PHP calls when a variable goes out of scope or when the script ends. This means every change you make through $storage is persisted to disk with no extra work. The class also exposes a ->save() method if you ever need to force an early write, but in normal use you will never need to call it.
3. Method reference
add($record): string
Purpose: Append a new record to the store. Returns the generated unique id.
$id = $storage->add([
"title" => "The Hobbit",
"author" => "J.R.R. Tolkien",
"year" => 1937,
"read" => false,
]);
// $id is something like "6642c1f4a3e2b"
The class generates a unique id with uniqid() and stores it inside the record as $record['id'] before saving. The same id is also returned so you can use it immediately if needed.
| Parameter | Type | Description |
|---|---|---|
$record | array or object | The data to store |
| Returns | Type | Description |
|---|---|---|
$id | string | The auto-generated unique id |
findById(string $id)
Purpose: Retrieve a single record by its id. Returns null if nothing matches.
$book = $storage->findById($id);
if (!$book) {
header("location: index.php");
exit();
}
echo $book["title"];
| Parameter | Type | Description |
|---|---|---|
$id | string | The id to look up |
| Returns | Type | Description |
|---|---|---|
| record | array|object|null | The matching record, or null |
findAll(array $params = []): array
Purpose: Return all records, optionally filtered by exact field matches.
All records:
$books = $storage->findAll();
Filtered — only books already read:
$readBooks = $storage->findAll(["read" => true]);
Every key-value pair in $params acts as an exact-match filter. All conditions must match (AND logic). Passing no arguments returns everything.
| Parameter | Type | Description |
|---|---|---|
$params | array | Optional key-value filter conditions |
| Returns | Type | Description |
|---|---|---|
| records | array | All matching records (empty array if none) |
findOne(array $params = [])
Purpose: Like findAll, but returns only the first match, or null if nothing is found.
// Find the first book with this exact title
$book = $storage->findOne(["title" => "Dune"]);
if ($book) {
echo $book["author"];
}
Useful when you know there can be at most one match (e.g. finding a user by username).
| Parameter | Type | Description |
|---|---|---|
$params | array | Optional key-value filter conditions |
| Returns | Type | Description |
|---|---|---|
| record | array|object|null | First matching record, or null |
update(string $id, $record)
Purpose: Replace the record at the given id entirely with the new data.
$storage->update($id, [
"title" => "Dune Messiah",
"author" => "Frank Herbert",
"year" => 1969,
"read" => true,
]);
The entire record is replaced — not merged. If you want to keep existing fields, read the record first and build the updated array yourself.
| Parameter | Type | Description |
|---|---|---|
$id | string | The id of the record to replace |
$record | array|object | The new data |
| Returns | — | nothing |
delete(string $id)
Purpose: Remove the record with the given id.
$storage->delete($id);
// record is gone; change saved automatically on script end
| Parameter | Type | Description |
|---|---|---|
$id | string | The id of the record to remove |
| Returns | — | nothing |
findMany(callable $condition): array
Purpose: Return all records for which a callback function returns true. More flexible than findAll because you can write any logic, not just exact-match conditions.
// All books published after 1950
$modern = $storage->findMany(fn($book) => $book["year"] > 1950);
// All unread books with a year before 1900
$oldUnread = $storage->findMany(
fn($book) => $book["read"] === false && $book["year"] < 1900
);
| Parameter | Type | Description |
|---|---|---|
$condition | callable | Receives each record; return true to include it |
| Returns | Type | Description |
|---|---|---|
| records | array | All records where the callback returned true |
updateMany(callable $condition, callable $updater)
Purpose: Apply a transformation to every record that matches a condition, in a single call.
// Mark all books published before 1900 as "classic"
$storage->updateMany(
fn($book) => $book["year"] < 1900, // which records to touch
function (&$book) { $book["classic"] = true; } // what to do to each one
);
The $updater callback receives each matching record by reference (&$book), so any changes you make inside it are saved.
| Parameter | Type | Description |
|---|---|---|
$condition | callable | Receives each record; return true to apply updater |
$updater | callable | Receives matching record by reference; modify it in place |
| Returns | — | nothing |
deleteMany(callable $condition)
Purpose: Remove every record for which the callback returns true.
// Remove all books published before 1800
$storage->deleteMany(fn($book) => $book["year"] < 1800);
| Parameter | Type | Description |
|---|---|---|
$condition | callable | Receives each record; return true to delete it |
| Returns | — | nothing |
4. The refactored CRUD
Here is how each file changed. Validation logic is identical to last session — only the data-access lines changed.
index.php — list all
Before:
$books = json_decode(file_get_contents('data.json'), true);
After:
require_once("Storage.php");
$storage = new Storage(new JsonIO("data.json"));
$books = $storage->findAll();
The foreach and HTML remain exactly the same.
show.php — single record
Before:
$books = json_decode(file_get_contents('data.json'), true);
$id = $_GET['id'] ?? null;
if (!isset($books[$id])) { header('location: index.php'); exit(); }
$book = $books[$id];
After:
require_once("Storage.php");
$storage = new Storage(new JsonIO("data.php"));
$id = $_GET['id'] ?? null;
$book = $storage->findById($id);
if (!$book) { header('location: index.php'); exit(); }
findById returns null when not found, so the guard simplifies from !isset($books[$id]) to just !$book.
new.php — create
Before:
$books = json_decode(file_get_contents("data.json"), true);
$books[] = ["title" => $title, ...];
file_put_contents("data.json", json_encode($books, JSON_PRETTY_PRINT));
After:
require_once("Storage.php");
$storage = new Storage(new JsonIO("data.json"));
$storage->add(["title" => $title, "author" => $author, "year" => intval($year), "read" => $read]);
Three lines become two. No manual encode/decode.
edit.php — update
Before:
$books[$id] = ["title" => $title, ...];
file_put_contents("data.json", json_encode($books, JSON_PRETTY_PRINT));
After:
$storage->update($id, ["title" => $title, "author" => $author, "year" => intval($year), "read" => $read]);
delete.php — delete
Before:
$books = json_decode(file_get_contents('data.json'), true);
unset($books[$id]);
file_put_contents("data.json", json_encode($books, JSON_PRETTY_PRINT));
header("location: index.php");
exit();
After:
require_once("Storage.php");
$storage = new Storage(new JsonIO("data.json"));
$id = $_GET['id'] ?? null;
$storage->delete($id);
header("location: index.php");
exit();
5. Method overview
| Method | Used in | What it does |
|---|---|---|
add($record) | new.php | Append a new record, returns its id |
findAll($params) | index.php | Return all (or filtered) records |
findById($id) | show.php, edit.php | Return one record by id, or null |
findOne($params) | — | Return first match by field values, or null |
update($id, $record) | edit.php | Replace a record entirely |
delete($id) | delete.php | Remove a record |
findMany($fn) | — | Return all records matching a callback |
updateMany($cond, $fn) | — | Modify all records matching a condition |
deleteMany($fn) | — | Remove all records matching a callback |
findOne, findMany, updateMany, and deleteMany were not needed for the basic book wishlist but are available whenever you need more powerful querying or bulk operations. The callback-based methods (findMany, updateMany, deleteMany) work exactly like JavaScript’s Array.filter and Array.forEach.
Summary
| Concept | Code | Returns |
|---|---|---|
| Load file | require_once("Storage.php") | — |
| Create instance | new Storage(new JsonIO("data.json")) | Storage |
| Auto-save | Happens when script ends — ->save() exists but is not needed | — |
| Add record | $storage->add($array) | string — the generated id |
| Get all | $storage->findAll() | array of all records |
| Get filtered | $storage->findAll(["field" => "value"]) | array of matching records |
| Get one by id | $storage->findById($id) | record (array) or null |
| Get first match | $storage->findOne(["field" => "value"]) | record (array) or null |
| Replace record | $storage->update($id, $newArray) | — |
| Remove record | $storage->delete($id) | — |
| Filter with logic | $storage->findMany(fn($r) => $r["year"] > 1950) | array of matching records |
| Bulk update | $storage->updateMany($cond, $updater) | — |
| Bulk delete | $storage->deleteMany(fn($r) => ...) | — |