PHP

Chapter 3: Control Flow, Decision Making, and Modern Pattern Matching

By Ali Naqi | November 07, 2026

Chapter 3: Control Flow, Decision Making, and Modern Pattern Matching

Modern PHP Mastery: From First Steps to Production

Chapter 3: Control Flow, Decision Making, and Modern Pattern Matching

In Chapter 2, we established how PHP manages state in memory using scalar types, explicit constants, and strict typing mode. Now it is time to give our programs direction.

Without control flow, a program is simply a linear script executing line by line from top to bottom. Real-world applications—whether authenticating a user session, routing an HTTP request, or checking inventory levels—require dynamic decisions.

In this chapter, we will master PHP's branching structures: traditional conditional statements (if, else, elseif), short-circuit evaluation, ternary and null-coalescing operators, iterative loops, and the modern match expression introduced in PHP 8 that largely supersedes the legacy switch statement.

Conditional Statements: The Core Logic Blocks

The most direct way to branch execution is through if, elseif, and else statements.

<?php

declare(strict_types=1);


$userRole = 'editor';

$isAccountActive = true;


if (! $isAccountActive) {

    echo "Access denied: Account suspended.";

} elseif ($userRole === 'admin') {

    echo "Redirecting to administrative dashboard.";

} elseif ($userRole === 'editor') {

    echo "Redirecting to content management workspace.";

} else {

    echo "Redirecting to public feed.";

}


The Power of Short-Circuit Evaluation

When using logical operators (&& for AND, || for OR), PHP evaluates expressions using short-circuit evaluation:

 * In an && chain, evaluation halts at the first expression that yields false.

 * In an || chain, evaluation halts at the first expression that yields true.

This is not just a performance optimization; it prevents fatal errors when performing defensive checks:

<?php

// If $user is null, $user->hasActiveSubscription() would cause a fatal crash.

// Because of short-circuit evaluation, PHP never calls the method if $user is null.

if ($user !== null && $user->hasActiveSubscription()) {

    // Process premium stream

}


The Clean Code Imperative: Escaping the "Arrow Anti-Pattern"

A common trap for developers is nesting multiple if conditions inside each other:

// The Arrow Anti-Pattern (Hard to read, hard to test)

if ($user !== null) {

    if ($user->isVerified()) {

        if ($cart->hasItems()) {

            if ($paymentGateway->isOnline()) {

                // Execute checkout

            }

        }

    }

}


This structural nesting creates cognitive friction. Modern PHP favors Guard Clauses (or early returns). You check for failure conditions first, exit immediately, and leave the primary execution path unnested and flat:

<?php

declare(strict_types=1);


function processCheckout(?User $user, Cart $cart, PaymentGateway $gateway): bool 

{

    // 1. Guard against unauthenticated state

    if ($user === null) {

        return false;

    }


    // 2. Guard against unverified status

    if (! $user->isVerified()) {

        return false;

    }


    // 3. Guard against empty checkout

    if ($cart->isEmpty()) {

        return false;

    }


    // 4. Guard against downstream dependency failure

    if (! $gateway->isOnline()) {

        return false;

    }


    // Primary logic remains clean and unindented

    return $gateway->charge($user, $cart->getTotal());

}


Compact Conditionals: Ternary and Null-Coalescing Operators

When assigning a value based on a simple boolean condition, a full if/else block can feel heavy. PHP provides compact, functional syntax alternatives.

1. The Ternary Operator (?:)

The ternary operator takes three operands: a condition, a result for true, and a result for false.

<?php

$stock = 5;

$status = ($stock > 0) ? 'In Stock' : 'Out of Stock';


You can also use the shorthand "Elvis" operator (?:), which returns the left-hand operand if it evaluates to true (truthy), or the right-hand operand otherwise:

<?php

$displayName = $userCustomName ?: 'Guest User';


2. The Null-Coalescing Operator (??)

Introduced to streamline fallback assignments, ?? checks whether a variable is set and not null. It suppresses undefined variable or undefined array key notices, making it ideal for reading query parameters and configuration options:

<?php

// Legacy approach:

// $page = isset($_GET['page']) ? (int) $_GET['page'] : 1;


// Modern approach with ??:

$page = (int) ($_GET['page'] ?? 1);


You can chain null-coalescing operators to check multiple fallbacks sequentially:

<?php

$contactEmail = $userProfile['work_email'] 

    ?? $userProfile['personal_email'] 

    ?? $systemDefaultEmail 

    ?? 'support@example.com';


PHP also provides the Null-Coalescing Assignment Operator (??=):

<?php

// Assigns 'anonymous' to $sessionUser ONLY if $sessionUser is null or undefined

$sessionUser ??= 'anonymous';


Evolution of Selection: From switch to match

For decades, the standard way to compare a single value against multiple cases was the switch statement:

<?php

// Traditional switch statement

switch ($statusCode) {

    case 200:

    case 201:

        $label = 'Success';

        break;

    case 400:

        $label = 'Bad Request';

        break;

    case 404:

        $label = 'Not Found';

        break;

    default:

        $label = 'Unknown Status';

        break;

}


While functional, switch carries critical design flaws:

 * Loose Equality (==): It compares values loosely, which can cause subtle type bugs.

 * Accidental Fallthrough: If you forget a break; statement, execution leaks into the next case silently.

 * Verbose Syntax: It cannot return a value directly as an expression; you must assign variables manually inside every block.

The Modern Solution: The match Expression

Introduced in PHP 8.0, match is an expression (it evaluates directly to a value) that addresses every drawback of switch:

 * Uses strict comparison (===).

 * Evaluates only the matching arm with no accidental fallthrough (no break needed).

 * Supports multiple comma-separated conditions per arm.

 * Throws an UnhandledMatchError if an unhandled case occurs without a default arm.

<?php

declare(strict_types=1);


$httpStatus = 404;


$message = match ($httpStatus) {

    200, 201 => 'Request succeeded',

    400 => 'Invalid payload provided',

    401, 403 => 'Authorization denied',

    404 => 'Resource not found',

    500 => 'Internal server anomaly',

    default => 'Unrecognized response code',

};


echo $message; // Output: Resource not found


You can also evaluate complex boolean expressions by matching against true:

<?php

declare(strict_types=1);


$age = 22;


$tier = match (true) {

    $age < 13 => 'Child',

    $age < 20 => 'Teenager',

    $age < 65 => 'Adult',

    default => 'Senior',

};


echo $tier; // Output: Adult


Loops and Iteration

Loops allow you to execute a code block repeatedly until a termination condition is met. PHP offers four primary looping structures:

1. The while Loop

Evaluates the condition before entering the block. If false on the first check, it never runs.

<?php

$attempts = 0;

while ($attempts < 3) {

    // Attempt operation...

    $attempts++;

}


2. The do-while Loop

Evaluates the condition after the block executes. It is guaranteed to run at least once, regardless of whether the initial condition is true or false.

<?php

$counter = 0;

do {

    // Generates a token at least once

    $token = bin2hex(random_bytes(16));

    $counter++;

} while ($counter < 1);


3. The for Loop

Used when the number of iterations is known upfront. It consists of three parts: initializer, condition, and incrementor.

<?php

for ($i = 1; $i <= 5; $i++) {

    echo "Step {$i}" . PHP_EOL;

}


4. The foreach Loop (Industry Standard for Collections)

The most frequently used loop in PHP web applications. It iterates cleanly over arrays and iterable objects without requiring an indexing counter.

<?php

$roles = [

    'admin' => 'System Administrator',

    'author' => 'Content Writer',

    'guest' => 'Visitor',

];


// Iterating over keys and values

foreach ($roles as $slug => $label) {

    echo "Slug: {$slug} | Role: {$label}" . PHP_EOL;

}


Loop Control: break and continue

 * break: Immediately terminates loop execution and jumps to the code following the loop.

 * continue: Skips the remainder of the current iteration and jumps to the next condition check.

<?php

for ($n = 1; $n <= 10; $n++) {

    if ($n % 2 === 0) {

        continue; // Skip even numbers

    }

    

    if ($n > 7) {

        break; // Stop completely once $n exceeds 7

    }

    

    echo "Odd Number: {$n}" . PHP_EOL;

}

// Outputs:

// Odd Number: 1

// Odd Number: 3

// Odd Number: 5

// Odd Number: 7


Alternative Syntax for Templates

When embedding PHP control flow inside HTML templates, using standard braces ({ ... }) can lead to mismatched closing tags and poor readability. PHP provides an alternative syntax specifically tailored for templating engines and views.

Under this syntax:

 * Replace { with :

 * Replace } with endif;, endforeach;, endfor;, or endwhile;

<?php

$notifications = [

    ['type' => 'success', 'body' => 'Your profile has been updated.'],

    ['type' => 'warning', 'body' => 'Your storage quota is at 85%.'],

];

?>


<div class="alerts-container">

    <?php if (empty($notifications)): ?>

        <p class="empty-state">No pending alerts.</p>

    <?php else: ?>

        <ul class="alert-list">

            <?php foreach ($notifications as $alert): ?>

                <li class="alert-badge alert-<?= htmlspecialchars($alert['type'], ENT_QUOTES, 'UTF-8') ?>">

                    <?= htmlspecialchars($alert['body'], ENT_QUOTES, 'UTF-8') ?>

                </li>

            <?php endforeach; ?>

        </ul>

    <?php endif; ?>

</div>


This syntax creates clean, visual demarcation within your markup, making templates easier to maintain.

Practical Example: Building an Access Gatekeeper Script

Let's combine these concepts into a production-style permission evaluation module. We will define user roles, verify authorization rules using guard clauses, map status codes using match, and render a clean view using the template syntax.

Create a file named gatekeeper.php:

<?php

declare(strict_types=1);


// User and session definition

$userSession = [

    'id' => 108,

    'username' => 'sarah_admin',

    'role' => 'manager',

    'is_banned' => false,

    'ip_locked' => false,

];


// Target operational request

$requestedAction = 'EXPORT_AUDIT_LOGS';


/**

 * Evaluates whether the requested action is authorized for the given session.

 */

function evaluateAccess(array $session, string $action): array 

{

    // Guard 1: Missing identity

    if (empty($session['id'])) {

        return ['allowed' => false, 'code' => 'UNAUTHENTICATED'];

    }


    // Guard 2: Security holds

    if ($session['is_banned'] || $session['ip_locked']) {

        return ['allowed' => false, 'code' => 'ACCOUNT_RESTRICTED'];

    }


    // Permission matrix using match expression

    $permitted = match ($session['role']) {

        'superadmin' => true,

        'manager' => in_array($action, ['VIEW_REPORTS', 'EXPORT_AUDIT_LOGS', 'MANAGE_STAFF'], true),

        'editor' => in_array($action, ['VIEW_REPORTS', 'CREATE_POST', 'UPDATE_POST'], true),

        'subscriber' => false,

        default => false,

    };


    return [

        'allowed' => $permitted,

        'code' => $permitted ? 'ACCESS_GRANTED' : 'INSUFFICIENT_PERMISSIONS',

    ];

}


$evaluation = evaluateAccess($userSession, $requestedAction);


// Map the authorization code to user-facing UI messages

$feedback = match ($evaluation['code']) {

    'ACCESS_GRANTED' => ['class' => 'success', 'title' => 'Access Approved', 'detail' => 'Audit export sequence initialized.'],

    'INSUFFICIENT_PERMISSIONS' => ['class' => 'denied', 'title' => 'Forbidden', 'detail' => 'Your role cannot trigger this procedure.'],

    'ACCOUNT_RESTRICTED' => ['class' => 'danger', 'title' => 'Suspended', 'detail' => 'Your profile has active security restrictions.'],

    'UNAUTHENTICATED' => ['class' => 'denied', 'title' => 'Login Required', 'detail' => 'Establish a verified session first.'],

    default => ['class' => 'neutral', 'title' => 'System Notice', 'detail' => 'Unrecognized gateway response.'],

};

?>

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <title>Gatekeeper Verification</title>

    <style>

        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; padding: 3rem 1rem; }

        .box { max-width: 480px; margin: 0 auto; background: #1e293b; border-radius: 12px; padding: 2rem; border: 1px solid #334155; }

        .tag { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; margin-bottom: 1rem; }

        .success { background: #065f46; color: #6ee7b7; }

        .denied { background: #831843; color: #fbcfe8; }

        .danger { background: #7f1d1d; color: #fca5a5; }

        .neutral { background: #374151; color: #d1d5db; }

        h1 { margin: 0 0 0.5rem 0; font-size: 1.5rem; }

        p { margin: 0; color: #94a3b8; font-size: 0.95rem; }

        .meta { margin-top: 1.5rem; border-top: 1px solid #334155; padding-top: 1rem; font-size: 0.85rem; color: #64748b; }

    </style>

</head>

<body>


<div class="box">

    <span class="tag <?= $feedback['class'] ?>"><?= $evaluation['code'] ?></span>

    <h1><?= $feedback['title'] ?></h1>

    <p><?= $feedback['detail'] ?></p>


    <div class="meta">

        <strong>Session User:</strong> <?= htmlspecialchars($userSession['username'], ENT_QUOTES, 'UTF-8') ?><br>

        <strong>Assigned Role:</strong> <?= htmlspecialchars($userSession['role'], ENT_QUOTES, 'UTF-8') ?><br>

        <strong>Target Action:</strong> <?= htmlspecialchars($requestedAction, ENT_QUOTES, 'UTF-8') ?>

    </div>

</div>


</body>

</html>


Run this file using php -S localhost:8000 and visit http://localhost:8000/gatekeeper.php. Modify $requestedAction or $userRole to observe how the clean guard architecture dynamically adapts.

Chapter 3 Key Takeaways

 * Short-circuiting halts condition evaluation immediately upon finding a conclusive truth/falsehood, providing defensive protection against null-pointer errors.

 * Favor guard clauses over nested if statements to avoid the Arrow Anti-Pattern and keep business logic maintainable.

 * Use null-coalescing (?? and ??=) for simple value fallbacks without throwing notices on missing keys.

 * Prefer match over switch; it operates with strict equality (===), returns values directly, and prevents fallthrough bugs.

 * Use alternative syntax (if: / endif;) when writing logic inside presentation markup to maintain clean, readable templates.

In Chapter 4, we will examine PHP's fundamental collection type: Arrays. We'll cover indexed versus associative arrays, array unpacking, and modern functional array transformation methods (array_map, array_filter, and array_reduce).