Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 13 · С нуля → практика

Операторы: сравнение, значения по умолчанию и вычисления

Как нулевая позиция, пустая строка и приоритет операторов создают ошибки в обычных бизнес-условиях.

Из цены и количества получить сумму заказа

В заказе две упаковки по 150 рублей и доставка 50. Оператор — знак действия над значениями. * перемножит цену и количество, + прибавит доставку, = сохранит результат. Начнём с отдельных шагов, чтобы было видно каждое число.

Новые значения возникают из известных
  1. 150 рублей за упаковку × 2 упаковки → 300 рублей за товары.
  2. 300 + 50 рублей доставки → 350 рублей к оплате.
  3. echo читает полученное 350 и добавляет подпись.
Посчитать и показать сумму с доставкойСамостоятельный пример
<?php
$unitPrice = 150;
$quantity = 2;
$delivery = 50;
$itemsTotal = $unitPrice * $quantity;
$payable = $itemsTotal + $delivery;
echo 'Товары: ', $itemsTotal, ' руб.', "\n";
echo 'Доставка: ', $delivery, ' руб.', "\n";
echo 'К оплате: ', $payable, ' руб.', "\n";
Ожидаемый результат
Товары: 300 руб.
Доставка: 50 руб.
К оплате: 350 руб.

Справа от каждого = сначала вычисляется выражение. Оно даёт одно значение; затем значение сохраняется слева. Цены намеренно целые. Для обычных сумм с копейками договор единиц требуется уточнить, например хранить 15000 копеек.

ЗаписьРезультат и применение
350 - 20330: уменьшить итог на фиксированную скидку.
300 / 2150: разделить сумму поровну; / не всегда возвращает int.
7 % 31: из 7 предметов составлены две полные группы по 3, один предмет остался.
2 ** 38: степень, например удвоение три раза.
'Заказ ' . 42Строка Заказ 42: точка соединяет текстовые представления, а не складывает числа.

Умножение связывается сильнее сложения: 150 * 2 + 50 даёт 350. Скобки меняют группировку. Не полагайтесь на догадку читателя в длинном расчёте: дайте промежуточным величинам имена или поставьте скобки.

Сравнение даёт ответ, а выбор превращает его в действие

Есть 3 упаковки, покупатель просит 2. Сравнение $stock >= $quantity отвечает, достаточно ли остатка: получится bool true. Условие if использует этот ответ, чтобы выбрать сообщение. && означает «выполнены оба условия»: сначала убедимся, что количество положительное, затем сравним с остатком.

Разрешить заказ только при подходящем количествеСамостоятельный пример
<?php
$stock = 3;
$quantity = 2;
$canOrder = $quantity > 0 && $stock >= $quantity;
if ($canOrder) {
    echo 'Можно заказать: ', $quantity, ' уп.', "\n";
} else {
    echo "Проверьте количество и остаток\n";
}
Ожидаемый результат
Можно заказать: 2 уп.

2 > 0 даёт true; 3 >= 2 тоже true; && даёт true. В canOrder хранится логический ответ, а человеку показано понятное действие. Если quantity станет 4, сработает вторая ветвь. Входы здесь заранее целые: && не заменяет проверку типа внешней формы.

Один знак = записывает значение. Два == сравнивают по правилам возможных преобразований, три === проверяют совпадение значения и типа. Для кодов и идентификаторов сначала определите тип, затем используйте подходящее сравнение. Проверка 2 === '2' даёт false: справа строка.

Разные требования дают разные скобкиСамостоятельный пример
<?php
$unitPrice = 150;
$quantity = 2;
$discountPerItem = 20;
$discountPerOrder = 20;
$byItem = ($unitPrice - $discountPerItem) * $quantity;
$byOrder = $unitPrice * $quantity - $discountPerOrder;
echo 'Скидка с каждой упаковки: ', $byItem, ' руб.', "\n";
echo 'Скидка один раз с заказа: ', $byOrder, ' руб.', "\n";
Ожидаемый результат
Скидка с каждой упаковки: 260 руб.
Скидка один раз с заказа: 280 руб.

Скобки в первой формуле объединяют цену и скидку до умножения: 130 × 2. Во второй сначала получаем 300, затем вычитаем 20 один раз. Правильность выбирается по требованию магазина, а не по тому, какая формула короче.

Далее разобраны все группы операторов и их крайние случаи. Битовые маски, операции над массивами, instanceof и конвейер требуют соответствующих типов и функций; у них есть свои последующие пояснения. Предварительно не используйте @ для сокрытия причины неудачи и обратные кавычки для обычного текста.

Выражение даёт значение, инструкция организует действие

Для чека нужно рассчитать две пачки чая по 150 рублей и добавить доставку 40. Выражение — запись, у которой PHP получает значение. Даже число 150 или чтение $price уже являются выражениями. Умножение соединяет два значения и получает третье. Само вычисление ещё не показывает покупателю результат: его нужно сохранить или передать в вывод.

ЗаписьПолученное значение при price=150, quantity=2
150Целое число 150, буквально записанное в коде.
$priceТекущее значение переменной: 150. Чтение его не меняет.
$quantity * $priceПроизведение 2×150: целое 300.
($quantity * $price) + 40300+40: целое 340. Скобки показывают группировку.
$total = 340;Инструкция присваивания: сохраняет 340. Само выражение присваивания до ; также имеет значение 340.
echo $total;Инструкция вывода использует значение; echo не возвращает сумму для дальнейшего расчёта.
Одни данные дают промежуточный и конечный результат
  1. quantity=2, price=150 → произведение 300.
  2. subtotal=300; добавляем доставку 40 → 340.
  3. total=340 → строка «К оплате: 340 руб.».
Рассчитать и показать каждую часть чекаСамостоятельный пример
<?php
$quantity = 2;
$price = 150;
$delivery = 40;
$subtotal = $quantity * $price;
$total = $subtotal + $delivery;
echo 'Товар: ', $quantity, ' × ', $price, ' = ', $subtotal, ' руб.', "\n";
echo 'Доставка: ', $delivery, ' руб.', "\n";
echo 'К оплате: ', $total, ' руб.', "\n";
Ожидаемый результат
Товар: 2 × 150 = 300 руб.
Доставка: 40 руб.
К оплате: 340 руб.

Сначала справа от каждого = вычисляется значение, затем сохраняется слева. Subtotal получает 300, total получает 340. Echo читает эти числа и подписи. Запись $total=($quantity*$price)+$delivery дала бы тот же итог, но отдельный subtotal полезен как самостоятельная строка чека и при проверке расчёта. Цены в этом примере заранее заданы целыми рублями.

Многие выражения становятся инструкцией после ;, например $total = 340;. Условная инструкция if организует выполнение, а её условие внутри круглых скобок — выражение. Поэтому нельзя подставить произвольную инструкцию туда, где требуется значение. В описаниях синтаксиса слово expr обозначает допустимое выражение, а не имя переменной, которое нужно скопировать.

Присваивание тоже отдаёт значение: читаем цепочку =

В чужом коде можно встретить $original = $current = 5;. Здесь два присваивания связаны справа: сначала current получает 5; значение этого присваивания тоже 5 и попадает в original. Это правило связывания оператора, а не общее обещание вычислять весь PHP-код справа налево. Для начала две отдельные строки часто читаются проще.

Сохранить первоначальный запас и уменьшить текущийСамостоятельный пример
<?php
$original = $current = 5;
$current -= 2;
echo 'Было на складе: ', $original, "\n";
echo 'Зарезервировано: ', $original - $current, "\n";
echo 'Доступно сейчас: ', $current, "\n";
Ожидаемый результат
Было на складе: 5
Зарезервировано: 2
Доступно сейчас: 3

Присваивание current=5 отдаёт число 5 в original. Затем -=2 сохраняет 3 только в current; original остаётся 5. Это независимые целочисленные значения, связи по ссылке нет. Разница 5−3=2 используется в строке резерва. Понятный развёрнутый эквивалент начала: $current=5; затем $original=$current;.

Похожая записьРазница
$a = $bПрисвоить текущее значение, само выражение получает присвоенное значение.
$a == $bСравнить значения по правилам нестрогого равенства; результат bool.
$a === $bПроверить значение и тип; результат bool.
$a =& $bСоздать связь имён; последующие изменения могут быть общими. Это отдельная тема ссылок.

Ошибка if ($stock = 3) меняет stock на 3 и проверяет истинность этого числа. Для проверки используйте if ($stock === 3). Синтаксически допустимое выражение не обязательно соответствует задаче.

+= меняет накопитель и возвращает обновлённую сумму

К корзине добавили ещё один товар. Оператор += складывает старое значение с новым и записывает итог в ту же переменную. Когда результат самого выражения сохраняется дополнительно, это итоговая сумма, а не цена добавленного товара.

Сохранить сумму на момент выдачи чекаСамостоятельный пример
<?php
$cartTotal = 300;
$receiptTotal = ($cartTotal += 40);
echo 'Сумма выданного чека: ', $receiptTotal, ' руб.', "\n";
$cartTotal += 100;
echo 'Корзина после нового товара: ', $cartTotal, ' руб.', "\n";
echo 'Прежний чек по-прежнему: ', $receiptTotal, ' руб.', "\n";
Ожидаемый результат
Сумма выданного чека: 340 руб.
Корзина после нового товара: 440 руб.
Прежний чек по-прежнему: 340 руб.

В скобках += читает 300, добавляет 40, сохраняет 340 в cartTotal и отдаёт 340 внешнему присваиванию receiptTotal. Позднее cartTotal меняется на 440, снимок receiptTotal остаётся 340. Так видно и изменение состояния, и возвращённое значение. В рабочем расчёте те же действия можно записать двумя строками: $cartTotal+=40; $receiptTotal=$cartTotal;.

Сходное назначение у -=, *=, /=, %=, **=, .= и битовых составных присваиваний; допустимые типы и смысл результата определяет конкретный оператор. Например .= накапливает строку, а /= может дать float. Полная группа разобрана ниже у операторов присваивания; сокращение не отменяет правил типов и ошибок.

Вызов функции — выражение со значением из return

Следующая ступень требует первого вызова функции. Объявление ниже создаёт правило стоимости упаковки. Параметр boxes принимает новое количество при вызове; return отдаёт вычисленное число туда, где стоят круглые скобки вызова. Тело объявленной функции ещё не выполняется до вызова.

Подставить стоимость упаковки прямо в итог заказаСамостоятельный пример
<?php
function packingCost($boxes) {
    return $boxes * 20;
}
$goodsTotal = 300;
$packing = packingCost(2);
$orderTotal = $goodsTotal + $packing;
echo 'Упаковка: ', $packing, ' руб.', "\n";
echo 'Заказ: ', $orderTotal, ' руб.', "\n";
echo 'Тот же итог одним выражением: ', $goodsTotal + packingCost(2), ' руб.', "\n";
Ожидаемый результат
Упаковка: 40 руб.
Заказ: 340 руб.
Тот же итог одним выражением: 340 руб.

В packingCost(2) число 2 становится местным boxes. Умножение даёт 40, return завершает вызов и отдаёт 40 в выражение справа от =, затем packing получает 40. В последнем echo второй самостоятельный вызов возвращает ещё одно 40 прямо в сложение с 300. Вся сумма 340 поступает в вывод. Это два вызова с одинаковыми входами и результатом; первый результат сам по себе повторно не используется во втором вызове.

От точного места вызова к продолжению
  1. packing = packingCost(2): присваивание ожидает число.
  2. Местный boxes=2 → boxes*20=40 → return 40.
  3. Присваивание продолжает работу: packing=40.
  4. orderTotal = 300+40 → 340; echo читает итог.

Если функция только печатает через echo, её вывод не становится возвращённой строкой. Обычная функция без return возвращает null; функция с void описывает отсутствие полезного возвращаемого значения, а never вообще не возвращает управление обычным путём. Эти случаи отдельно показаны в разборе return и побочного действия.

++ меняет счётчик, а выражение передаёт старое или новое число

У постфиксного $n++ значение выражения равно прежнему n; у префиксного ++$n — уже увеличенному. Изменение самой переменной происходит в обоих случаях. Сначала используйте целые счётчики; поведение строк, bool, null и ограничений типа не переносится с этого примера автоматически.

Напечатать текущий номер, затем номер после увеличенияСамостоятельный пример
<?php
function parcelLabel($number) {
    return 'Посылка №' . $number;
}
$nextNumber = 10;
$firstLabel = parcelLabel($nextNumber++);
echo $firstLabel, "\n";
echo 'Следующий номер после вызова: ', $nextNumber, "\n";
$nextNumber = 10;
$secondLabel = parcelLabel(++$nextNumber);
echo $secondLabel, "\n";
echo 'Номер после второго вызова: ', $nextNumber, "\n";
Ожидаемый результат
Посылка №10
Следующий номер после вызова: 11
Посылка №11
Номер после второго вызова: 11

В первом вызове аргумент выражения nextNumber++ равен 10: местный number получает 10, return возвращает строку в firstLabel. Внешний счётчик уже увеличен до 11. Затем мы намеренно начинаем независимое сравнение снова с 10; ++nextNumber даёт 11 и параметр второго вызова получает 11. Это не две последовательные выдачи номеров одного склада, а сравнение двух форм на одинаковом старте. Для последовательной выдачи 10 и 11 используйте два вызова с постфиксной формой без сброса.

У -- соответствующее правило для уменьшения. Вынос счётчика в отдельные строки делает изменение особенно заметным: сохраните текущий номер, увеличьте счётчик, затем передайте сохранённое число. Не пытайтесь предсказывать $n + $n++ или запись в массив по ключу, который одновременно меняется: общий порядок вычисления таких частей не гарантирован.

Условное выражение выбирает одно готовое значение

Сравнение возвращает bool. Полный оператор условие ? значениеПриДа : значениеПриНет вычисляет только выбранную ветку и отдаёт её значение. Ни знак ?, ни двоеточие сами ничего не печатают. Если веткам нужны несколько действий, используйте if/else.

Рассчитать доставку и использовать выбранную ценуСамостоятельный пример
<?php
$goodsTotal = 1200;
$freeDeliveryFrom = 1000;
$delivery = $goodsTotal >= $freeDeliveryFrom ? 0 : 150;
$total = $goodsTotal + $delivery;
echo 'Товары: ', $goodsTotal, ' руб.', "\n";
echo 'Доставка: ', $delivery, ' руб.', "\n";
echo 'К оплате: ', $total, ' руб.', "\n";
Ожидаемый результат
Товары: 1200 руб.
Доставка: 0 руб.
К оплате: 1200 руб.

Сравнение 1200>=1000 возвращает true. Выбирается выражение 0 после ?, и присваивание сохраняет 0 в delivery. Сложение использует это число: 1200+0=1200. Если заменить goodsTotal на 800, условие станет false, delivery будет 150, итог — 950. Нулевую выбранную цену нельзя затем заменить через ?: на 150: это снова сделало бы бесплатную доставку платной.

Истинность выраженияЧто учитывается в условии
false, null, числа 0 и 0.0, строки '' и '0', пустой массивПреобразуются в false; отсутствие вывода bool при echo не означает отсутствие результата.
Большинство остальных значений, включая строку 'false' и непустой массивПреобразуются в true. Содержимое строки не разбирается как слово true/false.
ОбъектОбычно true; отдельные внутренние классы имеют специальные правила. Для доменного решения проверяйте нужное свойство явно.

Сравнения <, >, <=, >=, ==, !=, <>, === и !== дают логическое решение; у <=> другой контракт: отрицательное число, ноль или положительное число. Полная таблица и правила преобразования находятся в типах и логических значениях. Непустая строка статуса не равна проверке конкретного разрешённого статуса.

Значением выражения бывает целый массив

Результат выражения не обязан быть одним числом. Строка, bool и null тоже значения; запись массива создаёт набор ключей и значений. Здесь элементы массива сами вычисляются. После первых массивов этот приём помогает собрать результат расчёта в одной структуре.

Собрать строку заказа из вычисленных значенийСамостоятельный пример
<?php
$quantity = 2;
$unitPrice = 150;
$line = [
    'title' => 'Чай',
    'quantity' => $quantity,
    'total' => $quantity * $unitPrice,
];
echo $line['title'], ': ', $line['quantity'], ' шт., ', $line['total'], ' руб.', "\n";
Ожидаемый результат
Чай: 2 шт., 300 руб.

Квадратные скобки создают массив; => связывает ключ с вычисленным значением. Quantity получает прочитанное 2, total — результат 2×150=300. Само выражение массива становится значением line. В echo три обращения по ключам получают строку Чай и числа 2/300. Изменение внешнего quantity после построения не пересчитает line автоматически. New создаёт объект как значение выражения; это следующая отдельная ступень после массивов в главе объектов.

Для чтения длинной записи ищите внешний потребитель результата: присваивание, аргумент, return, условие или echo. Затем разберите внутренние выражения и их значения. Приоритет определяет группировку операций, а не универсальный порядок побочных действий. Промежуточные переменные помогают увидеть шаги; разворачивайте неоднозначную запись, сохраняя её смысл и порядок нужных действий.

Как читать знак: операнды, результат и побочный эффект

В 12 + 3 два входных значения называются операндами, + — оператором, а всё выражение даёт 15. Унарному оператору нужен один операнд (-$price), бинарному — два ($price - $discount), условному ?: — три части: условие и два варианта. Одинаковый знак может иметь разный смысл: + складывает числа, а два массива объединяет по ключам.

Получение значения и действие над состоянием — разные стороны выражения. $stock -= 2 вычисляет новый остаток и записывает его в переменную. echo отправляет значения в вывод, а return завершает текущую функцию и отдаёт результат вызывающему коду. Ни echo, ни return не являются арифметическими операторами; попытка записать $x = echo 'да'; не даёт «результат печати».

Если нужно восстановить самые первые шаги чтения выражения, переходите к значению выражения и инструкции. Дальше уточняем правила уже знакомых операторов: группировку, тип результата и ошибку на неподходящем входе.

Приоритет, ассоциативность и порядок выполнения — три разных вопроса

Приоритет отвечает, какие части связываются сильнее. Ассоциативность разрешает цепочку операторов одного уровня: слева (30 - 8) - 2, справа 2 ** (3 ** 2). Это правила группировки исходника. Они сами по себе не обещают, какой независимый вызов функции произойдёт первым.

Как читается 2 ** 3 ** 2
  1. Оба знака ** имеют один приоритет
  2. Степень группируется справа: 2 ** (3 ** 2)
  3. Внутренняя степень даёт 9
  4. Внешняя степень даёт 512
Скобки показывают, какая операция получает какой результатСамостоятельный пример
<?php
echo 30 - 8 - 2, "\n";
echo 30 - (8 - 2), "\n";
echo 2 ** 3 ** 2, "\n";
echo (2 ** 3) ** 2, "\n";
echo -3 ** 2, "\n";
echo (-3) ** 2, "\n";
Ожидаемый результат
20
24
512
64
-9
9

Степень связывается сильнее унарного минуса, поэтому -3 ** 2 означает -(3 ** 2). В математическом условии задачи обязательно уточняйте, возводится в степень отрицательное число или меняется знак результата.

Неассоциативные операции одного уровня нельзя склеить без явной структуры. Проверка «количество между 1 и 10» записывается $n >= 1 && $n <= 10, а не 1 <= $n <= 10. Полные вложенные тернарные выражения требуют скобок в PHP 8.

Неверно: математическая цепочка сравненийФрагмент или команда
<?php
$quantity = 4;
var_dump(1 <= $quantity <= 10);

Намеренно неверный синтаксис: unexpected token "<=". Разделите проверки оператором &&. Одних скобок вокруг уже полученного bool недостаточно для проверки диапазона.

Неопределённый порядок нельзя превратить в договор программыФрагмент или команда
<?php
$position = 2;
$result = $position + $position++;
// Не закладывайте конкретный результат в бизнес-правило.

Это синтаксически допустимый, но намеренно неисполняемый образец неудачной конструкции. Приоритет не фиксирует общий порядок чтения $position и побочного изменения. Для точного поведения сначала сохраните старое значение, затем увеличьте счётчик, затем сложите выбранные значения.

Короткое замыкание &&/||, выбор одной ветки ?:/?? и последовательность pipe задают дополнительные правила вычисления. Они не превращают все прочие выражения в вычисляемые слева направо. Порядок вычисления аргументов разобран отдельно.

Две последовательные этикетки без спорного порядка побочных действийСамостоятельный пример
<?php
$nextNumber = 20;
$firstLabel = $nextNumber;
$nextNumber++;
$secondLabel = $nextNumber;
$nextNumber++;
echo 'Первая этикетка: ', $firstLabel, "\n";
echo 'Вторая этикетка: ', $secondLabel, "\n";
echo 'Следующий свободный номер: ', $nextNumber, "\n";
Ожидаемый результат
Первая этикетка: 20
Вторая этикетка: 21
Следующий свободный номер: 22

Сначала сохраняем 20 в firstLabel, потом увеличиваем nextNumber до 21. Затем сохраняем 21 во secondLabel и увеличиваем счётчик до 22. Каждая строка завершается до следующей, поэтому чтение номера не смешано с его изменением внутри одного арифметического выражения. Нумерация действует в одном процессе; параллельные запросы требуют общего хранилища и отдельной гарантии уникальности.

Таблица группировки для PHP 8: от сильного связывания к слабому

В одной строке таблицы — один уровень, верхний связывается сильнее нижнего. «Слева/справа» описывает группировку цепочки, «нет» запрещает неоднозначную цепочку, «—» означает, что обычная бинарная ассоциативность здесь неприменима. Специальные конструкции подчиняются ещё и собственной грамматике.

Операторы/конструкцииГруппировкаНазначение
new, clone—Создание и клонирование объектов
**справаСтепень
унарные + −, ++ --, ~, приведения типов, @—Унарные действия и приведения
instanceofслеваПринадлежность объекта типу
!—Логическое отрицание
* / %слеваУмножение, деление, остаток
+ −слеваСложение/вычитание, объединение массивов
<< >>слеваБитовые сдвиги
.слеваСцепление строк; положение для PHP 8+
|>слеваКонвейер, только PHP 8.5+
< <= > >=нетОтношения порядка
== != === !== <> <=>нетРавенство и трёхстороннее сравнение
&слеваПобитовое И
^слеваПобитовое исключающее ИЛИ
|слеваПобитовое ИЛИ
&&слеваЛогическое И
||слеваЛогическое ИЛИ
??справаВыбор первого значения, отличного от null
? :нетПолный условный оператор; вложения явно группируют
= += -= *= **= /= .= %= &= |= ^= <<= >>= ??=справаПрисваивание
yield from—Передача итерации генератору
=>—Разделение ключа и значения при yield
yield—Выдача значения генератором
print—Конструкция вывода со значением результата
andслеваЛогическое И с низким приоритетом
xorслеваИстинно ровно одно
orслеваЛогическое ИЛИ с низким приоритетом
include, include_once, require, require_once—Подключение файла
fn (…) =>—Граница выражения стрелочной функции
throw—Выброс исключения

Унарные приведения в этой группе: (int), (float), (string), (array), (object), (bool). Доступ [], ->, ?->, ::, распаковка ... имеют собственный синтаксис; таблица группировки не является перечнем всех PHP-токенов. Ссылки на yield, стрелочные функции и подключение файлов ведут к их объяснениям.

С PHP 8 точка связывается слабее +/- и сдвигов; до 8.0 она была на уровне сложения/вычитания и выше сдвигов. Для переносимого читаемого расчёта всё равно пишите 'Итого: ' . ($price + $delivery).

Грамматика присваивания не равна механическому чтению таблицыСамостоятельный пример
<?php
$available = true;
var_dump(!$available = false);
var_dump($available);

$available = false;
var_dump(!$available);
Ожидаемый результат
bool(true)
bool(false)
bool(true)

Первая запись разбирается как !($available = false): слева от присваивания нужна переменная, а не результат !$available. Такой код трудно читать. Последние две строки показывают прозрачную форму с отдельным присваиванием; в прикладном коде используйте её.

+, −, /, % и **: какое число получится

Когда семнадцать товаров нужно раздать трём отделам, обычное деление отвечает о средней доле, intdiv — о полной одинаковой доле, % — о нераспределённом остатке. Ниже уточняются именно тип и границы этих действий; полезный расчёт упаковки уже разобран на семи банках.

Унарный + приводит подходящее значение к int или float; унарный - дополнительно меняет знак. Это вычисление, не проверка входных данных. Для HTTP-цены сначала проверяют формат и диапазон, затем считают.

Числовая строка и точное делениеСамостоятельный пример
<?php
var_dump(+'14');
var_dump(-'2.5');
var_dump(14 / 2);
var_dump(15 / 2);
var_dump(14.0 / 2);
var_dump('14' / '2');
Ожидаемый результат
int(14)
float(-2.5)
int(7)
float(7.5)
float(7)
int(7)

Деление двух int (включая преобразованные целочисленные числовые строки) возвращает int, если делится нацело. В остальных случаях / возвращает float. Поэтому '/' не означает «всегда дробный тип».

ЗадачаИнструментЧто учитывать
Обычное частное/Нулевой делитель даёт DivisionByZeroError.
Число полных коробокintdiv($items, $capacity)Целое частное с отсечением к нулю; intdiv(−7,2) даёт −3.
Остаток целых предметов%Операнды переводятся в int; знак остатка совпадает со знаком делимого.
Остаток длины/веса с дробьюfmod($length, $piece)Работает с float, подвержен их погрешности; при нулевом делителе NAN.
Научное вычисление с INF/NANfdiv($a, $b)Нулевой делитель допускает INF/−INF/NAN; это не исправление бизнес-ошибки.

В следующем опыте try {...} catch (ТипОшибки $error) {...} отделяет рискованное действие от ответа на его неудачу. После подходящего catch программа продолжается. Полное устройство этой конструкции находится в главе обработки ошибок.

Ноль и неподходящая строка требуют разных решенийСамостоятельный пример
<?php
try {
    $average = 120 / 0;
} catch (DivisionByZeroError $error) {
    echo "Нет элементов для среднего\n";
}
try {
    $sum = 'нет цены' + 20;
} catch (TypeError $error) {
    echo "Цена не является числом\n";
}
try {
    intdiv(PHP_INT_MIN, -1);
} catch (ArithmeticError $error) {
    echo "Частное не помещается в int\n";
}
Ожидаемый результат
Нет элементов для среднего
Цена не является числом
Частное не помещается в int

try/catch здесь делает крайние случаи видимыми. Обычно отсутствие элементов проверяют до деления и явно выбирают «нет среднего» вместо искусственного нуля. PHP_INT_MIN — минимальный int текущей платформы.

Обычные операции с int могут выйти за пределы платформы и перейти во float; это уже потеря целочисленной точности для больших идентификаторов/денег. Смотрите границы int и float. Дробь в %, сдвиге или другом int-контексте не надо молча отбрасывать: начиная с 8.1 неявная потеря дробной части может вызывать deprecation.

Остаток, цикл по кругу и распределение без потери копейки

Отрицательная позиция часто появляется при кнопке «Предыдущая» на первой карточке. Нужно получить последний товар списка, а не просто проверить знак числа. Вопрос на Stack Overflow об отрицательном modulo дал эту прикладную ситуацию; поведение % проверено по PHP Manual.

Остаток отвечает «что не вошло в полные группы». Для -11 % 4 он равен −3. Если нужен индекс от 0 до 3, отрицательный остаток надо сдвинуть в этот диапазон. Сначала ограничьте размер кольца положительным целым.

Получить реальную предыдущую карточку в кольцеСамостоятельный пример
<?php
$slides = ['Чай', 'Кофе', 'Какао', 'Вода'];
$slideCount = count($slides);
$currentIndex = 0;
$previousIndex = ($currentIndex - 1) % $slideCount;
if ($previousIndex < 0) {
    $previousIndex += $slideCount;
}
echo 'Текущая карточка: ', $slides[$currentIndex], "\n";
echo 'Предыдущая карточка: ', $slides[$previousIndex], "\n";
echo 'Её индекс: ', $previousIndex, "\n";
echo 'Знаки остатка: ', 11 % 4, ', ', 11 % -4, ', ', -11 % 4, ', ', -11 % -4, "\n";
echo 'Остаток длины 7.5 м по 2 м: ', fmod(7.5, 2.0), ' м', "\n";
Ожидаемый результат
Текущая карточка: Чай
Предыдущая карточка: Вода
Её индекс: 3
Знаки остатка: 3, 3, -3, -3
Остаток длины 7.5 м по 2 м: 1.5 м

Count возвращает число карточек 4. Позиция перед нулём даёт -1; PHP сохраняет знак делимого в остатке, поэтому добавляем длину 4 и получаем индекс 3. Он действительно используется для чтения «Вода». Пустое кольцо недопустимо: делитель обязан быть положительным. Fmod принимает два float и возвращает дробный остаток; это не целочисленный %.

Для распределения суммы нельзя отбросить остаток после intdiv. В функции ниже $kopecks — неотрицательная сумма, $parts — положительное число участников. array_fill создаёт нужное число одинаковых частей; цикл добавляет по копейке первым участникам. int перед именами параметров задаёт их тип, : array — тип результата; return отдаёт полученные части вызывающему коду. Если этот синтаксис ещё новый, сначала откройте параметры, тело и результат функции.

101 копейка распределяется на три частиСамостоятельный пример
<?php
function splitKopecks(int $kopecks, int $parts): array
{
    if ($kopecks < 0 || $parts < 1 || $parts > 1000) {
        throw new InvalidArgumentException('Неверные границы');
    }
    $base = intdiv($kopecks, $parts);
    $remainder = $kopecks % $parts;
    $shares = array_fill(0, $parts, $base);
    for ($index = 0; $index < $remainder; $index++) {
        $shares[$index] += 1;
    }
    return $shares;
}
$shares = splitKopecks(101, 3);
echo implode(', ', $shares), "\n";
echo array_sum($shares), "\n";
Ожидаемый результат
34, 34, 33
101

Сумма частей сохранена. Порядок получателей определяет, кому достанутся дополнительные копейки; это должно быть частью бизнес-правила. Лимит 1000 ограничивает память примера, а не выражает общее правило PHP. Типы в сигнатуре описаны в главе функций.

Разбор splitKopecks(101, 3) из опыта выше
  1. Параметры kopecks = 101, parts = 3. Проверка допускает неотрицательную сумму и от 1 до 1000 частей.
  2. Intdiv возвращает base = 33; остаток remainder = 2.
  3. Array_fill(0, 3, 33) создаёт [33, 33, 33]: начальный ключ 0, три элемента, значение каждого 33.
  4. For посещает только индексы 0 и 1, добавляя по копейке: получается [34, 34, 33].
  5. Return отдаёт весь массив во внешнюю переменную shares. Implode соединяет его значения для вывода, array_sum считает контрольную сумму 101.

Степень **: рост величины, знак и неконечный результат

$base ** $exponent возводит основание в степень. Степень 0 даёт 1 для обычных конечных оснований; отрицательная степень даёт обратную величину. Символ ^ в PHP означает побитовое исключающее ИЛИ, а не возведение в степень.

Простые степени и ограниченное увеличение интервалаСамостоятельный пример
<?php
var_dump(3 ** 0);
var_dump(3 ** 3);
var_dump(2 ** -3);

$attempt = 4;
$baseSeconds = 2;
$delay = min(30, $baseSeconds * (2 ** ($attempt - 1)));
echo 'Пауза: ', $delay, "\n";
Ожидаемый результат
int(1)
int(27)
float(0.125)
Пауза: 16

Здесь только вычисляется пауза, sleep не вызывается. В реальном retry ограничивают число попыток и общее время, учитывают Retry-After и при необходимости jitter. Само min после вычисления не защищает от чрезмерной исходной степени: сначала ограничьте attempt.

Не использовать бесконечную площадь как нормальный расчётСамостоятельный пример
<?php
$side = 1.0e200;
$area = $side ** 2;
if (!is_finite($area)) {
    echo "Площадь не рассчитана: результат вне конечного диапазона\n";
}
$root = (-4.0) ** 0.5;
if (is_nan($root)) {
    echo "Корень не рассчитан: нет действительного результата\n";
}
Ожидаемый результат
Площадь не рассчитана: результат вне конечного диапазона
Корень не рассчитан: нет действительного результата

Степень side ** 2 переполнила диапазон float и дала INF. Is_finite возвращает false; отрицание ! выбирает сообщение о невозможном результате. Отрицательное основание в степени 0.5 даёт NAN: PHP не создаёт здесь комплексное число. Прикладной код решает, вернуть ошибку или запросить другой вход, прежде чем использовать результат в тарифе или отчёте.

++ и +=: счётчик попыток и накопление результата

Чем отличаются постфиксный и префиксный счётчикиСамостоятельный пример
<?php
$attempt = 1;
$shown = $attempt++;
echo 'Показали ', $shown, ', следующее ', $attempt, "\n";
$shown = ++$attempt;
echo 'Показали ', $shown, ', текущее ', $attempt, "\n";
$attempt--;
echo $attempt, "\n";
Ожидаемый результат
Показали 1, следующее 2
Показали 3, текущее 3
2

n++ возвращает прежнее значение выражения и увеличивает переменную; ++n возвращает уже увеличенное. В отдельной строке без использования результата они одинаково увеличивают счётчик. Аналогично работают --n и n--. Для понятного номера попытки зачастую лучше отдельное присваивание и отдельный ++.

Накопление суммы и сборка небольшой подписиСамостоятельный пример
<?php
$total = 0;
$label = 'Позиции: ';
foreach (['чай' => 150, 'кофе' => 200] as $name => $price) {
    $total += $price;
    $label .= $name . '; ';
}
echo $label, "\n", $total, "\n";
Ожидаемый результат
Позиции: чай; кофе; 
350

+= — прибавить и сохранить, .= — дописать строку. Есть -=, *=, /=, %=, **= и операции над битами. Для длинного списка подписей часто проще накопить массив и вызвать implode; большая выгрузка требует последовательной записи, чтобы не держать весь результат в памяти.

Не используйте ++ для генерации артикулов из букв: увеличение нечисловых строк устаревает (soft deprecation8.3, предупреждение deprecation8.5). Для требуемой буквенно-цифровой последовательности существует str_increment(PHP 8.3+); для ID нужен отдельный договор уникальности. Bool и null также не должны случайно стать вашим счётчиком.

Счётчик должен оставаться числом

Число отделено от оформления
  1. lastCode содержит строку009
  2. Копия number после ++ → int10
  3. str_pad строки10 до трёх знаков → строка010
  4. printed → подпись «Заказ №010»

Числовая строка может превратиться в число при ++; у bool, null и нечисловой строки свои исторические правила. Это причина проверять тип счётчика, а не способ конвертировать любые данные.

Число и его оформление разделяют. Сначала увидим потерю ведущих нулей у числовой строки при ++, затем восстановим выбранную ширину только для подписи. Str_pad получает строку, требуемую длину, заполнитель и сторону; возвращает новую строку, счётчик остаётся числом.

Следующий номер хранится числом, а печатается тремя цифрамиСамостоятельный пример
<?php
$lastCode = '009';
$number = $lastCode;
$number++;
$printed = str_pad((string) $number, 3, '0', STR_PAD_LEFT);
echo 'Исходная строка: ', $lastCode, "\n";
echo 'Следующее значение: ', $number, ' (', get_debug_type($number), ')', "\n";
echo 'Этикетка: Заказ №', $printed, "\n";
Ожидаемый результат
Исходная строка: 009
Следующее значение: 10 (int)
Этикетка: Заказ №010

Копия строки 009 при ++ стала int 10; исходная lastCode осталась строкой 009. Приведение (string) готовит "10" для форматирования; str_pad добавляет один ноль слева и возвращает 010 в printed. Это только оформление номера в одном процессе, уникальности между параллельными запросами здесь нет. В новом счётчике обычно сразу храните int, а строку формируйте при показе.

Получить значение до и после снятия единицы запасаСамостоятельный пример
<?php
$remaining = 3;
$afterFirst = --$remaining;
echo 'После первого снятия: ', $afterFirst, ' шт.', "\n";
$beforeSecond = $remaining--;
echo 'Перед вторым снятием было: ', $beforeSecond, ' шт.', "\n";
echo 'Теперь осталось: ', $remaining, ' шт.', "\n";
Ожидаемый результат
После первого снятия: 2 шт.
Перед вторым снятием было: 2 шт.
Теперь осталось: 1 шт.

Префиксное -- сначала сохраняет 2 в remaining, а значение выражения 2 попадает в afterFirst. Постфиксное -- отдаёт прежнее 2 в beforeSecond и уменьшает remaining до 1. Это два последовательных снятия из известного запаса 3; в реальном резервировании отдельно запрещают отрицательный остаток и согласуют параллельные изменения.

Тип/случайПоведение и версия
bool при ++/--Не меняется; с PHP8.3 E_WARNING. Не используйте как счётчик.
null при ++Получается int 1; это не доказательство корректной инициализации.
null при --Не меняется; с PHP8.3 E_WARNING.
Нечисловая строка при --Не меняется; с PHP8.3 E_WARNING.
ASCII буквенно-цифровая строка при ++Историческое увеличение с переносом букв; soft deprecation8.3, E_DEPRECATED8.5. Альтернатива str_increment().
Специализированный внутренний объектНекоторые расширения, например GMP, определяют арифметику; обычный пользовательский объект автоматически её не получает.
Буквенная последовательность без изменения типа на числовойСамостоятельный пример
<?php
// Только PHP 8.3+.
$code = 'R9';
$nextCode = str_increment($code);
echo $code, ' -> ', $nextCode, "\n";
echo str_increment('4e0'), "\n";
Ожидаемый результат
R9 -> S0
4e1

str_increment(string): string обрабатывает буквенно-цифровой ASCII-текст; пустая строка и другие символы дают ValueError. Строка 4e0 не становится float, в отличие от ++ над числовой строкой. Это преобразование кода, не генератор безопасных токенов и не счётчик заказов в базе.

Присваивание возвращает значение; ссылка связывает имена

Снимок не меняется вместе с остатком
  1. available=20 → результат присваивания20 → initial20
  2. available-=5 → available15
  3. initial20 − available15 → занято5
  4. available=7 → результат7 → уточнённый вывод

Левая сторона = должна допускать запись: переменная, элемент массива или доступное свойство. Значение всего присваивания — записанное значение. Цепочка группируется справа; это не означает, что все переменные навсегда связались.

Сохранить исходную вместимость и текущий свободный остатокСамостоятельный пример
<?php
$initial = $available = 20;
$available -= 5;
echo 'Вместимость: ', $initial, ' мест', "\n";
echo 'Занято: ', $initial - $available, ' мест', "\n";
echo 'Свободно: ', $available, ' мест', "\n";
echo 'После уточнения свободно: ', ($available = 7), ' мест', "\n";
Ожидаемый результат
Вместимость: 20 мест
Занято: 5 мест
Свободно: 15 мест
После уточнения свободно: 7 мест

В available сначала записано 20; значение этого присваивания 20 поступает в initial. -=5 меняет только available, разница равна 5. Последнее присваивание внутри скобок сохраняет 7 и отдаёт это же 7 в echo; это не сравнение. В обычной прикладной записи чаще яснее сначала отдельно обновить available, затем вывести его. Левая сторона = должна допускать запись; литералу вроде 20 присвоить значение нельзя.

Комбинированные присваивания: += -= *= /= %= **= для арифметики, .= для текста, &= |= ^= <<= >>= для битов и ??= для значения по умолчанию. += над двумя массивами — объединение, а не сумма элементов. Левая часть комбинированного присваивания вычисляется как место записи один раз; не копируйте сложное выражение с побочным эффектом дважды.

Отличить снимок склада от второго имени того же остаткаСамостоятельный пример
<?php
$stock = 10;
$snapshot = $stock;
$alias =& $stock;
$alias -= 2;
echo 'Склад после резерва: ', $stock, ' шт.', "\n";
echo 'Снимок до резерва: ', $snapshot, ' шт.', "\n";
unset($alias);
$stock = 3;
echo 'Склад после сверки: ', $stock, ' шт.', "\n";
echo 'Старый снимок: ', $snapshot, ' шт.', "\n";
Ожидаемый результат
Склад после резерва: 8 шт.
Снимок до резерва: 10 шт.
Склад после сверки: 3 шт.
Старый снимок: 10 шт.

Snapshot получает независимое целое 10. =& делает alias вторым именем значения stock; вычитание через alias меняет stock на 8, что видно в отчёте. Unset убирает только имя alias, stock остаётся доступен. Последующая сверка ставит stock=3, старый snapshot всё ещё 10. Амперсанд здесь создаёт связь переменных и не является побитовым И. Обычное присваивание объектного значения имеет другой договор, описанный ниже.

Для объекта обычное $b = $a копирует идентификатор того же объекта, а не создаёт PHP-ссылку & между переменными. Изменение свойства видно через оба имени; переназначение $b другому объекту не переназначает $a. Отдельную копию делают через clone, а особенности ссылок разобраны в отдельной главе. Старинная запись $b = &new SomeClass; недопустима.

Точка и .=: сначала текст, затем вывод

$a . $b создаёт сцепленную строку, а $a .= $b дописывает правую часть в $a. Пробел автоматически не появляется: добавьте его в одну из строк. echo $a, $b — вывод двух выражений через запятую, а не сохранённая строка.

Подпись сохранена отдельно от её выводаСамостоятельный пример
<?php
$label = 'Заказ';
$full = $label . ' №' . 41;
echo $full, "\n";
echo $label, "\n";
$label .= ' принят';
echo $label, "\n";
Ожидаемый результат
Заказ №41
Заказ
Заказ принят

Обычная точка не изменила label. .= изменила. Число 41 преобразовано для текстового представления; массив так собирать не следует, а bool false/null могут незаметно превратиться в пустую строку.

В HTML склейка не экранирует данные, а в SQL не создаёт безопасные параметры. Применяйте экранирование в контексте вывода и подготовленные запросы. Для списка подписей удобно implode; для большой выгрузки — последовательная запись. Не обещайте ускорение по одному лишь выбору '.' или '.='.

&&, || и !: проверяем данные до опасного действия

Допуск к проверке склада
  1. item=null → сравнение с null ложно
  2. hasStock не вызвана → сообщения функции нет
  3. item=[Чай, stock3] → hasStock получает эту запись
  4. return true → условие if истинно → карточка доступного товара

Пока товара нет, функцию проверки его остатка вызывать нельзя: её параметр требует массив. Левое условие проверяет наличие, правое — доступность количества. && вычисляет правую часть только после true слева. В учебном массиве поля title и stock уже проверены; одна проверка на null не заменяет проверку всей структуры.

Показать решение для отсутствующего и найденного товараСамостоятельный пример
<?php
function hasStock(array $item): bool {
    echo 'Проверяем остаток: ', $item['title'], "\n";
    return $item['stock'] > 0;
}
$item = null;
if ($item !== null && hasStock($item)) {
    echo "Первый поиск: товар доступен\n";
} else {
    echo "Первый поиск: доступного товара нет\n";
}
$item = ['title' => 'Чай', 'stock' => 3];
if ($item !== null && hasStock($item)) {
    echo 'Можно заказать: ', $item['title'], ', остаток ', $item['stock'], ' шт.', "\n";
}
Ожидаемый результат
Первый поиск: доступного товара нет
Проверяем остаток: Чай
Можно заказать: Чай, остаток 3 шт.

При null левое сравнение ложно, hasStock не вызвана: нет её сообщения и нет передачи null в параметр array. При найденной записи слева true; функция получает массив в местный item, показывает название и возвращает true из сравнения 3>0. Этот return становится правым операндом &&, итог true выбирает сообщение о доступности. У || достаточно true слева, чтобы не вызывать правую часть; ! меняет логический результат на противоположный.

Увидеть, как неверная группировка разрешает неоплаченный заказСамостоятельный пример
<?php
$accountActive = true;
$paid = false;
$ready = $accountActive && $paid;
$wrong = $accountActive and $paid;
$explicit = ($accountActive and $paid);
echo 'Через &&: ', $ready ? 'можно выдать' : 'ожидаем оплату', "\n";
echo 'Через and без скобок: ', $wrong ? 'ошибочно разрешили выдачу' : 'ожидаем оплату', "\n";
echo 'Через and со скобками: ', $explicit ? 'можно выдать' : 'ожидаем оплату', "\n";
Ожидаемый результат
Через &&: ожидаем оплату
Через and без скобок: ошибочно разрешили выдачу
Через and со скобками: ожидаем оплату

&& связывается сильнее =: ready получает результат true&&false, то есть false. And связывается слабее =: в wrong сначала записывается accountActive=true; дальнейшее and false уже не меняет wrong. Скобки в третьем варианте включают обе проверки в присваиваемое выражение, поэтому explicit=false. Код показывает три учебных решения, реальной выдачи товара нет. Xor означает ровно одно истинное условие и тоже имеет низкий приоритет; используйте явные скобки.

Логический оператор возвращает bool, а xor не сокращает вычисление

&& и || возвращают true/false, а не одно из исходных значений. Чтобы выбрать строку, используют ?: или ??. xor проверяет «истинно ровно одно»; для ответа ему нужны оба операнда. Слова and и or имеют те же логические результаты и короткое замыкание, что && и ||, но другой приоритет.

ЛевыйПравый&& / and|| / orxor
falsefalsefalsefalsefalse
falsetruefalsetruetrue
truefalsefalsetruetrue
truetruetruetruefalse
Проверить, что выбран ровно один способ полученияСамостоятельный пример
<?php
$pickup = true;
$courier = false;
$exactlyOne = ($pickup xor $courier);
if ($exactlyOne) {
    echo "Выбор принят: самовывоз\n";
}
$courier = true;
$exactlyOne = ($pickup xor $courier);
if (!$exactlyOne) {
    echo "Исправьте выбор: одновременно отмечены два способа\n";
}
$label = 'самовывоз' || 'курьер';
echo 'Тип результата строк под ||: ', get_debug_type($label), "\n";
Ожидаемый результат
Выбор принят: самовывоз
Исправьте выбор: одновременно отмечены два способа
Тип результата строк под ||: bool

Первый xor получает true и false и отдаёт true в exactlyOne. Во второй ситуации оба флага true: возвращается false, поэтому !exactlyOne выбирает сообщение. Если оба false, xor также false — тогда сообщение должно просить выбрать способ. Последнее || вычисляет истинность строк и отдаёт bool, а не выбирает одну из подписей. Скобки вокруг xor важны из-за его приоритета ниже присваивания.

Увидеть, когда запасное правило действительно вызываетсяСамостоятельный пример
<?php
function courierAvailable(): bool
{
    echo "Проверяем курьерскую доставку\n";
    return false;
}
$pickup = true;
$anyMethod = $pickup || courierAvailable();
if ($anyMethod) {
    echo "Есть хотя бы один способ: заказ можно продолжить\n";
}
$exactlyOne = ($pickup xor courierAvailable());
if ($exactlyOne) {
    echo "Доступен ровно один способ: самовывоз\n";
}
Ожидаемый результат
Есть хотя бы один способ: заказ можно продолжить
Проверяем курьерскую доставку
Доступен ровно один способ: самовывоз

У || левое true уже определяет ответ: вызов справа пропущен, сообщения о проверке нет. Для xor нужны оба значения: функция печатает сообщение и возвращает false в правый операнд; true xor false даёт true. Имя courierAvailable здесь задаёт локальное учебное правило, не выполняет сетевой запрос. Проверку нельзя использовать для обязательного побочного действия: короткое замыкание может его пропустить.

?:, ?? и ??=: выбор по условию или по отсутствию

Ноль тоже содержит сведения
  1. stock=0 → товар закончился
  2. ?: проверяет истинность → ошибочно выбирает 10
  3. ?? проверяет null/отсутствие → оставляет 0
  4. shownStock=0 → подпись «Товар закончился»

Значение по умолчанию выбирают по смыслу отсутствия. Нулевой запас — известное число, а null означает «сведения не получены» по договору этого примера. Сначала сравним две формы чтения; следующим отдельным файлом настроим сохранение defaults.

Сохранить настоящий нулевой остаток в карточкеСамостоятельный пример
<?php
$stock = 0;
$wrongStock = $stock ?: 10;
$shownStock = $stock ?? 10;
echo 'Ошибочная подстановка через ?: ', $wrongStock, ' шт.', "\n";
echo 'Известный остаток через ??: ', $shownStock, ' шт.', "\n";
echo $shownStock > 0 ? 'Можно заказать' : 'Товар закончился', "\n";
Ожидаемый результат
Ошибочная подстановка через ?: 10 шт.
Известный остаток через ??: 0 шт.
Товар закончился

Короткий ?: проверяет истинность: 0 ложно и выбирается 10. Оператор ?? подставляет запасное только для null или отсутствия; известное 0 сохраняется. Последнее полное условное выражение сравнивает число с нулём и возвращает строку «Товар закончился» в echo. Подмена через ?: дала бы ложное представление о наличии.

Сохранить недостающие настройки, не включив запрещённые повторыСамостоятельный пример
<?php
$settings = ['timeout' => null, 'retries' => 0];
$settings['timeout'] ??= 5;
$settings['retries'] ??= 3;
$settings['pause'] ??= 1;
echo 'Ожидание: ', $settings['timeout'], ' с', "\n";
echo 'Повторных попыток: ', $settings['retries'], "\n";
echo 'Пауза между попытками: ', $settings['pause'], ' с', "\n";
Ожидаемый результат
Ожидание: 5 с
Повторных попыток: 0
Пауза между попытками: 1 с

??= не только выбирает, но и записывает: null в timeout заменён на 5, отсутствующий pause создан со значением 1, заданный retries=0 сохранён. Пример готовит конфигурацию; он не спит, не повторяет запросы и не устанавливает таймаут PHP автоматически. Вложенные полные ?: в PHP 8 требуют явных скобок; для нескольких действий выбирайте if или match.

Запись "Остаток: " . ($item["stock"] ?? "неизвестен") намеренно содержит скобки. Без них сцепление строки и обращение к ключу могут произойти до выбора значения по умолчанию. Объектный оператор ?-> проверяет null у объекта; это другой инструмент, подробно описанный в работе с объектами.

Ветвь вычисляется при выборе; цепочка не проверяет схему данных

Почему может исчезнуть приписка «товаров»
  1. В count ?? 5 . " товаров" точка сначала связывает резервное 5 с текстом.
  2. При count = 0 значение уже существует и не равно null; ?? возвращает 0, не вычисляя резервную строку.
  3. Скобки (count ?? 5) сначала выбирают число, после чего точка добавляет приписку при любом выборе.

В $x ?: $backup выражение $x вычисляется один раз. В цепочке коротких ?: выбирается первое истинное значение, а в цепочке ?? — первое существующее значение, отличное от null. Число 0 и строка '0' для этих двух задач ведут себя по-разному. ?: читает левое значение обычным образом: отсутствующая переменная или ключ всё ещё вызывают предупреждение. У ?? для такой левой части есть специальная проверка существования.

Запасная функция не запускается, если значение уже естьСамостоятельный пример
<?php
function backupLabel()
{
    echo "Вычисляем запасную подпись\n";
    return 'Гость';
}
$name = 'Анна';
echo $name ?? backupLabel(), "\n";
$name = null;
echo $name ?? backupLabel(), "\n";
$custom = '';
$account = 'Покупатель';
echo $custom ?: $account ?: 'Гость', "\n";
Ожидаемый результат
Анна
Вычисляем запасную подпись
Гость
Покупатель

?? не исполняет правую сторону без необходимости. ?: с пустым custom берёт account. Если отсутствующая подпись должна стать ошибкой, запасной текст лишь замаскирует проблему — решение зависит от задачи.

Приписка относится ко всему выбранному значениюСамостоятельный пример
<?php
$count = 0;
echo $count ?? 5 . ' товаров', "\n";
echo ($count ?? 5) . ' товаров', "\n";
$input = [];
echo 'Имя: ' . ($input['name'] ?? 'не задано'), "\n";
Ожидаемый результат
0
0 товаров
Имя: не задано

В первой строке точка связывает только резервное значение 5 с припиской; поскольку count уже 0, ветка с припиской не нужна. Последняя запись безопасно читает отсутствующий ключ, но не доказывает, что input содержит все обязательные поля.

?? подавляет диагностику отсутствующего левого значения подобно isset, но не подавляет исключения произвольного вызова. Последний резервный операнд должен сам быть корректен. И ??, и ?: дают результат выражения, а не ссылку на выбранную переменную: их нельзя использовать как способ возвращать переменную по ссылке. В таком редком API нужна явная ветвь return нужной переменной.

Битовые операции: несколько флагов в одном числе

От битов к действию
  1. READ1 | WRITE2 → flags3
  2. actionLabels(3) → проверки масок1/2/4
  3. return [читать, записывать] → implode → подпись
  4. Сняли WRITE2 → flags1 → остаётся читать

Некоторые API требуют число флагов: например, читать, писать, удалять. Каждому независимому разрешению назначают отдельный бит:1,2,4. Оператор | объединяет их, & проверяет пересечение, ~ инвертирует биты, ^ переключает. Это компактный формат протокола, а не готовая система прав пользователя.

РазрешениеТри младших битаЧисло
Читать0011
Писать0102
Удалять1004
Читать и писать0113

Сначала ниже описана вспомогательная функция: она получает число flags, проверяет каждый известный бит и возвращает список разрешённых подписей. Вызывающий код сохраняет три состояния маски и показывает результат каждого. Это учебный формат флагов, а не полноценная проверка прав пользователя на документ.

Перевести изменяемую маску в доступные действияСамостоятельный пример
<?php
const READ_ACCESS = 1;
const WRITE_ACCESS = 2;
const DELETE_ACCESS = 4;
function actionLabels(int $flags): array {
    $labels = [];
    foreach ([READ_ACCESS => 'читать', WRITE_ACCESS => 'записывать', DELETE_ACCESS => 'удалять'] as $mask => $label) {
        if (($flags & $mask) === $mask) { $labels[] = $label; }
    }
    return $labels;
}
$flags = READ_ACCESS | WRITE_ACCESS;
echo 'Начало: ', implode(', ', actionLabels($flags)), "\n";
$flags &= ~WRITE_ACCESS;
echo 'После снятия записи: ', implode(', ', actionLabels($flags)), "\n";
$flags ^= READ_ACCESS;
$text = implode(', ', actionLabels($flags));
echo 'После переключения чтения: ', $text === '' ? 'действий нет' : $text, "\n";
Ожидаемый результат
Начало: читать, записывать
После снятия записи: читать
После переключения чтения: действий нет

OR объединяет 1 и 2 в 3. Первый вызов actionLabels получает местный flags=3: & с масками 1/2/4 даёт 1/2/0; первые две проверки истинны, return возвращает [читать,записывать] в implode. Implode соединяет их для echo. &=~2 убирает бит записи и оставляет 1; следующий вызов возвращает только чтение. ^=1 переключает оставшийся бит, получается 0 и пустой список. XOR при повторении отменит первое переключение, поэтому для безусловного снятия используйте &=~MASK. Скобки вокруг & нужны из-за приоритета сравнения.

Создаём маску из позиции флагаСамостоятельный пример
<?php
for ($position = 0; $position < 4; $position++) {
    echo 'Позиция ', $position, ': ', 1 << $position, "\n";
}
echo 8 >> 1, "\n";
Ожидаемый результат
Позиция 0: 1
Позиция 1: 2
Позиция 2: 4
Позиция 3: 8
4

Сдвиг перемещает биты внутри целого ограниченной разрядности. Переполнение, отрицательные числа и знак делают его плохой заменой обычному умножению. У строк &|^ работают побайтово, когда оба операнда строки; не путайте строковые "1" и "2" с числовой маской.

Две строки под & | ^ дают строку байтов

Число 12 и текст '12' — разные входы битового оператора. Если оба операнда &, |, ^ являются строками, операция идёт над байтами; результат тоже строка. Для ~ строка также обрабатывается побайтово. В остальных случаях арифметически допустимые операнды переводятся в int. Сдвиги всегда работают в int-контексте.

БайтЧислоБиты
A6501000001
a9701100001
Различающий бит3200100000
Байтовый результат виден через bin2hexСамостоятельный пример
<?php
echo bin2hex('A' & 'a'), "\n";
echo bin2hex('A' | 'a'), "\n";
echo bin2hex('A' ^ 'a'), "\n";
echo bin2hex(~'A'), "\n";
var_dump('12' & '10');
var_dump(12 & 10);
var_dump('12' << '1');
Ожидаемый результат
41
61
20
be
string(2) "10"
int(8)
int(24)

bin2hex показывает байты шестнадцатерично, включая непечатные. Это не операция над Unicode-буквами; инверсия байтов UTF-8 обычно разрушит текст. Последняя строка остаётся числовым сдвигом, хотя оба исходных значения — строки.

Для входной числовой маски формы сначала проверьте допустимую десятичную запись, диапазон и набор разрешённых битов, затем преобразуйте в int. Простое приведение произвольного мусора к 0 не является валидацией. Пересечение двух текстовых масок — другая операция, даже если текст похож на число.

Применение байтовой маски: в условном двухбайтовом сообщении первый байт содержит флаги, второй — код события. Запись "\x12\x34" содержит два байта с шестнадцатеричными значениями 12 и 34, а не восемь букв. Маска "\x00\xff" обнуляет первый байт и сохраняет все восемь бит второго. Bin2hex делает непечатные байты видимыми; ord возвращает числовое значение одного байта.

Из байтов сообщения получить понятное событиеСамостоятельный пример
<?php
$frame = "\x12\x34";
$eventBytes = $frame & "\x00\xff";
$eventCode = ord($eventBytes[1]);
$eventNames = [52 => 'Открыта дверь'];
echo 'Байты после маски: ', bin2hex($eventBytes), "\n";
echo 'Код события: ', $eventCode, "\n";
echo 'Событие: ', $eventNames[$eventCode], "\n";
Ожидаемый результат
Байты после маски: 0034
Код события: 52
Событие: Открыта дверь

Оба операнда & — строки, поэтому результат тоже строка из двух байтов 00/34. Индекс [1] читает второй байт, ord превращает hex34 в десятичное 52. Это число становится ключом eventNames и выбирает настоящую подпись. Формат сообщения здесь учебный и длина заранее равна двум байтам. Для внешнего потока отдельно проверьте длину, разрешённые флаги, код и правила протокола. Побайтовая операция не является преобразованием Unicode-текста.

Реальный вопрос для этой ступени: автор обсуждения ожидал числового поведения от двух строк с цифрами. Отсюда задача сначала установить договор входа: целая маска прав или строка байтов. Правило операции сверяется с PHP Manual; собственный пример выше извлекает событие из двух известных байтов.

Сдвиг, знак и граница машинного int

При сдвиге влево справа добавляются нули, старшие биты теряются. При сдвиге вправо слева повторяется знаковый бит. Поэтому правый сдвиг отрицательного числа — не то же самое, что intdiv с отсечением к нулю. Разрядность зависит от платформы: PHP_INT_SIZE * 8.

Отрицательное число, большой сдвиг и отрицательное расстояниеСамостоятельный пример
<?php
echo -7 >> 1, "\n";
echo intdiv(-7, 2), "\n";
$bits = PHP_INT_SIZE * 8;
echo 1 << $bits, "\n";
echo -1 >> $bits, "\n";
echo ~0, "\n";
try {
    $invalid = 1 << -1;
} catch (ArithmeticError $error) {
    echo "Отрицательный сдвиг запрещён\n";
}
Ожидаемый результат
-4
-3
0
-1
-1
Отрицательный сдвиг запрещён

Отрицательный сдвиг не означает сдвиг в другую сторону. Биты, вытолкнутые за границу int, не возвращаются. Если протокол использует большие беззнаковые значения, проверьте разрядность и выберите подходящее представление или GMP.

Разобрать учебный код устройства и восстановить его из полейСамостоятельный пример
<?php
$code = 0x1234;
$deviceType = ($code >> 8) & 0xff;
$command = $code & 0xff;
echo 'Тип устройства: ', $deviceType, "\n";
echo 'Команда: ', $command, "\n";
$restored = ($deviceType << 8) | $command;
echo 'Восстановленный числовой код: ', $restored, "\n";
if ($restored === $code) {
    echo "Оба поля восстановлены без потери\n";
}
Ожидаемый результат
Тип устройства: 18
Команда: 52
Восстановленный числовой код: 4660
Оба поля восстановлены без потери

В условном 16-битном формате старшие восемь бит — тип, младшие — команда. Сдвиг >> 8 переносит старшее поле вниз, & 0xff оставляет восемь младших битов. При сборке << 8 возвращает тип на место, | объединяет непересекающиеся биты. 0x1234 — шестнадцатеричный литерал числа 4660. Это разбор числа; отправка байтов в сеть и порядок байтов требуют отдельного договора протокола.

Плюс у массивов: объединение по ключам, а не добавление списка

Настройки определяют настоящую сумму
  1. custom: delivery0, methodСамовывоз
  2. defaults: delivery150, packaging20
  3. custom + defaults → delivery0, methodСамовывоз, packaging20
  4. goods300 + delivery0 + packaging20 → total320

Эта ступень использует ключи, элементы и foreach. У + над массивами правило определяется ключом: уже существующая левая запись выигрывает, даже если её значение 0 или null. Сначала применим это к настройкам, затем отдельно увидим ловушку списков с одинаковыми ключами 0 и 1.

Дополнить заказ настройкой упаковки и сохранить бесплатную доставкуСамостоятельный пример
<?php
$custom = ['delivery' => 0, 'method' => 'Самовывоз'];
$defaults = ['delivery' => 150, 'packaging' => 20];
$options = $custom + $defaults;
$goods = 300;
$total = $goods + $options['delivery'] + $options['packaging'];
echo 'Получение: ', $options['method'], "\n";
echo 'Доставка: ', $options['delivery'], ' руб.', "\n";
echo 'Упаковка: ', $options['packaging'], ' руб.', "\n";
echo 'К оплате: ', $total, ' руб.', "\n";
Ожидаемый результат
Получение: Самовывоз
Доставка: 0 руб.
Упаковка: 20 руб.
К оплате: 320 руб.

Delivery уже есть слева и остаётся 0; method тоже берётся из custom. Ключ packaging отсутствовал, поэтому справа добавляется 20. Итог использует реальные полученные настройки: 300+0+20=320. Это объединение только верхнего уровня. Для вложенных правил нужна отдельная политика; пример ниже объясняет её границы.

Увидеть, какие задачи потеряются при + вместо добавления спискаСамостоятельный пример
<?php
$morning = ['Чай', 'Кофе'];
$afternoon = ['Чашки', 'Тарелки'];
$union = $morning + $afternoon;
$merged = array_merge($morning, $afternoon);
echo "Результат +:\n";
foreach ($union as $index => $title) { echo $index, ': ', $title, "\n"; }
echo "Результат array_merge:\n";
foreach ($merged as $index => $title) { echo $index, ': ', $title, "\n"; }
Ожидаемый результат
Результат +:
0: Чай
1: Кофе
Результат array_merge:
0: Чай
1: Кофе
2: Чашки
3: Тарелки

Оба списка имеют ключи 0/1. При + правые записи с этими ключами не добавились: остались только Чай/Кофе. Array_merge добавляет элементы с числовыми ключами и нумерует результат заново, поэтому видны все четыре названия. При совпадении строковых ключей у array_merge побеждает правая запись. Ни один из этих способов не является универсальным глубоким объединением конфигурации.

Различить новый порядок полей и новый тип остаткаСамостоятельный пример
<?php
$saved = ['sku' => 'TEA', 'stock' => 2];
$incoming = ['stock' => 2, 'sku' => 'TEA'];
if ($saved == $incoming) { echo "Значения полей не изменились\n"; }
if ($saved !== $incoming) { echo "Строгий снимок отличается порядком полей\n"; }
$sameOrder = ['sku' => $incoming['sku'], 'stock' => $incoming['stock']];
if ($saved === $sameOrder) { echo "После явного порядка ключей снимки совпали\n"; }
$incoming['stock'] = '2';
if ($saved == $incoming) { echo "Нестрогое сравнение также приняло строку 2\n"; }
if (!is_int($incoming['stock'])) { echo "Вход отклонён: stock должен быть int\n"; }
Ожидаемый результат
Значения полей не изменились
Строгий снимок отличается порядком полей
После явного порядка ключей снимки совпали
Нестрогое сравнение также приняло строку 2
Вход отклонён: stock должен быть int

При первых входах == находит одинаковые пары независимо от порядка. === дополнительно требует одинаковые порядок и типы, поэтому первоначально снимки различаются. Явное построение sameOrder сохраняет значения и делает порядок одинаковым. После подмены int 2 строкой "2" нестрогое равенство всё ещё истинно, но отдельная проверка схемы отклоняет вход. Ни ==, ни === не заменяет проверку обязательных ключей, типов и диапазонов внешней записи; здесь все ключи заранее заданы.

Объединение не дополняет вложенную конфигурацию

У операции + над массивами совпадает или не совпадает ключ верхнего уровня. Если ключ уже есть слева, всё его значение сохранится, даже null или вложенный неполный массив. Комбинированное += записывает такое объединение обратно в левую переменную.

Увидеть реально применённые настройки и недостающий timeoutСамостоятельный пример
<?php
$options = ['http' => ['retries' => 0], 'label' => null];
$defaults = ['http' => ['timeout' => 5], 'label' => 'API', 'debug' => false];
$options += $defaults;
echo 'Повторов: ', $options['http']['retries'], "\n";
if (!array_key_exists('timeout', $options['http'])) {
    echo "Timeout отсутствует: вложенный словарь не дополнен\n";
}
if ($options['label'] === null) {
    echo "Подпись намеренно отсутствует\n";
}
echo 'Отладка: ', $options['debug'] ? 'включена' : 'выключена', "\n";
$options['http'] += $defaults['http'];
echo 'После отдельного дополнения HTTP: ', $options['http']['timeout'], ' с', "\n";
Ожидаемый результат
Повторов: 0
Timeout отсутствует: вложенный словарь не дополнен
Подпись намеренно отсутствует
Отладка: выключена
После отдельного дополнения HTTP: 5 с

Первый += сравнил только http, label, debug верхнего уровня. Http и label уже существуют слева, поэтому их целые значения сохранены, включая null; debug добавлен. Array_key_exists проверяет ключ независимо от null. Последний += применён явно к вложенному HTTP-массиву и добавил timeout = 5. Для неизвестной схемы такой отдельной строки недостаточно: нужен заранее согласованный алгоритм слияния.

Операторы неравенства массивов != и <> — отрицание ==; !== — отрицание ===. Для произвольных массивов нельзя считать < и > бизнес-сортировкой: число элементов, набор ключей и сравнение значений могут не соответствовать вашей задаче. Порядок товаров лучше задать явно по цене и ID.

Таблица нестрогих сравнений: сначала определите типы

Строгое сравнение не пытается угадать преобразование: === требует одинакового типа и значения (для объектов — той же идентичности), !== отрицает этот результат. == != <> < > <= >= <=> учитывают правила пары типов. Начинайте с первой подходящей строки таблицы.

Пара типовКак читать сравнение в PHP 8+
null и stringnull рассматривается как пустая строка; например null == '' истинно, null == '0' ложно.
bool с любым значением; остальные пары с nullОбычно сравнение логических значений: false меньше true. Для особых внутренних объектов учитывайте их правила и версионные изменения.
Две строкиЕсли обе числовые — сравнение чисел; иначе строковое сравнение байтов, с учётом регистра.
int/float и числовая строкаЧисловое сравнение; экспоненциальная запись тоже может быть числовой.
int/float и нечисловая строкаЧисло превращается в строку; правило изменено в PHP8. Нельзя переносить старый пример 0 == 'text' как истинный.
Числа и resourceСравнение числовых представлений; идентификатор ресурса не является предметной величиной.
Два массиваСначала число элементов; при равном размере важны ключи и сравнение значений. Разные ключи могут сделать значения несравнимыми.
Два объектаОбычные объекты одного класса сравниваются по состоянию при ==; разные классы/особые внутренние типы могут быть несравнимы. === проверяет один и тот же объект.
object и arrayОбъект считается больше массива; это техническое правило, не полезный порядок цены/товара.
array и скаляр/resourceМассив считается больше; правила bool/null выше имеют преимущество.
object и скаляр/resourceУчитывайте преобразование и договор конкретного класса. Например, сравнение обычного stdClass с числом может вызвать диагностику преобразования; не считайте объект всегда большим числом. Для цены сначала получите числовое поле.
Контрпримеры, которые полезнее запомнить вместе с типомСамостоятельный пример
<?php
var_dump(null == '');
var_dump(null == '0');
var_dump(null == 0);
var_dump(false == '0');
var_dump(0 == 'нет');
var_dump('020' == '20');
var_dump('020' === '20');
var_dump('0e12' == '0e34');
var_dump(20 === 20.0);
var_dump('A' == 'a');
var_dump(4 <> 5);
Ожидаемый результат
bool(true)
bool(false)
bool(true)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(false)
bool(true)

0e12 и 0e34 — разные тексты, но оба представляют числовой ноль. Для артикула с ведущими нулями нужен строковый договор. Проверку сохранённых паролей выполняют password_verify, сравнение секретных MAC — hash_equals при корректном договоре входа, а не подбором ==/===.

Сравнение не валидирует вход: int 20 и string '20' могут совпасть при ==, но сервер всё ещё обязан проверить структуру, диапазон и смысл поля. Встроенные правила также используются нестрогими конструкциями вроде switch; match использует строгую проверку.

<=> для сортировки: сравнить выбранные поля, а не весь ответ API

Что получает usortКак используется
Массив orders по ссылкеФункция переставляет его элементы; старые числовые ключи заменяются последовательными.
Callback с left и rightНа каждом вызове получает две записи; возвращает знак сравнения, а не строку для экрана.
Сначала price, при равенстве idЗаказ 2 идёт первым за 300; заказы 3 и 7 по 500 упорядочиваются по id.
Возврат usortВ современных PHP true; полезный отсортированный список читается из orders.

Оператор <=> даёт отрицательное число, ноль или положительное число: левое меньше, равно или больше правого. Для обычных сравнимых чисел результаты −1/0/1. Такой знак ждёт функция сортировки usort. Сначала приведите записи к согласованной схеме и типам.

Три отношения двух ценСамостоятельный пример
<?php
echo 120 <=> 200, "\n";
echo 200 <=> 200, "\n";
echo 300 <=> 200, "\n";
Ожидаемый результат
-1
0
1

Это не разность цен. Возвращается только отношение их порядка. Для текстовых числовых идентификаторов выбирайте строковое сравнение по договору, а не случайное числовое совпадение.

В следующем коде function ($left, $right) {...} — функция сравнения двух записей. usort много раз передаёт ей пары. Сначала сравниваем price, при равенстве используем id; возвращаем результат, а не печатаем его. Вход примера уже имеет целые price/id.

Одинаковая цена получает явный второй критерийСамостоятельный пример
<?php
$orders = [
    ['id' => 7, 'price' => 500],
    ['id' => 2, 'price' => 300],
    ['id' => 3, 'price' => 500],
];
usort($orders, function ($left, $right) {
    $byPrice = $left['price'] <=> $right['price'];
    if ($byPrice !== 0) {
        return $byPrice;
    }
    return $left['id'] <=> $right['id'];
});
foreach ($orders as $order) {
    echo $order['id'], ':', $order['price'], "\n";
}
Ожидаемый результат
2:300
3:500
7:500

Ответ API не сортируется автоматически по нужному бизнес-полю. Компаратор должен быть согласованным; произвольное сравнение двух массивов разного состава такого договора не даёт. usort меняет исходный массив и перенумеровывает числовые ключи.

NaN и float: не всякий числовой тип даёт сравнимый результат

NAN — специальное значение float для неопределённого результата. С числом или самим NAN сравнения ==, ===, <, >, <=, >= ложны; != и !== истинны. Поэтому проверка $value === NAN не обнаруживает NaN. Используйте is_nan, а для допустимого конечного результата — is_finite.

Отказать неопределённому среднему до проверки диапазонаСамостоятельный пример
<?php
$average = NAN;
if ($average < 0) {
    echo "Отрицательное среднее\n";
}
if (!is_finite($average)) {
    echo "Среднее не готово: нужен конечный результат\n";
} elseif ($average >= 0) {
    echo 'Среднее: ', $average, "\n";
}
Ожидаемый результат
Среднее не готово: нужен конечный результат

NAN не меньше нуля, но из этого не следует, что число допустимо или >= 0. Сравнения NAN с обычным числом и собой ==/===//<=/>= дают false, !=/!== дают true. Проверка конечности останавливает ошибочный маршрут прежде, чем число попадёт в отчёт. Для точного распознавания NAN используют is_nan.

Решить, укладывается ли измерение в согласованный допускСамостоятельный пример
<?php
$measuredMeters = 0.1 + 0.2;
$targetMeters = 0.3;
$toleranceMeters = 0.000001;
$difference = abs($measuredMeters - $targetMeters);
if (is_finite($measuredMeters) && $difference <= $toleranceMeters) {
    echo "Деталь принята: отклонение не больше 0.000001 м\n";
} else {
    echo "Деталь требует проверки\n";
}
echo 'Размер для подписи: ', $measuredMeters, ' м', "\n";
Ожидаемый результат
Деталь принята: отклонение не больше 0.000001 м
Размер для подписи: 0.3 м

Abs принимает разность и возвращает её величину без знака. Сначала проверяется конечность, затем величина отклонения. Арифметика float не обязана дать строго то же двоичное значение, что литерал 0.3; визуальная подпись 0.3 не доказывает точное равенство. Абсолютный допуск здесь выбран в метрах для этой задачи. Для других масштабов нужен согласованный относительный допуск; для денег этот приём не заменяет правила округления.

СPHP8.5 нестрогое сравнение несравнимого внутреннего объекта, например enum, с bool-переменной согласовано со сравнением с литералом true/false: объект приводится к true. Это совместимость языка, а не проверка состояния enum. Сравнивайте с нужным case или явно проверяйте тип.

Конвейер |> в PHP 8.5: читаем преобразования слева направо

Если строку нужно сначала очистить по краям, затем привести к верхнему регистру, вложенный вызов читается изнутри наружу. Pipe передаёт результат слева единственным аргументом вызываемому значению справа. Это удобство записи последовательности, а не параллельное выполнение.

Одинаковый результат у шагов, вложенного вызова и конвейераСамостоятельный пример
<?php
// Требуется PHP 8.5 или новее.
$raw = '  sku-17  ';
$trimmed = trim($raw);
$normal = strtoupper($trimmed);
echo $normal, "\n";
echo strtoupper(trim($raw)), "\n";
echo ($raw |> trim(...) |> strtoupper(...)), "\n";
Ожидаемый результат
SKU-17
SKU-17
SKU-17

trim(...) здесь создаёт вызываемое значение через first-class callable, а не вызывает trim без аргументов. Первый вариант подходит для поэтапной отладки; конвейер удобен, когда шаги короткие и понятные. На PHP 8.2–8.4 используйте первые два варианта.

Добавочный параметр задаём в отдельном преобразованииСамостоятельный пример
<?php
// Требуется PHP 8.5 или новее.
$prefix = 'ORDER-';
$result = '  17  '
    |> trim(...)
    |> (fn(string $number): string => $prefix . $number);
echo $result, "\n";
Ожидаемый результат
ORDER-17

Стрелочная функция принимает промежуточное значение и использует prefix из внешней области. Каждая ступень обязана принимать результат предыдущей. Исключение остановит конвейер; |> сам не перехватывает ошибки и не делает синхронный HTTP-запрос неблокирующим.

|> принимает вызываемое значение с одним входом

Конвейер передаёт результат слева в callable справа. Синтаксис trim(...) создаёт callable; trim() пытался бы вызвать функцию немедленно без нужного аргумента. Справа подходят Closure, first-class callable, допустимая строка функции или объект с __invoke. Это последовательный вызов, не параллельная задача.

Объект-преобразование и обычная функцияСамостоятельный пример
<?php
// Только PHP 8.5+.
class PrefixOrder {
    public function __invoke($number) {
        return 'ORDER-' . $number;
    }
}
$prefix = new PrefixOrder();
$result = '  41  ' |> trim(...) |> $prefix;
echo $result, "\n";
Ожидаемый результат
ORDER-41

__invoke позволяет вызвать объект как функцию. Конвейер сначала передал строку trim, затем его результат 41 — объекту prefix. То же можно написать промежуточными переменными на PHP 8.2–8.4.

Если функции нужны два обязательных аргумента, автоматически появится только один. Оберните её в функцию с одним входом и явно задайте остальные параметры. Функции, принимающие переданный аргумент по ссылке, для pipe не поддерживаются. Стрелочную функцию справа всегда заключайте в скобки.

Два разных нарушения договора конвейераСамостоятельный пример
<?php
// Только PHP 8.5+.
function joinCode($prefix, $number) {
    return $prefix . $number;
}
try {
    $result = 41 |> joinCode(...);
} catch (ArgumentCountError $error) {
    echo "Не хватает второго аргумента\n";
}
try {
    $result = 41 |> 123;
} catch (Error $error) {
    echo "Правая часть не callable\n";
}
$result = 41 |> (fn($number) => joinCode('ORDER-', $number));
echo $result, "\n";
Ожидаемый результат
Не хватает второго аргумента
Правая часть не callable
ORDER-41

Пример ловит ошибки намеренно. В рабочем коде контракт шагов известен заранее; исключение в одном шаге останавливает остальные. Добавление pipe не меняет таймауты, повторные запросы или безопасность операции.

Функция, меняющая массив по ссылке, требует явного шагаСамостоятельный пример
<?php
// Только PHP 8.5+.
$values = [7, 3];
try {
    $result = $values |> sort(...);
} catch (Error $error) {
    echo "Прямая передача по ссылке не поддерживается\n";
}
$sorted = $values |> (function ($items) {
    sort($items);
    return $items;
});
echo implode(', ', $values), "\n";
echo implode(', ', $sorted), "\n";
Ожидаемый результат
Прямая передача по ссылке не поддерживается
7, 3
3, 7

sort изменяет свой аргумент по ссылке и возвращает bool, а не отсортированный массив. Явный шаг получает локальную копию массива, сортирует её и возвращает массив следующему коду. Исходные values остались прежними. Такая обёртка задаёт смысл преобразования, а не просто устраняет сообщение об ошибке.

Приоритет pipe ниже точки и арифметики, выше сравнений. Для вопроса «длина больше 3» явно пишите ($text |> strlen(...)) > 3. Если результат уже нужен в нескольких местах или требуется отладка промежуточного значения, простые именованные переменные могут читаться лучше.

Что на самом деле делают @ и обратные кавычки

Оператор @ подавляет часть диагностических сообщений выражения, но не исправляет неудачу и не превращает false в данные. Ошибка может остаться в результате или исключении; пользовательский обработчик ошибок всё равно может быть вызван. Для разработки полезнее видеть причину, а для рабочего сайта — настроить журнал и безопасное сообщение посетителю.

Ошибка файла получает отдельную ветку решенияСамостоятельный пример
<?php
declare(strict_types=1);

$path = __DIR__ . '/missing-catalog.txt';
if (!is_file($path)) {
    echo "Каталог ещё не получен\n";
} else {
    $text = file_get_contents($path);
    if ($text === false) {
        throw new RuntimeException('Не удалось прочитать каталог');
    }
    echo $text;
}
Ожидаемый результат
Каталог ещё не получен

Для учебного отсутствующего файла мы отличили нормальное состояние «ещё не пришёл» от результата чтения. Проверка is_file не устраняет гонку: файл может исчезнуть после неё. В настоящем обработчике нужна согласованная политика предупреждений/исключений и проверка самого чтения. Приём с @ не заменяет эту политику.

Обратные кавычки `команда` запускают командную оболочку и возвращают её вывод, подобно shell_exec. Это не разновидность строковой кавычки. Там появляются правила оболочки, риск внедрения команды, права процесса, stderr и зависимость от ОС. Практические вызовы Python/Node.js, массив аргументов proc_open и разбор статуса показаны в отдельной главе внешних программ. Оператор instanceof проверяет принадлежность объекта типу; знакомство с ним продолжите на примере интерфейса.

@: предупреждение скрыто, но неудача и обработчик остаются

Подавление сообщения не создаёт текст файла
  1. absent.txt отсутствует → чтение даёт false
  2. @ скрывает обычное предупреждение
  3. Проверка false выбирает известную памятку
  4. echo выводит «Выдача заказов с 10 до 18»

Запись @выражение подавляет часть диагностики выражения: например, предупреждение открытия отсутствующего файла. Это не ветвление и не перехват исключения. @ нельзя поставить перед объявлением класса/функции, if или foreach.

Проверить неудачу чтения и подготовить памятку по известному правилуСамостоятельный пример
<?php
error_clear_last();
$text = @file_get_contents(__DIR__ . '/absent.txt');
$lastError = error_get_last();
if ($text === false) {
    $text = 'Выдача заказов с 10 до 18';
    echo "Использована памятка по умолчанию\n";
}
if ($lastError !== null && $lastError['type'] === E_WARNING) {
    echo "Зафиксировано предупреждение чтения\n";
}
echo 'Памятка: ', $text, "\n";
Ожидаемый результат
Использована памятка по умолчанию
Зафиксировано предупреждение чтения
Памятка: Выдача заказов с 10 до 18

Файл намеренно отсутствует. @ скрывает обычный вывод предупреждения, но file_get_contents возвращает false: это проверяется до использования text. Затем выбирается конкретная заранее допустимая памятка и выводится её текст. Error_get_last прочитан сразу после отказа: массив содержит type/message/file/line; следующая ошибка могла бы заменить запись. Внешнему читателю здесь показывается безопасная подпись, внутренний путь не выводится. Этот пример объясняет механизм @, а не правило скрывать любые ошибки.

Пользовательский обработчик ошибок всё равно вызывается. В PHP 8 проверяют бит нужного уровня в error_reporting(), а не равенство всего значения нулю: под @ остаются критические уровни. Следующий обработчик только для учебного опыта и восстанавливается в finally.

Обработчик получает предупреждение, а исключение требует отдельного catchСамостоятельный пример
<?php
set_error_handler(function ($severity, $message) {
    echo (error_reporting() & $severity) === 0
        ? "Обработчик вызван для подавленного уровня\n"
        : "Обработчик вызван для включённого уровня\n";
    return true;
});
try {
    $text = @file_get_contents(__DIR__ . '/absent.txt');
    if ($text === false) { echo "Файл не прочитан, содержимое не используется\n"; }
} finally {
    restore_error_handler();
}
try {
    $result = @(1 / 0);
} catch (DivisionByZeroError) {
    echo "Деление не выполнено: делитель равен нулю\n";
}
Ожидаемый результат
Обработчик вызван для подавленного уровня
Файл не прочитан, содержимое не используется
Деление не выполнено: делитель равен нулю

PHP всё равно вызывает наш обработчик с severity/message. Проверка пересечения битов обнаруживает, что этот уровень подавлен. Return true относится к обработке предупреждения, а не к результату чтения: text остаётся false и следующая ветка отказывается от содержимого. Finally восстанавливает обработчик. Во втором опыте @ не убирает DivisionByZeroError: вместо значения в result управление переходит к catch. В PHP 8 под @ остаются критические уровни, поэтому проверять error_reporting()===0 неправильно.

Обратные кавычки: исторический запуск оболочки, deprecated с PHP8.5

Обратная кавычка — символ с кодом 96, не обычная одинарная кавычка строки. Содержимое такой пары запускается через командную оболочку; её стандартный вывод возвращается в PHP как результат. Это поведение shell_exec, а не выполнение PHP-кода.

Старый синтаксис, который можно встретить при чтении проектаФрагмент или команда
<?php
$output = `echo READY`;
// Исторический shell-синтаксис; не образец для нового кода.

Контекстный исторический фрагмент, команда здесь не запускалась. Вывод команды попал бы в $output, а не автоматически в HTTP-ответ. СPHP8.5 оператор обратных кавычек deprecated. Синтаксис зависит от оболочки; не подставляйте пользовательский текст в команду.

Если shell_exec отключён, этот оператор также недоступен. Внутри двойной PHP-строки обратные кавычки не получают специального смысла. Для нового взаимодействия с внешней программой выберите явный API; proc_open с массивом аргументов, stdout/stderr и статусом разбирается с работающими файлами. Не используйте содержимое stdout как доказательство exit 0.

instanceof: перед нами объект нужного типа или только похожие данные

В $value instanceof Receipt слева проверяется значение, справа указан тип. Результат bool. Это не создание объекта и не сравнение всех свойств. Сначала простой класс с одним полем; public разрешает доступ к этому полю, new создаёт экземпляр.

Объект, строка с его именем и nullСамостоятельный пример
<?php
class Receipt {
    public $number = 41;
}
$receipt = new Receipt();
$className = Receipt::class;
var_dump($receipt instanceof Receipt);
var_dump($className instanceof Receipt);
var_dump(null instanceof Receipt);
var_dump(!($receipt instanceof stdClass));
Ожидаемый результат
bool(true)
bool(false)
bool(false)
bool(true)

Receipt::class возвращает строку имени. Она сама не объект Receipt. ! меняет результат проверки; скобки делают это явно читаемым. Необъект слева даёт false; литералы допустимы с PHP7.3.

Проверка принимает наследника и объект, реализующий интерфейс. В примере PrintableReceipt задаёт метод text; GiftReceipt наследует Receipt и реализует этот договор. Так код выбирает допустимое действие, а не угадывает его по названию класса.

Практика: печатать только объект с договором text()Самостоятельный пример
<?php
interface PrintableReceipt {
    public function text(): string;
}
class Receipt {}
class GiftReceipt extends Receipt implements PrintableReceipt {
    public function text(): string { return 'Подарочный чек №41'; }
}
$receipt = new GiftReceipt();
var_dump($receipt instanceof Receipt);
if ($receipt instanceof PrintableReceipt) {
    echo $receipt->text(), "\n";
}
$type = PrintableReceipt::class;
var_dump($receipt instanceof $type);
$another = new GiftReceipt();
var_dump($receipt instanceof $another);
var_dump($receipt === $another);
var_dump($receipt instanceof ('Printable' . 'Receipt'));
Ожидаемый результат
bool(true)
Подарочный чек №41
bool(true)
bool(true)
bool(false)
bool(true)

Строка или объект справа задают проверяемый класс/интерфейс, но не требуют идентичности экземпляра. Произвольное выражение справа поддерживается с PHP8.0, оборачивается в скобки и должно дать строку имени. Для строки имени применим is_a с явным allow_string, для существования определения — class_exists; это разные вопросы.

Неопределённый тип справа не превращает неизвестное значение в объект нужного вида; проверка отсутствующего класса даёт false. Namespace и импорт влияют на записанное имя класса; строковый $type должен уже содержать полное имя. Не используйте instanceof как валидацию массива JSON: структура массива проверяется отдельно.

Равенство должно отвечать бизнес-вопросу

Наглядная модель

Найдено в начале строки — не значит «не найдено»

Ищем артикул в тексте. Нужно отличить позицию первого символа от отсутствия совпадения.

  • strpos вернул 0

    Совпадение найдено на позиции 0 — в самом начале.

  • strpos вернул false

    Совпадения в строке нет.

Проверка сохраняет различие типов$position !== false

Проверка if ($position) смешала бы эти состояния: и 0, и false считаются ложными в условии. Строгое сравнение помогает назвать именно тот исход, который нужен программе.

== допускает преобразование типов, === сравнивает также тип. Для нормализованных идентификаторов, кодов статуса и sentinel-результатов функций обычно нужен строгий вариант. Если форма вернула строку "12", а база int12, сначала договоритесь о типе на границе, затем сравнивайте. Механическая замена === на== ради прохождения одного условия оставляет неявное преобразование во всем сценарии.

Артикул найден в начале названияСамостоятельный пример
<?php
declare(strict_types=1);

$position = strpos('SKU-204 Cable', 'SKU-');
if ($position !== false) {
    echo "Найдено на позиции {$position}\n";
}
echo str_starts_with('SKU-204 Cable', 'SKU-') ? "Начинается сSKU\n" : "Нет\n";
Ожидаемый результат
Найдено на позиции 0
Начинается сSKU

if ($position) пропустит совпадение вначале: позиция 0 преобразуется в false. Когда позиция не нужна, str_contains или str_starts_with точнее выражают вопрос. Это повторяющаяся проблема из обсуждений разработчиков.

??, ?: и nullsafe решают разные задачи

Нулевой остаток должен остаться нулевымСамостоятельный пример
<?php
declare(strict_types=1);

$product = ['stock' => 0, 'subtitle' => null];
echo $product['stock'] ?? 99, "\n";
echo $product['stock'] ?: 99, "\n";
echo $product['subtitle'] ?? 'Без подзаголовка', "\n";
Ожидаемый результат
0
99
Без подзаголовка

?? выбирает запасное значение только для отсутствия/null. Сокращенный тернарный ?: проверяет истинность и заменит 0. Для склада это искажает данные. Nullsafe?-> останавливает цепочку при null-объекте, но не подавляет исключения методов и не валидирует JSON-массив.

Полный тернарный оператор подходит для короткого выбора, а вложенные ветки лучше оформить if или match. PHP 8 требует явно расставлять скобки в неоднозначных вложенных тернарных выражениях. Оператор . соединяет строки, + складывает числа или объединяет массивы по ключам. Если ожидаете сумму значений массива, + над массивами не выполняет такой расчет.

Приоритет не должен быть загадкой для читателя

&& и|| вычисляются с коротким замыканием: правую часть не выполняют, если результат уже известен. Это позволяет сначала проверить тип, затем обращаться к полю. and/or имеют другой приоритет относительно присваивания; в прикладных условиях используйте &&/|| и скобки. Не полагайтесь на порядок вычисления независимых выражений с побочными эффектами. Разделите изменение счетчика и вызов функции на строки, чтобы код не зависел от неочевидных правил.

Проверка перед чтением вложенного значенияСамостоятельный пример
<?php
declare(strict_types=1);

$input = ['quantity' => '4'];
$valid = isset($input['quantity'])
    && is_string($input['quantity'])
    && preg_match('/\A[1-9][0-9]?\z/', $input['quantity']) === 1;
echo $valid ? "Можно продолжить\n" : "Ошибка формы\n";
Ожидаемый результат
Можно продолжить

Проверки читаются слева направо, и регулярное выражение не получит массив. В реальном обработчике добавьте проверку метода, CSRF и полномочий — они не возникают автоматически от правильного оператора.

Деление, остаток и границы

/ обычно дает число с дробной частью; intdiv возвращает целую часть частного, % — целочисленный остаток. Для страниц каталога количество страниц округляют вверх, для разбиения суммы на равные части сохраняют остаток и распределяют отдельно. Деление на 0 дает исключение. Произведения огромных int могут выйти за пределы платформы; ограничивайте вход. Побитовые операторы полезны для флагов API, но плохо читаются как замена обычным условиям без настоящей необходимости.

Попробуйте сами. Поле скидки может быть 0, null или отсутствовать. Как назначить 10 только для null/отсутствия?

Разбор решения

$discount = $row["discount"] ?? 10; затем проверьте тип и диапазон. ?: ошибочно заменит законный 0. Если null означает «обязательно уточнить», резервное значение вообще не подходит — верните ошибку контракта.

Проверьте правило словами, затем выберите оператор

Практическое упражнение: покупатель выбирает одно из двух условий скидки, сумма хранится в копейках, а подпись должна показывать 0 без подмены. Укажите типы каждого поля и сформулируйте отдельно: сложить, проверить равенство, выбрать резервное значение или проверить два условия. Только потом выбирайте знак.

Вопрос задачиПодходТипичная ошибка
Обязательный код статуса равен строке PAID?Проверить string, затем === 'PAID'Нестрого сравнить произвольный JSON-тип.
Подпись количества с допустимым нулём?($count ?? 'нет данных') . ' шт.'Использовать ?: и потерять 0 либо забыть скобки.
Сумма поровну с сохранением копеек?intdiv + %, остаток распределить по договоруОтбросить остаток или использовать float без правил.
Включены все требуемые биты?($flags & $required) === $requiredПроверить лишь ненулевое пересечение, когда нужны все.
Нужно вызвать метод объекта?Проверить договор типа/интерфейсаСчитать строку имени класса самим объектом.

Вернитесь к примеру распределения 101 копейки и поменяйте сумму на 0, затем на 102. Ожидаемые части:0,0,0 и 34,34,34. Поменяйте parts на 0: должна сработать проверка, а не деление. Для маски 6 (биты 2 и 4) сравните flags2 и flags6: ненулевое пересечение есть у обоих, полный набор — только у второго.

Обсуждения, из которых взяты реальные вопросы, а не готовый код:

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.