<< Home

Chapter 5: Flow Control

Programs choose between paths, repeat work, and bail out of work using flow control. PHP provides several constructs for branching and looping, each tuned for a slightly different kind of decision. This chapter introduces the most useful patterns and how to write them clearly.

if, elseif, else

The classic way to branch is to test a condition and execute one of several blocks. Each condition should read like a true or false statement; when a condition is true, the related block runs.

Writing a clear chained if ladder

Use a single chained ladder for mutually exclusive branches. The first condition that evaluates to true selects the block and the rest are skipped.

<?php
$score = 73;

if ($score >= 90) {
  echo "A";
} elseif ($score >= 80) {
  echo "B";
} elseif ($score >= 70) {
  echo "C";
} else {
  echo "Try again";
}
?>
💡 Prefer explicit comparisons over relying on truthy or falsy values. For example, write $count > 0 instead of just $count when you mean "more than zero".

Using blocks and scoping correctly

Always use braces, even for a single statement; this prevents accidental binding to the wrong if when you add more lines later.

<?php
$isMember = true;

if ($isMember) {
  $discount = 0.15;
  echo "Member discount applied";
}
?>

Combining conditions safely

Group conditions with parentheses to make intent obvious and to control evaluation order.

<?php
$inStock = true;
$vip = false;
$quantity = 3;

if (($inStock && $quantity >= 1) || $vip) {
  echo "Proceed to checkout";
}
?>
⚠️ Avoid assigning inside conditions unless it is deliberate. Writing if ($x = 1) assigns the value and then evaluates as true; prefer === for equality with type safety.

match Expressions

A match expression selects a result based on a tested value and returns that result. It uses strict comparison for its arms and it must be exhaustive; this encourages precise branching.

Returning a value from match

Because match is an expression, it produces a value that you can assign or echo directly.

<?php
$status = 404;

$message = match ($status) {
  200 => "OK",
  301 => "Moved Permanently",
  404 => "Not Found",
  default => "Unknown status",
};

echo $message;
?>

Relying on strict comparison in match

Each arm uses === semantics. A string "1" does not match the integer 1; be explicit about types.

<?php
$val = "1";

$result = match ($val) {
  1 => "int one",
  "1" => "string one",
  default => "something else",
};

echo $result; // prints "string one"
?>
💡 Combine labels with commas to handle multiple exact values in a single arm, like 0, null => … .

Guarding with conditions in match using true

When you need range checks, match against true and let each arm supply a boolean condition.

<?php
$score = 73;

$grade = match (true) {
  $score >= 90 => "A",
  $score >= 80 => "B",
  $score >= 70 => "C",
  default => "Try again",
};

echo $grade;
?>

Loops: for, while, do while, foreach

Loops repeat a block while a condition holds or while there are items to process. Choose the loop that reads closest to the problem you are solving.

Counting with for

Use for for indexed counting where you control initialization, condition, and increment.

<?php
for ($i = 0; $i < 3; $i++) {
  echo $i . PHP_EOL; // 0, 1, 2
}
?>

Looping until a condition changes with while and do while

while checks before the body runs; do while checks after the body, ensuring the body runs at least once.

<?php
$lines = ["a", "b", "c"];
$i = 0;

while ($i < count($lines)) {
  echo $lines[$i] . PHP_EOL;
  $i++;
}

$j = 0;
do {
  echo "Index: $j" . PHP_EOL;
  $j++;
} while ($j < 1);
?>

Iterating collections with foreach

foreach iterates arrays and Traversable objects without manual indexing. Retrieve the value or both key and value.

<?php
$prices = ["apple" => 1.20, "pear" => 1.00];

foreach ($prices as $item => $price) {
  echo "$item: $price" . PHP_EOL;
}
?>
LoopBest useChecks
forCounting with an indexBefore each iteration
whileRepeat until condition changesBefore each iteration
do whileRun body at least onceAfter each iteration
foreachCollections and generatorsImplicit over items
⚠️ Modifying an array while iterating over it can produce surprising results. When you plan to add or remove entries, iterate over a copy using foreach (array_values($arr) as …) or collect changes and apply them afterward.

continue, break, and goto

Inside loops you can skip the rest of the current iteration with continue or exit the loop entirely with break. The optional numeric argument targets outer loops. PHP also supports goto with labels, although structured control usually reads better.

Skipping iterations with continue

Use continue to move directly to the next iteration when a condition is not relevant.

<?php
for ($i = 1; $i <= 5; $i++) {
  if ($i % 2 === 0) {
    continue; // skip even numbers
  }
  echo $i . " ";
}
// 1 3 5
?>

Exiting loops with break and targeting outer loops

Provide a level to break out of nested loops. A level of 2 exits two loops, and so on.

<?php
for ($i = 0; $i < 3; $i++) {
  for ($j = 0; $j < 3; $j++) {
    if ($i === 1 && $j === 1) {
      break 2; // leave both loops
    }
    echo "($i,$j) ";
  }
}
?>

Understanding when goto harms readability

goto jumps to a label written as label:. Prefer structured loops and functions since they show intent. Reserve goto for simple error handling in very small scripts when refactoring would be disproportionate.

<?php
$ok = false;

if (!$ok) {
  goto bail;
}

echo "All good";
bail:
echo "Cleaning up";
?>
💡 Extract code into a function and return early instead of using goto; early returns are easier to follow.

Declaring and Using switch

A switch statement compares a value against several cases and runs matching blocks until a break is encountered. It is a control statement and does not produce a value.

Writing a basic switch with case and break

Place the target expression after switch, write case labels, and end each handled block with break to prevent falling through.

<?php
$ext = "jpg";

switch ($ext) {
  case "jpg":
  case "jpeg":
    echo "Image";
    break;

  case "mp3":
    echo "Audio";
    break;

  default:
    echo "Unknown";
}
?>

Falling through deliberately and documenting it

When you want a fall through, leave out break deliberately and place a comment that explains why.

<?php
$level = "warning";

switch ($level) {
  case "error":
    logError();
    // fall through
  case "warning":
    notifyTeam();
    break;
}
?>
⚠️ switch uses loose comparison for case labels in many situations. If you depend on type distinctions, use match or add strict checks inside case blocks.

Pattern-style Branching with match vs switch

match suits value to value selection that returns a result with strict rules; switch suits statement based control with optional fall through. Choose based on whether you need a value or a block-oriented path.

Choosing an expression when you need a value

Use match when the outcome is a value to compute. The expression is concise, exhaustive, and clear about types.

<?php
function taxBand(int $income): string {
  return match (true) {
    $income < 12570 => "none",
    $income < 50270 => "basic",
    $income < 125140 => "higher",
    default => "additional",
  };
}
?>

Choosing a statement when you need side effects

Use switch when you want to run statements that do work such as logging, updating metrics, or calling different functions.

<?php
function handleCommand(string $cmd): void {
  switch ($cmd) {
    case "start":
      startService();
      break;
    case "stop":
      stopService();
      break;
    default:
      echo "Unknown command";
  }
}
?>

Comparing behavior at a glance

Aspectmatchswitch
KindExpression that returns a valueStatement that executes blocks
ComparisonStrict ===Often loose; watch types
ExhaustivenessRequires full coverage via default or armsdefault optional
Fall throughNot allowedAllowed; requires care
Best forMapping input to resultRunning side effects
💡 When you start with a switch that only sets a variable, consider rewriting it as a match expression for clarity.