Exam Cheatsheet
Introduction
This cheatsheet covers every technique you need for the exam — one compact reference per concept, with a minimal example. Read it top-to-bottom at least once before the exam so nothing surprises you.
The exam has six independent tasks: three JavaScript, three PHP. You can solve them in any order.
Data format may vary. The exam starter package includes the same data in multiple formats inside a data/ folder. In JavaScript you will typically get an array of objects. In PHP the data may be an array of associative arrays ($v["title"]), an array of objects ($v->title), or even an array of arrays with numeric indices — always check the format of the file you copy in and access properties accordingly.
Part 1 — JavaScript
1. Array methods
In JavaScript the data is provided as an array of objects — access fields with dot notation (v.title, v.score). These are the methods you will reach for most often.
All examples below use this sample dataset:
const items = [
{ id: "a1b2c3d4", title: "Alpha - Long Night", score: 1_200_000, duration: 185, tags: ["Top 10", "Favorites"] },
{ id: "e5f6a7b8", title: "Beta - Deep Dive", score: 450_000, duration: 310, tags: ["Top 10"] },
{ id: "c9d0e1f2", title: "Gamma - Open Road", score: 80_000, length: 95, tags: ["Favorites", "Shorts"] },
];
filter — keep items that match a condition
Returns a new array with only the items where the callback returns true.
const longItems = items.filter(v => v.duration > 200);
// → [ { title: "Beta - Deep Dive", duration: 310, ... } ]
// (only the one item longer than 200 seconds)
const popular = items.filter(v => v.score > 100_000);
// → [ { title: "Alpha - Long Night", ... }, { title: "Beta - Deep Dive", ... } ]
find — first item that matches (or undefined)
Stops as soon as it finds a match — returns the object itself, not a new array.
const hit = items.find(v => v.score > 1_000_000);
// → { id: "a1b2c3d4", title: "Alpha - Long Night", score: 1200000, ... }
hit.title; // "Alpha - Long Night"
const missing = items.find(v => v.score > 99_000_000);
// → undefined (no match found)
some — true if at least one item matches
const hasHighScore = items.some(v => v.score > 1_000_000);
// → true ("Long Night" has 1 200 000 score)
const hasVeryHighScore = items.some(v => v.score > 1_000_000_000);
// → false (none reach a billion)
every — true if all items match
const allHaveTitle = items.every(v => v.title.length > 0);
// → true (every object has a non-empty title)
const allAboveThreshold = items.every(v => v.score > 1_000_000);
// → false ("Deep Dive" and "Open Road" are below 1M)
map — transform every item into something else
Returns a new array of the same length, with each item replaced by the callback’s return value.
const titles = items.map(v => v.title);
// → ["Alpha - Long Night", "Beta - Deep Dive", "Gamma - Open Road"]
// Building an HTML string — always .join("") at the end!
const html = items.map(v => `<li>${v.title} — ${v.score}</li>`).join("");
// → "<li>Alpha - Long Night — 1200000</li><li>Beta - Deep Dive — 450000</li>..."
reduce — collapse an array into a single value
The second argument (0, items[0], etc.) is the starting value of the accumulator.
// Sum all views
const total = items.reduce((sum, v) => sum + v.score, 0);
// → 1_730_000 (1200000 + 450000 + 80000)
// Average views
const avg = items.reduce((sum, v) => sum + v.score, 0) / items.length;
// → 576_666.67 (1730000 / 3)
// Object with the maximum views
const topItem = items.reduce((best, v) => v.score > best.score ? v : best, items[0]);
// → { title: "Alpha - Long Night", score: 1200000, ... }
topItem.title; // "Alpha - Long Night"
sort — sort in-place, returns the same array
The comparator returns a negative number to put a first, positive to put b first.
// Descending by views (highest first)
items.sort((a, b) => b.score - a.score);
// items[0] → { title: "Alpha - Long Night", score: 1200000 }
// items[1] → { title: "Beta - Deep Dive", score: 450000 }
// items[2] → { title: "Gamma - Open Road", score: 80000 }
// Alphabetical A → Z by title
items.sort((a, b) => a.title.localeCompare(b.title));
// items[0] → { title: "Beta - Deep Dive", ... }
// items[1] → { title: "Alpha - Long Night", ... }
// items[2] → { title: "Gamma - Open Road", ... }
sort mutates the original array. If you need to keep the original order, sort a shallow copy: [...items].sort(...).
includes — check whether a value exists in an array
items[0].tags; // ["Top 10", "Favorites"]
items[0].tags.includes("Top 10"); // → true
items[0].tags.includes("Shorts"); // → false
items[2].tags.includes("Shorts"); // → true
flatMap — map then flatten one level
Useful for collecting all tags from all items into one flat array:
const allTags = items.flatMap(v => v.tags);
// → ["Top 10", "Favorites", // from item[0]
// "Top 10", // from item[1]
// "Favorites", "Shorts"] // from item[2]
2. Unique values
Use a Set — it automatically removes duplicates:
const allTags = items.flatMap(v => v.tags);
// → ["Top 10", "Favorites", "Top 10", "Favorites", "Shorts"] (duplicates present)
const unique = [...new Set(allTags)];
// → ["Top 10", "Favorites", "Shorts"] (each value appears only once)
// Joining into a readable string:
unique.join(", "); // → "Top 10, Favorites, Shorts"
3. Finding which value appears most often
Build a frequency map, then find the key with the highest count:
// Step 1 — count how many items each tag appears in
const counts = {};
items.forEach(v => {
v.tags.forEach(pl => {
counts[pl] = (counts[pl] ?? 0) + 1;
});
});
// counts → { "Top 10": 2, "Favorites": 2, "Shorts": 1 }
// Step 2a — find the key with the highest count (starts from first key)
const topTag = Object.keys(counts).reduce(
(best, pl) => counts[pl] > counts[best] ? pl : best
);
// → "Top 10" (first one found when counts are tied)
// Step 2b — alternative with Object.entries and a ["" ,0] sentinel
// Safer when the object could be empty, or you want an explicit starting point:
const [topLabel] = Object.entries(counts).reduce(
(best, entry) => entry[1] > best[1] ? entry : best,
["", 0] // starting pair [key, count] — any real entry beats count=0
);
// topLabel → "Top 10"
4. Number & time formatting
Seconds → MM:SS
function formatTime(seconds) {
const m = Math.floor(seconds / 60); // whole minutes
const s = Math.floor(seconds % 60); // remaining seconds
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
// padStart(2, "0") ensures single digits become "03" not "3"
}
formatTime(185); // → "03:05" (3 minutes, 5 seconds)
formatTime(65); // → "01:05" (1 minute, 5 seconds)
formatTime(3600); // → "60:00" (this format has no hours)
Round to N decimal places
const total = 1_730_000;
const avg = total / 3; // 576666.6666...
avg.toFixed(2); // → "576666.67" (string, rounded)
avg.toFixed(0); // → "576667" (no decimals)
// Writing to the DOM:
element.textContent = avg.toFixed(2); // element shows "576666.67"
5. DOM selection & output
// querySelector returns ONE element (the first match), or null
const el = document.querySelector("#taskA"); // by id
const card = document.querySelector(".card"); // by class (first one)
// querySelectorAll returns ALL matches as a NodeList
const rows = document.querySelectorAll(".item-row"); // iterate with forEach
rows.forEach(row => console.log(row.textContent));
// Writing content
el.textContent = "Hello"; // sets plain text — safe, no HTML parsing
el.innerHTML = "<strong>Hi</strong>"; // sets HTML — only use with your own strings
Rendering a list from an array
const list = document.querySelector("#item-list");
// Each item becomes a <div>.
// If items have a unique id field, store it as data-id.
// If not, use the loop index (i) as data-index — so you can look up the object later.
list.innerHTML = items.map((v, i) => `
<div class="card" data-id="${v.id}" data-index="${i}">
<h2>${v.title}</h2>
<span>${v.score}</span>
</div>
`).join("");
// Without .join("") the commas from the array appear literally in the HTML!
// Result in the DOM:
// <div class="card" data-id="a1b2c3d4" data-index="0"><h2>Alpha - Long Night</h2>...</div>
// <div class="card" data-id="e5f6a7b8" data-index="1"><h2>Beta - Deep Dive</h2>...</div>
// Reading back in a click handler:
// const index = Number(card.dataset.index); // → 0 (always convert to number)
// const item = items[index]; // look up by index when there is no id field
Always call .join("") after map — otherwise the commas from the array become part of the HTML.
6. Events & event delegation
Direct event listener
const btn = document.querySelector("#my-btn");
btn.addEventListener("click", (event) => {
// event.target → the exact element that was clicked
console.log("clicked", event.target);
});
Event delegation — one listener, many children
Why? Because cards are generated dynamically — they don’t exist yet when your script runs. Attach the listener to the parent (which always exists) and use closest() to identify which child was clicked.
const list = document.querySelector("#item-list");
list.addEventListener("click", (event) => {
// event.target might be the <h2> inside the card, not the card itself.
// closest(".card") climbs up until it finds the .card ancestor.
const card = event.target.closest(".card");
if (!card) return; // clicked somewhere inside the list but outside any card
const id = card.dataset.id; // reads the data-id="a1b2c3d4" attribute
// → "a1b2c3d4"
console.log("clicked card id:", id);
// Now find the matching object in state and do something:
const item = items.find(v => v.id === id);
console.log("clicked item:", item.title); // → "Alpha - Long Night"
});
closest() walks up the DOM tree from the clicked element until it finds a match (or returns null). This is the correct way to handle clicks on dynamically generated lists.
change on a <select>
const select = document.querySelector("#tag-list");
select.addEventListener("change", () => {
const value = select.value; // the value of the selected <option>
// If the user picks "All": value === ""
// If the user picks "Top 10": value === "Top 10"
render(value);
});
7. Toggling classes & reading data-*
// classList methods — no need to manipulate className strings manually
card.classList.add("selected"); // adds the class (safe if already there)
card.classList.remove("selected"); // removes the class (safe if not there)
card.classList.toggle("selected"); // adds if missing, removes if present
card.classList.contains("selected"); // → true or false
// Example: toggle on click, then check
card.classList.toggle("selected"); // first click → class added
card.classList.contains("selected"); // → true
card.classList.toggle("selected"); // second click → class removed
card.classList.contains("selected"); // → false
// Reading data-* attributes
// HTML: <div class="card" data-id="a1b2c3d4" data-index="0">
const id = card.dataset.id; // → "a1b2c3d4" (always a string)
const index = Number(card.dataset.index); // → 0 (convert to number when needed)
8. State-driven UI — the golden rule
Never read state from the DOM. Keep state in JavaScript variables and re-render when state changes.
// State
let items = [...originalItems]; // working copy
let selectedIds = new Set();
let currentTag = ""; // "" = all
// Render from state
function render() {
const filtered = currentTag === ""
? items
: items.filter(v => v.tags.includes(currentTag));
list.innerHTML = filtered.map(v => `
<div class="card ${selectedIds.has(v.id) ? "selected" : ""}" data-id="${v.id}">
${v.title}
</div>
`).join("");
updateSum(filtered);
}
// Update derived display
function updateSum(filtered) {
const selected = filtered.filter(v => selectedIds.has(v.id));
const sum = selected.length > 0
? selected.reduce((s, v) => s + v.score, 0)
: filtered.reduce((s, v) => s + v.score, 0);
document.querySelector("#selected-score").textContent = sum;
}
// Event: toggle selection + nested delete action
list.addEventListener("click", (event) => {
const card = event.target.closest(".card");
if (!card) return;
// Handle a nested delete button inside the card (if present)
if (event.target.closest(".delete-btn")) {
const index = Number(card.dataset.index);
// Remove only the currently active category from this item's tags —
// does NOT delete the whole item, just detaches it from one category:
items[index].tags = items[index].tags.filter(pl => pl !== currentTag);
render();
return; // stop here — do not also toggle selection
}
const id = card.dataset.id;
if (selectedIds.has(id)) selectedIds.delete(id);
else selectedIds.add(id);
render();
});
9. Canvas API
Getting the context
const canvas = document.querySelector("#my-canvas");
const ctx = canvas.getContext("2d"); // always "2d" for 2D drawing
Clear the canvas
Do this at the start of every redraw so previous frames don’t bleed through:
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Erases everything from (0,0) to the bottom-right corner
Drawing an image
The exam passes you a ready-made image object as a parameter — draw it directly, no loading needed:
// ctx.drawImage(image, x, y, width, height)
ctx.drawImage(logoImage, 50, 20, 80, 80);
// Draws the image with its top-left corner at (50, 20), sized 80×80 px
ctx.drawImage(iconImage, 200, 20, 80, 80);
// A second image next to the first
Rectangles
ctx.fillStyle = "#F6DD04"; // set the fill colour BEFORE drawing
ctx.fillRect(100, 120, 300, 50); // (x, y, width, height)
// Draws a yellow rectangle: top-left at (100,120), 300 px wide, 50 px tall
Paths (shapes, pie slices, rounded rects)
Every path follows the same lifecycle: begin → describe → style → fill/stroke
ctx.beginPath(); // start a fresh path (always first!)
ctx.moveTo(50, 50); // move the "pen" to (50,50) without drawing
ctx.lineTo(150, 50); // draw a line to (150,50)
ctx.lineTo(100, 120); // draw a line to (100,120)
ctx.closePath(); // straight line back to (50,50) — closes the triangle
ctx.fillStyle = "#F6DD04";
ctx.fill(); // fill the triangle with yellow
// ctx.stroke(); // alternatively: draw only the outline
Pie slice
// Angles in canvas: 0 = 3 o'clock, Math.PI = 9 o'clock, 2*Math.PI = full circle
const sliceAngle = (item.score / totalScore) * 2 * Math.PI;
// e.g. item.score=450000, totalScore=1730000 → sliceAngle ≈ 1.635 rad (about 94°)
ctx.beginPath();
ctx.moveTo(centerX, centerY); // start at the circle's centre
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
// arc() draws the curved edge from startAngle to startAngle+sliceAngle
ctx.closePath(); // straight line back to centre — completes the slice
ctx.fillStyle = `#${item.id.substring(0, 6)}`;
// e.g. item.id = "e5f6a7b8" → fillStyle = "#e5f6a7" (first 6 hex chars)
ctx.fill();
Bar / column chart
Canvas y-axis grows downward. To make bars grow upward, subtract the bar height from the canvas height:
// Canvas is 800 px wide, 400 px tall.
// columnWidth = 800 / items.length (e.g. 800/3 ≈ 267 px)
ctx.fillStyle = `#${item.id.substring(0, 6)}`;
ctx.fillRect(
index * columnWidth, // x: 0 for first bar, 267 for second, 534 for third
400 - item.score, // y: top of bar (bar grows upward from y=400)
columnWidth, // width of the bar
item.score // height of the bar (= score as pixels)
);
// e.g. item.score=105: top-left at (0, 295), size 267×105 px
Text
ctx.fillStyle = "#222"; // text colour
ctx.font = "700 34px Arial, sans-serif"; // weight size family
ctx.textAlign = "center"; // "left" | "center" | "right" — pivot point horizontally
ctx.textBaseline = "middle"; // "top" | "middle" | "bottom" — pivot point vertically
ctx.fillText("Hello world!", 400, 200);
// Draws "Hello world!" centred both horizontally and vertically around (400, 200)
Part 2 — PHP
10. PHP basics reminder
<?php
$name = "User";
$views = 42;
echo $name . " has " . $views . " views"; // → User has 42 views
// . is string concatenation in PHP (NOT +)
?>
<!-- Short echo tag inside HTML — use this in templates -->
<h2><?= $item["title"] ?></h2>
<!-- Same as: <h2><?php echo $item["title"]; ?></h2> -->
Associative array vs object vs indexed array
The exam data folder contains the same dataset in several formats. Pick one and copy it into your task folder — then access fields consistently throughout your code.
// Associative array → access with ['key'] (most common in our practicals)
$item = ["title" => "Item A", "score" => 105];
echo $item["title"]; // → Item A
echo $item["score"]; // → 105
// Object → access with ->property
$item = (object)["title" => "Item A", "score" => 105];
echo $item->title; // → Item A
echo $item->views; // → 105
// Indexed (positional) array → access with [index]
$item = ["Item A", 105, 2009];
echo $item[0]; // → Item A
echo $item[1]; // → 105
echo $item[2]; // → 2009
Open the data file first and check which format it uses before writing any code. All three work fine — just be consistent.
11. Loops and array functions
foreach — standard and alternative syntax
// Standard
foreach ($items as $item) {
echo $item["title"];
}
// Alternative (cleaner inside HTML templates)
foreach ($items as $item): ?>
<div><?= $item["title"] ?></div>
<?php endforeach; ?>
Useful built-in functions
$items = [
["title" => "Long Night", "score" => 105],
["title" => "Deep Dive", "score" => 44],
["title" => "Open Road", "score" => 3],
];
count($items); // → 3
// Sum a single field across all rows
$totalScore = array_sum(array_column($items, "score"));
// array_column extracts → [105, 44, 3]
// array_sum adds them → 152
// array_map — transform each element
$titles = array_map(fn($v) => $v["title"], $items);
// → ["Long Night", "Deep Dive", "Open Road"]
// array_filter — keep elements where callback returns true
$popular = array_filter($items, fn($v) => $v["score"] > 10);
// → [ ["title"=>"Long Night",views=>105], ["title"=>"Deep Dive",views=>44] ]
// Note: keys are preserved. Wrap with array_values() if you need 0,1,2 keys.
// usort — sort in-place by a custom comparator
usort($items, fn($a, $b) => $b["score"] - $a["score"]); // descending
// After sort: Long Night (105), Deep Dive (44), Open Road (3)
// For alphabetical: fn($a,$b) => strcmp($a["title"], $b["title"])
Formatting numbers for output
// Seconds → MM:SS (same problem as JS §4, PHP syntax)
function formatLength(int $seconds): string {
$m = intdiv($seconds, 60); // whole minutes (intdiv = integer division, no remainder)
$s = $seconds % 60; // remaining seconds
return sprintf('%02d:%02d', $m, $s);
// sprintf('%02d', 5) → "05" (pad single digit with leading zero)
// sprintf('%02d', 75) → "75" (no padding needed)
}
formatLength(312); // → "05:12"
formatLength(65); // → "01:05"
formatLength(3600); // → "60:00"
// Large number display with grouped thousands
number_format(1450000, 0, ',', ' '); // → "1 450 000" (space as thousands separator)
number_format(4990, 0, ',', ','); // → "4,990" (comma separator)
number_format(3.14159, 2, '.', ''); // → "3.14" (2 decimal places)
Accumulating a value per category
When each item belongs to multiple categories and you need a per-category total, build an associative array keyed by category name and add to it in a nested loop:
$items = [
["title" => "Long Night", "tags" => ["Top 10", "Favorites"], "score" => 105],
["title" => "Deep Dive", "tags" => ["Top 10"], "score" => 44],
["title" => "Open Road", "tags" => ["Favorites", "Shorts"], "score" => 3],
];
$scorePerTag = [];
foreach ($items as $item) {
foreach ($item["tags"] as $tag) {
$scorePerTag[$tag] = ($scorePerTag[$tag] ?? 0) + $item["score"];
}
}
// $scorePerTag → ["Top 10" => 149, "Favorites" => 108, "Shorts" => 3]
// "Top 10" = Long Night (105) + Deep Dive (44) = 149
// "Favorites" = Long Night (105) + Open Road (3) = 108
// "Shorts" = Open Road (3) = 3
// Render each category:
foreach ($scorePerTag as $tag => $total) {
echo "<li>$tag: $total</li>\n";
}
Collecting unique values from a nested field
$items = [
["title" => "Long Night", "tags" => ["Top 10", "Favorites"]],
["title" => "Deep Dive", "tags" => ["Top 10"]],
["title" => "Open Road", "tags" => ["Favorites", "Shorts"]],
];
$allTags = [];
foreach ($items as $v) {
foreach ($v["tags"] as $t) {
$allTags[] = $t; // append to the flat array
}
}
// $allTags → ["Top 10", "Favorites", "Top 10", "Favorites", "Shorts"]
$uniqueTags = array_unique($allTags);
// → ["Top 10", "Favorites", "Shorts"] (duplicates removed)
Finding the most popular
// Sort descending — the winner is always $items[0] afterwards
usort($items, fn($a, $b) => $b["score"] - $a["score"]);
$topItem = $items[0];
echo $topItem["title"]; // → Long Night
echo $topItem["score"]; // → 105
12. Forms and user input
$_POST vs $_GET
$_GET | $_POST | |
|---|---|---|
| Where | URL query string | Request body |
| Use for | Read/filter operations | Create / update |
| Visible in URL | Yes | No |
$name = $_POST["name"] ?? ""; // "" when the form hasn't been submitted yet
$mode = $_GET["mode"] ?? "all"; // "all" when no ?mode= in the URL
// Check whether the form was submitted:
$submitted = $_SERVER["REQUEST_METHOD"] === "POST";
Validation pattern
Collect errors in an array. Show success only when the array is empty.
<?php
$errors = [];
$submitted = $_SERVER["REQUEST_METHOD"] === "POST";
if ($submitted) {
$name = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");
$quantity = trim($_POST["quantity"] ?? "");
$product = trim($_POST["product"] ?? "");
$delivery = trim($_POST["delivery"] ?? "");
// Required fields
if ($name === "") $errors[] = "Name is required.";
if ($email === "") $errors[] = "Email is required.";
if ($quantity === "") $errors[] = "Quantity is required.";
if ($product === "") $errors[] = "Product is required.";
if ($delivery === "") $errors[] = "Delivery mode is required.";
// Minimum length
if (strlen($name) < 3) $errors[] = "Name must be at least 3 characters.";
// Email format
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = "Invalid email address.";
// Integer check — ctype_digit requires every character to be a digit (rejects "-3", "1.5", " 2")
// Then confirm the value is at least 1 (not zero)
if (!ctype_digit($quantity) || intval($quantity) < 1) $errors[] = "Quantity must be a positive whole number.";
// Alternative: filter_var($quantity, FILTER_VALIDATE_INT) — also accepts "-3" and "+5"
// Whitelist (only allowed values)
$allowedProducts = ["Widget", "mug", "poster"];
$allowedDelivery = ["pickup", "delivery"];
if (!in_array($product, $allowedProducts)) $errors[] = "Invalid product.";
if (!in_array($delivery, $allowedDelivery)) $errors[] = "Invalid delivery mode.";
}
?>
Sticky form — re-populate inputs after failed submission
<input name="name" value="<?= $name ?? "" ?>">
<input name="email" value="<?= $email ?? "" ?>">
<!-- Sticky select -->
<select name="product">
<option value="basic" <?= ($product ?? "") === "basic" ? "selected" : "" ?>>Basic</option>
<option value="premium" <?= ($product ?? "") === "premium" ? "selected" : "" ?>>Premium</option>
</select>
Show success / errors in HTML
<?php if ($submitted && count($errors) === 0): ?>
<div id="success">Order placed successfully!</div>
<?php endif; ?>
<?php if ($submitted && count($errors) > 0): ?>
<ul id="errors">
<?php foreach ($errors as $error): ?>
<li><?= $error ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
13. File-based storage (JSON CRUD)
All persistent data lives in a JSON file. Every operation follows the same Read → Modify → Write cycle.
// --- helpers ---
function loadData(string $file): array {
if (!file_exists($file)) return []; // first run — file doesn't exist yet
return json_decode(file_get_contents($file), true); // true → assoc array, not object
}
function saveData(string $file, array $data): void {
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// JSON_PRETTY_PRINT — indented, human-readable output
// JSON_UNESCAPED_UNICODE — keeps é, ü, ő, etc. as-is instead of \u00e9
}
$file = "data.json";
Read all
$items = loadData($file);
// → [ ["id"=>"abc","name"=>"Widget","price"=>29], ... ]
// → [] if the file doesn't exist yet
Create (add)
$items = loadData($file);
// Option A — string id with uniqid():
$items[] = ["id" => uniqid(), "name" => $name, "price" => $price];
// uniqid() generates a unique string id, e.g. "6652f3a1bc4e7"
// Option B — integer id (max existing id + 1):
$maxId = 0;
foreach ($items as $item) {
if ($item["id"] > $maxId) $maxId = $item["id"];
}
$items[] = ["id" => $maxId + 1, "name" => $name, "price" => $price];
saveData($file, $items);
// The JSON file now has one more entry.
Update by name (upsert — update if exists, else add)
$items = loadData($file);
$found = false;
foreach ($items as &$item) { // & — reference: modifying $item modifies the array
if ($item["name"] === $name) {
$item["price"] = $price; // overwrite just the price field
$found = true;
break; // no need to keep looping
}
}
if (!$found) {
$items[] = ["id" => uniqid(), "name" => $name, "price" => $price];
}
saveData($file, $items);
Delete by id
$items = loadData($file);
// $id from GET: string id → $id = $_GET["id"] ?? ""; integer id → $id = intval($_GET["id"] ?? -1);
$items = array_filter($items, fn($i) => $i["id"] !== $id);
// array_filter keeps items where the callback returns true — i.e. all except the one with matching id
$items = array_values($items); // re-index from 0 (array_filter preserves original keys)
saveData($file, $items);
Post-Redirect-Get (PRG) — prevent double-submit on refresh
After every write operation, redirect immediately:
header("Location: index.php");
exit();
Always call exit() right after header("Location: ..."). Without it, the rest of the script still runs.
Using the provided Storage helper (if available in boilerplate)
If storage.php is included in the task folder, you can use the Storage class instead of manual file_get_contents / json_encode. It saves automatically when the script finishes (via __destruct) — no explicit save call needed.
require_once 'storage.php';
// Open a JSON file — the file must already exist on disk
$storage = new Storage(new JsonIO('data.json'));
// Read all items — returns an associative array keyed by their string id
$items = $storage->findAll();
// → [ "6652f3..." => ["id"=>"6652f3...", "name"=>"Widget", "price"=>29], ... ]
// Find a single item by its string id
$id = $_GET['id'] ?? '';
$item = $storage->findById($id); // → item array, or null if not found
// Find first item matching a field value
$item = $storage->findOne(['name' => $name]); // → first match, or null
// Add a new item — id is auto-generated (uniqid), no save call needed
$storage->add(['name' => $name, 'price' => $price]);
// Update an item by id (replace the whole record)
$item['price'] = $newPrice;
$storage->update($id, $item);
// Delete an item by id
$storage->delete($id);
// findMany — custom filter (like array_filter)
$expensive = $storage->findMany(fn($item) => $item['price'] > 1000);
// The JSON file is written automatically when $storage goes out of scope
The Storage class assigns string ids via uniqid(). To find an item by a URL parameter: $id = $_GET['id'] ?? ''; $item = $storage->findById($id); — no intval() needed.
14. Sessions and authentication
Setup — must be the very first line
<?php
session_start(); // MUST come before any output, even blank lines
Read / write $_SESSION
// Write — store any value across requests
$_SESSION["user_id"] = $user["id"]; // e.g. stores "admin"
$_SESSION["counter"] = 0;
// Read — use ?? to avoid errors if the key doesn't exist
$userId = $_SESSION["user_id"] ?? null; // null if not set
// Delete one key (e.g. on logout)
unset($_SESSION["user_id"]);
Guard pattern — protect a page
Put this at the top of every protected page:
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
header("Location: login.php");
exit();
}
Password hashing
Never store plain-text passwords. Always hash before saving.
// Creating / registering a user — hash before saving to the file:
$hashed = password_hash("admin", PASSWORD_DEFAULT);
// → something like "$2y$10$eImiTXuWVxfM37uY4JANjQ..."
// The hash is different every time, even for the same password.
// Logging in — compare the typed password against the stored hash:
if (password_verify("admin", $storedHash)) {
// Typed password matches — log the user in
$_SESSION["user_id"] = $user["id"];
header("Location: index.php");
exit();
}
// If password_verify returns false, just fall through — show an error
Storing hashed users in a JSON file
// Generating the initial admin user (leave this commented out after first run):
// $users = [["username" => "admin", "password" => password_hash("admin", PASSWORD_DEFAULT)]];
// file_put_contents("users.json", json_encode($users));
// Login check
$users = json_decode(file_get_contents("users.json"), true);
$username = trim($_POST["username"] ?? "");
$password = trim($_POST["password"] ?? "");
$found = null;
foreach ($users as $u) {
if ($u["username"] === $username && password_verify($password, $u["password"])) {
$found = $u;
break;
}
}
if ($found) {
$_SESSION["user_id"] = $found["username"];
header("Location: index.php");
exit();
} else {
$loginError = "Invalid credentials.";
}
Logout
session_start();
session_destroy();
header("Location: login.php");
exit();
Quick-reference summary
| Need | Use |
|---|---|
| Count array | count($arr) / items.length |
| First match | array_filter + reset() / find() |
| Any match | some() / array_filter + count > 0 |
| Sum a field | array_sum(array_column(...)) / reduce |
| Average | sum ÷ count, then toFixed(2) / number_format |
| Unique values | array_unique / [...new Set(...)] |
| Sort descending | usort($a, fn($a,$b)=>$b-$a) / .sort((a,b)=>b-a) |
| Draw image | ctx.drawImage(img, x, y, w, h) |
| Draw filled shape | beginPath → moveTo/lineTo/arc → closePath → fill |
| Draw text | set font, textAlign, textBaseline, then fillText |
| Persist data | read JSON → modify → write JSON |
| Redirect after write | header("Location: x.php"); exit(); |
| Protect a page | check $_SESSION["user_id"], redirect if missing |
| Hash password | password_hash($pw, PASSWORD_DEFAULT) |
| Verify password | password_verify($pw, $hash) |