Conditional Statements #

Conditional statements are the mechanism that lets a program make decisions — running different blocks of code depending on the conditions in effect at runtime. PHP provides several mechanisms for this: if/elseif/else for complex, non-linear conditions, switch for comparing a single expression against many values, and match (PHP 8.0+), which is a safer version of switch with strict comparison. What separates experienced PHP developers from beginners isn’t just mastering the syntax — it’s knowing when to use each one, how to avoid the fall-through trap in switch, and how to write clean conditions using the early return and guard clause patterns.

if — The Basic Condition #

if is the most basic form of conditional selection. The code block inside it only runs if the condition expression evaluates to true.

<?php
$stock = 15;

if ($stock > 0) {
    echo "Product available";
}

// The condition can be any expression that produces a boolean
$user = findUser(42);

if ($user !== null) {
    echo "Hello, {$user->name}!";
}

// For single-line blocks, curly braces are optional — but still use them
// ANTI-PATTERN: without braces, bugs appear when you add lines
if ($stock > 0)
    echo "available";
    echo "price: ..."; // this ALWAYS runs, it's not part of the if!

// CORRECT: always use curly braces
if ($stock > 0) {
    echo "available";
    echo "price: ..."; // this is genuinely part of the if
}

if...else — Two Branches #

else provides an alternative block that runs when the if condition evaluates to false.

<?php
$age = 17;

if ($age >= 18) {
    echo "Access granted";
} else {
    echo "Access denied — must be at least 18 years old";
}

The Early Return Pattern — Avoiding Unnecessary else #

One hallmark of clean code is avoiding unnecessary else through the early return pattern. If an if block ends with return, throw, or exit, the else block isn’t needed because execution will never reach the code after it:

<?php
// ANTI-PATTERN: unnecessary else after a return
function calculateDiscount(float $price, string $code): float
{
    if ($code === 'SALE10') {
        return $price * 0.10;
    } else {                      // this else isn't needed
        if ($code === 'SALE20') {
            return $price * 0.20;
        } else {                  // this one isn't needed either
            return 0;
        }
    }
}

// CORRECT: early return — each condition returns immediately, no else
function calculateDiscountV2(float $price, string $code): float
{
    if ($code === 'SALE10') {
        return $price * 0.10;
    }

    if ($code === 'SALE20') {
        return $price * 0.20;
    }

    return 0; // default — no discount
}

if...elseif...else — Many Branches #

To check more than two conditions in sequence, use elseif. PHP checks conditions from top to bottom and runs the first block whose condition is true, then skips the rest.

<?php
$score = 78;

if ($score >= 90) {
    $grade = 'A';
} elseif ($score >= 80) {
    $grade = 'B';
} elseif ($score >= 70) {
    $grade = 'C';
} elseif ($score >= 60) {
    $grade = 'D';
} else {
    $grade = 'E';
}

echo "Grade: $grade"; // Grade: C

The Guard Clause — Validation at the Top of a Function #

The guard clause pattern places all validation and rejection conditions at the top of a function, so the main logic stays flat and easy to read. This is the opposite of the “happy path buried in nesting” style:

<?php
// ANTI-PATTERN: deeply nested validation
function processOrder(array $order): string
{
    if (isset($order['user_id'])) {
        if ($order['user_id'] > 0) {
            if (!empty($order['items'])) {
                if (count($order['items']) <= 50) {
                    // the main logic only lives here, 4 levels deep!
                    return "Order processed successfully";
                } else {
                    return "Too many items";
                }
            } else {
                return "No items";
            }
        } else {
            return "Invalid user ID";
        }
    } else {
        return "User ID missing";
    }
}

// CORRECT: guard clauses — validation up top, happy path below, flat
function processOrderV2(array $order): string
{
    if (!isset($order['user_id'])) {
        return "User ID missing";
    }

    if ($order['user_id'] <= 0) {
        return "Invalid user ID";
    }

    if (empty($order['items'])) {
        return "No items";
    }

    if (count($order['items']) > 50) {
        return "Too many items";
    }

    // Main logic — clean, flat, easy to read
    return "Order processed successfully";
}
flowchart TD
    A[Start processOrder] --> B{user_id set?}
    B -- No --> Z1[return: User ID missing]
    B -- Yes --> C{user_id > 0?}
    C -- No --> Z2[return: Invalid user ID]
    C -- Yes --> D{items not empty?}
    D -- No --> Z3[return: No items]
    D -- Yes --> E{items <= 50?}
    E -- No --> Z4[return: Too many items]
    E -- Yes --> F[Process order...]
    F --> Z5[return: Success]

    style Z1 fill:#fee2e2
    style Z2 fill:#fee2e2
    style Z3 fill:#fee2e2
    style Z4 fill:#fee2e2
    style Z5 fill:#dcfce7

switch — One Value, Many Possibilities #

switch compares a single expression against many values. It’s more concise than a chain of if/elseif when all conditions compare the same variable.

<?php
$paymentMethod = "transfer";

switch ($paymentMethod) {
    case "transfer":
        echo "Please transfer to BCA account 1234567890";
        break;
    case "credit_card":
        echo "Enter your credit card details";
        break;
    case "digital_wallet":
    case "gopay":
    case "ovo":
        echo "Choose your digital wallet app";
        break; // one break for several cases (intentional fall-through)
    default:
        echo "Unknown payment method";
}

The Fall-Through Trap in switch #

The most dangerous behavior in switch is fall-through — execution continues to the next case if break is forgotten. PHP gives no warning for this:

<?php
$status = "active";

// ANTI-PATTERN: forgotten break — unintended fall-through
switch ($status) {
    case "active":
        echo "User active\n";
        // forgot break! execution continues to the next case
    case "pending":
        echo "Awaiting verification\n"; // this also runs!
        break;
    case "inactive":
        echo "User inactive\n";
        break;
}
// Output:
// User active
// Awaiting verification  ← not expected!

// CORRECT: every case has an explicit break
switch ($status) {
    case "active":
        echo "User active\n";
        break; // don't forget this
    case "pending":
        echo "Awaiting verification\n";
        break;
    case "inactive":
        echo "User inactive\n";
        break;
    default:
        echo "Unknown status\n";
}

switch Uses == (Loose) #

switch uses loose comparison ==, not ===. This can cause subtle bugs:

<?php
$value = 0;

switch ($value) {
    case false:   // 0 == false → true!
        echo "false";
        break;
    case null:    // never reached because the previous case matched
        echo "null";
        break;
    case 0:
        echo "zero";
        break;
}
// Output: "false" — may be surprising!

// This is one of the reasons match is safer for values
// that could be of mixed types — match uses ===

match — A Safer Switch (PHP 8.0+) #

match is an expression introduced in PHP 8.0 to replace many switch use cases. The three main differences that make it safer:

  1. Uses strict === comparison, not loose ==
  2. Is an expression — it produces a value that can be assigned directly
  3. No fall-through — each arm executes only one expression
  4. Must be exhaustive — if no arm matches and there’s no default, it throws UnhandledMatchError
<?php
$statusCode = 404;

// match as an expression — assign directly to a variable
$message = match ($statusCode) {
    200, 201 => "Success",
    301, 302 => "Redirect",
    400      => "Bad Request",
    401      => "Unauthorized",
    403      => "Forbidden",
    404      => "Not Found",
    500      => "Internal Server Error",
    default  => "Unknown Status",
};

echo $message; // "Not Found"

match vs switch — Direct Comparison #

<?php
// With switch
$type = "1"; // string "1", not integer 1

switch ($type) {
    case 1:          // "1" == 1 → true (type conversion!)
        echo "integer";
        break;
    case "1":
        echo "string"; // never reached!
        break;
}
// Output: "integer" — possibly wrong!

// With match
$result = match ($type) {
    1    => "integer", // "1" !== 1 → no match
    "1"  => "string",  // "1" === "1" → match!
    default => "other",
};
echo $result; // "string" — correct!

match Must Be Exhaustive #

If no arm matches and there’s no default, PHP throws UnhandledMatchError:

<?php
$color = "purple";

// ANTI-PATTERN: match without default, uncovered colors will error
try {
    $code = match ($color) {
        "red"   => "#FF0000",
        "green" => "#00FF00",
        "blue"  => "#0000FF",
        // no default — "purple" will throw UnhandledMatchError
    };
} catch (\UnhandledMatchError $e) {
    echo "Unknown color: $color";
}

// CORRECT: always include a default for unexpected values
$code = match ($color) {
    "red"   => "#FF0000",
    "green" => "#00FF00",
    "blue"  => "#0000FF",
    default => throw new \InvalidArgumentException("Unknown color: $color"),
};

match with Complex Conditions #

match(true) allows boolean expressions as conditions — useful as a more expressive alternative to if/elseif:

<?php
$temperature = 35; // degrees Celsius

// With regular if/elseif
if ($temperature >= 40) {
    $category = "Very Hot";
} elseif ($temperature >= 30) {
    $category = "Hot";
} elseif ($temperature >= 20) {
    $category = "Warm";
} elseif ($temperature >= 10) {
    $category = "Cool";
} else {
    $category = "Cold";
}

// With match(true) — more concise but still clear
$category = match (true) {
    $temperature >= 40 => "Very Hot",
    $temperature >= 30 => "Hot",
    $temperature >= 20 => "Warm",
    $temperature >= 10 => "Cool",
    default            => "Cold",
};

echo $category; // "Hot"

match Together with Enums (PHP 8.1+) #

match works beautifully with enum — both use === and the compiler can validate exhaustiveness:

<?php
enum OrderStatus
{
    case Pending;
    case Processing;
    case Shipped;
    case Delivered;
    case Cancelled;
}

function statusLabel(OrderStatus $status): string
{
    return match ($status) {
        OrderStatus::Pending    => "Awaiting Payment",
        OrderStatus::Processing => "Processing",
        OrderStatus::Shipped    => "In Transit",
        OrderStatus::Delivered  => "Delivered",
        OrderStatus::Cancelled  => "Cancelled",
        // No default needed because all enum cases are covered
        // PHPStan/Psalm can verify this statically
    };
}

echo statusLabel(OrderStatus::Shipped); // "In Transit"

Alternative Syntax for Templates #

PHP provides alternative syntax for if and switch that’s cleaner when used inside HTML templates — replacing { and } with : and endif/endswitch:

<!-- PHP template — alternative syntax is cleaner in HTML -->
<?php if ($user->isAdmin()): ?>
    <div class="admin-panel">
        <a href="/admin">Admin Panel</a>
    </div>
<?php elseif ($user->isEditor()): ?>
    <div class="editor-panel">
        <a href="/editor">Editor Panel</a>
    </div>
<?php else: ?>
    <div class="user-panel">
        <a href="/dashboard">Dashboard</a>
    </div>
<?php endif; ?>

<!-- Alternative switch syntax -->
<?php switch ($theme): ?>
<?php case 'dark': ?>
    <link rel="stylesheet" href="/css/dark.css">
<?php break; ?>
<?php case 'light': ?>
    <link rel="stylesheet" href="/css/light.css">
<?php break; ?>
<?php default: ?>
    <link rel="stylesheet" href="/css/default.css">
<?php endswitch; ?>

This alternative syntax is very useful in view/template files because a closing curly brace } is hard to attribute when mixed with lots of HTML, while endif and endswitch clearly show their context.


Choosing the Right Mechanism #

With three main mechanisms available, the following guide helps you pick the right one for each situation:

flowchart TD
    A{Comparing\nwhat?} --> B{Single variable\nvs many values?}
    B -- Yes --> C{PHP 8.0+\navailable?}
    C -- Yes --> D{Need strict\ntype safety?}
    D -- Yes --> E[Use match\n=== comparison\nNo fall-through]
    D -- No --> F{Intentional\nfall-through?}
    F -- Yes --> G[Use switch\nDocument the fall-through]
    F -- No --> E
    C -- No --> G
    B -- No --> H{Complex/\ndifferent conditions?}
    H -- Yes --> I[Use if/elseif/else\nGuard clause pattern]
    H -- No --> J{Only two\nbranches?}
    J -- Yes --> K{Simple\nexpression?}
    K -- Yes --> L[Use ternary ?\nOr null coalescing ??]
    K -- No --> I
    J -- No --> I
SituationBest Choice
Complex conditions with different variablesif/elseif/else
One variable compared against many values, PHP 8+match
One variable, needs intentional fall-throughswitch
Two possibilities, simple expressionTernary ?:
Default value for null/undefinedNull coalescing ??
Many simple conditions in a templateAlternative syntax if:...endif
A set of type-safe valuesmatch + enum

Common Anti-Patterns #

<?php
// ✗ Anti-pattern 1: redundant boolean conditions
$active = true;

if ($active === true) {  // ✗ comparing to true is unnecessary
    echo "active";
}

if ($active) {           // ✓ cleaner — $active is already a boolean
    echo "active";
}

// ✗ Anti-pattern 2: confusing double negation
if (!$user->isInactive()) { // ✗ not (not active) is hard to read
    echo "user active";
}

if ($user->isActive()) {     // ✓ positive method names are clearer
    echo "user active";
}

// ✗ Anti-pattern 3: deep nested ifs that could be flattened
function checkAccess(User $user, string $resource): bool
{
    if ($user->isLoggedIn()) {
        if ($user->hasPermission($resource)) {
            if (!$user->isBanned()) {
                return true;
            }
        }
    }
    return false;
}

// ✓ Flattened version with guard clauses
function checkAccessV2(User $user, string $resource): bool
{
    if (!$user->isLoggedIn())             return false;
    if (!$user->hasPermission($resource)) return false;
    if ($user->isBanned())                return false;

    return true;
}

// ✗ Anti-pattern 4: switch without default for external values
function processStatus(string $status): void
{
    switch ($status) {
        case "active":
            activate();
            break;
        case "inactive":
            deactivate();
            break;
        // no default — unexpected values are silently ignored!
    }
}

// ✓ Always handle unexpected cases
function processStatusV2(string $status): void
{
    switch ($status) {
        case "active":
            activate();
            break;
        case "inactive":
            deactivate();
            break;
        default:
            throw new \InvalidArgumentException("Unknown status: $status");
    }
}

// ✗ Anti-pattern 5: assignment inside a condition (a common typo)
$data = fetchData();

if ($data = null) {          // ✗ this ASSIGNS null to $data, always false!
    echo "data empty";
}

if ($data === null) {        // ✓ this COMPARES
    echo "data empty";
}

// To avoid this typo, use a "Yoda condition" — put the literal on the left
if (null === $data) {        // ✓ if you accidentally write =, you get a parse error
    echo "data empty";
}

Summary #

  • if/elseif/else for conditions involving different expressions or complex logic. Use the guard clause pattern — validation up top, happy path below — to avoid deep nesting.
  • Avoid unnecessary else after return, throw, or exit — the remaining code is implicitly in the other branch.
  • switch uses == (loose) — be careful when comparing values that might have different types. Always include break in every case unless fall-through is intentional and documented.
  • match (PHP 8.0+) is safer than switch: it uses ===, has no fall-through, is an expression (produces a value), and throws UnhandledMatchError when no arm matches.
  • match(true) can be used as a more expressive alternative to if/elseif for conditions that take the form of boolean expressions.
  • match + enum is the strongest combination for modeling type-safe state machines or domain logic — PHPStan and Psalm can verify exhaustiveness statically.
  • Alternative syntax (if:...endif, switch:...endswitch) is cleaner for HTML templates because endif and endswitch clearly show the closing context.
  • Always handle default in switch and match for values coming from the outside (user input, database, API) — unexpected values should produce an error, not be silently ignored.

← Previous: Operators   Next: Loops →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact