Magyar zászlóMagyar

Exam Cheatsheet

2026-05-27 6 min read

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)
// 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
WhereURL query stringRequest body
Use forRead/filter operationsCreate / update
Visible in URLYesNo
$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

NeedUse
Count arraycount($arr) / items.length
First matcharray_filter + reset() / find()
Any matchsome() / array_filter + count > 0
Sum a fieldarray_sum(array_column(...)) / reduce
Averagesum ÷ count, then toFixed(2) / number_format
Unique valuesarray_unique / [...new Set(...)]
Sort descendingusort($a, fn($a,$b)=>$b-$a) / .sort((a,b)=>b-a)
Draw imagectx.drawImage(img, x, y, w, h)
Draw filled shapebeginPath → moveTo/lineTo/arc → closePath → fill
Draw textset font, textAlign, textBaseline, then fillText
Persist dataread JSON → modify → write JSON
Redirect after writeheader("Location: x.php"); exit();
Protect a pagecheck $_SESSION["user_id"], redirect if missing
Hash passwordpassword_hash($pw, PASSWORD_DEFAULT)
Verify passwordpassword_verify($pw, $hash)