Головна відмінність не в синтаксисі, а в тому, що це різні мовні категорії. switch — оператор: він передає керування в гілку, а результат ви мусите самі кудись покласти, тому кожен case закінчується присвоєнням у тимчасову змінну і break. match, що зʼявився в PHP 8.0, — вираз: він обчислюється у значення, тому пишеться там, де очікується значення — праворуч від =, у return, в аргументі виклику, у тілі стрілочної функції, у рядковій інтерполяції через {}. Звідси й дрібна деталь синтаксису, на якій спотикаються: після закриваючої фігурної дужки match ставиться крапка з комою, бо це закінчення виразу, а не блоку.
Друга відмінність — семантика порівняння. switch порівнює тему з кожним case нежорстко, через ==, з усіма приведеннями типів; match — строго, через ===, тобто збіг має бути і за значенням, і за типом. PHP 8.0 прибрав найгидкішу пастку switch, змінивши порівняння числа з нечисловим рядком (0 == 'foo' тепер false), але сам == нікуди не подівся: '1' == 1, '1e2' == '100', '0' == false, null == false — усе це в switch досі дає збіг. Практичний наслідок видно в прикладі коду: значення '1', що прийшло з query-рядка, потрапляє в case 1 у switch і не потрапляє в гілку 1 => у match. Це не баг match, а причина №1 регресій при механічній заміні однієї конструкції на іншу — тип теми треба нормалізувати явно ((int) $page, Status::from($raw)).
Третя відмінність — поведінка на невідомому значенні. switch без відповідного case і без default мовчки не робить нічого; це зручно рівно доти, доки на цьому «нічого» випадково не почала триматися логіка. match зобовʼязаний повернути значення, тому відсутність збігу для нього — помилковий стан: інтерпретатор кидає \UnhandledMatchError. Важлива для співбесіди деталь: цей клас успадковує \Error, а не \Exception, тож catch (Exception $e) його не перехопить — потрібні catch (\UnhandledMatchError), catch (\Error) або \Throwable. Найцінніше це на enum: match по кейсах enum без default дає вичерпність, яку перевіряє статичний аналізатор на етапі CI, а рантайм страхує помилкою. Дописаний «щоб не падало» default => null вимикає обидва рівні захисту — і забутий новий кейс перетворюється з голосної помилки на тихий null, який спливе за три екрани звідси.
Решта відмінностей дрібніші, але їх теж питають. Провалювання (fallthrough) в match немає взагалі: кілька значень в одній гілці перелічуються комою (200, 201, 204 =>), а break там не пишеться і не парситься — гілка є виразом, а не блоком інструкцій. Через це ж обмеження гілка не може містити кілька інструкцій: багатокрокова логіка або витягується в метод, або лишається в switch. Для діапазонів і складених умов є ідіома match (true), де кожна гілка — булевий вираз; вона читається краще за драбину if/elseif, бо повертає значення, але порядок гілок стає критичним, а умови після першого збігу не обчислюються взагалі. Щодо продуктивності: коли всі умови — літерали одного типу (int або string), обидві конструкції компілюються в таблицю переходів (ZEND_SWITCH_LONG/ZEND_SWITCH_STRING для switch, ZEND_MATCH для match), тож обирати між ними за швидкістю немає сенсу.
Межі варто назвати чесно. match — не патерн-матчинг: у PHP немає ні деструктуризації, ні guard-умов, ні матчингу за типом (instanceof доводиться писати руками всередині match (true)). Порівняння через === для звичайних обʼєктів означає ідентичність екземпляра, а не рівність вмісту, тому два еквівалентні DateTimeImmutable у match не збігатимуться — з enum це працює лише тому, що його кейси є синглтонами. І якщо match розростається до пари десятків гілок, це вже не питання вибору між ним і switch: там просилася або мапа array<string, callable>, або поліморфізм із окремим класом на кожен випадок.
declare(strict_types=1);
$page = '1'; // усе, що приходить з HTTP, — рядок
switch ($page) { // switch порівнює через ==
case 1: // '1' == 1 → true, гілка спрацює
$bySwitch = 'перша сторінка';
break; // без break провалиться в default
default:
$bySwitch = 'інша';
}
echo $bySwitch; // 'перша сторінка'
$byMatch = match ($page) { // match порівнює через ===
1 => 'перша сторінка', // '1' !== 1 → гілка НЕ спрацює
'1' => 'перша сторінка (рядок)',
default => 'інша',
}; // match — вираз, тому крапка з комою
echo $byMatch; // 'перша сторінка (рядок)'
$status = 503;
// match повертає значення: присвоюємо одразу, тимчасова змінна не потрібна
$label = match (true) { // умови перевіряються згори вниз
$status >= 500 => 'помилка сервера',
$status >= 400 => 'помилка клієнта',
$status >= 200 => 'успіх',
default => 'невідомо',
};
echo $label; // 'помилка сервера'
try {
// default немає; кілька значень в одній гілці — через кому, без fallthrough
echo match ($status) {
200, 201, 204 => 'тіло можна кешувати',
301, 302 => 'редірект',
};
} catch (\UnhandledMatchError $e) { // це Error, а не Exception
echo $e->getMessage(); // у повідомленні — незіставлене значення
}
Одна фраза, яка закриває питання: «`switch` — оператор із `==` і провалюванням, `match` — вираз із `===` і без провалювання, а на невідоме значення `switch` мовчить, `match` кидає `UnhandledMatchError`». Далі додайте головний практичний наслідок: `match` по enum без `default` перетворює забутий новий case із тихого бага на помилку.