Александр Некрасов · Автономное руководство

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

Разделы этой страницы
  1. Условия и циклы на реальных данных
  2. if: выбрать сообщение о наличии товара
  3. for: напечатать три этикетки и остановиться
  4. while: повторять, пока осталась работа; do…while: сначала выполнить
  5. foreach, continue и break: составить список доступных товаров
  6. switch и match: из кода доставки получить понятную памятку
  7. Инструкция и блок: какую часть кода выбирает условие
  8. if, else и elseif: одна ветка или несколько независимых действий
  9. К какому if относится else
  10. Двоеточие и закрывающее слово: читаемый PHP-шаблон
  11. for: пустые части, запятые и реальная стоимость условия
  12. while проверяет условие между проходами, не после каждой строки
  13. do…while(false): один блок с ранним выходом
  14. foreach получает значения: присвоить переменной и изменить источник — разные действия
  15. Разобрать строку таблицы прямо в заголовке foreach
  16. Объект тоже можно обходить, но не любой объект является iterable
  17. break и continue считают уровни конструкций, а не фигурные скобки
  18. switch: начало выполнения выбирает case, конец задаёт break
  19. match: строгий результат и вычисление только нужной ветки
  20. declare задаёт директиву, а не вызывает функцию
  21. Tick — событие инструкции, а не таймер ожидания
  22. encoding описывает исходный файл, а не кодировку ответа
  23. return выбирает границу: вызов функции, подключённый файл или основной скрипт
  24. Подключение исполняет файл в области переменных точки вызова
  25. Путь, include_path и выбор разрешённого шаблона
  26. Вывод файла и результат include — разные каналы
  27. include и require различаются политикой неудачи
  28. once помнит подключение, но не запоминает результат вычисления
  29. goto: переход в той же области, а не вызов другой части программы
  30. throw сообщает об отказе и прекращает текущий обычный путь
  31. catch выбирается по типу, а выполнение продолжается после всей конструкции
  32. finally освобождает ресурс перед продолжением, возвратом или передачей ошибки
  33. return и throw внутри finally способны изменить исход операции
  34. Граница гарантии finally: exit завершает запрос немедленно
  35. Генератор при yield приостанавливается; finally не выполняется после каждой выдачи
  36. Соберите маршрут обработки, прежде чем выбирать ключевые слова
  37. do…while: первая попытка происходит до проверки продолжения
  38. break и continue: закончить цикл или пропустить одну запись
  39. Альтернативный синтаксис: где PHP стоит внутри HTML
  40. include, require и once: как несколько файлов становятся программой
  41. declare и goto: узнавать синтаксис и понимать его назначение
  42. Как выбрать for, foreach и while для своей задачи
  43. Ветка — это решение в предметной области
  44. foreach работает с элементами, а не с догадками об индексах
  45. Изменение через ссылку: почему после foreach требуется unset
  46. Цикл должен иметь понятный предел
  47. Два цикла вместе: страницы API и записи внутри страницы
  48. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

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

Условия и циклы на реальных данных

Выбор доставки, обработка партий и безопасное завершение обхода без потерянных элементов.

if: выбрать сообщение о наличии товара

Карточка должна сообщить, можно ли заказать товар. Когда остаток больше нуля, показываем «Есть в наличии», иначе — «Ожидаем поставку». До сих пор строки выполнялись подряд. if позволяет выполнить лишь подходящий блок.

При $stock = 0
  1. Вычисляется 0 > 0 → false.
  2. Блок после if пропускается.
  3. Блок после else выводит «Ожидаем поставку».
  4. После закрывающей } выполнение продолжается общей строкой.
ЭлементСмысл
if ($stock > 0)if — «если»; круглые скобки содержат условие, > сравнивает числа.
{ ... }Все инструкции внутри относятся к этой ветви. Отступ сам по себе не связывает строки.
elseИначе: блок для ложного условия. У этой части не пишется собственное условие.
Сначала выбранная ветвь, затем общее продолжениеСамостоятельный пример
<?php
$stock = 0;
if ($stock > 0) {
    echo "Есть в наличии\n";
} else {
    echo "Ожидаем поставку\n";
}
echo "Каталог доступен для просмотра\n";
Ожидаемый результат
Ожидаем поставку
Каталог доступен для просмотра

Последняя строка стоит после обеих ветвей и выполняется независимо от наличия. Замените 0 на 3: изменится только сообщение о наличии. После if (...) не ставят случайную ; — иначе получится отдельная пустая инструкция.

Если вариантов больше двух, добавляют elseif (условие). PHP проверяет сверху вниз и выполняет только первую подходящую ветвь. В примере сначала проверяем границу «нет», затем «мало», затем общий положительный остаток.

Различить отсутствие и небольшой остатокСамостоятельный пример
<?php
$stock = 2;
if ($stock === 0) {
    echo "Нет в наличии\n";
} elseif ($stock < 5) {
    echo 'Осталось мало: ', $stock, ' уп.', "\n";
} else {
    echo 'В наличии: ', $stock, ' уп.', "\n";
}
Ожидаемый результат
Осталось мало: 2 уп.

2 не равно 0, но 2 меньше 5; выбирается средний блок. Если поставить условие stock > 0 раньше stock < 5, общий случай перехватит маленький остаток. Заранее считаем stock неотрицательным целым; ошибки внешних данных обрабатываются до этой классификации.

for: напечатать три этикетки и остановиться

Нужно подписать три коробки: «Коробка 1 из 3», «Коробка 2 из 3», «Коробка 3 из 3». Число повторов известно. Цикл for объединяет начало счётчика, условие продолжения и изменение после каждого прохода. Один проход тела называют итерацией.

Часть forКогда и зачем выполняется
$number = 1Один раз перед началом: начинаем с первой коробки.
$number <= $boxCountПеред каждым проходом: не вышли ли за последнюю коробку?
$number = $number + 1После тела: перейти к следующему номеру.
Две ; внутри ()Разделяют три части цикла; это не конец всей инструкции for.
Количество коробок управляет числом строкСамостоятельный пример
<?php
$boxCount = 3;
for ($number = 1; $number <= $boxCount; $number = $number + 1) {
    echo 'Коробка ', $number, ' из ', $boxCount, "\n";
}
echo "Этикетки подготовлены\n";
Ожидаемый результат
Коробка 1 из 3
Коробка 2 из 3
Коробка 3 из 3
Этикетки подготовлены

После третьего тела number становится 4. Условие 4 <= 3 ложно, поэтому четвёртая этикетка не появляется. При boxCount = 0 тело не выполнится ни разу, но сообщение после цикла выполнится.

$number++ — короткая запись увеличения на 1, часто встречающаяся в for. Сначала прочитайте её как отдельный шаг после тела. Если убрать изменение счётчика, условие может никогда не стать ложным.

while: повторять, пока осталась работа; do…while: сначала выполнить

Есть 5 упаковок, в коробку входит не больше 2. Число коробок пока не записано: будем брать следующую порцию, пока оставшееся количество положительно. while проверяет условие перед телом. На каждом шаге remaining обязательно уменьшается, поэтому работа заканчивается.

Распределить пять упаковок по коробкамСамостоятельный пример
<?php
$remaining = 5;
$boxNumber = 1;
while ($remaining > 0) {
    $inBox = 2;
    if ($remaining < $inBox) {
        $inBox = $remaining;
    }
    echo 'Коробка ', $boxNumber, ': ', $inBox, ' уп.', "\n";
    $remaining = $remaining - $inBox;
    $boxNumber = $boxNumber + 1;
}
echo "Всё упаковано\n";
Ожидаемый результат
Коробка 1: 2 уп.
Коробка 2: 2 уп.
Коробка 3: 1 уп.
Всё упаковано

remaining проходит значения 5 → 3 → 1 → 0. В последней коробке if уменьшает порцию до 1, чтобы не упаковать несуществующий товар. Изменяемая величина и граница остановки видны непосредственно в коде.

У do…while проверка находится после тела. Такая форма нужна, когда первое действие обязательно, даже если продолжение не потребуется. Для знакомства выполним одну подготовку упаковочного стола и проверим заранее заданное «дальше не повторять». Это не опрос реальной очереди и не сетевой запрос.

Обязательное первое действие перед проверкой продолженияСамостоятельный пример
<?php
$prepareAgain = false;
do {
    echo "Упаковочный стол подготовлен\n";
} while ($prepareAgain);
echo "Можно начать выдачу\n";
Ожидаемый результат
Упаковочный стол подготовлен
Можно начать выдачу

Тело сработало один раз; затем false остановил повтор. В этой форме после while (...) нужна ;. У обычного while (false) тело не выполнилось бы ни разу.

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

foreach, continue и break: составить список доступных товаров

Из каталога нужно показать товары с положительным остатком. Создадим массив ['Чай' => 0, 'Кофе' => 2]: [] задаёт набор, => связывает ключ-название с числом остатка, запятая отделяет пары. В foreach форма as $title => $stock получает очередную пару. Подробное построение такой структуры находится в главе массивов.

Для остатков Чай → 0, Кофе → 2, Какао → 1
  1. Чай: stock равно 0, continue сразу переходит к следующему элементу.
  2. Кофе: условие нулевого остатка ложно; выводится строка товара.
  3. Какао: выводится следующая строка.
  4. После последнего элемента foreach завершается.
Пропустить отсутствующий товар и продолжить просмотр каталогаСамостоятельный пример
<?php
$stockByTitle = ['Чай' => 0, 'Кофе' => 2, 'Какао' => 1];
foreach ($stockByTitle as $title => $stock) {
    if ($stock === 0) {
        continue;
    }
    echo 'Можно заказать: ', $title, ' — ', $stock, ' уп.', "\n";
}
echo "Просмотр завершён\n";
Ожидаемый результат
Можно заказать: Кофе — 2 уп.
Можно заказать: Какао — 1 уп.
Просмотр завершён

continue не завершает функцию и не заканчивает весь цикл. Он пропускает оставшуюся часть только текущего прохода. Поэтому кофе и какао обработаны после отсутствующего чая. Здесь заранее заданные остатки — неотрицательные целые.

У превью на главной странице другое правило: показать только первый доступный товар. Когда он найден и строка подготовлена, больше перебирать не нужно. Для этого break завершает текущий цикл целиком.

Остановиться после первого подходящего товараСамостоятельный пример
<?php
$stockByTitle = ['Чай' => 0, 'Кофе' => 2, 'Какао' => 1];
$preview = 'Сейчас нет доступных товаров';
foreach ($stockByTitle as $title => $stock) {
    if ($stock === 0) {
        continue;
    }
    $preview = 'Сегодня доступен: ' . $title;
    break;
}
echo $preview, "\n";
Ожидаемый результат
Сегодня доступен: Кофе

Чай пропущен. Кофе записан в preview, затем break выходит из foreach: Какао не рассматривается. Если все остатки нулевые или массив пустой, остаётся заранее подготовленная фраза об отсутствии. В отличие от return, break оставляет возможность выполнять код после цикла.

Вложенные циклы могут использовать числовой уровень break 2 или continue 2. Это уже отдельное уточнение ниже: уровень считают по реально окружающим конструкциям, включая switch. Не переносите «2» из чужого примера без чтения его вложенности.

switch и match: из кода доставки получить понятную памятку

Программа хранит короткий код pickup либо courier, а покупателю нужна русская подпись. switch выбирает блок инструкций; match возвращает значение выражения. Сначала увидим switch. Круглые скобки содержат проверяемое значение; case задаёт сравниваемый вариант, двоеточие начинает инструкции; break прекращает switch.

Выполнить подходящие инструкции выбранного способаСамостоятельный пример
<?php
$deliveryMethod = 'pickup';
switch ($deliveryMethod) {
    case 'pickup':
        echo "Способ: самовывоз\n";
        echo "Получение с 10:00 до 19:00\n";
        break;
    case 'courier':
        echo "Способ: курьер\n";
        echo "Согласуем интервал доставки\n";
        break;
    default:
        echo "Выберите способ получения заказа\n";
}
echo "Спасибо за заказ\n";
Ожидаемый результат
Способ: самовывоз
Получение с 10:00 до 19:00
Спасибо за заказ

После двух строк pickup выполняется break, поэтому инструкции courier не идут следом. Без break switch способен продолжить следующую ветвь: это разрешённое поведение, которое легко получить случайно. Последний echo находится после switch и выполнится при любом варианте.

С PHP 8.0 match подходит, когда нужен один результат для сохранения. В записи 'pickup' => 'Самовывоз' стрелка связывает вариант с результатом; это не создание массива. После закрывающей } нужна ;, потому что присваивание должно завершиться.

Получить подпись и использовать её в общей строке заказаСамостоятельный пример
<?php
$deliveryMethod = 'pickup';
$label = match ($deliveryMethod) {
    'pickup' => 'Самовывоз',
    'courier' => 'Курьер',
    default => 'Способ не выбран',
};
echo 'Заказ 42. Получение: ', $label, "\n";
Ожидаемый результат
Заказ 42. Получение: Самовывоз

Выражение match возвращает строку Самовывоз, присваивание сохраняет её в label, а echo оформляет общую строку. Только выбранное выражение результата вычисляется; перехода в следующую ветвь нет.

Свойствоswitchmatch
Что выбираетИнструкции подходящей ветви.Значение выражения подходящей ветви.
СравнениеНестрогое, с правилами преобразования типов.Строгое: значение и тип; 1 и "1" различаются.
Нет подходящего вариантаВыполняется default, если есть; иначе блок пропускается.Выполняется default; без него возникает UnhandledMatchError.
Переход к следующей ветвиВозможен без break.Отсутствует.

Для произвольных диапазонов, например stock < 5, часто проще if/elseif. Для подписей известных кодов — match. Для группы действий и осмысленного перехода между case — switch. Ошибка неизвестного кода и запасной текст — разные договоры; выбирайте поведение явно.

Инструкция и блок: какую часть кода выбирает условие

После первых опытов с условиями и циклами полезно уточнить их общую грамматику. Инструкция — одно действие программы: присваивание, вызов, условие, цикл. Фигурные скобки объединяют несколько инструкций в один блок. Отступы показывают замысел человеку, но границы исполнения определяются знаками.

У карточки stock = 0
  1. Условие stock > 0 ложно
  2. Весь блок с двумя действиями пропускается
  3. После блока выполняется общий вывод
Скобки связывают оба действия с условиемСамостоятельный пример
<?php
$stock = 0;
$label = 'Нет в наличии';
if ($stock > 0) {
    $label = 'Можно заказать';
    echo "Показать кнопку покупки\n";
}
echo $label, "\n";
Ожидаемый результат
Нет в наличии

Без скобок if управлял бы только одной следующей инструкцией. Здесь присваивание подписи и вывод кнопки должны зависеть от одного условия.

Лишняя точка с запятой создаёт пустое телоСамостоятельный пример
<?php
$allowed = false;
// Намеренная ошибка для наблюдения:
if ($allowed);
{
    echo "Этот блок всё равно выполнился\n";
}
// Исправленная запись:
if ($allowed) {
    echo "Разрешённое действие\n";
}
echo "Проверка закончена\n";
Ожидаемый результат
Этот блок всё равно выполнился
Проверка закончена

Первая ; сама является пустой инструкцией, которой управляет if. Следующий блок уже независим. Это допустимый синтаксис с ошибочным смыслом; похожая опечатка после foreach оставляет цикл без полезного тела.

Конструкции echo, print, array, list и eval тоже входят в грамматику языка, хотя часть описана в справочнике функций. Их нельзя автоматически считать обычными вызываемыми функциями. Выражение и инструкцию подробнее различает глава о выражениях.

if, else и elseif: одна ветка или несколько независимых действий

Условие приводится к bool. Число 0, пустая строка и строка '0' ложны, поэтому вопрос «строка задана?» нельзя всегда заменить на if ($text). Проверяйте именно нужный смысл. В цепочке if/elseif/else выбирается первая подошедшая ветка; у нескольких отдельных if каждый вопрос проверяется заново.

Ход ближайшего опыта
  1. room содержит строку '0'
  2. if(room) выбирает else: логически ложно
  3. Независимый if(room !== '') истинен
  4. Второй вывод: текст не пуст
Комната с номером 0 должна попасть в адресСамостоятельный пример
<?php
$room = '0';
if ($room) {
    echo 'Ошибочный вариант: комната ', $room, "\n";
} else {
    echo "Ошибочный вариант потерял номер комнаты\n";
}
if ($room !== '') {
    echo 'Адрес доставки: комната ', $room, "\n";
}
Ожидаемый результат
Ошибочный вариант потерял номер комнаты
Адрес доставки: комната 0

Строка 0 ложна при логическом преобразовании, поэтому первый if пропускает полезную подпись. Вторая проверка задаёт конкретный вопрос: строка не пустая? Ответ true; номер попадает в адрес. Вход здесь заранее известен как строка. Внешнее значение сначала проверяют на строковый тип, иначе массив тоже не равен пустой строке.

Один тариф, но два независимых признакаСамостоятельный пример
<?php
$total = 7000;
if ($total >= 5000) {
    echo "Тариф: бесплатная доставка\n";
} elseif ($total >= 2000) {
    echo "Тариф: сниженная доставка\n";
} else {
    echo "Тариф: обычная доставка\n";
}
if ($total >= 2000) {
    echo "Начислить бонусы\n";
}
if ($total >= 5000) {
    echo "Показать подарок\n";
}
Ожидаемый результат
Тариф: бесплатная доставка
Начислить бонусы
Показать подарок

После первого подходящего тарифа остальные elseif не проверяются. Бонусы и подарок независимы, поэтому оба if выполняют свои действия. Это локальная модель правил, без платежей или записи в базу.

В обычной форме со скобками else if допустим как вложенный if после else и даёт привычное ветвление. В альтернативной форме с двоеточием требуется слитное elseif. Вложенность можно делать глубже, но ранние возвраты часто позволяют читать проверку без большой лесенки.

К какому if относится else

Без фигурных скобок else связывается с ближайшим незавершённым if, независимо от отступа. Для интерфейса это может означать, что предложение войти в аккаунт вообще не появится.

Ход ближайшего опыта
  1. signedIn = false
  2. В первом фрагменте внутренний if и его else не посещаются
  3. Общая строка всё равно печатается
  4. Исправленная внешняя ветка else печатает «Войти»
Неверный отступ не меняет границу веткиСамостоятельный пример
<?php
$signedIn = false;
$hasOrders = true;
// Намеренно запутанная запись без скобок:
if ($signedIn)
    if ($hasOrders)
        echo "Заказы\n";
else
    echo "Войти\n";
echo "После первой проверки\n";

if ($signedIn) {
    if ($hasOrders) {
        echo "Заказы\n";
    }
} else {
    echo "Войти\n";
}
Ожидаемый результат
После первой проверки
Войти

Первый else принадлежит hasOrders, до которого программа не дошла из-за signedIn=false. Во второй записи скобки явно прикрепляют else к проверке входа. HTML-кнопка сама по себе не является серверной проверкой доступа.

Двоеточие и закрывающее слово: читаемый PHP-шаблон

Вместо открывающей фигурной скобки пишут :, а вместо закрывающей — специальное слово с ;. Условия и число итераций от этого не меняются. Меняется граница, которую человек видит среди HTML.

НачалоКонец
if (...):endif;
while (...):endwhile;
for (...):endfor;
foreach (...):endforeach;
switch (...):endswitch;
declare (...):enddeclare;
Карточка с пустым остатком
  1. PHP выбирает elseif stock === 0
  2. HTML этой ветки отправляется в ответ
  3. Чужие ветки пропускаются
  4. endif возвращает к общему шаблону
HTML есть только у выбранного состоянияСамостоятельный пример
<?php $stock = 0; ?>
<?php if ($stock > 0): ?>
<p>Можно заказать</p>
<?php elseif ($stock === 0): ?>
<p>Пока закончился</p>
<?php else: ?>
<p>Уточните остаток</p>
<?php endif; ?>
Ожидаемый результат
<p>Пока закончился</p>

Код проверен в CLI: ожидаемый результат — байты HTML, которые сервер передал бы браузеру. В этом примере текст постоянный; пользовательские строки перед выводом экранируют.

Те же границы для while, for и switchСамостоятельный пример
<?php
$remaining = 1;
while ($remaining > 0):
    echo "W\n";
    $remaining--;
endwhile;
for ($label = 1; $label <= 2; $label++):
    echo 'F', $label, "\n";
endfor;
switch ('pickup'):
    case 'pickup':
        echo "Самовывоз\n";
        break;
    default:
        echo "Доставка\n";
endswitch;
Ожидаемый результат
W
F1
F2
Самовывоз

У do…while и match такой пары end-слов нет. Альтернативный foreach с экранированием уже показан ниже в примере списка товаров. declare будет разобран отдельно.

Неверно: else if с двоеточиемФрагмент или команда
<?php
if (false):
    echo 'A';
else if (true):
    echo 'B';
endif;

Намеренная синтаксическая ошибка: после else ожидается двоеточие, а не if. Исправление: elseif (true):. Не смешивайте для одного блока двоеточие и закрывающую фигурную скобку.

Неверно: HTML между switch и первым caseФрагмент или команда
<?php
switch ('pickup'): ?>
  <p>Промежуточный текст</p>
<?php case 'pickup':
    break;
endswitch;

Здесь текст является T_INLINE_HTML там, где нужны case/default/endswitch. Даже отправляемые пробелы могут стать причиной ошибки. Оставляйте первый case внутри того же PHP-участка; не полагайтесь на поглощение одного переноса строки закрывающим тегом.

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

В for (начало; проверка; шаг) обе точки с запятой разделяют три части. Любую часть можно оставить пустой. Пустая проверка означает true: нужен иной явный выход. В части с несколькими выражениями запятая разделяет действия; в проверке решает только результат последнего выражения.

Два счётчика одного for
  1. Один раз: boxIndex = 0, shelfNumber = 3
  2. Перед телом: shelfNumber >= 1?
  3. Чай идёт на место 3, Кофе на 2, Какао на 1
  4. После каждого тела: boxIndex++, shelfNumber--; новая проверка
Подписать коробки и соответствующие места в обратном порядкеСамостоятельный пример
<?php
$boxes = ['Чай', 'Кофе', 'Какао'];
for ($boxIndex = 0, $shelfNumber = 3; $shelfNumber >= 1; $boxIndex++, $shelfNumber--) {
    echo $boxes[$boxIndex], ': место ', $shelfNumber, "\n";
}
for ($number = 1; false, $number <= 2; $number++) {
    echo 'Проверка запятой: этикетка ', $number, "\n";
}
Ожидаемый результат
Чай: место 3
Кофе: место 2
Какао: место 1
Проверка запятой: этикетка 1
Проверка запятой: этикетка 2

В инициализации boxIndex получает 0, shelfNumber — 3. После каждой подписи первый увеличивается, второй уменьшается: пары 0/3, 1/2, 2/1. Когда shelfNumber становится 0, тело больше не выполняется. Второй цикл намеренно показывает ловушку: false перед запятой не запрещает проход, решение задаёт последнее выражение number <= 2. Для двух обязательных условий используют &&.

Пустая проверка с ограниченным выходомСамостоятельный пример
<?php
$attempt = 1;
for (;;) {
    echo 'Попытка ', $attempt, "\n";
    if ($attempt === 2) {
        break;
    }
    $attempt++;
}
Ожидаемый результат
Попытка 1
Попытка 2

Этот опыт ограничен двумя проходами. При известном пределе обычный for с attempt<=2 короче объясняет цель. Пустые части полезно узнавать в чужом коде, но они не снимают обязанность обеспечить остановку.

Проверка условия может повторно вызывать загрузку числа этикетокСамостоятельный пример
<?php
function numberOfLabels(): int
{
    echo "Получаем число этикеток: 2\n";
    return 2;
}
echo "Первый вариант\n";
for ($number = 1; $number <= numberOfLabels(); $number++) {
    echo 'Этикетка заказа №', $number, "\n";
}
echo "Второй вариант\n";
$limit = numberOfLabels();
for ($number = 1; $number <= $limit; $number++) {
    echo 'Этикетка заказа №', $number, "\n";
}
Ожидаемый результат
Первый вариант
Получаем число этикеток: 2
Этикетка заказа №1
Получаем число этикеток: 2
Этикетка заказа №2
Получаем число этикеток: 2
Второй вариант
Получаем число этикеток: 2
Этикетка заказа №1
Этикетка заказа №2

NumberOfLabels каждый раз возвращает int 2 прямо в сравнение number <= ... . Поэтому первый вариант вызывает её трижды: перед номерами 1, 2 и неуспешной проверкой 3. Второй сохраняет return 2 в limit один раз. Это правильно, только если число этикеток не должно меняться во время цикла. Функция учебная: сообщения показывают вызовы, сетевой запрос не выполняется.

Обычный count($array) берёт уже хранимый размер массива и не обходит все элементы. Нельзя по этому примеру объявить его медленным. COUNT_RECURSIVE, пользовательский Countable и собственный расчёт могут выполнять дополнительную работу. Для готового массива обычно удобнее foreach; сетевой запрос внутри условия гораздо существеннее выбора символа ++.

while проверяет условие между проходами, не после каждой строки

Изменение флага в середине тела не прерывает оставшиеся строки. Для немедленного выхода существует break; continue завершает текущий проход. В for после continue выполняется шаг, а в while приходится самим позаботиться об изменении условия.

Ход ближайшего опыта
  1. running = true: вход разрешён
  2. Тело меняет running на false
  3. Остаток начатого тела ещё выполняется
  4. Следующая проверка заканчивает цикл
Флаг снят, но начатое тело заканчиваетсяСамостоятельный пример
<?php
$running = true;
while ($running) {
    echo "Начали проход\n";
    $running = false;
    echo "Закончили начатый проход\n";
}
echo "После цикла\n";
Ожидаемый результат
Начали проход
Закончили начатый проход
После цикла

while снова проверит running только у границы следующего прохода. Если дальнейшая работа уже недопустима, после изменения флага нужен break, а не надежда на немедленную реакцию условия.

Шаг выполнен до возможного continueСамостоятельный пример
<?php
$number = 0;
while ($number < 4) {
    $number++;
    if ($number === 2) {
        continue;
    }
    echo $number, "\n";
}
Ожидаемый результат
1
3
4

При number=2 остаток тела пропускается, но счётчик уже увеличен. Если поставить единственный ++ после continue, можно застрять на значении 2. В интеграции кроме числа повторов нужны сроки и договор повторного запроса.

do…while(false): один блок с ранним выходом

Обычная задача do…while — сначала получить порцию, потом решить о продолжении. В старых проектах встречается и do {...} while (false);: блок выполняется один раз, а break позволяет досрочно дойти до кода после него. Это не функция и не возврат её результата.

Ход ближайшего опыта
  1. Вход в do без предварительного условия
  2. Пустой email выбирает причину отказа
  3. break выводит из однократного блока
  4. Общий итог после блока выполняется
Отказ в подготовке не пропускает общий итогСамостоятельный пример
<?php
$email = '';
$result = 'Отправка не подготовлена';
do {
    if ($email === '') {
        $result = 'Не указан адрес';
        break;
    }
    $result = 'Можно подготовить письмо';
} while (false);
echo $result, "\n";
echo "Общий итог записан\n";
Ожидаемый результат
Не указан адрес
Общий итог записан

Письмо не отправляется: показан лишь выбор результата. Для нового кода ту же задачу часто проще выразить функцией с ранним return или if/else. do…while не имеет альтернативного enddo-синтаксиса; после while(...) нужна ;.

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

В обычном foreach очередное значение присваивается переменной цикла. Новое присваивание этой переменной само по себе не записывает новый элемент массива. С явной ссылкой &$value поведение другое; существующий раздел ниже показывает обязательное завершение такой связи.

Ход ближайшего опыта
  1. stocks: A=2, B=3
  2. Первый цикл меняет только переменную stock
  3. В источнике остаются 2,3
  4. Второй цикл пишет по ключам: источник становится 12,13
Показать, какое присваивание действительно пополнило складСамостоятельный пример
<?php
$stocks = ['TEA-01' => 2, 'COF-02' => 3];
foreach ($stocks as $stock) {
    $stock += 10;
}
echo "После изменения только переменной цикла\n";
foreach ($stocks as $sku => $stock) {
    echo $sku, ': ', $stock, ' шт.', "\n";
}
foreach ($stocks as $sku => $stock) {
    $stocks[$sku] = $stock + 10;
}
echo "После записи в элемент склада\n";
foreach ($stocks as $sku => $stock) {
    echo $sku, ': ', $stock, ' шт.', "\n";
}
Ожидаемый результат
После изменения только переменной цикла
TEA-01: 2 шт.
COF-02: 3 шт.
После записи в элемент склада
TEA-01: 12 шт.
COF-02: 13 шт.

В первом цикле stock по очереди получает 2 и 3, но увеличение этого отдельного имени не пишет в массив stocks. Во втором изменяющем цикле sku получает ключ TEA-01 или COF-02; запись stocks[sku] адресует реальный элемент. Последующий отчёт читает уже 12 и 13. Для объектов значение элемента содержит идентификатор объекта: изменение свойства — отдельное правило, оно не отменяет разницу присваиваний.

Обход не передвигает внутренний указатель currentСамостоятельный пример
<?php
$items = ['Чай', 'Кофе'];
echo current($items), "\n";
foreach ($items as $item) {
    echo $item, "\n";
}
echo current($items), "\n";
Ожидаемый результат
Чай
Чай
Кофе
Чай

current смотрит на внутренний указатель массива; foreach имеет свой механизм обхода. Не используйте current как способ узнать, на каком элементе сейчас находится foreach.

После непустого цикла переменная элемента обычно остаётся с последним присвоенным значением: фигурные скобки не создают отдельную область переменных. При пустом массиве присваивания вообще не было. Для скаляра или неопределённой переменной foreach выдаёт диагностику, а @foreach синтаксически не является способом её подавить. Данные API сначала проверяют как массив нужной формы.

Разобрать строку таблицы прямо в заголовке foreach

Строка источникаКак читается заголовокЧто попадёт в дальнейший код
[A, internal-1, 2][$sku, , $quantity] берёт позиции 0 и 2sku = A, quantity = 2; служебный столбец не назначен.
[quantity => 2, sku => A]['sku' => $sku, 'quantity' => $quantity] выбирает ключиТе же A и 2 независимо от порядка полей.

Если каждый элемент — массив с известной структурой, as [$sku, $quantity] раскладывает его элементы 0 и 1 в две переменные. Пропуск между запятыми игнорирует позицию. list(...) — более старая запись того же разбора; квадратные скобки доступны с PHP 7.1.

Ход ближайшего опыта
  1. Выбрать ['A', 'internal-1', 2]
  2. Позиция 0 → sku; позиция 1 пропущена; позиция 2 → quantity
  3. Вывести A:2
  4. Повторить для строки B с количеством 0
Две колонки и пропуск служебной позицииСамостоятельный пример
<?php
$rows = [['A', 'internal-1', 2], ['B', 'internal-2', 0]];
foreach ($rows as [$sku, , $quantity]) {
    echo $sku, ':', $quantity, "\n";
}
foreach ($rows as list($sku)) {
    echo 'Код ', $sku, "\n";
}
Ожидаемый результат
A:2
B:0
Код A
Код B

Лишние позиции не мешают: их можно не назначать переменным. Недостающая позиция в PHP 8 вызывает предупреждение Undefined array key и даёт null; старые описания могут называть это notice. Поэтому произвольную внешнюю строку до распаковки проверяют.

Именованные поля вместо догадки о порядке колонокСамостоятельный пример
<?php
$rows = [['quantity' => 2, 'sku' => 'A'], ['sku' => 'B', 'quantity' => 0]];
foreach ($rows as ['sku' => $sku, 'quantity' => $quantity]) {
    echo $sku, ':', $quantity, "\n";
}
Ожидаемый результат
A:2
B:0

Ключи выбираются явно, поэтому перестановка полей не меняет смысл. Это распаковка уже проверенной схемы, а не проверка наличия и типов полей. Подробные варианты находятся в главе массивов.

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

Объект тоже можно обходить, но не любой объект является iterable

У обычного объекта foreach видит свойства, доступные из текущей области. public означает доступ снаружи, private ограничивает его классом. Объект Traversable может задать иной порядок через Iterator/IteratorAggregate. Функция is_iterable проверяет массив или Traversable, поэтому обычный объект с доступными свойствами способен дать false и всё же допускать такой обход свойств.

Ход ближайшего опыта
  1. Создать StockSummary: public available=4, private internalLimit=100
  2. is_iterable возвращает false: это не массив и не Traversable
  3. foreach снаружи выбирает доступные свойства
  4. Вывести available:4; private поле не выдано
Снаружи перебираются только публичные свойстваСамостоятельный пример
<?php
class StockSummary {
    public $available = 4;
    private $internalLimit = 100;
}
$summary = new StockSummary();
var_dump(is_iterable($summary));
foreach ($summary as $name => $value) {
    echo $name, ':', $value, "\n";
}
Ожидаемый результат
bool(false)
available:4

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

Смысл class/new и ограничений доступа объясняется сначала на простом объекте, а управляемый обход — в итераторах и генераторах.

break и continue считают уровни конструкций, а не фигурные скобки

У каждого выхода есть конкретное место назначения. До запуска покажите пальцем, где программа продолжится: после switch, у следующего элемента внешнего foreach или после всего foreach. Фигурные скобки if не добавляют уровень к числу break/continue.

По умолчанию уровень равен 1. Уровнями считаются for, foreach, while, do…while и switch; обычный if не добавляет уровень. break 2 выходит из двух конструкций. continue 2 завершает текущий проход внешнего цикла; в for перед новой проверкой выполнится его шаг.

foreach → switch → continue 2
  1. foreach выбрал запись B
  2. switch распознал тип skip
  3. Пропускаются switch и оставшееся тело этого foreach
  4. foreach начинает запись C
Пропустить запись или только закончить switchСамостоятельный пример
<?php
$rows = ['A' => 'normal', 'B' => 'skip', 'C' => 'stop'];
foreach ($rows as $id => $type) {
    switch ($type) {
        case 'skip':
            continue 2;
        case 'stop':
            echo 'Остановка на ', $id, "\n";
            break 2;
        default:
            break;
    }
    echo 'Обработана ', $id, "\n";
}
echo "После обхода\n";
Ожидаемый результат
Обработана A
Остановка на C
После обхода

break в default завершил только switch, поэтому A дошла до общего вывода. B целиком пропущена. break 2 на C завершил и switch, и foreach. Ни одна из конструкций не завершает окружающую функцию — это сделал бы return.

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

continue не перескакивает мимо шага for. Это отличается от ручного ++ в конце тела while, который continue способен пропустить.

Неверно: вычислять уровень через переменнуюФрагмент или команда
<?php
$levels = 1;
while (true) {
    break $levels;
}

В PHP 8 уровень задают положительным целым литералом и не превышают число охватывающих конструкций. Переменная здесь недопустима. Это compile error, а не случай, который можно поймать соседним catch.

Старый continue вместо break в switchФрагмент или команда
<?php
switch ('skip') {
    case 'skip':
        continue;
}

Форма распознаётся, но начиная с PHP 7.3 выдаёт E_WARNING: continue целится в switch и ведёт себя как break. Для выхода из switch пишите break; для внешнего цикла — соответствующий уровень continue. Здесь фрагмент только lint-проверяется.

switch: начало выполнения выбирает case, конец задаёт break

Выражение после switch вычисляется один раз. PHP ищет первое совпадение по ==, затем выполняет инструкции от найденной метки до break или конца блока. После начала исполнения следующая метка case уже не проверяется как новое условие: это и есть проваливание.

Ход ближайшего опыта
  1. verbosity = 'debug'
  2. Войти в case debug
  3. Без break продолжить инструкции info
  4. Без break дойти до общего итога default
Явное каскадное включение сообщенийСамостоятельный пример
<?php
$verbosity = 'debug';
switch ($verbosity) {
    case 'debug':
        echo "Технические подробности\n";
        // Намеренно продолжаем: debug включает и info.
    case 'info':
        echo "Ход обработки\n";
        // Оба режима включают итог.
    default:
        echo "Итог\n";
}
Ожидаемый результат
Технические подробности
Ход обработки
Итог

Это осознанный каскад для локального отчёта. В выборе одного тарифа такое отсутствие break было бы ошибкой. Когда общий вывод нужен всегда, часто проще вынести его после switch.

Источник кода читается один раз; несколько case могут иметь одно телоСамостоятельный пример
<?php
function selectedMethod()
{
    echo "Читаем способ\n";
    return 'locker';
}
switch (selectedMethod()) {
    case 'pickup':
    case 'locker':
        echo "Получение в пункте\n";
        break;
    case 'courier':
        echo "Получение дома\n";
        break;
    default:
        echo "Способ неизвестен\n";
}
Ожидаемый результат
Читаем способ
Получение в пункте

Пустой case объединяет варианты. Сложность selectedMethod не умножилась на число сравнений, но это не универсальное обещание, что switch быстрее if: значение можно заранее сохранить и для if.

default может стоять раньше остальных case, но выбирается лишь при отсутствии совпадения; после входа в него действует обычное проваливание. Для ясности ставьте его последним. Без совпадения и без default блок ничего не делает. Два default — ошибка компиляции. Значение case может быть выражением: оно вычисляется само и сравнивается с результатом switch; произвольное условие обычно яснее записать через if либо switch(true).

Диапазоны требуют логического предмета сравненияСамостоятельный пример
<?php
$weight = 7;
switch (true) {
    case $weight <= 2:
        echo "Малая посылка\n";
        break;
    case $weight <= 10:
        echo "Средняя посылка\n";
        break;
    default:
        echo "Крупная посылка\n";
}
Ожидаемый результат
Средняя посылка

Каждый case даёт bool, который сравнивается с true. Нельзя механически поставить switch($weight), оставив логические case: это будут не те сравнения. Вход примера заранее проверен как положительный вес.

После case/default пишите двоеточие. Исторически PHP принимал там и точку с запятой; с PHP 8.5 эта форма deprecated. Альтернативная форма switch(...): … endswitch; сохраняет тот же выбор и break.

match: строгий результат и вычисление только нужной ветки

Из проверки получается значение
  1. Match получает один предмет сравнения и ищет строгий === вариант; строка "2" отличается от int 2.
  2. Выражение выбранной ветки даёт значение всего match; чужие результаты не вычисляются.
  3. Присваивание сохраняет результат в переменную, дальнейший echo или расчёт использует её.
  4. Без совпадения и default выбрасывается UnhandledMatchError; этот путь не возвращает пустую строку автоматически.

match доступен с PHP 8.0. Выражение-предмет вычисляется один раз, условия веток проверяются по очереди через ===. После первого совпадения вычисляется только его правая часть. Несколько вариантов перед одной стрелкой разделяются запятой. Если результат присваивается или match стоит отдельной инструкцией, после выражения нужна ;.

Ход ближайшего опыта
  1. code содержит строку '1'
  2. switch сравнивает её с числом 1 через == и выбирает case
  3. match сравнивает через ===
  4. Совпадает строковая ветка '1', а не числовая 1
Строковый код и числовой код — разные договорыСамостоятельный пример
<?php
$code = '1';
switch ($code) {
    case 1:
        echo "switch: совпало с числом\n";
        break;
}
echo match ($code) {
    1 => 'match: число',
    '1' => 'match: строка',
}, "\n";
Ожидаемый результат
switch: совпало с числом
match: строка

Внешние коды часто приходят строками. Выберите тип по договору API; не исправляйте любой непонятный вход простым (int), который может превратить мусор в 0.

Оставшиеся проверки и чужой результат не вычисляютсяСамостоятельный пример
<?php
function candidate($value)
{
    echo 'Проверка ', $value, "\n";
    return $value;
}
function unusedResult()
{
    echo "Чужой результат\n";
    return 'unused';
}
$label = match (2) {
    candidate(1) => unusedResult(),
    candidate(2), candidate(3) => 'Подготовлено',
    default => 'Нет варианта',
};
echo $label, "\n";
Ожидаемый результат
Проверка 1
Проверка 2
Подготовлено

candidate выводит метку проверки и возвращает переданный код. Проверка 3 и unusedResult не нужны. Правая часть ветки — одно выражение; несколько последовательных действий вынесите в понятную функцию или используйте обычный блок if/switch.

Неизвестный вариант и осмысленный match(true)Самостоятельный пример
<?php
try {
    $fee = match ('unknown') {
        'pickup' => 0,
        'courier' => 300,
    };
} catch (UnhandledMatchError) {
    echo "Нет правила доставки\n";
}
$weight = 7;
$size = match (true) {
    $weight <= 2 => 'S',
    $weight <= 10 => 'M',
    default => 'L',
};
echo $size, "\n";
Ожидаемый результат
Нет правила доставки
M

Полнота match означает, что для текущего значения нужен подходящий вариант или default. PHP не доказывает при компиляции полноту вашего предметного справочника. Повторный default запрещён. В match(true) условия должны возвращать именно bool; произвольное число не тождественно true.

declare задаёт директиву, а не вызывает функцию

У declare три поддерживаемые директивы: ticks, encoding и strict_types. PHP читает их при компиляции исходника, поэтому значение должно быть литералом, а не переменной или константой. Правила формы зависят от директивы: ticks допускает участок, strict_types задают первой инструкцией файла.

ДирективаНазначениеГраница
strict_types=1Строгие скалярные вызовы из файлаНе проверяет поля массива и не превращает сайт в глобально типизированный.
ticks=NСобытия через N подходящих инструкцийНе миллисекунды и не параллельный поток.
encoding='…'Кодировка исходного PHP-текстаТребует настроенной поддержки; не HTTP Content-Type.
Путь данных ближайшего примера
  1. Example.php объявляет strict_types=1 и загружает определение из boxes.php.
  2. Строка "2" отклоняется на входе строгого вызова.
  3. Int 2 поступает в boxes → return 12 → строка накладной.
  4. Weak-caller.php делает свой вызов: строка приводится к 2, возврат также 12.

В архиве три файла: example.php делает строгий вызов; boxes.php определяет расчёт; weak-caller.php делает отдельный вызов без strict_types. Каждый файл начинается своим тегом PHP. Тип int у параметра boxes описывает количество, а : int после скобок — число предметов в результате. Функция умножает количество коробок на шесть; вызывающий код использует возврат для строки накладной.

Один расчёт вызывается из двух файлов с разными правилами типовСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__ . '/boxes.php';
try {
    itemsInBoxes('2');
} catch (TypeError) {
    echo "Строгий вызов: строка количества отклонена\n";
}
$items = itemsInBoxes(2);
echo 'Накладная строгого вызова: ', $items, ' предметов', "\n";
require __DIR__ . '/weak-caller.php';
Ожидаемый результат
Строгий вызов: строка количества отклонена
Накладная строгого вызова: 12 предметов
Накладная нестрогого вызова: 12 предметов

Первый вызов передаёт строку "2" из строгого example.php: TypeError возникает до тела функции. Второй передаёт int 2; местный boxes получает 2, return возвращает 12 в items, echo использует его в накладной. При подключении weak-caller.php исполняется вызов, записанный уже в этом файле: строка "2" допускает приведение к int 2 и тоже даёт 12. Место объявления функции не сделало все вызовы строгими. Проверку возвращаемого типа определяет файл объявления; int допустим для float. Вызовы через внутренние функции имеют отдельные особенности.

Неверно: константа как значение ticksФрагмент или команда
<?php
const INTERVAL = 2;
declare(ticks=INTERVAL);

Директивы требуют буквального значения в исходнике. Исправление — declare(ticks=2);, а не const или переменная. Это проверка компиляции.

declare(ticks=1); действует на последующий код этого файла; declare(ticks=1) {...} ограничивает участок, declare(ticks=1): ... enddeclare; — его альтернативная форма. Директива в подключённом файле не меняет автоматически родительский файл. Подробный договор скалярных типов раскрыт в главе типов.

Tick — событие инструкции, а не таймер ожидания

Это специализированное средство наблюдения за выполнением. До опыта нужны анонимная функция и use, общая ссылка на переменную и try/finally. Регистрация передаёт PHP само действие handler; она не вызывает его как обычный $handler() в этой строке. Когда PHP создаёт tick-событие, тело обработчика записывает новое значение stage в seen; после снятия регистрации этого побочного действия больше нет.

Сначала регистрируют обработчик: функцию, которую PHP вызовет на tick-событии. Затем участок с ticks=N создаёт события по числу подходящих инструкций. Не каждая операция является tickable: выражения условий и аргументов обычно не дают отдельного tick. Количество строк редактора не равно количеству событий.

Путь данных ближайшего примера
  1. Начало: stage=lastSeen, seen пуст.
  2. Tick после присваивания замечает «Товары собраны».
  3. Следующее изменение даёт «Заказ упакован».
  4. После unregister этап доставки меняется, но журнал остаётся из двух прежних строк.

Сделаем журнал наблюдавшихся этапов. Переменная stage хранит текущую подпись; seen — список замеченных изменений. Callback сравнивает stage с lastSeen: одинаковые повторные наблюдения не дублируются. Use с & связывает его с тремя переменными внешнего кода. Такой журнал демонстрирует наблюдение, а не надёжную запись бизнес-событий: tick не привязан к каждому изменению произвольной переменной.

Увидеть содержимое журнала до снятия обработчикаСамостоятельный пример
<?php
$stage = 'Начало';
$lastSeen = $stage;
$seen = [];
$handler = function () use (&$stage, &$lastSeen, &$seen) {
    if ($stage !== $lastSeen) {
        $seen[] = $stage;
        $lastSeen = $stage;
    }
};
register_tick_function($handler);
try {
    declare(ticks=1):
        $stage = 'Товары собраны';
        $stage = 'Заказ упакован';
    enddeclare;
} finally {
    unregister_tick_function($handler);
}
declare(ticks=1) {
    $stage = 'Передан в доставку';
}
foreach ($seen as $observed) { echo 'Наблюдали: ', $observed, "\n"; }
echo 'Текущий этап: ', $stage, "\n";
Ожидаемый результат
Наблюдали: Товары собраны
Наблюдали: Заказ упакован
Текущий этап: Передан в доставку

После двух присваиваний участка ticks=1 обработчик замечает строки этапов и добавляет их в seen. Дополнительное событие без изменения stage не добавит дубликат. Finally снимает именно handler. Последующий участок меняет stage на «Передан в доставку», но записи этого этапа в seen нет: функция больше не зарегистрирована. Echo показывает и настоящий список наблюдений, и текущее значение. Это не утверждение о двух tick-событиях: число событий и число записанных изменений — разные величины. Явные записи журнала в нужных местах прикладной операции обычно легче контролировать.

Регистрация допускает несколько обработчиков. Callback выполняется синхронно и добавляет работу к программе; он не прерывает уже блокирующий системный/сетевой вызов. Для таймаутов HTTP используйте параметры клиента, для фонового задания — очередь. В прикладном импорте явный вызов проверки после каждой порции обычно легче контролировать. С PHP 8.5 handlers остаются активными дольше при shutdown — до завершения shutdown-функций, деструкторов и очистки output handlers; не оставляйте временную регистрацию случайно.

Путаница между строкой редактора и tick видна в вопросе о двух вызовах обработчика. Поэтому пример показывает наблюдавшиеся данные и явно отделяет их от количества внутренних событий. Это синхронный callback: ожидание сети само по себе не запускает его по таймеру.

encoding описывает исходный файл, а не кодировку ответа

Директива declare(encoding='…'); относится к разбору текста PHP. Для Zend Multibyte нужны доступное расширение mbstring и настройка zend.multibyte. Обычный UTF-8 исходник не требует включать этот режим. Настройка zend.script_encoding задаёт исходную кодировку по умолчанию; директива файла имеет приоритет.

Ход ближайшего опыта
  1. До компиляции окружение включает Zend Multibyte при необходимости
  2. declare сообщает кодировку исходного текста
  3. PHP разбирает инструкции этого файла
  4. Кодировка HTTP-ответа остаётся отдельной настройкой
Синтаксис для окружения с настроенным Zend MultibyteФрагмент или команда
<?php
declare(encoding='UTF-8');
namespace Workshop\Source;
echo "Исходник UTF-8\n";

Контекстный фрагмент: запуск зависит от mbstring и zend.multibyte, включённого до компиляции. С namespace разрешена форма declare(encoding='…');, а не оборачивающий namespace блок declare(...){...}. Эта директива не заменяет header('Content-Type: ...; charset=UTF-8') и не преобразует автоматически данные внешнего API.

Не добавляйте encoding как попытку исправить любые испорченные символы. Отдельно определите байты исходного файла, входных данных, соединения с базой и HTTP-ответа. Для строк см. кодировки и байты, для ответа — HTTP-заголовки.

return выбирает границу: вызов функции, подключённый файл или основной скрипт

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

В функции return $value; завершает текущий вызов и подставляет значение в место вызова. Он не печатает значение. return; без выражения отдаёт null. Это конструкция, скобки как у вызова не нужны; пустое return(); — ошибка синтаксиса.

Первый вызов showDiscount(false)
  1. enabled получает false
  2. Условие !enabled истинно
  3. return завершает вызов со значением null
  4. Внешний var_dump печатает NULL; программа продолжается
Ранний отказ заканчивает только один вызовСамостоятельный пример
<?php
function showDiscount($enabled)
{
    if (!$enabled) {
        return;
    }
    echo "Скидка включена\n";
}
var_dump(showDiscount(false));
showDiscount(true);
echo "Основная программа продолжается\n";
Ожидаемый результат
NULL
Скидка включена
Основная программа продолжается

false привёл к раннему return без вывода. В успешной ветке echo делает действие; конец функции тоже даёт null, но второй вызов не отправлен в var_dump. Возврат и побочный вывод — независимые вещи.

Получить настройки из файла и продолжить расчёт основного скриптаСамостоятельный пример
<?php
echo "Начинаем чек\n";
$settings = require __DIR__ . '/settings.php';
$itemsTotal = 300;
$total = $itemsTotal + $settings['delivery'];
echo $settings['shop'], ': ', $total, ' руб.', "\n";
echo "Чек завершён\n";
return;
echo "Эта строка основного файла не выполняется\n";
Ожидаемый результат
Начинаем чек
Мастерская: 350 руб.
Чек завершён

Return внутри settings.php отдаёт массив в выражение require. Затем внешнее присваивание сохраняет его в settings и основной файл продолжает работу: берёт delivery=50 и считает 300+50=350. Код после return в дочернем файле не исполняется. Позднее отдельный return основного example.php завершает уже его код: последняя строка не печатается. В каждом случае границу задаёт место return. Это не установка системного кода завершения процесса через exit.

При объявленном типе результата соблюдайте его договор: в обычной функции с : int нельзя писать пустой return, а : void допускает только return без значения. Возврат по ссылке и тип never разобраны в главе функций. Внутри eval return заканчивает исполняемую строку и отдаёт значение этому eval; это не причина заменять обычную функцию выполнением строки кода. Отдельный exit завершает процесс/запрос и имеет договор exit-status; число после return основного PHP-файла не является заменой кода завершения exit.

Подключение исполняет файл в области переменных точки вызова

Все многoфайловые опыты ниже доступны отдельными ZIP: в них example.php и названные рядом файлы. Распакуйте архив в отдельную папку, сохраните вложенные каталоги и выполните php example.php. Подключаемый файл исполняется в точке include/require: его вывод виден сразу, а его return может дать значение окружающему присваиванию.

include/require — конструкции языка, а не обычные функции: нельзя вызывать их через переменную с именем или передавать им именованные аргументы. Подключаемый файл начинает разбираться вне PHP-режима: его PHP-код требует собственного <?php. Текст вне тегов сразу попадает в вывод.

Путь данных ближайшего примера
  1. ShowLine получает title=Чай, price=150, quantity=2.
  2. Include читает эти местные значения и создаёт rowTotal=300.
  3. Файл выводит строку заказа; return функции отдаёт 300.
  4. Внешний orderTotal получает 300, внешний title остаётся «Общий каталог».

Подключаемый файл ниже читает местные title, price и quantity, считает rowTotal и печатает одну строку. Функция затем явно возвращает rowTotal; только этот return доставляет число во внешний orderTotal. Include сам не превращает локальные переменные в глобальные.

Шаблон использует местные данные и оставляет расчёт для функцииСамостоятельный пример
<?php
$title = 'Общий каталог';
function showLine($title, $price, $quantity): int {
    include __DIR__ . '/line.php';
    return $rowTotal;
}
$orderTotal = showLine('Чай', 150, 2);
echo 'К оплате: ', $orderTotal, ' руб.', "\n";
echo 'Внешний заголовок: ', $title, "\n";
Ожидаемый результат
Чай: 2 × 150 = 300 руб.
К оплате: 300 руб.
Внешний заголовок: Общий каталог

Вызов showLine передаёт три аргумента в местные параметры: Чай, 150 и 2. Include исполняет line.php в этой области: созданный там rowTotal=300 доступен следующему return функции. Число возвращается в orderTotal и используется в итоге. Внешний title остаётся «Общий каталог», потому что параметр title принадлежит вызову. RowTotal не появился снаружи; его значение вышло через return. Если убрать return функции, присваивание orderTotal не получит расчёт автоматически. В HTML-шаблоне дополнительно нужно экранировать данные по контексту.

Загрузить правило номера и использовать его после подключенияСамостоятельный пример
<?php
function loadOrderHelpers(): string {
    return include __DIR__ . '/helpers.php';
}
$loadMessage = loadOrderHelpers();
echo $loadMessage, "\n";
$number = orderPrefix() . '-41';
echo 'Накладная: ', $number, "\n";
Ожидаемый результат
Правило номера загружено
Накладная: ORD-41

Внутренний include получает строку из return файла; внешний return loadOrderHelpers передаёт её в loadMessage. Безусловная функция orderPrefix объявлена в helpers.php и доступна после загрузки, хотя её текст ниже return. Её вызов возвращает ORD в соединение со строкой -41, результат ORD-41 сохраняется в number. Условные объявления функций имеют другие правила времени регистрации. Строка echo после return не исполняется; чтение файла внутри функции не сделало объявление orderPrefix локальным.

Это не текстовая подстановка файла в исходник: например, __DIR__ и другие магические константы относятся к месту, где записаны. Повторное обычное include файла с безусловной функцией может закончиться повторным объявлением. Для определения кода используйте согласованную загрузку once или Composer; для повторного отображения шаблона once часто не подходит.

Путь, include_path и выбор разрешённого шаблона

Имя без пути может искаться через include_path, затем учитываются каталог вызывающего скрипта и текущий рабочий каталог. Явный абсолютный путь или путь с ./ либо ../ обходят include_path. Относительный путь с точкой зависит от рабочего каталога; __DIR__ . '/file.php' привязывает нужный файл к каталогу исходника.

Путь данных ближайшего примера
  1. Временный include_path указывает на library.
  2. Короткое settings.php возвращает доставку 50 → итог 350.
  3. Явный __DIR__/settings.php возвращает доставку 80 → итог 380.
  4. Finally восстанавливает прежний путь поиска.

В архиве есть два файла settings.php. Один лежит в library, второй рядом с example.php. Они возвращают одинаковые поля shop/delivery, но разные значения. Get_include_path возвращает старый путь поиска; set_include_path временно задаёт новый; finally восстанавливает прежний даже при ошибке.

Показать, какие настройки на самом деле попали в чекСамостоятельный пример
<?php
$oldPath = get_include_path();
set_include_path(__DIR__ . '/library');
try {
    $settings = include 'settings.php';
    echo $settings['shop'], ': ', 300 + $settings['delivery'], ' руб.', "\n";
    $settings = include __DIR__ . '/settings.php';
    echo $settings['shop'], ': ', 300 + $settings['delivery'], ' руб.', "\n";
} finally {
    set_include_path($oldPath);
}
Ожидаемый результат
Библиотечное правило: 350 руб.
Местное правило: 380 руб.

Первое имя без пути найдено в include_path: массив содержит доставку 50, поэтому чек равен 350. Второе подключение задаёт полный путь от __DIR__, выбирает местный файл с доставкой 80 и даёт 380. Прежняя переменная settings получает новое значение. Это два самостоятельных источника, не магическая перезапись одного файла. Если программа обязана брать конкретную настройку, явный путь делает выбор понятным.

Код раздела выбирает путь из белого спискаСамостоятельный пример
<?php
$page = 'help';
$views = ['help' => __DIR__ . '/views/help.php'];
if (!is_string($page) || !isset($views[$page])) {
    echo "Раздел не найден\n";
} else {
    include $views[$page];
}
Ожидаемый результат
<p>Помощь покупателю</p>

page — код, не фрагмент пути. При значении '../private' ветка подключения не выполнится. В реальном HTTP-входе сначала проверяют тип параметра, права на раздел и подходящий статус ответа; подбор имени файла сам по себе не является авторизацией.

Наличие PHP-кода в строке, JSON или файле не даёт оснований исполнять его через include. Локальное имя file.php?x=1 содержит эти символы имени, а не GET-параметр. Историческое URL-подключение требует разрешающих wrapper/ini-настроек, включая allow_url_include (deprecated с PHP 7.4): удалённая сторона может выполнить PHP у себя, а полученный текст затем разбирается локально. Это другой контекст и потенциальное выполнение чужого кода. Для интеграции получают данные HTTP-клиентом и проверяют их схему; выполняемый PHP загружают из доверенного развёртывания. См. потоки и обёртки и клиент API.

Вывод файла и результат include — разные каналы

Успешное подключение без явного return даёт целое 1. При return из файла результатом становится именно возвращённое значение, в том числе false или null. Поэтому include ... === false само по себе не отличает невозможность открыть файл от его собственного решения вернуть false; нужен известный договор файла и диагностика.

Путь данных ближайшего примера
  1. Heading.php печатает «Чек заказа» и без return даёт int 1.
  2. Settings.php возвращает shop=Мастерская и delivery=50.
  3. Родительский файл считает 300+50 и печатает 350 руб.
  4. Optional.php успешно возвращает false: это решение файла, а не отказ открытия.
Заголовок чека печатается, настройки возвращаются для расчётаСамостоятельный пример
<?php
$headerStatus = include __DIR__ . '/heading.php';
echo 'Результат подключения заголовка: ', $headerStatus, ' (', get_debug_type($headerStatus), ')', "\n";
$settings = require __DIR__ . '/settings.php';
$itemsTotal = 300;
$payable = $itemsTotal + $settings['delivery'];
echo 'Магазин: ', $settings['shop'], "\n";
echo 'К оплате: ', $payable, ' руб.', "\n";
$optional = include __DIR__ . '/optional.php';
if ($optional === false) {
    echo "Необязательная возможность отключена настройкой файла\n";
}
Ожидаемый результат
Чек заказа
Результат подключения заголовка: 1 (int)
Магазин: Мастерская
К оплате: 350 руб.
Необязательная возможность отключена настройкой файла

Heading.php печатает заголовок и заканчивается без return: конструкция include даёт int 1 в headerStatus. Settings.php ничего не печатает, а возвращает массив; settings получает его, доставка 50 используется вместе с товарами 300. Optional.php успешно исполняется и намеренно возвращает false. Поэтому один false не доказывает ошибку открытия: нужен договор подключаемого файла и диагностика.

Присваивайте результат отдельной строкой или оборачивайте всю конструкцию: $result = include $path;, затем сравнение. Запись include($path) == true может сгруппировать сравнение внутри аргумента include из-за приоритета; скобки вокруг похожего «вызова функции» не исправляют смысл.

Шаблон письма превращается в строку через буферСамостоятельный пример
<?php
$title = '<Чай>';
ob_start();
try {
    require __DIR__ . '/message.php';
    $html = ob_get_contents();
} finally {
    ob_end_clean();
}
echo 'Содержимое: ', $html, "\n";
Ожидаемый результат
Содержимое: <strong>&lt;Чай&gt;</strong>

Буфер собирает вывод доверенного шаблона, а не его return. finally закрывает собственный буфер и при исключении. Этот шаблон сам не меняет уровни буферизации. В реальном приложении договор владения буфером должен быть явным; письмо здесь не отправляется.

Более сложное владение буферами, шаблоны и отправка ответа разобраны в главе об output buffering. Для автоматического подключения перед/после основного скрипта существуют auto_prepend_file и auto_append_file в конфигурации; это не замена явному маршруту загрузки проекта.

include и require различаются политикой неудачи

Если файл недоступен, include выдаёт предупреждение и false; require в PHP 8 после диагностики выбрасывает Error. Без перехвата такой Error прекращает обычный ход программы. В старых версиях до 8.0 это был неперехватываемый compile-level отказ. Просто поставить require внутрь try недостаточно, чтобы сделать предшествующее предупреждение бесшумным.

Путь данных ближайшего примера
  1. Optional include: предупреждение → false → разрешённая доставка 50 → итог 350.
  2. Required require: предупреждение → Error; следующего расчёта нет.
  3. Catch сообщает, что чек не сформирован.
  4. В обоих файлах finally восстанавливает обработчик предупреждений.

Два следующих архива самостоятельны. В каждом absent.php специально не поставляется: отсутствие является входом опыта. Чтобы не смешать внутренний путь временной папки с учебным выводом, локальный обработчик E_WARNING сохраняет фиксированную диагностику. Это не меняет договор include/require. Параметры callback поступают от PHP; return true сообщает, что предупреждение обработано, а finally восстанавливает прежнюю настройку.

После отказа необязательного файла применить известное правилоСамостоятельный пример
<?php
$diagnostic = '';
set_error_handler(function ($severity, $message) use (&$diagnostic) {
    $diagnostic = 'Необязательный файл настроек не прочитан';
    return true;
}, E_WARNING);
try {
    $settings = include __DIR__ . '/absent.php';
} finally {
    restore_error_handler();
}
if ($settings === false) {
    echo $diagnostic, "\n";
    $settings = ['delivery' => 50];
}
echo 'Чек с запасным правилом: ', 300 + $settings['delivery'], ' руб.', "\n";
Ожидаемый результат
Необязательный файл настроек не прочитан
Чек с запасным правилом: 350 руб.

Файл absent.php намеренно отсутствует. Include выдаёт предупреждения; временный callback получает severity/message, сохраняет наше диагностическое сообщение и возвращает true, принимая обработку предупреждения на себя. Сам include всё равно даёт false в settings. После восстановления обработчика программа выбирает заранее разрешённое запасное значение 50 и считает 350. Этот договор разрешает только массив либо отказ открытия: произвольный корректный return false у существующего файла потребовал бы отдельного различения. Обработчик здесь относится к узкому учебному участку, а не скрывает все ошибки приложения.

Отказ обязательного правила не выдаёт выдуманный чекСамостоятельный пример
<?php
$diagnostic = '';
set_error_handler(function ($severity, $message) use (&$diagnostic) {
    $diagnostic = 'Обязательный файл правил не прочитан';
    return true;
}, E_WARNING);
try {
    $settings = require __DIR__ . '/absent.php';
    echo 'Чек: ', 300 + $settings['delivery'], ' руб.', "\n";
} catch (Error) {
    echo $diagnostic, "\n";
    echo "Чек не сформирован: исправьте комплект файлов\n";
} finally {
    restore_error_handler();
}
echo "Обработка отказа завершена\n";
Ожидаемый результат
Обязательный файл правил не прочитан
Чек не сформирован: исправьте комплект файлов
Обработка отказа завершена

Require того же отсутствующего файла сначала даёт предупреждение, затем выбрасывает Error. Обработчик предупреждений не отменяет Error. Присваивание settings не получает результат, строка расчёта после require пропускается, catch показывает отказ. Finally восстанавливает прежний обработчик, затем выполняется общий код. Без catch необработанный Error прервал бы обычное выполнение. Это демонстрация отсутствия файла; ParseError внутри найденного файла — другая причина, её нельзя выдавать за нормальную необязательную настройку.

Ошибки кода внутри найденного файла — отдельный случай: например, синтаксическая ошибка подключаемого файла в PHP 8 даёт ParseError. То, что optional include допускает отсутствие файла, не означает «любая ошибка внутри него безопасна». require выбирают для обязательного кода; optional include требует продуманного запасного результата.

once помнит подключение, но не запоминает результат вычисления

include_once и require_once проверяют, исполнялся ли уже этот файл в текущем процессе/запросе. При повторе файл пропускается, результат конструкции — true. Это касается и файла, ранее подключённого обычным include/require. Различие include/require при неудаче сохраняется.

Путь данных ближайшего примера
  1. Первый require_once печатает «Настройки прочитаны» и возвращает массив в settings.
  2. Второй require_once пропускает файл, secondResult получает true.
  3. Два чека используют settings: 300+50=350 и 500+50=550.
  4. Include_once тоже пропускает известный файл: третий результат имеет тип bool.
Сохранить настройки один раз и использовать их для двух чековСамостоятельный пример
<?php
$settings = require_once __DIR__ . '/settings.php';
$secondResult = require_once __DIR__ . '/settings.php';
if ($secondResult === true) {
    echo "Повторное исполнение settings.php пропущено\n";
}
$totals = [300, 500];
foreach ($totals as $itemsTotal) {
    $payable = $itemsTotal + $settings['delivery'];
    echo $settings['shop'], ': к оплате ', $payable, ' руб.', "\n";
}
$thirdResult = include_once __DIR__ . '/settings.php';
echo 'Тип третьего результата: ', get_debug_type($thirdResult), "\n";
Ожидаемый результат
Настройки прочитаны
Повторное исполнение settings.php пропущено
Мастерская: к оплате 350 руб.
Мастерская: к оплате 550 руб.
Тип третьего результата: bool

Первый require_once выполнил файл: видна строка «Настройки прочитаны», а return передал массив в settings. Повторный require_once дал true в secondResult, не повторяя чтение. Два чека используют сохранённый settings, не второй bool. Include_once разделяет сведения о ранее включённых файлах: третий результат тоже true. Once относится к текущему процессу/запросу, не ко всем посетителям сайта.

Повторный шаблон заказа требует повторного выполненияСамостоятельный пример
<?php
$orderId = 41;
include __DIR__ . '/heading.php';
$orderId = 42;
$result = include_once __DIR__ . '/heading.php';
if ($result === true) {
    echo "Once пропустил повторный заголовок\n";
}
include __DIR__ . '/heading.php';
Ожидаемый результат
Заказ №41
Once пропустил повторный заголовок
Заказ №42

Обычный include уже выполнил heading.php при orderId=41. Следующий include_once учитывает это подключение, пропускает файл и даёт true: изменение переменной до 42 не заставляет once снова отобразить шаблон. Последний обычный include выполняет файл ещё раз и печатает новый номер 42. Once полезен для загрузки определений; повторяемое отображение шаблона имеет другой договор. Файл здесь только печатает заголовок, функций не объявляет.

Одинаковый файл и разные файлы с одинаковым кодом — разные вопросы. Не выстраивайте загрузку на различиях регистра путей и символьных ссылках между платформами; используйте единый явный путь. Для классов обычно достаточно автозагрузки Composer. Дополнительная проверка once имеет стоимость, но её нельзя объявить главной причиной медленного сайта без измерения.

goto: переход в той же области, а не вызов другой части программы

Метка записывается finished:, переход — goto finished;. Регистр имени метки важен. Цель должна быть в том же файле и контексте: нельзя войти в функцию/метод или выйти из него переходом, нельзя прыгнуть снаружи внутрь цикла или switch. Из вложенных циклов наружу выйти можно.

Путь данных ближайшего примера
  1. Полка А: TEA и COFFEE не совпадают с CUP.
  2. Полка Б: CUP совпадает, foundShelf получает Б.
  3. Goto выходит к searchFinished; PLATE не проверяется.
  4. Общий код печатает поручение взять CUP с полки Б.
Найти место комплектации и перейти к общему выводуСамостоятельный пример
<?php
$shelves = ['А' => ['TEA', 'COFFEE'], 'Б' => ['CUP', 'PLATE']];
$requested = 'CUP';
$foundShelf = null;
foreach ($shelves as $shelf => $skus) {
    foreach ($skus as $sku) {
        if ($sku === $requested) {
            $foundShelf = $shelf;
            goto searchFinished;
        }
    }
}
searchFinished:
if ($foundShelf === null) {
    echo 'Артикул ', $requested, ' не найден', "\n";
} else {
    echo 'Возьмите ', $requested, ' с полки ', $foundShelf, "\n";
}
Ожидаемый результат
Возьмите CUP с полки Б

Внешний foreach сначала выбирает полку А, внутренний проверяет TEA/COFFEE. Затем shelf=Б, первый sku=CUP совпадает: в foundShelf записывается Б, goto переносит исполнение к метке после обоих циклов. Дальнейшая проверка читает сохранённое Б и составляет поручение. PLATE не посещается. Если заменить запрос на SPOON, обход закончится обычным путём, foundShelf останется null и будет сообщение об отсутствии. Метка не вызывает функцию и не передаёт параметры.

Сравните с отдельной функцией. Ниже return покидает сразу весь вызов, независимо от двух циклов. Полученное место можно использовать многократно, не привязывая внешний код к метке. Break 2 — ещё один вариант выхода именно из двух окружающих циклов.

Выразить тот же поиск отдельной функциейСамостоятельный пример
<?php
function shelfForSku(array $shelves, string $requested): ?string {
    foreach ($shelves as $shelf => $skus) {
        foreach ($skus as $sku) {
            if ($sku === $requested) { return $shelf; }
        }
    }
    return null;
}
$shelves = ['А' => ['TEA', 'COFFEE'], 'Б' => ['CUP', 'PLATE']];
$location = shelfForSku($shelves, 'CUP');
echo 'Место для CUP: ', $location ?? 'не найдено', "\n";
$location = shelfForSku($shelves, 'SPOON');
echo 'Место для SPOON: ', $location ?? 'не найдено', "\n";
Ожидаемый результат
Место для CUP: Б
Место для SPOON: не найдено

Два аргумента становятся местными shelves и requested. При CUP внутренний if находит совпадение, return отдаёт Б из всего вызова в location. При SPOON завершаются оба цикла и последний return отдаёт null; ?? выбирает подпись «не найдено». : ?string допускает строку либо null. Данные полок здесь заранее имеют строковые ключи и списки артикулов. Это тот же линейный поиск с другим выражением границы выхода; если артикул встречается дважды, выбирается первая найденная полка.

Неверно: войти переходом внутрь циклаФрагмент или команда
<?php
goto inside;
for ($i = 0; $i < 2; $i++) {
inside:
    echo $i;
}

Компиляция отвергает goto внутрь loop/switch. Нельзя перескочить подготовку и управление конструкцией. Это отдельный запрещённый фрагмент, он не выполняется.

throw сообщает об отказе и прекращает текущий обычный путь

throw new RuntimeException('причина'); создаёт объект исключения и передаёт его механизму обработки. После throw следующие инструкции этого пути не выполняются: PHP ищет подходящий catch, при необходимости выходит из вызванных функций. Исключение — объект Throwable; обычный объект или строку бросать нельзя.

Проверка строки импорта
  1. readQuantity('bad') не может дать количество
  2. throw InvalidArgumentException
  3. Результат функции не присваивается
  4. Внешний catch записывает понятный отказ; дальше идёт следующая инструкция
Отказ во вложенной функции доходит до вызывающего кодаСамостоятельный пример
<?php
function readQuantity($value)
{
    if (!is_int($value) || $value < 0) {
        throw new InvalidArgumentException('Нужно неотрицательное целое');
    }
    return $value;
}
try {
    $quantity = readQuantity('bad');
    echo "Эта строка не выполняется\n";
} catch (InvalidArgumentException $error) {
    echo $error->getMessage(), "\n";
}
echo "После обработки отказа\n";
Ожидаемый результат
Нужно неотрицательное целое
После обработки отказа

new создаёт объект исключения; catch получает его в error, а ->getMessage() читает сообщение. Это контролируемый текст нашего примера. Сообщения внутренних SQL/файловых ошибок нельзя бездумно показывать посетителю.

В PHP 8 throw можно использовать в выраженииСамостоятельный пример
<?php
$record = [];
try {
    $id = $record['id'] ?? throw new UnexpectedValueException('Нет ID');
} catch (UnexpectedValueException $error) {
    echo $error->getMessage(), "\n";
}
try {
    throw new stdClass();
} catch (Error) {
    echo "Бросать можно только Throwable\n";
}
Ожидаемый результат
Нет ID
Бросать можно только Throwable

Первый опыт проверяет отсутствие/null ID, но не его формат — это следующий шаг валидации. До PHP 8 throw был только инструкцией; современная форма позволяет ветке выражения завершиться исключением. stdClass — обычный объект, поэтому второй опыт специально получает Error.

Если обработчик не найден, неперехваченный Throwable завершает выполнение с диагностикой. Глобальный set_exception_handler может оформить последнюю реакцию, но не возвращает управление на строку после неудачного вызова. Раздел обработки ошибок объясняет политику HTTP-ответа и журнала.

catch выбирается по типу, а выполнение продолжается после всей конструкции

try выделяет действия, которые могут бросить исключение. У него должен быть хотя бы один catch или finally. Первый подходящий catch получает управление; прочие catch этого try уже не выполняются. Обычно конкретные типы ставят раньше общего Throwable. Exception и Error — разные ветви Throwable: catch(Exception) не охватывает каждый Error.

Ход ближайшего опыта
  1. Первый try бросает InvalidArgumentException
  2. Подходит конкретный catch; общий Throwable пропускается
  3. Второй try бросает LengthException
  4. Подходит объединённый catch; далее общий вывод
Конкретный отказ, общий тип и объединённая обработкаСамостоятельный пример
<?php
try {
    throw new InvalidArgumentException('Неверная строка');
} catch (InvalidArgumentException $error) {
    echo "Исправить вход\n";
} catch (Throwable $error) {
    echo "Общий отказ\n";
}
try {
    throw new LengthException('Слишком много строк');
} catch (LengthException | UnexpectedValueException) {
    echo "Отчёт не соответствует договору\n";
}
echo "После обоих try\n";
Ожидаемый результат
Исправить вход
Отчёт не соответствует договору
После обоих try

Вертикальная черта объединяет допустимые типы catch с PHP 7.1; это не битовая операция над ошибками. Переменную можно не указывать с PHP 8.0, если объект не нужен. При нормальном завершении try все catch пропускаются.

Новый отказ из catch ищет внешний обработчикСамостоятельный пример
<?php
try {
    try {
        throw new InvalidArgumentException('Строка не разобрана');
    } catch (InvalidArgumentException $error) {
        echo "Добавляем контекст импорта\n";
        throw new RuntimeException('Импорт остановлен', 0, $error);
    } catch (RuntimeException) {
        echo "Этот соседний catch не выполняется\n";
    }
} catch (RuntimeException $error) {
    echo $error->getMessage(), "\n";
    echo $error->getPrevious()->getMessage(), "\n";
}
Ожидаемый результат
Добавляем контекст импорта
Импорт остановлен
Строка не разобрана

Соседние catch обслуживают исходный try, а не исключения друг друга. Третий параметр конструктора сохраняет исходную причину; getPrevious читает её. Если новый контекст не нужен, throw $error повторно передаёт тот же объект.

Предупреждение PHP не превращается автоматически в Throwable. Если библиотека возвращает false, сначала применяют её контракт проверки. И наоборот, пустой catch(Throwable) не означает успешную обработку: он способен скрыть программную ошибку. Реакция должна быть определена задачей — отказ всего импорта, отчёт о конкретной строке или безопасная повторная попытка.

finally освобождает ресурс перед продолжением, возвратом или передачей ошибки

Маршрут операции с освобождением ресурса
  1. Функция открывает поток и держит полученный доступ в местной переменной.
  2. Try читает полезное содержимое и готовит строку для return.
  3. Перед выходом из функции finally закрывает ресурс.
  4. Только после очистки вызывающий код получает строку в место вызова; при исключении получает путь ошибки.

finally можно поставить после catch или вместо него. Он выполняет завершающее действие при нормальном выходе из try, после обработанного отказа и при передаче исключения наружу. Поэтому закрытие ресурса не приходится дублировать во всех ветках. Это не команда «объявить всю операцию успешной».

Чтение файла до результата
  1. Открыть локальный файл
  2. try читает строку и вычисляет return
  3. finally закрывает поток
  4. Вызывающий код получает строку
Поток закрывается до получения результата вызывающим кодомСамостоятельный пример
<?php
function readFirstLine($path)
{
    $stream = fopen($path, 'r');
    if ($stream === false) {
        throw new RuntimeException('Не удалось открыть файл');
    }
    try {
        $line = fgets($stream);
        if ($line === false) {
            throw new UnexpectedValueException('Нет строки');
        }
        return trim($line);
    } finally {
        fclose($stream);
        echo "Поток закрыт\n";
    }
}
echo readFirstLine(__DIR__ . '/report.txt'), "\n";
try {
    readFirstLine(__DIR__ . '/empty.txt');
} catch (UnexpectedValueException) {
    echo "Отчёт пуст\n";
}
Ожидаемый результат
Поток закрыт
Отчёт готов
Поток закрыт
Отчёт пуст

Текст готового результата вычислен в try, но сначала выполняется fclose и метка из finally. Пустой fixture вызывает отказ, а поток также закрывается. Файлы ограничены и локальны; это не полный импорт произвольного большого отчёта.

Внутренний finally происходит раньше внешнего catchСамостоятельный пример
<?php
try {
    try {
        throw new RuntimeException('Сбой чтения');
    } finally {
        echo "Освободили внутренний ресурс\n";
    }
} catch (RuntimeException) {
    echo "Обработали отказ снаружи\n";
} finally {
    echo "Закончили внешний участок\n";
}
Ожидаемый результат
Освободили внутренний ресурс
Обработали отказ снаружи
Закончили внешний участок

Порядок определяется вложенностью. Для одного try с catch и finally сначала работает его catch, затем finally. При раскрутке стека внутренний finally может встретиться раньше внешнего catch.

return и throw внутри finally способны изменить исход операции

Значение return вычисляется в момент встречи return, но отдаётся после finally. Новый return из finally заменяет уже подготовленный результат и может скрыть исключение; для обычной очистки он не нужен. Если finally сам бросает исключение, наружу уходит новое, а прежняя причина включается в цепочку previous.

Ход ближайшего опыта
  1. preparedValue готовит строку ready для return
  2. finally меняет локальный status на closed
  3. Возвращается уже вычисленная строка ready
  4. Отдельная replacedValue подменяет результат новым return
Изменение локальной переменной не то же самое, что новый returnСамостоятельный пример
<?php
function preparedValue()
{
    $status = 'ready';
    try {
        return $status;
    } finally {
        $status = 'closed';
    }
}
function replacedValue()
{
    try {
        return 'ready';
    } finally {
        return 'closed'; // Намеренная ловушка для сравнения.
    }
}
echo preparedValue(), "\n";
echo replacedValue(), "\n";
Ожидаемый результат
ready
closed

В первом случае возвращаемая строка уже вычислена. Во втором новый return подменил её. Это пример скалярного значения; у возвращаемого объекта изменения его свойств могут быть видны через тот же идентификатор.

Не потерять причину при двух последовательных отказахСамостоятельный пример
<?php
try {
    try {
        throw new RuntimeException('Импорт', 0, new Exception('Строка'));
    } finally {
        throw new LogicException('Журнал', 0, new Exception('Диск'));
    }
} catch (Throwable $error) {
    do {
        echo $error->getMessage(), "\n";
        $error = $error->getPrevious();
    } while ($error !== null);
}
Ожидаемый результат
Журнал
Диск
Импорт
Строка

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

Граница гарантии finally: exit завершает запрос немедленно

exit/die из try или catch пропускает finally, exit из внутреннего finally пропускает внешние finally. Невосстановимое завершение процесса, например убийство процесса или исчерпание памяти, тоже нельзя покрыть обещанием «всегда». Критическая операция должна опираться на транзакцию и проверяемое состояние, а не на последнюю строку скрипта.

Ход ближайшего опыта
  1. try печатает «Завершаем процесс»
  2. exit(0) заканчивает этот PHP-процесс
  3. Обычный finally пропущен; его вывода нет
Отдельный процесс показывает, что очистка не вызванаСамостоятельный пример
<?php
try {
    echo "Завершаем процесс\n";
    exit(0);
} finally {
    echo "Эта строка не выполняется\n";
}
Ожидаемый результат
Завершаем процесс

Каждый учебный запуск работает в отдельном PHP-процессе; exit 0 завершает только его. В библиотеке не подменяйте им возвращаемый результат или исключение, которое вызывающий код должен обработать.

В HTTP-контроллере конец ответа иногда действительно является концом запроса. До такого exit сначала явно завершите нужную работу. Регистрация shutdown-функции не превращает процесс в надёжную систему доставки событий; полноценный commit/rollback описан в транзакциях.

Генератор при yield приостанавливается; finally не выполняется после каждой выдачи

Генератор — функция с yield, которая отдаёт элементы постепенно и может сохранять незавершённый вызов. yield не равен return: исполнение приостанавливается, включая открытый try. finally сработает при завершении либо закрытии генератора. Наличие внешней ссылки на генератор влияет на момент закрытия.

Сохранённый генератор после одного элемента
  1. yield отдаёт A и приостанавливает функцию
  2. break завершает foreach, но rows всё ещё хранит генератор
  3. unset(rows) освобождает последнюю ссылку этого примера
  4. Закрытие выполняет finally
break не обязан сразу закрывать сохранённый генераторСамостоятельный пример
<?php
function reportRows()
{
    try {
        yield 'A';
        yield 'B';
    } finally {
        echo "Генератор закрыт\n";
    }
}
$rows = reportRows();
foreach ($rows as $row) {
    echo $row, "\n";
    break;
}
echo "Ссылка ещё сохранена\n";
unset($rows);
echo "После освобождения\n";
Ожидаемый результат
A
Ссылка ещё сохранена
Генератор закрыт
После освобождения

Здесь нет других ссылок и циклических зависимостей: unset освобождает именно последний сохранённый генератор. В сложном приложении время жизни задаёт владелец ресурса; не рассчитывайте на магическое закрытие от любого break.

Отказ потребителя освобождает временный генераторСамостоятельный пример
<?php
function transientRows()
{
    try {
        yield 'R-1';
        yield 'R-2';
    } finally {
        echo "Источник освобождён\n";
    }
}
try {
    foreach (transientRows() as $row) {
        echo $row, "\n";
        throw new RuntimeException('Потребитель отказался');
    }
} catch (RuntimeException) {
    echo "Отказ обработан снаружи\n";
}
Ожидаемый результат
R-1
Источник освобождён
Отказ обработан снаружи

Ошибка возникла в потребителе, не внутри тела генератора: его собственный catch не должен автоматически её получить. Здесь временный генератор больше никем не удерживается, поэтому finally исполнился до внешнего catch.

Нельзя выдавать следующий элемент при принудительном закрытииФрагмент или команда
<?php
function unfinishedRows()
{
    try {
        yield 'A';
    } finally {
        yield 'B';
    }
}
$rows = unfinishedRows();
$rows->current();
unset($rows);

Синтаксис допустим, но этот сценарий завершается Error: Cannot yield from finally in a force-closed generator. При обычном продолжении yield внутри finally возможен; при принудительном закрытии выдавать новое значение уже некому. Ошибочный сценарий проверяется отдельным subprocess-probe, а не предлагается как рабочая очистка.

Сочетание exit и освобождения генератора имеет исторические различия: в PHP 8 finally временного генератора может выполниться при shutdown, хотя внешний обычный finally уже пропущен. В PHP 7.1.14+ и 7.2.2+ до 8.0 такой внутренний вывод в соответствующем примере отсутствовал, тогда как более ранние варианты 7.x его давали. Не делайте из этого механизма договор завершения заказа. Основы yield и владения источником находятся в главе итераторов и генераторов.

Соберите маршрут обработки, прежде чем выбирать ключевые слова

Задача: импортировать несколько строк отчёта, пропускать только заранее разрешённые ошибки данных, закрывать источник при отказе хранилища и вернуть понятный итог. Назовите четыре границы: одна ветка, один проход, весь обход, весь вызов. После этого выбирать continue/break/return/throw гораздо проще.

СитуацияЧто нужно выразитьПредлагаемое решение
Строка отключена поставщикомПропустить одну записьcontinue в цикле строк, если частичный импорт разрешён.
Найден нужный товарЗавершить поискreturn из функции поиска либо понятный break нужного уровня.
Неизвестный обязательный статусНе выдать ложный успехПолный match с явным отказом или валидация до выбора.
Файл содержит код определенийНе объявлять функцию повторноСогласованный require_once/Composer; настройки хранить отдельным значением.
Ошибка после открытия потокаЗакрыть свой ресурс и передать отказtry/finally вокруг владения ресурсом, catch на подходящей границе.

Упражнения: в примере levels-switch замените тип C на normal — ожидается обработка A и C. В примере once-value сохраните first и выведите его shop второй раз без нового require_once. В finally-file сделайте report.txt пустым — вместо результата должен появиться отказ, а метка закрытия должна сохраниться. В цикле с кэшированным пределом объясните, почему изменение коллекции может потребовать другого договора границы.

Разобранные реальные вопросы:

do…while: первая попытка происходит до проверки продолжения

While спрашивает «надо ли начинать?» до первой итерации. Do…while сначала выполняет тело и лишь затем решает, повторять ли его. Например, локальная очередь возвращает пустой список только после чтения: первый запрос данных всё равно нужен. Ограничение попыток остаётся обязательным.

Цикл с проверкой после действия
  1. Получить одну порцию
  2. Обработать порцию
  3. Проверить: есть продолжение и не превышен лимит?
  4. Повторить или закончить
Ровно одна попытка даже при нулевом начальном счётчикеСамостоятельный пример
<?php
$attempt = 0;
do {
    $attempt++;
    echo 'Попытка ', $attempt, "\n";
} while ($attempt < 2);
Ожидаемый результат
Попытка 1
Попытка 2

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

Обрабатываем заготовленные страницы интеграцииСамостоятельный пример
<?php
declare(strict_types=1);

$pages = [['A', 'B'], ['C'], []];
$pageNumber = 0;
do {
    $items = $pages[$pageNumber] ?? [];
    foreach ($items as $item) {
        echo $item, "\n";
    }
    $pageNumber++;
} while ($items !== [] && $pageNumber < 10);
Ожидаемый результат
A
B
C

Массив pages заменяет сетевой источник для воспроизводимого запуска. В настоящем API условие задаёт его договор: next, cursor или пустой ответ. Отдельно ограничивают число запросов, общий срок и повторяющийся cursor. Не делайте паузу после каждой страницы без причины: правила частоты разбираются в главе rate limits.

break и continue: закончить цикл или пропустить одну запись

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

$items = [
    ['sku' => 'A', 'active' => true],
    ['sku' => 'B', 'active' => false],
    ['sku' => 'C', 'active' => true],
];
foreach ($items as $item) {
    if (!$item['active']) {
        continue;
    }
    echo $item['sku'], "\n";
}
Ожидаемый результат
A
C

Continue переходит к следующей итерации. Если поставить break, после B цикл вообще закончится и C не будет обработан. Это разные решения задачи, а не взаимозаменяемые способы убрать одну строку.

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

$warehouses = [['A', 'B'], ['C', 'D'], ['E']];
$found = null;
foreach ($warehouses as $warehouseNumber => $products) {
    foreach ($products as $sku) {
        if ($sku === 'C') {
            $found = $warehouseNumber;
            break 2;
        }
    }
}
echo 'Найдено на складе ', $found, "\n";
Ожидаемый результат
Найдено на складе 1

Число 2 означает выйти из двух охватывающих циклов. Для continue2 — перейти к следующей итерации внешнего. При усложнении структуры удобнее выделить функцию поиска и сделать return: число уровней не будет зависеть от добавленной вложенности. В switch continue без уровня ведёт себя как break и с PHP 7.3 предупреждает; внутри switch во внешнем цикле для его следующей итерации нужен continue2.

Альтернативный синтаксис: где PHP стоит внутри HTML

В шаблоне страницы удобнее видеть, где закрывается условие: if (...): заканчивается endif;, а foreach (...): — endforeach;. Это те же условия и циклы, только другой способ обозначить блок. Для одного блока не смешивают двоеточие с фигурной скобкой.

Небольшой HTML-список без сборки тегов конкатенациейСамостоятельный пример
<?php $products = ['Чай', '<Кофе>']; ?>
<ul>
<?php foreach ($products as $product): ?>
<li><?= htmlspecialchars($product, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></li>
<?php endforeach; ?>
</ul>
Ожидаемый результат
<ul>
<li>Чай</li>
<li>&lt;Кофе&gt;</li>
</ul>

Текст между PHP-тегами отправляется как HTML. Короткий <?= — вывод выражения, но он сам не экранирует данные: htmlspecialchars нужен отдельно. Подобный синтаксис есть у if/elseif/else, while, for, foreach, switch и declare; для do…while используют обычные скобки. Этот пример можно выполнить в CLI, чтобы увидеть передаваемые браузеру байты.

include, require и once: как несколько файлов становятся программой

На сайте расчёт, настройки и шаблон часто лежат отдельно. Подключение выполняет PHP-файл в текущем ходе программы. Это не HTTP-запрос к нему и не автоматическая загрузка всех файлов каталога. Путь через __DIR__ привязан к расположению текущего файла.

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

$settings = require __DIR__ . '/settings.php';
echo $settings['shop'], ': ', $settings['delivery'], "\n";
Ожидаемый результат
Мастерская: 250

В архив примера включён settings.php. Его return возвращает массив в выражение require; выполнение вызывающего файла затем продолжается. Если подключаемый файл обязателен, require явно выражает это требование.

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

require_once __DIR__ . '/delivery.php';
require_once __DIR__ . '/delivery.php';
echo deliveryLabel(250), "\n";
Ожидаемый результат
Доставка: 250

Без once повторное объявление той же функции завершилось бы ошибкой. Once полезен для определения кода, но повторный require_once файла настроек не возвращает заново его массив: уже подключённый файл не исполняется. Для классов Composer предлагает загрузчик по имени; его устройство объясняется отдельно.

КонструкцияЕсли файл не подключилсяПовтор
includeПредупреждение и false; выполнение может продолжитьсяВыполняет заново
requireВ PHP 8 ошибка Error; неперехваченная останавливает программуВыполняет заново
include_onceКак includeУже подключённый файл пропускается
require_onceКак requireУже подключённый файл пропускается

Не собирайте путь include из непроверенного параметра запроса: это позволяет выполнять неожиданный код. Подключённый файл видит область переменных точки подключения, поэтому include внутри функции отличается от верхнего уровня. Шаблону передавайте понятный ограниченный набор данных; классы подключайте через согласованный autoload.

declare и goto: узнавать синтаксис и понимать его назначение

Declare задаёт режим выполнения участка или файла. Наиболее часто вы увидите declare(strict_types=1); — режим скалярных вызовов этого файла. ticks регистрирует события через заданное число подходящих инструкций; это не таймер и не способ сделать sleep асинхронным. encoding относится к обработке исходного текста при соответствующей поддержке, а не задаёт HTTP-кодировку ответа.

Переход к метке в небольшом демонстрационном сценарииСамостоятельный пример
<?php
$skipImport = true;
if ($skipImport) {
    goto finished;
}
echo "Импорт\n";
finished:
echo "Сценарий закончен\n";
Ожидаемый результат
Сценарий закончен

Goto переносит управление на метку в допустимой области. Нельзя перескочить в другую функцию или войти внутрь цикла/switch извне. В обычном прикладном коде if, ранний return и функции чаще дают понятнее структуру. Здесь небольшой пример нужен, чтобы вы узнали конструкцию в старом проекте и могли заменить её осознанно.

Return в функции прекращает её выполнение и отдаёт результат вызывающему месту; в подключённом файле возвращается из файла, а на верхнем уровне основного скрипта завершает его ход. Exit завершает скрипт целиком: не подменяйте им ошибку, которую вызывающая функция должна обработать. Try/catch/finally разобраны в главе исключений.

Как выбрать for, foreach и while для своей задачи

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

Три задачи — три причины повторять действие

Для одного заказа печатаем этикетки, обрабатываем товары и забираем следующие страницы API. Способ повторения зависит от того, что известно заранее.

  • for: три грузовых места

    Счётчик 1 → 2 → 3. На каждом шаге печатаем одну этикетку.

  • foreach: товары в заказе

    Чай → кофе → мёд. Берём очередной элемент готовой коллекции.

  • while: страницы API

    Запрашиваем следующую страницу, пока ответ сообщает о продолжении.

У while должны быть условие завершения и защитный предел. do…while сначала выполняет действие, а потом проверяет условие. Выбирайте цикл по устройству задачи, а не по привычке к одному слову.

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

ЗадачаЦиклПочему он здесь удобен
Распечатать по этикетке на каждое из трёх грузовых местforКоличество известно; номер шага нужен в результате.
Проверить товары в полученном заказе или показания датчиковforeachРаботаем с самими элементами и их ключами, не угадываем индексы.
Забирать страницы API, пока поставщик возвращает следующий курсорwhileЧисло страниц заранее неизвестно; продолжение определяет ответ.
Сначала выполнить действие, затем решить, нужно ли повторятьdo/whileОдин запуск обязателен. Дополнительные пределы всё равно нужны.
for: три этикетки для одной доставкиСамостоятельный пример
<?php
declare(strict_types=1);

$orderId = 'ORD-42';
$parcelCount = 3;
if ($parcelCount < 1 || $parcelCount > 100) {
    throw new InvalidArgumentException('Нужно от 1 до 100 грузовых мест');
}
for ($parcel = 1; $parcel <= $parcelCount; $parcel++) {
    echo $orderId, '-', str_pad((string) $parcel, 2, '0', STR_PAD_LEFT),
        ' / ', $parcelCount, "\n";
}
Ожидаемый результат
ORD-42-01 / 3
ORD-42-02 / 3
ORD-42-03 / 3

Сначала задаётся номер 1, перед каждым проходом проверяется предел, после прохода номер увеличивается. В реальной форме parcelCount проверяют как число заданного диапазона: нельзя разрешить клиенту случайно заказать миллион итераций. Здесь генерируется текст этикеток; запросов в доставку и печати нет.

Если товары пришли с ключами SKU-A и SKU-B, выражение $items[0] может вообще не существовать. foreach получает каждую пару ключ → значение независимо от того, числовой это список или словарь. Он подходит и для вложенного заказа: внешний foreach перебирает заказы, внутренний — позиции конкретного заказа. Практику с проверкой данных смотрите в пути показаний устройства до базы и ответа.

Ветка — это решение в предметной области

if подходит, когда проверка зависит от нескольких условий: пользователь авторизован, адрес подтвержден, товар есть на складе. match выбирает результат по конкретному значению и использует строгое сравнение; ветви не проваливаются в следующие, как иногда бывает у switch без break. У match должен быть подходящий вариант либо default, иначе возникает UnhandledMatchError. Для неизвестного статуса интеграции часто правильнее явная ошибка, чем молчаливый default «успешно».

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

function deliveryFee(string $method): int
{
    return match ($method) {
        'pickup' => 0,
        'courier' => 29900,
        'express' => 59900,
        default => throw new InvalidArgumentException('Неизвестный способ'),
    };
}
echo deliveryFee('courier'), "\n";
try {
    deliveryFee('teleport');
} catch (InvalidArgumentException $exception) {
    echo "Выберите способ доставки\n";
}
Ожидаемый результат
29900
Выберите способ доставки

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

foreach работает с элементами, а не с догадками об индексах

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

$rows = [['sku' => 'A', 'stock' => 2], ['sku' => 'B', 'stock' => -1], ['sku' => 'C', 'stock' => 0]];
$accepted = [];
$errors = [];
foreach ($rows as $index => $row) {
    if ($row['stock'] < 0) {
        $errors[] = 'Строка ' . ($index + 1);
        continue;
    }
    $accepted[$row['sku']] = $row['stock'];
}
echo json_encode($accepted, JSON_THROW_ON_ERROR), "\n";
echo implode(', ', $errors), "\n";
Ожидаемый результат
{"A":2,"C":0}
Строка 2

continue пропускает только текущую строку. break остановил бы весь импорт, return вышел бы из функции. В данном fixture форма строк уже известна; внешний CSV/JSON требует проверки каждого типа. Решение частично принять импорт должно быть согласовано, а не возникать случайно из continue.

Изменение через ссылку: почему после foreach требуется unset

Иногда старый код обновляет каждый остаток через foreach ($stocks as &$stock). Знак & связывает переменную stock с самой ячейкой массива. После последней итерации эта связь сохраняется: следующее обычное присваивание stock изменило бы последнюю ячейку. unset($stock) разрывает связь имени; товар из stocks при этом не удаляется.

Обновить остатки и безопасно повторно использовать имяСамостоятельный пример
<?php
$stocks = ['Чай' => 3, 'Кофе' => 2];
foreach ($stocks as &$stock) {
    $stock = $stock + 1;
}
unset($stock);
foreach (['Печенье' => 5, 'Сок' => 10] as $title => $stock) {
    echo 'Другой склад: ', $title, ' — ', $stock, ' уп.', "\n";
}
foreach ($stocks as $title => $stock) {
    echo 'Основной склад: ', $title, ' — ', $stock, ' уп.', "\n";
}
Ожидаемый результат
Другой склад: Печенье — 5 уп.
Другой склад: Сок — 10 уп.
Основной склад: Чай — 4 уп.
Основной склад: Кофе — 3 уп.

Остатки первого склада стали 4 и 3. unset удалил только связь местного имени с последней ячейкой. Второй цикл использовал имя stock для других чисел; итоговая ведомость показывает, что кофе не превратился в остаток сока 10.

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

Цикл должен иметь понятный предел

for удобен для известного количества попыток, while — пока есть следующая порция, do/while — когда тело нужно выполнить хотя бы раз. При страницах API нужен не только признак has_more, но и лимит страниц, проверка повторяющегося курсора и политика ошибки. Иначе неправильный ответ поставщика заставит воркер крутиться бесконечно. При обработке большого файла читайте поток, а не создавайте сразу массив всех строк. Наличие цикла само по себе не делает код медленным: часто основная стоимость — сетевой или SQL-запрос внутри каждого шага.

Не меняйте структуру обхода непредсказуемо: если добавляете элементы в ту же коллекцию, заранее изучите семантику и ограничьте рост. Для очереди используйте явную структуру последовательности. Условие empty($row["stock"]) уберет и 0, хотя нулевой остаток нужно сохранить. И проверьте точку с запятой после foreach: она означает пустое тело, а последующий блок выполнится лишь однажды.

Попробуйте сами. При импорте 1000 позиций недоступность базы произошла на строке 50. Достаточно ли continue?

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

Нет. Неверная строка поставщика и отказ хранилища — разные ошибки. При недоступности базы останавливайте или откладывайте повторно операцию с контрольной точкой/транзакцией. Continue допустим для явно предусмотренной частичной валидации; в отчете должны быть причины и объем принятого.

Два цикла вместе: страницы API и записи внутри страницы

Сервис доставки отдаёт две записи и next="page-2". Во второй странице остаётся одна запись, next=null. Мы не знаем число страниц заранее, поэтому продолжаем, пока курсор не стал null. Список ответов ниже — локальный двойник API; код можно запустить без токена. Запрос в настоящую службу скрывался бы в функции fetchPage с тем же договором.

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

function collectShipments(array $pages): array
{
    $cursor = 'first';
    $seen = [];
    $result = [];
    $pageCount = 0;
    while ($cursor !== null) {
        if (++$pageCount > 10) {
            throw new LengthException('Превышен бюджет страниц');
        }
        if (isset($seen[$cursor])) {
            throw new UnexpectedValueException('Сервис повторил курсор');
        }
        $seen[$cursor] = true;
        $page = $pages[$cursor] ?? throw new UnexpectedValueException('Страница отсутствует');
        if (!is_array($page) || !is_array($page['items'] ?? null)
            || !array_is_list($page['items']) || count($page['items']) > 100
            || !array_key_exists('next', $page)
            || ($page['next'] !== null && (!is_string($page['next']) || $page['next'] === ''))) {
            throw new UnexpectedValueException('Неверный ответ страницы');
        }
        foreach ($page['items'] as $item) {
            if (!is_array($item) || !is_string($item['id'] ?? null)) {
                throw new UnexpectedValueException('Нет ID отправления');
            }
            $result[] = $item['id'];
        }
        $cursor = $page['next'];
    }
    return $result;
}
$pages = [
    'first' => ['items' => [['id' => 'S-1'], ['id' => 'S-2']], 'next' => 'page-2'],
    'page-2' => ['items' => [['id' => 'S-3']], 'next' => null],
];
echo implode(', ', collectShipments($pages)), "\n";
$pages['page-2']['next'] = 'first';
try { collectShipments($pages); }
catch (UnexpectedValueException $error) { echo $error->getMessage(), "\n"; }
Ожидаемый результат
S-1, S-2, S-3
Сервис повторил курсор

while управляет получением страниц, foreach — обработкой элементов внутри страницы: эти циклы работают вместе. Десять страниц по сто элементов ограничивают этот учебный результат тысячей элементов. При ошибке функция не выдаёт молча неполный список. У реального API отдельно ограничивают размер ответа, время, повтор запросов и проверяют его собственную схему курсора; нельзя подставлять этот договор в Bitrix без адаптации.

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

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

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

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

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

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