php language basics
Milestone: Control Flow
This milestone checks whether the first pieces of PHP are starting to work together. You have seen values, operators, branches, loops, functions, scope, and basic failure handling. Real PHP work combines those ideas instead of using them one at a time.
The goal is not to build a full application yet. The goal is to read a small piece of business logic, predict the path it takes, and change it without breaking the edge cases.
A Small Order Validator
The example below validates a list of orders and prints which ones can move to shipping.
<?php
function isPaidOrder(array $order): bool
{
return ($order['status'] ?? '') === 'paid';
}
function hasPositiveTotal(array $order): bool
{
return ($order['total_cents'] ?? 0) > 0;
}
$orders = [
['id' => 701, 'status' => 'paid', 'total_cents' => 2500],
['id' => 702, 'status' => 'pending', 'total_cents' => 1800],
['id' => 703, 'status' => 'paid', 'total_cents' => 0],
];
foreach ($orders as $order) {
if (!isPaidOrder($order)) {
echo "Skip order {$order['id']}: not paid\n";
continue;
}
if (!hasPositiveTotal($order)) {
echo "Skip order {$order['id']}: invalid total\n";
continue;
}
echo "Ship order {$order['id']}\n";
}
// Prints:
// Ship order 701
// Skip order 702: not paid
// Skip order 703: invalid total
This code uses several beginner skills at once. The functions name the checks. The foreach loop processes every order. The if statements decide which path each order takes. continue skips to the next order after a failure.
Why The Functions Help
The checks could be written directly inside the loop, but naming them makes the rule easier to review:
isPaidOrder()answers one question;hasPositiveTotal()answers one question;- the loop decides what to do with each answer.
This is the habit to build early. A function should hide repetitive detail, not hide important decisions.
Missing Keys And Defaults
The example uses ?? when reading status and total_cents. That prevents a missing key from causing a warning in this small script, and it gives the validation rule a safe fallback.
This does not mean missing data is fine. It means the code decides what missing data means. In this example, a missing status is not paid, and a missing total is not a positive total.
When To Throw Instead
Sometimes a failure is expected and should be reported, as with an unpaid order. Sometimes a failure means the caller gave the function invalid data. In those cases, throwing an exception can be clearer.
<?php
function requireOrderId(array $order): int
{
if (!isset($order['id'])) {
throw new InvalidArgumentException('Order ID is required.');
}
return $order['id'];
}
echo requireOrderId(['id' => 701]) . "\n";
// Prints:
// 701
Use exceptions for failures the current function cannot sensibly handle. Use return values or branches for ordinary business choices the caller expects.
What This Milestone Proves
You are ready to move on when you can:
- trace which branch runs for each input;
- explain why a loop skips or processes an item;
- split a repeated check into a named function;
- pass data into functions instead of relying on hidden globals;
- handle missing data deliberately;
- decide whether a failure should be a branch or an exception.
Practice
Task: Validate Paid Orders
Task
Write a script that loops over orders and prints only the orders that are paid and have a positive total.
Use functions for the two checks:
isPaidOrder(array $order): boolhasPositiveTotal(array $order): bool
Use this data:
<?php
$orders = [
['id' => 801, 'status' => 'paid', 'total_cents' => 3200],
['id' => 802, 'status' => 'failed', 'total_cents' => 3200],
['id' => 803, 'status' => 'paid', 'total_cents' => 0],
];
The script should print only Ship order 801.
Show solution
Solution
<?php
function isPaidOrder(array $order): bool
{
return ($order['status'] ?? '') === 'paid';
}
function hasPositiveTotal(array $order): bool
{
return ($order['total_cents'] ?? 0) > 0;
}
$orders = [
['id' => 801, 'status' => 'paid', 'total_cents' => 3200],
['id' => 802, 'status' => 'failed', 'total_cents' => 3200],
['id' => 803, 'status' => 'paid', 'total_cents' => 0],
];
foreach ($orders as $order) {
if (!isPaidOrder($order) || !hasPositiveTotal($order)) {
continue;
}
echo "Ship order {$order['id']}\n";
}
// Prints:
// Ship order 801
Explanation
The loop handles the list. The two functions name the business checks. The continue skips any order that fails either check.
Task: Predict Missing Paid Key
Task
Before running this code, predict what it prints.
<?php
function isPaidOrder(array $order): bool
{
return ($order['status'] ?? '') === 'paid';
}
$orders = [
['id' => 901, 'status' => 'paid'],
['id' => 902],
];
foreach ($orders as $order) {
echo isPaidOrder($order) ? "paid\n" : "not paid\n";
}
Then run the code and compare the real output with your prediction.
Hints
??uses the fallback when the key is missing.- The fallback in this function is an empty string.
Show solution
Solution
<?php
function isPaidOrder(array $order): bool
{
return ($order['status'] ?? '') === 'paid';
}
$orders = [
['id' => 901, 'status' => 'paid'],
['id' => 902],
];
foreach ($orders as $order) {
echo isPaidOrder($order) ? "paid\n" : "not paid\n";
}
// Prints:
// paid
// not paid
Explanation
The first order has status set to paid, so the function returns true. The second order has no status key, so ?? uses '', which is not equal to paid.
Task: Build Status Router
Task
Write a function named statusMessage() that accepts an order status string and returns the message for that status.
Rules:
paidreturnsReady to ship;pendingreturnsWaiting for payment;failedreturnsPayment failed;- any other value returns
Unknown status.
Call the function with paid, failed, and refunded, then print each message.
Hints
matchis a good fit for exact status values.- Include a
defaultbranch.
Show solution
Solution
<?php
function statusMessage(string $status): string
{
return match ($status) {
'paid' => 'Ready to ship',
'pending' => 'Waiting for payment',
'failed' => 'Payment failed',
default => 'Unknown status',
};
}
echo statusMessage('paid') . "\n";
echo statusMessage('failed') . "\n";
echo statusMessage('refunded') . "\n";
// Prints:
// Ready to ship
// Payment failed
// Unknown status
Explanation
match keeps the exact status-to-message mapping in one expression. The default branch handles unexpected values deliberately instead of letting the function fail with no answer.