THU.JUN.18
2026
23:35:46
← back to modules MODULE · 02 · PHP PART 2
0 / 10 chapters complete · 0%

Control Flow — if, match, loops

This chapter is the skeleton of your programs — the parts that decide what runs in what order. If-statements pick between paths, loops repeat work, switches and match expressions pick from many options. The shapes will feel familiar if you've programmed in any C-family language. PHP 8 added a particularly lovely match expression that we'll use throughout the rest of the course.
if/elseif/else for branches. match (PHP 8+) beats switch for value mapping. foreach is the workhorse loop for arrays. Early returns beat nested if pyramids — make them a habit.

The mental model

Control flow is just decisions and repetition. Every program is a sequence of "do this, then if X do this, otherwise do that, then repeat for each item in the list." That's it. The syntax in each language is different but the shapes are the same everywhere. You're already doing this in your head — "if the dishwasher is full, run it, otherwise add more dishes." Programming is just writing those decisions down so a computer can follow them.

if / elseif / else

The classic. Pick one branch based on conditions:

if ($score >= 90) {
    $grade = 'A';
} elseif ($score >= 75) {
    $grade = 'B';
} elseif ($score >= 50) {
    $grade = 'C';
} else {
    $grade = 'F';
}

Two small things worth noting. elseif is one word in PHP. The two-word else if also works but it's less idiomatic — pick one style and stick with it, doesn't really matter which. Second, you'll sometimes see the alternate "colon syntax" for templates:

<?php if ($logged_in): ?>
  <a href="/logout">Sign out</a>
<?php else: ?>
  <a href="/login">Sign in</a>
<?php endif; ?>

That's the same as if/else, just with endif; instead of curly braces. When you're mixing PHP and HTML, this form is way more readable because you can clearly see where the PHP control flow ends. We saw the same pattern with foreach in chapter 1.

Ternary and null coalescing

The ternary operator is a one-line shorthand for if/else that returns a value:

$label = $is_admin ? 'Admin' : 'User';

Read it as: "if $is_admin is truthy, $label = 'Admin'; otherwise $label = 'User'." Same logic as a four-line if/else, but on one line. Great for simple assignments. PHP has a few more compact variants of this idea:

// Short ternary — returns left if truthy, else right
$display = $name ?: 'Anonymous';

// Null coalescing — returns left if SET and not null
$name = $_GET['name'] ?? 'guest';

// Null coalescing assignment — assign default if null
$cache['key'] ??= compute_expensive_value();

The differences are subtle but important. The short ternary ?: uses the left value if it's truthy. So 0 ?: 'fallback' returns 'fallback' because 0 is falsy. The null coalescing ?? uses the left value if it's set and not null. So 0 ?? 'fallback' returns 0, because 0 is not null. The distinction matters more than you'd think — using ?: on a number field will treat 0 as "missing" when it's actually a real input. Default to ?? for "is this missing" checks.

One rule for keeping your sanity: don't nest ternaries. $a ? $b : ($c ? $d : $e) is hard to read; pyramids of ternaries are unreadable. If you're reaching for a triple-nested ternary, just write a proper if/else. Your teammates (including future-you) will love you for it.

match — the modern switch (PHP 8+)

Switch statements have been a C-family staple forever, but they have some warts — fall-through behavior, loose comparison, no value return. PHP 8 added a new match expression that fixes all of that. For value-mapping (turning one value into another), match is just better:

$color = match ($status) {
    'pending', 'review' => 'yellow',
    'done'              => 'green',
    default             => 'grey',
};

Why match beats switch on every front:

  • It returns a value — you can assign the whole expression directly. No need for break or manual assignment in each case.
  • It uses === (strict equality) — no sneaky type coercion.
  • No fall-through — each arm is independent. Group multiple cases by listing them together ('pending', 'review').
  • Throws if nothing matches — if there's no default and the value doesn't match any case, you get an UnhandledMatchError. This forces you to handle every possibility, which is usually exactly what you want.

Whenever you need to map one value to another value, use match. The classic switch is still useful for "do different things in each branch" (function calls, side effects), but for value mapping, match wins.

🐍 Python: Almost identical to Python 3.10's match statement. Same keyword, same shape. PHP actually got there first by a year. Python's match supports deeper structural patterns; PHP's is simpler but covers the 90% case.

Loops — for, while, foreach

Three classic loop shapes plus PHP's beloved foreach. Each has its sweet spot:

// for — when you know the count
for ($i = 0; $i < 10; $i++) { echo $i; }

// while — when condition is the focus
$i = 0;
while ($i < 10) { echo $i++; }

// do-while — when the body must run at least once
do {
    $line = read_input();
} while ($line !== '');

// foreach — the workhorse (you'll use this 90% of the time)
foreach ($tags as $tag) {
    echo $tag;
}

foreach ($user as $key => $value) {
    echo "$key: $value\n";
}

Quick rule of thumb on choosing: for when the count matters and you have a clear start/end/step. while when the condition is the main focus and the loop might run indefinitely until something changes. do-while when the body has to run at least once before you can check the condition (reading input, polling). And foreach for everything else — iterating arrays is by far the most common loop you'll write.

break and continue

Two keywords for jumping around inside a loop:

foreach ($items as $item) {
    if ($item->archived) continue;   // skip this iteration
    if ($item->poison)   break;      // exit the loop entirely
    process($item);
}

continue means "skip the rest of this iteration and start the next one." break means "exit the loop entirely." Both can take an optional integer to break/continue out of nested loops: break 2; exits two levels. Occasionally useful. If you find yourself writing break 3, that's a sign your function should probably be split up into smaller pieces.

Early returns beat deep nesting

This is a universal coding rule but worth saying loud here because it makes a massive difference in how readable your code is. When you have a function that has several "if this is bad, bail" checks, the temptation is to nest them. Resist. Bail out at the top, leave the happy path flat.

Compare these two versions of the same function. They do exactly the same thing:

// Flat (early returns) — winner
function process(?User $u): string {
    if ($u === null)        return 'no user';
    if (!$u->is_active())   return 'inactive';
    if ($u->is_banned())    return 'banned';

    // happy path is flat and easy to follow
    return do_the_thing($u);
}

// Nested — harder to read
function process(?User $u): string {
    if ($u !== null) {
        if ($u->is_active()) {
            if (!$u->is_banned()) {
                return do_the_thing($u);
            } else {
                return 'banned';
            }
        } else {
            return 'inactive';
        }
    } else {
        return 'no user';
    }
}

The first version reads top-to-bottom like a story: "first check this, then check that, then do the work." The second forces you to hold three levels of nested context in your head while you read. Flat reads better, every single time. It's like reading a recipe vs reading a flowchart — recipes win for sequential work.

Build: Med Reminder Status Board

This is the first project in what's going to become MedTrack — a real medication tracker we'll build progressively over the rest of Part 2. For this chapter, the data is just a hardcoded array; in chapter 8 we'll wire it up to a real database. By chapter 10 it'll be a proper multi-user authenticated app. For now, focus on getting the rendering and filtering right.

The goal is a list of meds with status badges and color coding, plus filter chips at the top that let you narrow by status. The match expression is going to do the heavy lifting for color and label decisions. By the end you'll have a real-feeling status board.

  1. Create /home/erictey/server/meds.php.
  2. Paste:
    <?php
    declare(strict_types=1);
    
    // Hardcoded meds for now. Database persistence comes later.
    $meds = [
        ['name' => 'Ibuprofen',     'dose' => 200,  'status' => 'due'],
        ['name' => 'Vitamin D',     'dose' => 1000, 'status' => 'taken'],
        ['name' => 'Paracetamol',   'dose' => 500,  'status' => 'overdue'],
        ['name' => 'Multivitamin',  'dose' => 1,    'status' => 'taken'],
        ['name' => 'Magnesium',     'dose' => 200,  'status' => 'skipped'],
    ];
    
    function status_color(string $status): string {
        return match ($status) {
            'taken'   => '#4eff7a',
            'due'     => '#5bf0ff',
            'overdue' => '#ff4e6a',
            'skipped' => '#ffa64e',
            default   => '#b9adcf',
        };
    }
    
    $filter = $_GET['filter'] ?? 'all';
    ?>
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        body { background:#07050d; color:#f0e9ff; font-family:monospace; padding:30px; }
        h1 { color:#ff2e88; }
        .filters { margin-bottom: 20px; }
        .filters a { color:#5bf0ff; margin-right: 16px; text-decoration: none; padding: 4px 10px; border: 1px solid #5bf0ff55; }
        .filters a.active { background:#5bf0ff22; border-color:#5bf0ff; }
        .med { padding: 12px; margin: 8px 0; border-left: 3px solid; background: rgba(255,255,255,0.03); }
        .badge { display:inline-block; padding:2px 8px; font-size:12px; color:#07050d; margin-left:10px; }
      </style>
    </head>
    <body>
      <h1>Med Reminders</h1>
      <div class="filters">
        <a href="?filter=all"     class="<?= $filter==='all'?'active':''?>">All</a>
        <a href="?filter=due"     class="<?= $filter==='due'?'active':''?>">Due</a>
        <a href="?filter=overdue" class="<?= $filter==='overdue'?'active':''?>">Overdue</a>
        <a href="?filter=taken"   class="<?= $filter==='taken'?'active':''?>">Taken</a>
      </div>
    
      <?php foreach ($meds as $med): ?>
        <?php if ($filter !== 'all' && $med['status'] !== $filter) continue; ?>
        <?php $c = status_color($med['status']); ?>
        <div class="med" style="border-color:<?= $c ?>">
          <strong><?= htmlspecialchars($med['name']) ?></strong>
          <?= $med['dose'] ?> mg
          <span class="badge" style="background:<?= $c ?>"><?= strtoupper($med['status']) ?></span>
        </div>
      <?php endforeach; ?>
    </body>
    </html>
  3. Visit http://192.168.0.19/meds.php. You should see the list with colored status badges.
  4. Click the filter chips. Notice the URL updates AND only matching meds show. That's $_GET driving the view via the continue in the foreach loop.

Stretch goals:

  • Add a count of total meds + count per status near the top of the page.
  • Add a fifth status like "missed" with its own color. Just edit the match expression and add it to the filters.
  • Try using match(true) instead of match($status) for the color — it lets you use expressions in each arm. Useful trick to have in your back pocket.

What you flexed: match expressions for value mapping, foreach with continue for filtering, alternate syntax for templates (foreach...endforeach), URL params driving page state, ternary expressions for conditional CSS classes, function definitions with type hints. Solid first piece of the MedTrack app.