Средний уровень PHP — практика на тесте с подвохами
Если вы уже знаете, как объявить переменную и написать цикл for, самое время двигаться дальше. PHP — это не только про «вывести текст на экран», но и про умение думать как программист. На среднем уровне проверяют: а вы точно понимаете, как работает массив по ссылке? Можете ли вы отличить глобальную переменную от статической? А PDO для базы данных — это просто страшные буквы или вы знаете, зачем оно нужно?
В этой статье я подробно разберу тест PHP среднего уровня. Мы пойдём по каждому вопросу: сначала посмотрим, что спрашивают, потом — почему правильный ответ именно такой, а в конце — какие подводные камни могут сбить даже опытных. Объясню простыми словами — чтобы не надо было гуглить каждую строчку. С примерами, ассоциациями и пояснениями.
Вопрос 1. Какое значение будет выведено при выполнении следующего кода?
public static function test() {
- В классе A определена константа A = 1 и статический метод test(), который возвращает static::A.
- Ключевое слово static:: использует позднее статическое связывание (late static binding). Это значит:
→ смотри, в каком именно классе вызывается метод, а не в каком он определён. - Класс B переопределяет константу A = 2.
- Класс C не определяет константу A, но наследует всё от B.
Когда вызывается C::test(), PHP:
1. Ищет метод test() — он есть в A, значит, выполнится он.
2. Но внутри test() написано static::A — это значит: ищи A в классе C или выше по иерархии.
3. В C нет своей константы A, но она есть в B.
Представь, что у тебя есть три коробки с надписью:
- Первая коробка (A) говорит: «Моя ценность — 1».
- Вторая коробка (B, дочка) переклеивает ярлык: «Теперь ценность — 2».
- Третья коробка (C) ничего не меняет, просто берёт всё от второй.
Теперь ты говоришь: «Коробка C, скажи, какова твоя ценность (A)?»
А она смотрит на свой ярлык — ничего.
Смотрит у мамы (B) — «О! Вот тут 2!»
Вывод: C::test() возвращает static::A, а значит — B::A
Вопрос 2. Фрагмент PHP-кода:
Какое значение будет выведено и почему?
Оператор % — остаток от деления.
То есть:
Представь, что у тебя 10 конфет, и ты хочешь раздать их поровну по 3 штуки.
- Ты раздашь 3, 3 и 3 — всего 9.
- 1 конфета останется. Вот это и есть остаток.
- И теперь ты записываешь это оставшееся количество обратно в коробку.
1 — так как оператор %= присваивает переменной остаток от деления её текущего значения
Вопрос 3. У вас есть ассоциативный массив $data, содержащий строки (возможно, повторяющиеся с разным регистром). Вы хотите:
1. Сохранить только первое вхождение строки.
2. Игнорировать регистр (Apple и apple — одно и то же).
3. Сохранить оригинальное значение строки (если встретилось "Apple" — оно остаётся).
4. Сохранить ключ первого вхождения.
- Сравнение должно быть регистр-независимым.
- Сохраняются первые вхождения.
- Ключи остаются исходными.
- Оригинальное значение сохраняется, а не приведённое к нижнему регистру.
if (!in_array(strtolower($v), array_map('strtolower', $result), true)) {
- array_map('strtolower', $result) — приводит все уже добавленные значения к нижнему регистру.
- in_array(..., true) — ищет строгое вхождение по значению.
- strtolower($v) — понижает регистр текущего значения для сравнения.
- !in_array(...) — пропускает повторы с разным регистром.
- $result[$k] = $v; — сохраняет исходный ключ и оригинальное значение.
array_unique($data)
— неверно:
- Сохраняет только первые вхождения, но учитывает регистр (то есть "Apple" и "apple" — разные).
- Не решает задачу.
- Заменяет ключи на strtolower($v).
- Последнее вхождение перезаписывает предыдущее.
- Теряются исходные ключи и первое вхождение не сохраняется.
Вариант 1 — с in_array(strtolower(...))
и array_map
.
Вопрос 4. Что делает этот PHP-код с массивом заказов?
$orders = [1001 => 'новый', 'новый', 1005 => 'оплачен', 'отправлен', 1003 => 'доставлен'];
Последовательно разберём, что происходит:
$orders = [1001 => 'новый', 'новый', 1005 => 'оплачен', 'отправлен', 1003 => 'доставлен'];
Инициализация массива со следующими ключами:
- 1001 => 'новый'
- PHP сам присвоит следующий ключ — 1002 → 'новый'
- 1005 => 'оплачен'
- PHP снова автоматически назначает следующий 1006 → 'отправлен'
- 1003 => 'доставлен'
PHP берёт максимальный числовой ключ (1006) и увеличивает на 1 → 1007
Добавляет:
Максимальный ключ — теперь 1007 → следующий: 1008
Это как если бы ты вела список заказов на бумаге и каждому присваивала номер. Некоторые номера ты пишешь вручную (1001, 1005), некоторые — просто добавляешь по порядку. Если ты вычеркнешь один номер (1005), то новый заказ получит новый следующий номер, а не "перепрыгнет" на старый.
Вопрос 5. Вы разрабатываете функцию process_data, которая должна:
1. Принимать произвольное количество отдельных аргументов — например: process_data("a", "b", "c").
2. Принимать массив как единственный аргумент — например: process_data(["a", "b", "c"]).
3. Выводить все значения через пробел.
- Понимать, когда ей передан массив как один аргумент.
- Понимать, когда переданы отдельные значения.
- Универсально обрабатывать оба случая.
- Работает только с отдельными аргументами.
- Не обрабатывает случай, когда передан массив как один аргумент.
function process_data(...$args) {
if (count($args) === 1 && is_array($args[0])) {
- ...$args — собирает все аргументы (в том числе массив как один элемент).
- Если передан единственный массив, он разворачивается.
- Поддерживает оба случая.
Полностью соответствует требованиям. Верно.
function process_data(...$args) {
function process_data(array $args) {
function process_data($args) {
Функция как сотрудник на складе. Ему либо приносят коробку с заказами (массив), либо приносят заказы по одному (переменные). Хороший сотрудник должен уметь открыть коробку и разобрать её, или взять каждый заказ по отдельности. Второй вариант функции делает это правильно.
Вариант 2 — с ...$args и проверкой count($args) === 1 && is_array($args[0]).
Вопрос 6. Вы работаете с PDO и MySQL. Нужно выполнить транзакцию — серию SQL-запросов (INSERT, UPDATE), при этом:
- Если в любом из запросов произойдёт ошибка, изменения не должны попасть в базу.
- По умолчанию в PDO включён автокоммит, поэтому его нужно отключить вручную, начав транзакцию.
- Также необходима обработка ошибок через try-catch.
Что требуется для правильной реализации:
1. Начать транзакцию с $pdo->beginTransaction().
3. В случае успеха — вызвать $pdo->commit().
4. В случае исключения — вызвать $pdo->rollBack().
- Используется PDO-специфичная функция beginTransaction() вместо query("BEGIN") или query("START TRANSACTION").
- Включена корректная обработка ошибок.
- commit() вызывается только если все запросы прошли успешно.
- rollBack() вызывается в catch, если что-то пошло не так.
Ты кладёшь два товара в корзину. Пока не оплатишь — ничего не зафиксировано.
Если при оплате произойдёт сбой — всё откатится, как будто ты вообще ничего не добавлял.
Последний вариант (нижний блок с beginTransaction(), try, commit(), rollBack()) — реализует корректную транзакцию с откатом при ошибке:
$pdo->query("INSERT INTO orders (id, amount) VALUES (10, 100)");
$pdo->query("UPDATE accounts SET balance = balance - 100 WHERE id = 5");
Вопрос 7. У вас есть класс Car с защищённым свойством $engineStatus и методом startEngine().
Нужно, чтобы вызов startEngine() менял $engineStatus на true (включал двигатель).
protected $engineStatus = false;
public function startEngine() {
Задача: Обратиться к свойству текущего объекта и изменить его значение.
В PHP это делается через $this, потому что:
- $this — это ссылка на текущий объект, внутри которого вызывается метод.
- Обращение к свойствам объекта — $this->имя_свойства.
- parent::$engineStatus = true; — обращение к статическому свойству родительского класса. $engineStatus — не static, и parent:: используется только в контексте наследования.
- self::$engineStatus = true; — обращение к статическому свойству текущего класса, а $engineStatus — экземплярное, не static.
- class::$engineStatus = true; — некорректныйсинтаксис.
- object->engineStatus = true; — такой переменной (object) нет и это не в контексте класса.
Если ты водитель машины (объект), и у тебя в руке ключ зажигания (метод), ты не идёшь к другой машине или инструкции — ты просто поворачиваешь ключ в своей машине. Вот это и делает $this->engineStatus = true;.
1. $this->engineStatus = true;
Вопрос 8. Нужно выбрать ключевое слово, которое должно стоять на месте ? в строке:
Цель: сделать так, чтобы класс Cat наследовал поведение класса Animal и мог вызывать метод родителя через parent::speak().
Ключевое слово extends используется в PHP, чтобы объявить наследование между классами:
- private — уровень доступа, не используется в объявлении классов.
- public — тоже модификатор доступа, не относится к наследованию.
- static — используется для методов и свойств, а не для классов.
- implements — используется, когда класс реализует интерфейс, а не наследует другой класс.
Если Animal — это родитель, у которого есть функция «говорить», то Cat — его ребёнок. Чтобы ребёнок мог использовать поведение родителя и немного его изменить, мы должны сказать: «Cat наследует Animal» — и для этого в PHP есть слово extends.
Вопрос 9. Вы выполняете SQL-запрос на добавление нового пользователя, и возможна ошибка из-за нарушения уникального ограничения (например, email уже существует в БД).
Нужно корректно отловить эту ошибку.
Если вы используете PDO, то при нарушении уникального ключа база данных сгенерирует исключение (если установлен режим ERRMODE_EXCEPTION):
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
И тогда можно корректно отловить ошибку:
$stmt = $pdo->prepare("INSERT INTO users (email) VALUES (?)");
if ($e->getCode() == '23000') { // SQLSTATE code for integrity constraint violation
echo "Email уже зарегистрирован.";
throw $e; // пробросим другие ошибки дальше
Почему остальные варианты неподходящие:
- throw внутри SQL-запроса — невозможно. SQL не понимает PHP-операторы.
- finally { ... } после запроса — не ловит ошибки, просто выполняется всегда. Это не для обработки ошибок.
- if ($result === false) — работает только при ERRMODE_SILENT, а это не лучший выбор в современных приложениях.
- $_ERROR_HANDLER — не существует как глобальная переменная в PHP.
Когда ты отправляешь запрос в базу, это как письмо в банк.
Если база говорит: «Такой email уже есть» — ты должен поймать это сообщение в ловушку (catch) и вежливо ответить пользователю.
1. Обернуть выполнение SQL-запроса в блок try и перехватить ошибку в блоке catch.
Вопрос 10. Проанализировать работу кода с пользовательскими исключениями в PHP.
new ZeroNumberException("Zero value not allowed");
Вот в чём проблема:
Конструктор исключения вызывается, но исключение не выбрасывается, потому что отсутствует ключевое слово throw.
new ZeroNumberException("..."); // создаёт объект, но ничего не происходит
После этого выполнение продолжается и доходит до строки:
- Фатальная ошибка: деление на ноль
- Исключение не было выброшено, значит блок catch не сработает
- Код завершится аварийно
Программа как будто видит предупреждение, но не бросает его в сторону обработчика ошибок (catch). Вместо этого она просто создаёт объект ошибки и идёт дальше — прямо в деление на 0, что в PHP заканчивается фатально.
3. Код сгенерирует фатальную ошибку Division by zero и ничего не выведет, потому что new ZeroNumberException(...) не прерывает выполнение.
Вопрос 11.
- Принимает дату и время в UTC.
- Прибавляет 1 день.
- Переводит результат в указанный часовой пояс (в данном случае "America/New_York").
- Форматирует результат как Y-m-d H:i:s P.
1. Исходная дата:
2024-06-15 15:30:00 UTC
2. Прибавляем 1 день:
2024-06-16 15:30:00 UTC
3. Переводим в "America/New_York":
· На дату 2024-06-16 в Нью-Йорке действует летнее время (EDT):
→ смещение от UTC = -04:00
15:30:00 UTC - 4 часа = 11:30:00 (EDT)
Всё происходит в UTC, потом добавляется день, и только потом переводится в Нью-Йоркское время. Так что 15:30 UTC +1 день = 16 июня 15:30 UTC → Нью-Йорк: 11:30 EDT.
Вопрос 12. Данные на сервере выводятся на экран с помощью следующей строки кода: echo$_POST['email']. Какое поле ввода должно быть в форме, чтобы после заполнения формы на сервере данные были доступны в глобальном массиве $_POST с ключом 'email'?
Значит, в HTML-форме должно быть поле:
Параметр name определяет ключ в массиве $_POST, а не id, type и т. д.
- ❌ <input type="e-mail" id="email" name="e-mail">
→ type неверный (e-mail — невалидный тип). - ❌ <input type="email" id="email" name="mail">
→ name="mail" → попадёт в $_POST['mail'], а не email. - ❌ <input type="email" id="e-mail" name="e-mail">
→ name="e-mail" → $_POST['e-mail'], не email. - ✅ <input type="email" id="e-mail" name="email">
→ name="email" — именно это и нужно. id может быть любым. - ❌ <input type="mail" id=”email” name="e-mail">
→ type="mail" — невалидный HTML5-тип. И name="e-mail" — не то, что требуется.
4. <input type="email" id="e-mail" name="email">
Заключение:
Если вы прошли этот тест и поняли, почему важен try/catch, как работают prepared statements для защиты от SQL-инъекций, и чем отличается self:: от static:: — вы уже серьёзный специалист. Такие знания нужны не только для прохождения тестов на HH или собеседований, но и для реальной работы: написания безопасного, надёжного и поддерживаемого кода. А если вы пока ошибались — не беда. Важно, что теперь вы знаете, почему именно так работает PHP. И это уже половина успеха.