PHP Language Basics

Control Flow

Control flow determines which statements run, how often they run, and when execution leaves a block, loop, function, or script.

Without control flow, PHP evaluates a file from top to bottom once. Real programs need choices and repetition: reject invalid input, map a status to a message, process each record, skip unusable rows, stop after finding a result, and retry only while a limit remains.

A reliable control structure makes its condition, fallback, and termination rule visible.

Conditions Must Produce Booleans

An if statement evaluates an expression in a logical context:

PHP example
<?php

$isPaid = true;

if ($isPaid) {
    echo "Payment received\n";
}

The block runs because $isPaid is true.

Prefer conditions that already express a clear boolean rule:

PHP example
<?php

$totalCents = 7200;
$country = 'GB';

$meetsThreshold = $totalCents >= 5000;
$isSupportedCountry = $country === 'GB';
$qualifiesForFreeShipping = $meetsThreshold && $isSupportedCountry;

if ($qualifiesForFreeShipping) {
    echo "Free shipping\n";
}

Named conditions let a reviewer inspect each rule separately.

Do Not Let Raw Text Choose A Path Accidentally

PHP can interpret strings, integers, arrays, null, and other values as booleans. That convenience can hide boundary mistakes.

For example, the string '0' is falsy in a logical context even though it is a present one-character input:

PHP example
<?php

$quantityInput = '0';

var_dump((bool) $quantityInput);

Output:

bool(false)

Do not use a raw form or CLI string as if it were an already validated business decision. Validate it, produce an intended application type, then branch on an explicit comparison:

PHP example
<?php

$quantityInput = '3';
$quantity = filter_var(
    $quantityInput,
    FILTER_VALIDATE_INT,
    ['options' => ['min_range' => 1]],
);

if ($quantity === false) {
    echo "Invalid quantity\n";
} else {
    echo "Quantity accepted: $quantity\n";
}

The branches now describe validation success rather than PHP's general truthiness rules.

Use if, elseif, And else For Ordered Decisions

An if chain checks branches from top to bottom and runs the first matching branch:

PHP example
<?php

$orderStatus = 'paid';

if ($orderStatus === 'paid') {
    echo "Ship the order\n";
} elseif ($orderStatus === 'pending') {
    echo "Wait for payment\n";
} elseif ($orderStatus === 'failed') {
    echo "Ask the customer to retry\n";
} else {
    echo "Ask support to review\n";
}

Only one branch runs. Once 'paid' matches, later branches are not checked.

Order specific cases before broader conditions:

PHP example
<?php

$totalCents = 12000;

if ($totalCents >= 10000) {
    echo "Premium order\n";
} elseif ($totalCents >= 5000) {
    echo "Standard order\n";
} else {
    echo "Small order\n";
}

If the >= 5000 branch came first, it would also catch 12000, making the premium branch unreachable.

Always decide what unknown input means. An else branch can reject, log, or label an unexpected state rather than silently doing nothing.

Use Guard Clauses To Protect The Main Path

A guard clause handles a blocking case early:

PHP example
<?php

$quantityInput = '0';
$quantity = filter_var(
    $quantityInput,
    FILTER_VALIDATE_INT,
    ['options' => ['min_range' => 1]],
);

if ($quantity === false) {
    echo "Quantity must be a positive integer\n";
    return;
}

echo "Preparing $quantity items\n";

At top level, return ends execution of the current file. If that file was included by another file, control returns to the including file. Inside a function, return leaves that function; functions are covered next.

The guard keeps the successful path unindented. Multiple nested if blocks can obscure which prerequisites have already been established.

Use exit() when the entire process must terminate with a documented status, especially in a CLI entry script. Do not use abrupt process termination deep inside reusable code; let the caller decide how to report failure.

Use match For Exact Value Mapping

A match expression compares its subject strictly and returns a value:

PHP example
<?php

$paymentStatus = 'failed';

$message = match ($paymentStatus) {
    'paid' => 'Ship the order',
    'pending' => 'Wait for payment',
    'failed' => 'Ask the customer to retry',
    default => 'Ask support to review',
};

echo $message, PHP_EOL;

match uses identity comparison like ===. A string '1' does not match integer 1. Arms do not fall through, and only the selected result expression is evaluated.

A match must be exhaustive. If no arm matches and there is no default, PHP throws UnhandledMatchError. Use default when unknown values need a fallback. Deliberately omit it only when an unknown value should be treated as a hard programming failure and that behavior is understood.

Several exact values can share a result:

PHP example
<?php

$method = 'PATCH';

$category = match ($method) {
    'GET', 'HEAD' => 'read',
    'POST', 'PUT', 'PATCH' => 'write',
    'DELETE' => 'delete',
    default => 'unknown',
};

var_dump($category);

Use if for range checks or branches containing several statements. Use match when one exact input maps cleanly to one value.

Recognise switch In Existing Code

Older and existing PHP code often uses switch. Unlike match, switch uses loose comparison and cases can fall through unless execution reaches break.

For new exact-value mappings, match usually provides stricter comparison and fewer fall-through mistakes. Do not mechanically replace a switch without tests; some code may intentionally execute multiple cases or depend on its comparison behavior.

Iterate Values With foreach

foreach is the clearest loop for arrays and other iterable values:

PHP example
<?php

$statuses = ['paid', 'pending', 'failed'];

foreach ($statuses as $status) {
    echo "Status: $status\n";
}

The loop runs once for each value in order.

Capture keys when they matter:

PHP example
<?php

$settings = [
    'currency' => 'GBP',
    'locale' => 'en_GB',
];

foreach ($settings as $name => $value) {
    echo "$name=$value\n";
}

Use singular names for one current item: $orders as $order, $emails as $email, or $settings as $name => $value.

The loop variable normally receives the current value. Avoid reference iteration with & until you specifically need in-place mutation and understand reference cleanup; ordinary value iteration is safer for beginner code.

Skip One Iteration With continue

continue stops the current iteration and moves to the next item:

PHP example
<?php

$orders = [
    ['id' => 501, 'status' => 'paid'],
    ['id' => 502, 'status' => 'pending'],
    ['id' => 503, 'status' => 'paid'],
];

foreach ($orders as $order) {
    if ($order['status'] !== 'paid') {
        continue;
    }

    echo "Ship order {$order['id']}\n";
}

Output:

Ship order 501
Ship order 503

This loop uses a guard inside the iteration: non-paid orders leave early, while the main action remains clear.

Do not use continue when an else branch would express two equally important outcomes more clearly. It is strongest when one path simply excludes the current item.

Stop A Search With break

break leaves the loop completely:

PHP example
<?php

$emails = [
    'bad-address',
    'admin@example.com',
    'support@example.com',
];
$firstValidEmail = null;

foreach ($emails as $email) {
    if (!str_contains($email, '@')) {
        continue;
    }

    $firstValidEmail = $email;
    break;
}

var_dump($firstValidEmail);

Once the first acceptable value is found, later items cannot improve the answer defined by the variable name. Continuing would waste work and could accidentally overwrite the result.

After the loop, handle the possibility that no match was found. Initialising $firstValidEmail to null makes that state explicit.

Use while When Repetition Depends On State

A while loop checks its condition before each iteration:

PHP example
<?php

$attemptsRemaining = 3;

while ($attemptsRemaining > 0) {
    echo "Attempt with $attemptsRemaining remaining\n";
    $attemptsRemaining--;
}

The loop terminates because every iteration reduces $attemptsRemaining, eventually making the condition false.

Before writing while, identify:

  1. the state tested by the condition;
  2. the statement that changes that state;
  3. the value that ends the loop;
  4. any maximum bound needed for safety.

If the state never changes, the loop can run indefinitely. Network retries and polling loops also need timeouts, delays, and cancellation policies; a decreasing counter alone is not a complete production retry strategy.

Use do-while Only When One Run Is Required

A do-while checks its condition after the body, so the body runs at least once:

PHP example
<?php

$page = 1;

do {
    echo "Fetch page $page\n";
    $page++;
} while ($page <= 3);

Choose it only when one initial execution is part of the contract. Otherwise, a normal while makes the precondition visible before work begins.

Use for For A Clear Counter Range

A for loop places initialisation, condition, and update in one header:

PHP example
<?php

for ($page = 1; $page <= 3; $page++) {
    echo "Fetch page $page\n";
}

Output:

Fetch page 1
Fetch page 2
Fetch page 3

Use <= 3 when the endpoint is inclusive. Use < 3 when it is exclusive. This single-character difference is a common off-by-one bug.

Do not use a counter loop merely to access every item in an array. foreach states that intent more directly and does not require manual index bounds.

Keep Loop Effects Understandable

Avoid changing the collection's shape while iterating over it unless the behavior is explicitly designed and tested. Removing, adding, or reindexing items during iteration can make the visited sequence difficult to reason about.

Prefer one of these beginner patterns:

  • print or inspect each item without mutating the collection;
  • count or accumulate a separate result;
  • build a new collection;
  • stop after finding a defined answer.

The arrays lesson introduces dedicated transformation functions for filtering and mapping.

Trace A Path Before Running It

For each control structure, write down:

  • the initial values;
  • the first condition result;
  • the selected branch or loop body;
  • every state change;
  • the next condition result;
  • the exact termination point.

For a loop with continue or break, record which items reach the output statement. This manual trace catches reversed conditions and unreachable branches before runtime output becomes the only evidence.

Common Control-Flow Mistakes

Symptom Likely cause
Specific branch never runs Broader earlier condition already catches its values
Unknown state does nothing Missing fallback or default policy
Present input takes false path Raw value relied on PHP truthiness
Loop never ends Condition state is not changed toward termination
Final item is missing Exclusive < used where inclusive <= was intended
One skipped item stops everything break used instead of continue
Items after a match still run Missing break after search result
match throws unexpectedly No arm and no default handled the subject
Loop behavior changes unpredictably Collection was mutated while iterating

What You Should Be Able To Do

After this lesson, you should be able to validate before branching, order if conditions from specific to broad, write a guard clause, map exact values with exhaustive match, iterate arrays with foreach, use continue and break deliberately, prove that while and do-while loops terminate, choose correct for bounds, and trace the path a script will take before executing it.

Official references include PHP's control structures overview, match, foreach, and while.

Practice

Task: Ship Paid Orders

Task

Write a PHP script that loops over orders and prints only the orders that are ready to ship.

Use this starting data:

PHP example
<?php

$orders = [
    ['id' => 501, 'status' => 'paid', 'quantity' => 2],
    ['id' => 502, 'status' => 'pending', 'quantity' => 1],
    ['id' => 503, 'status' => 'paid', 'quantity' => 0],
    ['id' => 504, 'status' => 'paid', 'quantity' => 3],
    ['id' => 505, 'status' => 'cancelled', 'quantity' => 4],
];

Your script should:

  • loop over every order with foreach;
  • skip an order when its status is not exactly paid;
  • skip a paid order when its quantity is less than 1;
  • count every order that was skipped;
  • print a shipping line for each paid order with a valid quantity;
  • print the skipped count after the loop.

The exact output should be:

Ship order 501 (2 items)
Ship order 504 (3 items)
Skipped orders: 3

Hints

  • Start $skippedOrders at 0.
  • Use a separate guard condition for each reason an order cannot ship.
  • Increment the counter immediately before each continue.
  • Keep the shipping output inside the loop and the summary after it.
Show solution

Solution

PHP example
<?php

$orders = [
    ['id' => 501, 'status' => 'paid', 'quantity' => 2],
    ['id' => 502, 'status' => 'pending', 'quantity' => 1],
    ['id' => 503, 'status' => 'paid', 'quantity' => 0],
    ['id' => 504, 'status' => 'paid', 'quantity' => 3],
    ['id' => 505, 'status' => 'cancelled', 'quantity' => 4],
];

$skippedOrders = 0;

foreach ($orders as $order) {
    if ($order['status'] !== 'paid') {
        $skippedOrders++;
        continue;
    }

    if ($order['quantity'] < 1) {
        $skippedOrders++;
        continue;
    }

    echo "Ship order {$order['id']} ({$order['quantity']} items)\n";
}

echo "Skipped orders: $skippedOrders\n";

// Prints:
// Ship order 501 (2 items)
// Ship order 504 (3 items)
// Skipped orders: 3

Explanation

The foreach loop still visits all five orders, but the two guard conditions decide whether the current order reaches the shipping line.

Orders 502 and 505 fail the status guard. Order 503 passes the status guard but fails the quantity guard. Each rejected order increments $skippedOrders immediately before continue moves execution to the next array item.

Only orders 501 and 504 pass both guards. The final echo is outside the loop because the skipped total should be printed once, after every order has been considered.

Task: Predict Loop Output

Task

Before running this code, predict exactly what it prints.

PHP example
<?php

$codes = ['draft', 'paid', 'cancelled', 'paid'];
$visited = 0;

foreach ($codes as $code) {
    $visited++;
    echo "Visit $visited: $code\n";

    if ($code === 'draft') {
        continue;
    }

    if ($code === 'cancelled') {
        break;
    }

    echo "Process {$code}\n";
}

echo "Visited: $visited\n";

Write down:

  1. the exact output, including its order;
  2. which values reach the Process line;
  3. the final value of $visited.

Then run the code and compare the real output with your prediction. Do not change the code before making the prediction.

Hints

  • continue skips only the current loop item.
  • break stops the whole loop.
  • The visit line and increment happen before either condition.
  • The second paid value is after cancelled.
Show solution

Solution

PHP example
<?php

$codes = ['draft', 'paid', 'cancelled', 'paid'];
$visited = 0;

foreach ($codes as $code) {
    $visited++;
    echo "Visit $visited: $code\n";

    if ($code === 'draft') {
        continue;
    }

    if ($code === 'cancelled') {
        break;
    }

    echo "Process {$code}\n";
}

echo "Visited: $visited\n";

// Prints:
// Visit 1: draft
// Visit 2: paid
// Process paid
// Visit 3: cancelled
// Visited: 3

Explanation

The loop starts the draft iteration by incrementing $visited and printing its visit line. continue then skips the remaining statements in that iteration, so draft never reaches Process.

The first paid value is visit 2. Neither condition matches, so its process line is printed.

cancelled is still counted and its visit line is printed before the break condition is checked. break then leaves the entire loop. The final paid array value is never visited, so $visited remains 3.

Execution resumes after the loop and prints the summary. This is the key difference: continue advances to another iteration, while break prevents any later iteration from starting.

Task: Fix Shipping Branch

Task

Fix this shipping message script. It currently checks the broad condition first, so express orders never reach the express branch.

PHP example
<?php

$shippingMethod = 'express';

if ($shippingMethod !== '') {
    echo "Standard shipping\n";
} elseif ($shippingMethod === 'express') {
    echo "Express shipping\n";
} else {
    echo "Choose a shipping method\n";
}

Rewrite the branches so:

  • express prints Express shipping;
  • standard prints Standard shipping;
  • an empty string prints Choose a shipping method;
  • any other value prints Unknown shipping method.

Use an if / elseif / else chain for this exercise. After fixing it, run the script four times with these values:

PHP example
$shippingMethod = 'express';
$shippingMethod = 'standard';
$shippingMethod = '';
$shippingMethod = 'drone';

Change only the assignment between runs. Confirm that each input reaches exactly one expected branch.

Hints

  • Compare known values with ===.
  • Put exact known-value checks before the empty-input check.
  • Keep an else branch for non-empty values the application does not support.
Show solution

Solution

PHP example
<?php

$shippingMethod = 'express';

if ($shippingMethod === 'express') {
    echo "Express shipping\n";
} elseif ($shippingMethod === 'standard') {
    echo "Standard shipping\n";
} elseif ($shippingMethod === '') {
    echo "Choose a shipping method\n";
} else {
    echo "Unknown shipping method\n";
}

// Prints:
// Express shipping

Explanation

The original code checked $shippingMethod !== '' first. That condition is true for both express and standard, so the more specific express branch could never run.

The fixed version checks exact known values first, then handles the empty value, then keeps a final fallback for unexpected input. Because this is one if chain, PHP stops checking as soon as one condition matches.

The four requested runs produce:

express  -> Express shipping
standard -> Standard shipping
empty    -> Choose a shipping method
drone    -> Unknown shipping method

Testing all four paths matters. A single successful express run proves that one branch works, but it does not prove that the empty and fallback paths are reachable.