Основы языка PHP
PHP — динамически типизированный язык с приводящим движком: тип принадлежит не переменной, а значению, и конвертация происходит не при присваивании, а в момент, когда значение попадает в оператор. Оператор объявляет, какой тип ему нужен, и движок молча приводит операнд к этому типу — '5' + 3 даёт int(8), 5 . '3' даёт '53', if ($x) приводит к bool. Отсюда правило, которое отделяет кандидата, понимающего язык, от кандидата, заучившего синтаксис: declare(strict_types=1) не выключает жонглирование — он ужесточает только границу функции, аргументы и возвращаемое значение, но не операторы.
Вторая половина темы — про то, чего в значении может не быть. null в PHP различают четыре разные конструкции, и они расходятся ровно в том месте, где вы ошибётесь: isset не отличает отсутствующий ключ от сохранённого null, empty считает '0' отсутствием, а ?? наследует семантику isset. Назовём ловушки сразу: PHP 8 починил самое одиозное сравнение (0 == 'abc' теперь false), но '1' == '01' и '10' == '1e1' по-прежнему истина — две числовые строки всегда сравниваются как числа; match сравнивает через ===, а switch — через ==; global не копирует значение, а привязывает ссылку; include на отсутствующем файле продолжит выполнение, а require остановит скрипт. Разбор по слоям — ниже.
Карта темы
- Жонглирование типами — где именно движок конвертирует значение, что такое числовая строка и почему
strict_typesне властен над операторами. - Сравнение == и === — правила нестрогого сравнения после PHP 8, идентичность объектов и массивов, ловушка
in_arrayбез третьего аргумента. - Работа с null — чем
isset,empty,is_nullиarray_key_existsрасходятся, что делают??,??=и?->. - Выражение match —
matchкак выражение со строгим сравнением без провала вниз и идиомаmatch (true). - Строковые литералы — одинарные и двойные кавычки, синтаксис интерполяции, heredoc против nowdoc и отступ закрывающего маркера.
- Ссылки и & — передача по значению с copy-on-write, что на самом деле делает
&, почему объект — это не «передача по ссылке» и какforeach (&$v)портит массив. - Область видимости — своя область у каждой функции,
globalкак ссылка на$GLOBALS,staticвнутри функции и переменные переменные. - include и require — предупреждение против фатальной ошибки, что на самом деле кеширует
_onceи почему в прикладном коде вместо этого стоит автозагрузчик.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Ждать, что declare(strict_types=1) остановит '5' + 3 | Строгие типы работают только на границе функции — оператор всё равно приведёт операнд |
Считать '0' истинным, потому что строка непустая | '0' — falsy: проверка на непустоту пропустит валидный ввод |
Проверять isset($a['k']) для поля, которое законно может быть null | Сохранённый null неотличим от отсутствующего ключа — нужен array_key_exists |
Ждать, что ?? подставит запасное значение вместо 0 или '' | 0 и '' установлены и не null — сработает ?:, а не ?? |
Использовать ?: на $_GET['page'] | Оператор не гасит предупреждение об отсутствующем ключе, в отличие от ?? |
Считать, что ?-> глотает и отсутствующий метод | ?-> защищает только от null-получателя; отсутствующий метод по-прежнему Error |
Думать, что 0 == 'abc' всё ещё истина | В PHP 8 нечисловая строка сравнивается со строковым представлением числа — уже false |
Проверять токен по белому списку через in_array без третьего аргумента | Сравнение идёт через ==: in_array(0, ['0']) истинно, чужое значение пройдёт |
Ждать от match нестрогого сравнения | Арм сверяется через ===: match ('1') не попадёт в арм 1 |
Не писать default в match | Непокрытое значение бросит UnhandledMatchError прямо в проде |
Ждать интерполяции $name в одинарных кавычках | Строка выводится буквально — переменная не подставится |
| Отступить закрывающий маркер heredoc иначе, чем тело | Отступ маркера срезается с каждой строки — сдвигается вся строка, а меньший отступ тела — это ParseError |
| Считать, что объекты передаются «по ссылке» | По значению копируется дескриптор: мутация объекта видна снаружи, а переприсваивание параметра — нет |
Забыть unset($v) после foreach ($a as &$v) | Ссылка на последний элемент жива: следующий foreach затирает его на каждой итерации |
Ждать, что global $x скопирует значение | global привязывает ссылку на $GLOBALS['x'] — присваивание переписывает глобал |
| Рассчитывать на неявный захват внешней области внутри функции | В PHP его нет: значение приходит только параметром, через use или global |
Ждать, что include остановит скрипт на отсутствующем файле | Будет предупреждение, и выполнение продолжится с неполным состоянием — останавливает require |
Считать, что _once кеширует результат файла | Он лишь помнит разрешённый путь и не подключает его повторно |
Значение для собеседований
Основы языка — это фильтр первого круга. Спрашивают не определения, а модель исполнения: где движок конвертирует значение, что именно хранит переменная и что происходит с ней при передаче в функцию. Кандидат, который говорит «== в PHP 8 сначала спрашивает, числовая ли строка, и только потом решает, сравнивать как числа или как строки», сразу отделяется от того, кто отвечает «== сравнивает без учёта типа».
Что обычно проверяют:
- Разницу
==и===и то, что изменилось в PHP 8. - Границу действия
strict_types— почему он не трогает операторы. - Чем расходятся
isset,empty,is_nullиarray_key_exists. - Чем
??отличается от?:и что делает??=. - Почему
matchстрогий, аswitch— нет. - Что копируется при передаче массива и что при передаче объекта.
- Что делает
globalи почему параметр лучше. - Чем
includeотличается отrequire.
Типичный неверный ответ: «isset проверяет, что переменная существует». Полуправда, на которой ломаются: isset возвращает false и для сохранённого null — а значит, ваша проверка «поле пришло?» молча отбросит поле, которое пришло со значением null. Второй классический провал — «объекты в PHP передаются по ссылке»: по значению копируется дескриптор, поэтому $obj->x = 1 внутри функции видно снаружи, а $obj = new Foo() — нет.