Magyar zászlóMagyar

Practice 10b - PHP: The Storage Class

2025-05-08 6 min read GitHub

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:

StatementFile missing →Included twice →
includeWarning, continuesIncluded again
include_onceWarning, continuesSkipped
requireFatal error, stopsIncluded again
require_onceFatal error, stopsSkipped

The rule of thumb:

  • Use require_once for 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 include for 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.

ParameterTypeDescription
$recordarray or objectThe data to store
ReturnsTypeDescription
$idstringThe 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"];
ParameterTypeDescription
$idstringThe id to look up
ReturnsTypeDescription
recordarray|object|nullThe 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.

ParameterTypeDescription
$paramsarrayOptional key-value filter conditions
ReturnsTypeDescription
recordsarrayAll 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).

ParameterTypeDescription
$paramsarrayOptional key-value filter conditions
ReturnsTypeDescription
recordarray|object|nullFirst 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.

ParameterTypeDescription
$idstringThe id of the record to replace
$recordarray|objectThe 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
ParameterTypeDescription
$idstringThe 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
);
ParameterTypeDescription
$conditioncallableReceives each record; return true to include it
ReturnsTypeDescription
recordsarrayAll 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.

ParameterTypeDescription
$conditioncallableReceives each record; return true to apply updater
$updatercallableReceives 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);
ParameterTypeDescription
$conditioncallableReceives 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

MethodUsed inWhat it does
add($record)new.phpAppend a new record, returns its id
findAll($params)index.phpReturn all (or filtered) records
findById($id)show.php, edit.phpReturn one record by id, or null
findOne($params)Return first match by field values, or null
update($id, $record)edit.phpReplace a record entirely
delete($id)delete.phpRemove 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

ConceptCodeReturns
Load filerequire_once("Storage.php")
Create instancenew Storage(new JsonIO("data.json"))Storage
Auto-saveHappens 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) => ...)