Программирование на PHP
Повседневный PHP-код почти целиком стоит на двух структурах: массив и строка. Обе устроены не так, как ожидает человек, пришедший из другого языка. Массив PHP — не список и не словарь, а одна структура: упорядоченная хеш-карта, где ключ — int или string, а порядок обхода равен порядку вставки. Строка PHP — не последовательность символов, а последовательность байтов, и кодировка в ней нигде не записана. Из этих двух фактов выводится большая часть ловушек темы, и именно их проверяют на кодинг-секции.
Поверх языка лежит стандартная библиотека, накопленная за тридцать лет: функции с несогласованным порядком аргументов, нестрогие сравнения по умолчанию, побайтовые операции рядом с многобайтовыми. Назовём главные ловушки сразу: array_merge перенумеровывает целые ключи, а + — нет, поэтому слияние по id надо делать союзом; array_filter сохраняет ключи и оставляет дыры, из-за которых json_encode внезапно отдаёт объект вместо массива; sort возвращает bool, а не отсортированный массив; замыкание захватывает use по значению в момент создания, а стрелочная функция — автоматически и тоже по значению; strlen считает байты; unserialize на пользовательских данных — вектор атаки. Разбор по слоям — ниже.
Карта темы
- Массивы PHP — упорядоченная хеш-карта под капотом,
O(1)-счётчик, copy-on-write,array_mergeпротив+, ключи послеarray_filterи семейство сортировок. - Основы функций — типы и
strict_typesна границе вызова, значения по умолчанию, variadic и spread..., именованные аргументы и оператор|>из PHP 8.5. - Замыкания —
Closureкак объект, захват черезuseпо значению и по ссылке, стрелочные функцииfnи синтаксисstrlen(...). - Строковые функции —
str_containsвместо ловушкиstrpos,explodeпротивpreg_split,preg_matchпротивpreg_match_allи URI-расширение PHP 8.5. - Многобайтовые строки — почему
strlenсчитает байты, какsubstrрежет букву пополам и что делает семействоmb_*. - Сериализация —
json_encodeкак формат обмена противserializeкак снимка PHP-объекта, и почемуunserializeнельзя звать на чужих данных. - Потоковый ввод-вывод — константная память вместо
file_get_contents, построчное чтение, генераторы и передача дескриптора вместо строки. - Внедрение зависимостей — зависимости в конструкторе вместо
newвнутри, продвижение свойств, интерфейс вместо реализации и почему service locator — это не DI. - Кэш cache-aside — читаем кэш, промахи берём у источника и дописываем обратно; пакетный вариант, TTL, инвалидация и лавина промахов.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать индексный и ассоциативный массив разными структурами | Это один тип: $a['8'] и $a[8] — один слот, ключ-строка приводится к int |
Слить массивы по id через array_merge | Целые ключи перенумеруются — идентификаторы уедут; для этого нужен + |
Ждать, что array_filter перенумерует ключи | Ключи сохраняются, остаются дыры — и json_encode отдаёт объект вместо массива |
Писать $sorted = sort($arr) | sort сортирует по ссылке и возвращает bool — в $sorted окажется true |
Применить sort к ассоциативному массиву | Ключи выбрасываются и перенумеровываются с нуля — данные теряют привязку |
Читать первый ключ через reset | reset возвращает значение и двигает внутренний указатель — ключ даёт array_key_first |
Ждать, что array_walk вернёт изменённый массив | Он возвращает bool и меняет массив на месте — и только если параметр взят по ссылке |
Захватить в use ($x) и ждать, что замыкание увидит позднейшие изменения | Захват по значению происходит в момент создания замыкания — нужен use (&$x) |
| Ждать от стрелочной функции захвата по ссылке или тела из нескольких инструкций | fn захватывает только по значению и содержит ровно одно выражение |
Писать if (strpos($h, $n)) | Совпадение на нулевой позиции даёт 0 — falsy, проверка тихо ломается |
Резать UTF-8 через strlen / substr / ucwords | Байты вместо символов: длина вдвое больше, буква разрезана пополам, на выходе кракозябры |
Передать в explode регулярное выражение | Разделитель понимается буквально — нужен preg_split |
Доверять parse_url валидацию URL | Она не валидирует и не нормализует — для этого в PHP 8.5 есть Uri\Rfc3986\Uri и Uri\WhatWg\Url |
Вызвать unserialize на данных из cookie или тела запроса | Вектор object injection: атакующий конструирует объект и цепочку __destruct |
Ждать от json_decode восстановления класса | Приходит stdClass или массив — JSON не несёт информации о классе |
Прочитать большой файл через file() / file_get_contents() | Память растёт пропорционально размеру файла, воркер умирает на лимите |
Поднять memory_limit вместо потокового чтения | Проблема переносится на следующий файл побольше — буфер как был, так и остался |
Создавать зависимость через new внутри метода | Зависимость невидима в сигнатуре, класс невозможно изолировать в тесте |
| Внедрить контейнер и назвать это внедрением зависимостей | Это service locator: зависимости снова скрыты, а отсутствие привязки всплывает в рантайме |
Считать промахи кэша по значениям, а не по array_keys($cached) | Попадания не исключаются — источник дёргается за тем, что уже лежит в кэше |
Значение для собеседований
Этот блок проверяют не вопросом, а задачей — в редакторе или у доски. Интервьюер смотрит не на то, вспомнили ли вы имя функции, а на то, назовёте ли вы ловушку до того, как она сработает: перенумеруются ли ключи, останутся ли дыры после фильтрации, закроете ли дескриптор, возьмёте ли mb_* для кириллицы, оставите ли шов для подмены зависимости.
Что обычно проверяют:
- Как устроен массив PHP и почему
count()работает заO(1). - Чем
array_mergeотличается от+и когда какой нужен. - Что копируется при передаче массива — и когда copy-on-write делает настоящую копию.
- Разницу захвата по значению и по ссылке в замыкании; чем
fnотличается отfunction. - Почему строка — это байты и где обязателен
mb_*. - Когда
json_encode, когдаserialize, и почемуunserializeопасен. - Как прочитать файл на несколько гигабайт при константной памяти.
- Почему конструктор лучше, чем
newвнутри и чем контейнер внутри.
Типичный неверный ответ: «Массивы в PHP бывают индексные и ассоциативные — это разные вещи». Тип один, и как только это произнесено правильно, становятся объяснимы и $a['8'] === $a[8], и порядок вставки при foreach, и перенумерация в array_merge. Второй классический провал — «ucwords работает с UTF-8, файл же в UTF-8». Кодировка исходника ни при чём: ucwords обрабатывает байты и портит любую многобайтовую букву — для Юникода нужен mb_convert_case.