Типы и обработка ошибок
Объявление типа в PHP — не подсказка и не аннотация: это проверка в рантайме, выполняемая на границе функции при каждом вызове. Компилятор ничего не выводит и не стирает — он лишь запоминает объявление, а сверяет значение движок в момент передачи аргумента. Отсюда два важных следствия. Во-первых, режим приведения решает файл, который делает вызов: declare(strict_types=1) действует по файлу и на его собственные вызовы, поэтому строгий файл, зовущий нестрогую библиотеку, проверяется строго, а нестрогий вызывающий вашей строгой библиотеки всё равно получает приведение на входе. Во-вторых, нарушение объявления — это не «предупреждение анализатора», а брошенный TypeError.
Модель ошибок с PHP 7 разделена надвое, и это второе, что здесь спрашивают. Throwable — интерфейс, а под ним две параллельные ветви: Exception — прикладные сбои, которые вы обязаны обработать, и Error — сбои движка (TypeError, ValueError, DivisionByZeroError). Ветви не пересекаются, поэтому catch (Exception $e) никогда не поймает Error, и на границе приложения ловить надо \Throwable. Рядом живёт третий, совсем другой мир — диагностика движка (warning, notice, deprecation): это не брошенные объекты, их catch не видит вовсе, и превратить их в исключения можно только через set_error_handler(). Ловушки темы стоит назвать сразу: return внутри finally перекрывает значение из try и проглатывает брошенное исключение; #[\NoDiscard] из PHP 8.5 поднимает Warning, а не фатальную ошибку, и глушится новым приведением (void); а атрибут сам по себе не делает ничего — пока его кто-нибудь не прочитает через Reflection. Разбор по слоям — ниже.
Карта темы
- Объявления типов — union, intersection, nullable и
never, проверка на границе при каждом вызове и вариантность сигнатур при переопределении. - strict_types и приведение скаляров — почему директива действует по файлу, почему решает место вызова и какое единственное расширение разрешено даже в строгом режиме.
- Ошибки против исключений — три яруса диагностики, что PHP 7 сделал перехватываемым, что осталось фатальным и что делает
#[\NoDiscard]. - Иерархия Throwable — интерфейс
Throwableи две ветви под ним, почемуcatch (Exception)не ловитErrorи как строить иерархию исключений модуля. - try, catch и finally — почему возвращаемое значение вычисляется до
finally, какreturnизfinallyтеряет исключение и что делает multi-catch. - Собственные обработчики ошибок — что перехватывает
set_error_handler(), чем от него отличаетсяset_exception_handler()и как вообще увидеть фатальную ошибку. - Атрибуты и Reflection — почему атрибут инертен без читателя, чем он лучше docblock-аннотации и что происходит в
newInstance().
Частые ошибки и ловушки
| Ошибка | Последствие | |||
|---|---|---|---|---|
Считать, что strict_types объявляющего файла управляет вызывающим | Наоборот: режим берётся из файла, который делает вызов — ваша строгая библиотека не защищена от нестрогих вызывающих | |||
Ждать, что strict_types влияет на классовые, array- или iterable-объявления | Директива меняет поведение только скалярных объявлений; остальные проверяются строго всегда | |||
Добавить declare(strict_types=1) во все файлы разом | Молчаливые приведения массово превращаются в TypeError — прямо в проде | |||
| Читать `int\ | string как «может быть и null`» | Union не nullable сам по себе: нужен явный `int\ | string\ | null` |
Читать & в Countable&Iterator как union | Intersection требует соответствия всем типам сразу; в нём допустимы лишь классы и интерфейсы | |||
Считать never синонимом void | void-функция возвращается, просто без значения; never-функция не возвращается вовсе | |||
Считать, что catch (Exception $e) подберёт TypeError | Error стоит в параллельной ветви под Throwable — ловить надо \Throwable | |||
Ждать, что catch поймает warning или notice | Диагностика — не брошенный объект; в исключение её превращает только set_error_handler() | |||
Ждать, что set_error_handler() поймает fatal error | Исчерпание памяти и лимит времени в него не приходят — их видно лишь из register_shutdown_function | |||
Считать return в try пропускающим finally | finally выполняется всегда: значение вычисляется, откладывается, и лишь потом функция возвращает | |||
Делать return (или throw) из finally | Значение из try перекрыто, а брошенное там исключение молча потеряно | |||
Ждать от #[\NoDiscard] фатальной ошибки | Он поднимает Warning; намеренно отбросить результат позволяет новое приведение (void), а не @ | |||
Глушить отброшенный результат оператором @ | @ подавляет диагностику без разбора и прячет настоящие проблемы | |||
| Ждать, что атрибут выполнится сам при загрузке класса | Атрибут инертен: движок его разбирает и останавливается — нужен читатель через Reflection | |||
| Строить иерархию исключений вокруг места сбоя | Вызывающий не может выбрать catch — иерархию строят вокруг того, как он обязан отреагировать | |||
| Класть код отказа внутрь строки сообщения | Вызывающему остаётся разбор текста через str_contains(); данные несут типизированным свойством |
Значение для собеседований
Это тема, где кандидата ловят на двух точных механизмах. Первый: «где именно действует declare(strict_types=1)?» — правильный ответ звучит как «по файлу, и решает тот файл, который делает вызов», а не «включает строгую типизацию в проекте». Второй: «поймает ли catch (Exception $e) вызов метода на null?» — правильный ответ «нет, это Error из параллельной ветви». Оба вопроса проверяют не словарь, а модель: понимает ли кандидат, что типы проверяются в рантайме на границе вызова, а сбои движка с PHP 7 стали такими же брошенными объектами, как и прикладные исключения, но по другой ветви.
Что обычно проверяют:
- Что union принимает и что отвергает; почему
?int— это ровноint|null. - Что требует intersection-тип и почему
int&stringневозможен. - Чем
neverотличается отvoid. - Где действует
strict_typesи чей файл это решает. - Разницу warning, fatal error и исключения; что PHP 7 сделал перехватываемым.
- Связь
Throwable,ErrorиException— и почемуcatch (Exception)не покрывает оба мира. - Порядок вычисления
returnиfinally; что случается с исключением приreturnизfinally. - Что перехватывает
set_error_handler()и зачем перебрасывать warning какErrorException. - Зачем нужны атрибуты, если есть docblock, и что делает
getAttributes()/newInstance().
Типичный неверный ответ: «strict_types включает строгую типизацию во всём приложении». За ним обычно следует план «добавим директиву во все 400 файлов и посмотрим» — и продовый инцидент, потому что каждая числовая строка из формы и из колонки базы, до сих пор молча приводившаяся к int, начинает бросать TypeError. Второй классический провал — единственный catch (\Exception $e) на границе приложения: сбой движка вроде вызова метода на null через него проходит насквозь и превращается в белый экран, а корреляционного id, по которому поддержка нашла бы строку лога, не остаётся вовсе.