Выражения: почти всё имеет значение

В PHP выражение — это фрагмент кода, у которого есть значение. Литерал 10, переменная $price, вызов trim($name), присваивание $x = 5, сравнение $age >= 18 — всё это выражения. Управляющие конструкции используют выражения, чтобы решить, какую ветку выполнить, когда остановить цикл или какое значение вернуть.

<?php

$subtotal = 900;
$delivery = $subtotal >= 1000 ? 0 : 250;
$total = $subtotal + $delivery;

echo $total; // 1150

Здесь $subtotal >= 1000 даёт false, тернарный оператор выбирает 250, а затем арифметическое выражение считает итог. Это пересекается с \Типы и strict_types\: типы помогают описать допустимые значения, но сами условия всё равно строятся из выражений.

flowchart TD A["Выражение"] --> B{"Нужно принять булево решение?"} B -->|Да: if, while, тернарный оператор, логическое ИЛИ| C["Значение приводится к bool"] C --> D["Ветка выполняется или пропускается"] B -->|Нет: присваивание, return, ветка match| E["Исходное значение участвует дальше"] E --> F["Его можно сохранить, вернуть или сравнить"]
flowchart TD
    A["Выражение"] --> B{"Нужно принять булево решение?"}
    B -->|Да: if, while, тернарный оператор, логическое ИЛИ| C["Значение приводится к bool"]
    C --> D["Ветка выполняется или пропускается"]
    B -->|Нет: присваивание, return, ветка match| E["Исходное значение участвует дальше"]
    E --> F["Его можно сохранить, вернуть или сравнить"]
Как выражение либо превращается в условие, либо продолжает участвовать в вычислении.
Быстрое повторение
В PHP строка `$delivery = $subtotal >= 1000 ? 0 : 250;` содержит несколько выражений. Какое значение получит `$delivery`, если `$subtotal` равен `900`, и почему?

Приоритет операторов и скобки

Приоритет отвечает за то, как PHP группирует выражение. * связывается сильнее, чем +, поэтому 1 + 5 * 3 равно 16, а не 18. Скобки меняют группировку:

<?php

echo 1 + 5 * 3;   // 16
echo (1 + 5) * 3; // 18

В реальном коде скобки нужны не только машине, но и человеку. Особенно там, где смешиваются конкатенация, арифметика, ??, тернарный оператор и логические операторы.

<?php

$x = 4;
echo 'x minus one equals ' . ($x - 1); // x minus one equals 3

С PHP 8.0 у конкатенации . приоритет ниже, чем у + и -, но привычка ставить скобки вокруг вычисляемой части всё равно остаётся хорошей: она делает намерение явным.

Отдельная ловушка — and / or против && / ||. Они похожи по смыслу, но имеют другой приоритет относительно присваивания.

<?php

$a = true && false;
$b = true and false;

var_dump($a); // bool(false)
var_dump($b); // bool(true), потому что читается как ($b = true) and false

В прикладном коде обычно выбирают && и ||, а and / or оставляют для редких случаев, где низкий приоритет действительно читается лучше.

Быстрое повторение
Что выведет PHP и почему? ```php <?php echo 1 + 5 * 3; ```

Сравнение: == не то же самое, что ===

PHP различает нестрогое и строгое сравнение. == сравнивает значения с приведением типов, === требует совпадения и значения, и типа.

<?php

var_dump('10' == 10);  // bool(true)
var_dump('10' === 10); // bool(false)

Для бизнес-условий, идентификаторов, статусов и enum-подобных строк чаще выбирают ===. Нестрогое сравнение полезно реже: оно может быть удобным на грязной границе ввода, но эту границу лучше нормализовать явно. Для данных из формы см. \GET, POST и фильтрация ввода\.

Оператор <=>, «spaceship», сравнивает два значения и возвращает -1, 0 или 1: левое меньше, равно или больше правого. Он особенно удобен в сортировках.

<?php

$users = [
    ['name' => 'Ada', 'age' => 37],
    ['name' => 'Grace', 'age' => 85],
    ['name' => 'Rasmus', 'age' => 56],
];

usort($users, fn (array $a, array $b): int => $a['age'] <=> $b['age']);

print_r(array_column($users, 'name'));
// Array
// (
//     [0] => Ada
//     [1] => Rasmus
//     [2] => Grace
// )
Быстрое повторение
Почему для статусов, id и enum-подобных строк в PHP обычно выбирают `===`, а не `==`?

??, ?: и выбор значения

Null coalescing operator ?? возвращает левую часть, если она существует и не равна null; иначе возвращает правую. Он часто встречается при чтении массивов, query params и конфигов.

<?php

$page = $_GET['page'] ?? '1';

Это не то же самое, что ?:. Сокращённый тернарный оператор проверяет truthy/falsy-значение, поэтому пустая строка, 0, '0', пустой массив и false поведут себя как «нет значения».

<?php

$limit = $_GET['limit'] ?? 20; // отсутствует или null -> 20
$label = $input ?: 'Без названия'; // любое falsy -> строка по умолчанию

Если важно отличить «ключа нет» от «значение есть, но оно пустое», используйте array_key_exists() или явную проверку. Это особенно важно рядом с \Массивы как ordered map\, потому что ключи и значения в PHP-массивах имеют собственные правила приведения.

if, switch и match

if — базовая конструкция для ветвления. Выражение внутри скобок приводится к bool.

<?php

if ($userId === null) {
    throw new InvalidArgumentException('User id is required');
}

if ($role === 'admin') {
    $canEdit = true;
} elseif ($role === 'viewer') {
    $canEdit = false;
} else {
    $canEdit = false;
}

Исключение в guard-условии связано с \Ошибки, исключения и Throwable\: ветка не «возвращает false», а останавливает некорректный сценарий.

switch удобен для нескольких вариантов, но сравнение в case исторически нестрогое, и без break выполнение проваливается в следующий case.

<?php

switch ($status) {
    case 'draft':
        $label = 'Черновик';
        break;
    case 'published':
        $label = 'Опубликовано';
        break;
    default:
        $label = 'Неизвестно';
}

match появился как более строгая альтернатива для выбора значения. Он использует строгое сравнение, не проваливается между ветками и сам является выражением.

<?php

$label = match ($status) {
    'draft' => 'Черновик',
    'published' => 'Опубликовано',
    'archived' => 'Архив',
    default => 'Неизвестно',
};

Если у match нет default и ни одна ветка не подошла, PHP бросит UnhandledMatchError. Поэтому match хорошо подходит для закрытых наборов статусов, особенно когда позже появятся \Enum в PHP\.

Циклы и foreach

PHP поддерживает while, do while, for и foreach. while проверяет условие до каждой итерации, do while — после, поэтому тело выполнится хотя бы один раз.

<?php

$i = 0;
while ($i < 3) {
    echo $i . PHP_EOL;
    $i++;
}

for компактно собирает инициализацию, условие и шаг:

<?php

for ($i = 0; $i < 3; $i++) {
    echo $i . PHP_EOL;
}

foreach — основной способ обходить массивы и объекты, которые можно итерировать. Для массивов он хорошо сочетается с моделью «ordered map»: порядок обхода соответствует порядку элементов в массиве.

<?php

$prices = [
    'book' => 1200,
    'delivery' => 250,
];

foreach ($prices as $code => $amount) {
    echo $code . ': ' . $amount . PHP_EOL;
}

Можно обходить по ссылке, но это место требует дисциплины: после foreach (&$value) переменная остаётся ссылкой на последний элемент. Обычно её сразу сбрасывают через unset($value).

<?php

$items = [1, 2, 3];

foreach ($items as &$item) {
    $item *= 10;
}
unset($item);

print_r($items); // [10, 20, 30]

Для ленивого обхода больших последовательностей лучше смотреть в сторону \Генераторы и Fiber\, а для объектных итераторов — \SPL, итераторы и коллекции\.

Типичные ловушки

Первая ловушка — полагаться на нестрогое сравнение там, где нужен точный контракт. Вторая — писать слишком плотные выражения без скобок: PHP их поймёт, но следующий разработчик может прочитать иначе. Третья — забывать break в switch, если fall-through не был осознанным приёмом.

Четвёртая — использовать foreach по ссылке и потом случайно переиспользовать ту же переменную. Это редкий баг, но неприятный: код выглядит нормально, а последний элемент массива меняется не там, где ожидается.

См. также

Источники

  1. PHP Manual: Operator Precedence
  2. PHP Manual: Comparison Operators
  3. PHP Manual: match
  4. PHP Manual: foreach
  5. PHP Manual: Expressions
  6. PHP Manual: switch
  7. PHP Manual: if
  8. PHP Manual: for