Switch Default First Or Last

ID

php.switch_default_first_or_last

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

Php

Tags

code-style, control-flow, switch

Description

Reports a switch whose default clause is neither the first nor the last clause.

A switch with fewer than three clauses has no middle and is never reported. The alternative syntax (switch (…): … endswitch;) is covered the same way.

Rationale

<?php
function statusName(int $code): string
{
    switch ($code) {
        case 200:
            return 'ok';
        default:                        // FLAW — buried between the cases
            return 'other';
        case 404:
            return 'missing';
    }
}

PHP does not care where default sits, so its position carries no meaning to the engine — only to the reader, who scans the clause list for the fallback and expects it at one end. A default in the middle is routinely missed during review, and it is often the fossil of a case that was appended after it by mistake.

In PHP the readability cost comes with a correctness trap. Clauses fall through unless they break or return, and a mid-list default falls into the case written below it:

switch ($code) {
    case 200:
        $name = 'ok';
        break;
    default:
        $name = 'other';    // no break — falls into case 404
    case 404:
        $name = 'missing';  // every unmatched code ends up here
        break;
}

The fallback silently becomes "missing" for every unknown code. Nothing about the source says so at a glance, because the reader’s eye stops at the assignment inside default.

Remediation

Move default to the end of the switch — or to the very start, when the fallback is what the reader should see first — and check the break on both the clause you moved and the one it used to precede.

<?php
function statusName(int $code): string
{
    switch ($code) {
        case 200:
            return 'ok';
        case 404:
            return 'missing';
        default:
            return 'other';
    }
}

When the switch only maps a value to another value, a match expression removes the question entirely: its arms are exhaustive, they do not fall through, and default reads as an arm rather than a clause.

return match ($code) {
    200 => 'ok',
    404 => 'missing',
    default => 'other',
};

Configuration

This rule has no configurable properties. Three points are worth spelling out.

A clause with stacked labels counts by the position of the whole clause and is reported at its first label. case 201: default: is one clause sharing one body, so it holds one position in the list; PHP keeps no separate node for an individual label.

A nested switch is judged on its own. Each switch is examined independently, so an inner one with a buried default is reported whether or not the outer one has the same problem. Whether the nesting itself is a smell is `php.no_nested_switch’s finding.

match is out of scope, as it is for php.no_nested_switch. Its default arm is the same fallback idea, but an arm list is a comma-separated expression list read in one glance rather than a sequence of statement blocks, and a mid-list default in a match cannot swallow a following arm, because match has no fall-through.

A switch with no default clause at all is a different finding — see php.missing_switch_default. A clause that falls through for want of a break is `php.no_fallthrough’s.

© 2026 Xygeni. All rights reserved