Language Recap And Exercises
This recap checks whether the language features from the track now work together. The target is not memorising every PHP function. It is being able to read, design, run, and explain a small strict PHP program with explicit data shapes, control flow, helper contracts, validation, and output boundaries.
The next three lessons are capstone scripts. Use this recap to identify gaps before those projects combine more code at once.
Start With The Program Contract
Before reading individual lines, answer:
- What input does the program receive?
- What output does success produce?
- Which invalid values are expected?
- Does one invalid record stop the process or only skip that record?
- Which values have units such as cents or percentages?
- Which files define behavior, data, and the entry-point workflow?
A clear contract prevents syntax details from hiding the actual task.
Read Function Signatures Before Bodies
Consider:
function normaliseProduct(array $product): array
function formatCents(int $cents): string
function formatProductLine(array $product): string
The signatures describe a pipeline: raw product array to normalised product array, integer cents to display text, and product record to report line.
Then inspect each body to confirm it fulfils that promise. Check every return path, exception path, and hidden dependency. An array declaration does not guarantee nested keys, so the body must establish the cleaned shape it returns.
Trace Data Shape Changes
Use a table when a value changes representation:
| Stage | Example value | Contract |
|---|---|---|
| Raw input | ['name' => ' Pen ', 'price_cents' => 199] |
untrusted array shape |
| Normalised record | ['name' => 'Pen', 'price_cents' => 199] |
required keys and types established |
| Formatted line | Pen: GBP 1.99 |
presentation string |
| Output | line written to standard output | side effect at entry-point boundary |
Do not pass presentation strings back into calculations. Keep integer cents through validation and arithmetic, then format once for output.
Review A Complete Script
<?php
declare(strict_types=1);
function normaliseProduct(array $product): array
{
$name = $product['name'] ?? null;
$priceCents = $product['price_cents'] ?? null;
if (!is_string($name) || trim($name) === '') {
throw new InvalidArgumentException('Product name is required.');
}
if (!is_int($priceCents) || $priceCents < 0) {
throw new InvalidArgumentException('Price must be a non-negative integer.');
}
return [
'name' => trim($name),
'price_cents' => $priceCents,
];
}
function formatCents(int $cents): string
{
return 'GBP ' . number_format($cents / 100, 2, '.', ',');
}
function formatProductLine(array $product): string
{
return $product['name'] . ': ' . formatCents($product['price_cents']);
}
$rawProducts = [
['name' => ' Notebook ', 'price_cents' => 1299],
['name' => 'Pen', 'price_cents' => 199],
['name' => ' ', 'price_cents' => 899],
['name' => 'Sticker', 'price_cents' => 0],
];
$acceptedProducts = [];
$rejectedCount = 0;
foreach ($rawProducts as $index => $rawProduct) {
try {
$product = normaliseProduct($rawProduct);
$acceptedProducts[] = $product;
echo formatProductLine($product), PHP_EOL;
} catch (InvalidArgumentException $exception) {
$rejectedCount++;
$recordNumber = $index + 1;
echo "Rejected record $recordNumber\n";
}
}
$totalCents = 0;
foreach ($acceptedProducts as $product) {
$totalCents += $product['price_cents'];
}
echo 'Catalog total: ' . formatCents($totalCents) . PHP_EOL;
echo 'Accepted: ' . count($acceptedProducts) . PHP_EOL;
echo "Rejected: $rejectedCount\n";
Expected output:
Notebook: GBP 12.99
Pen: GBP 1.99
Rejected record 3
Sticker: GBP 0.00
Catalog total: GBP 14.98
Accepted: 3
Rejected: 1
A zero price is valid because the stated rule is non-negative, not positive. This boundary decision must be consistent in validation, tests, and expected output.
Trace Control Flow Per Record
For each record, ask which statements run:
- The record enters
normaliseProduct(). - Name validation may throw.
- Price validation may throw.
- A cleaned array returns on success.
- The caller stores and formats it.
- The catch records a rejection on failure.
- The loop proceeds to the next record.
Because the try and catch are inside the loop, one bad record does not stop the catalog. Because appending occurs after successful normalization, rejected data never enters the total calculation.
Check Scope And Side Effects
The helpers receive all dependencies through parameters. Their local variables do not modify the top-level arrays or counters.
The visible side effects are the echo statements in the workflow. formatCents() and formatProductLine() return values, making them usable by a CLI report, HTML template, JSON builder, or test.
Ask whether any function reads a global, uses a static variable, prints unexpectedly, or depends on a variable inherited through an include. If so, make that dependency explicit or move the effect to the application boundary.
Check Types And Business Rules Separately
int $cents rejects a string in a strict caller, but it does not reject -1. array $product accepts an empty array. string $name accepts whitespace-only text.
For every input, list two levels:
- PHP type requirement;
- application validity requirement.
A parser or normalizer often owns both conversion from external text and business validation. Deeper calculation functions should receive application-ready types.
Check Array Access Deliberately
For every key read, decide whether it is required or optional.
Required external keys should be checked before use. Optional keys need a meaningful fallback. Use array_key_exists() when null differs from absence; use isset() when null should count as unavailable.
After filtering a list, remember that keys may have gaps. Use array_values() when a later consumer or JSON output requires a zero-based list.
When building a lookup with array_column(), confirm index values are unique so later records do not silently replace earlier ones.
Check String Output By Context
Plain CLI text, HTML, JSON, URLs, SQL parameters, and shell arguments have different output rules.
The recap script emits controlled CLI text. If product names were rendered in HTML, use htmlspecialchars() at that HTML boundary. Do not store HTML-escaped names in the cleaned product array, because the same data may later be used in another context.
Keep byte-oriented functions such as strlen() separate from human-character limits that require multibyte handling.
Check File Boundaries
When helpers move to another file:
- put
declare(strict_types=1);in each file; - use
require __DIR__ . '/helpers.php';from the entry point; - keep helper files free of surprise output;
- let config files return data;
- never build require paths from user input;
- run the entry script from a different working directory to verify stable paths.
File movement should not change the helper contracts. Run behavior checks before and after extraction.
Debug From Evidence
When output is wrong:
- reproduce with one smallest failing input;
- inspect value and type with
var_dump(); - verify the branch condition manually;
- check whether the array key exists and what it contains;
- confirm the function actually returns the expected type;
- inspect exact output bytes when spaces or newlines matter;
- fix the owning rule rather than patching the final string.
Remove temporary debugging output after the cause is understood. Do not leave var_dump() in public responses.
Minimum Verification Before A Capstone
For each script:
- run
php -lon every PHP file; - execute one normal input;
- execute boundary values such as zero and one;
- execute missing, empty, and wrongly typed input where relevant;
- predict exact output before comparing it;
- check exit status for CLI failures;
- verify included paths from another working directory;
- run
git diff --checkbefore committing.
A successful happy-path run is not enough evidence for branches and validation.
Verify Invariants, Not Only Lines
An invariant is a fact that should remain true across the workflow. In the catalog example:
- accepted count plus rejected count equals raw record count;
- every accepted product has a non-empty name and non-negative integer price;
- total cents equals the sum of accepted prices only;
- each raw record produces exactly one accepted or rejected outcome;
- formatting does not change the stored numeric price.
Check these relationships after individual output lines. They catch missing counter increments, duplicate processing, and invalid records leaking into calculations.
When refactoring, compare the same invariants before and after the change. Identical visible output can still conceal a damaged internal collection that fails on the next feature.
Readiness Checklist
You are ready for the capstones when you can:
- explain variables, values, operators, and strict comparisons;
- trace
if,match, loops,continue, andbreak; - write typed functions that return rather than print;
- identify global, local, static, and captured closure state;
- throw and catch specific expected exceptions;
- create, transform, search, sort, and safely read arrays;
- handle string interpolation, searching, formatting, and output encoding;
- distinguish type declarations from business validation;
- load helper and config files with stable paths;
- choose procedural flow, functions, or a future object based on evidence;
- test exact results and failure paths.
A missing item is not a reason to restart the track. Return to the relevant lesson, repair the specific gap, and then attempt the capstone again.
What Comes Next
Lesson 19 builds a CLI receipt calculator with validated command-line input and integer-cent arithmetic. Lesson 20 builds a data-cleaning pipeline that separates raw and accepted records. Lesson 21 moves a working report across several files while preserving its contracts and output.
The three recap exercises isolate price formatting, output prediction with optional keys, and required-field validation before those larger projects begin.
Practice
Task: Price Recap
Task
Write formatPrice(int $cents): string in a strict PHP file. Throw InvalidArgumentException when cents are negative; otherwise return a GBP label with two decimals.
Loop over these products:
$products = [
['name' => 'Notebook', 'price_cents' => 1299],
['name' => 'Sticker', 'price_cents' => 0],
['name' => 'Broken row', 'price_cents' => -1],
];
Catch the expected exception per record. Print valid product lines and Rejected price for the invalid one.
Expected output:
Notebook: GBP 12.99
Sticker: GBP 0.00
Rejected price
Show solution
Solution
<?php
declare(strict_types=1);
function formatPrice(int $cents): string
{
if ($cents < 0) {
throw new InvalidArgumentException('Price cannot be negative.');
}
return 'GBP ' . number_format($cents / 100, 2, '.', ',');
}
$products = [
['name' => 'Notebook', 'price_cents' => 1299],
['name' => 'Sticker', 'price_cents' => 0],
['name' => 'Broken row', 'price_cents' => -1],
];
foreach ($products as $product) {
try {
echo $product['name'] . ': ' . formatPrice($product['price_cents']) . "\n";
} catch (InvalidArgumentException $exception) {
echo "Rejected price\n";
}
}
Explanation
The helper owns price validation and formatting while the loop owns records and output. Zero is valid under the stated non-negative contract.
Per-record handling lets the script report the invalid price without changing the behavior of earlier valid products.
Task: Predict Recap Output
Task
Predict every output line before running:
<?php
declare(strict_types=1);
function label(array $product): string
{
$stock = ($product['in_stock'] ?? false) ? 'available' : 'unavailable';
return $product['name'] . ' is ' . $stock;
}
$products = [
['name' => 'Notebook', 'in_stock' => true],
['name' => 'Pen', 'in_stock' => false],
['name' => 'Mug'],
['name' => 'Bag', 'in_stock' => null],
];
foreach ($products as $product) {
echo label($product), PHP_EOL;
}
Explain why missing and null values follow the same path, then run the unchanged code.
Show solution
Solution
Notebook is available
Pen is unavailable
Mug is unavailable
Bag is unavailable
Explanation
Only the explicit boolean true produces available. Explicit false selects the other ternary branch.
The null-coalescing operator uses false when in_stock is missing or contains null, so both Mug and Bag become unavailable. This is a deliberate policy; if null had a distinct meaning, the function would need array_key_exists() and a separate branch.
Task: Build Validation Recap
Task
Write requireProductName(array $product): string.
It must:
- read
namewith a safe missing-key fallback; - reject a non-string value with
InvalidArgumentException; - trim string values;
- reject an empty cleaned name;
- return the cleaned name otherwise.
Loop over these records and catch failures per record:
[
['name' => ' Mug '],
[],
['name' => 42],
['name' => 'Pen'],
]
Expected output:
Accepted: Mug
Rejected name
Rejected name
Accepted: Pen
Show solution
Solution
<?php
declare(strict_types=1);
function requireProductName(array $product): string
{
$name = $product['name'] ?? null;
if (!is_string($name)) {
throw new InvalidArgumentException('Product name must be text.');
}
$name = trim($name);
if ($name === '') {
throw new InvalidArgumentException('Product name is required.');
}
return $name;
}
$products = [
['name' => ' Mug '],
[],
['name' => 42],
['name' => 'Pen'],
];
foreach ($products as $product) {
try {
echo 'Accepted: ' . requireProductName($product) . "\n";
} catch (InvalidArgumentException $exception) {
echo "Rejected name\n";
}
}
Explanation
The helper validates PHP type before applying string operations. It does not cast integer 42 into text and accidentally accept a malformed record.
Missing, wrongly typed, empty, and valid states have explicit outcomes. The per-record catch preserves processing of the final valid product.