May 31

Средний уровень PHP — практика на тесте с подвохами

Если вы уже знаете, как объявить переменную и написать цикл for, самое время двигаться дальше. PHP — это не только про «вывести текст на экран», но и про умение думать как программист. На среднем уровне проверяют: а вы точно понимаете, как работает массив по ссылке? Можете ли вы отличить глобальную переменную от статической? А PDO для базы данных — это просто страшные буквы или вы знаете, зачем оно нужно?

В этой статье я подробно разберу тест PHP среднего уровня. Мы пойдём по каждому вопросу: сначала посмотрим, что спрашивают, потом — почему правильный ответ именно такой, а в конце — какие подводные камни могут сбить даже опытных. Объясню простыми словами — чтобы не надо было гуглить каждую строчку. С примерами, ассоциациями и пояснениями.

Вопрос 1. Какое значение будет выведено при выполнении следующего кода?

<?php

class A {

const A = 1;

public static function test() {

return static::A;

}

}

class B extends A {

const A = 2;

}

class C extends B {

const B = 3;

}

print(C::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 »

Вопрос 2. Фрагмент PHP-кода:

$x = 10;

$x %= 3;

echo $x;

Какое значение будет выведено и почему?

Что делает оператор %=?

Это сокращённая форма записи:

$x = $x % 3;

Оператор % — остаток от деления.
То есть:

  • 10 % 3 = 1
    (3 умещается в 10 трижды, остаток — 1)

Объяснение:

Представь, что у тебя 10 конфет, и ты хочешь раздать их поровну по 3 штуки.

  • Ты раздашь 3, 3 и 3 — всего 9.
  • 1 конфета останется. Вот это и есть остаток.
  • И теперь ты записываешь это оставшееся количество обратно в коробку.

Что произойдёт?

$x = 10;

$x %= 3; // теперь $x == 1

echo $x; // выведет 1

Выбранный ответ:

1 — так как оператор %= присваивает переменной остаток от деления её текущего значения

Вопрос 3. У вас есть ассоциативный массив $data, содержащий строки (возможно, повторяющиеся с разным регистром). Вы хотите:

1.     Сохранить только первое вхождение строки.

2.     Игнорировать регистр (Apple и apple — одно и то же).

3.     Сохранить оригинальное значение строки (если встретилось "Apple" — оно остаётся).

4.     Сохранить ключ первого вхождения.

Что важно:

  • Сравнение должно быть регистр-независимым.
  • Сохраняются первые вхождения.
  • Ключи остаются исходными.
  • Оригинальное значение сохраняется, а не приведённое к нижнему регистру.

Разбор вариантов:

Вариант 1:

$result = [];

foreach ($data as $k => $v) {

if (!in_array(strtolower($v), array_map('strtolower', $result), true)) {

$result[$k] = $v;

}

}

  • array_map('strtolower', $result) — приводит все уже добавленные значения к нижнему регистру.
  • in_array(..., true) — ищет строгое вхождение по значению.
  • strtolower($v) — понижает регистр текущего значения для сравнения.
  • !in_array(...) — пропускает повторы с разным регистром.
  • $result[$k] = $v; — сохраняет исходный ключ и оригинальное значение.

Итог: Все условия соблюдены.

Вариант3:

array_unique($data) — неверно:

  • Сохраняет только первые вхождения, но учитывает регистр (то есть "Apple" и "apple" — разные).
  • Не решает задачу.

Вариант 5:

foreach ($data as $k => $v) {

$result[strtolower($v)] = $v;

}

  • Заменяет ключи на strtolower($v).
  • Последнее вхождение перезаписывает предыдущее.
  • Теряются исходные ключи и первое вхождение не сохраняется.

Выбранный ответ:

Вариант 1 — с in_array(strtolower(...)) и array_map.

Вопрос 4. Что делает этот PHP-код с массивом заказов?

$orders = [1001 => 'новый', 'новый', 1005 => 'оплачен', 'отправлен', 1003 => 'доставлен'];

$orders[] = 'новый';

unset($orders[1005]);

$orders[] = 'возврат';

print_r($orders);

Последовательно разберём, что происходит:

Строка 1:

$orders = [1001 => 'новый', 'новый', 1005 => 'оплачен', 'отправлен', 1003 => 'доставлен'];

Инициализация массива со следующими ключами:

  • 1001 => 'новый'
  • PHP сам присвоит следующий ключ — 1002 → 'новый'
  • 1005 => 'оплачен'
  • PHP снова автоматически назначает следующий 1006 → 'отправлен'
  • 1003 => 'доставлен'

👉 На этом этапе массив:

[

1001 => 'новый',

1002 => 'новый',

1005 => 'оплачен',

1006 => 'отправлен',

1003 => 'доставлен'

]

Строка 2:

$orders[] = 'новый';

PHP берёт максимальный числовой ключ (1006) и увеличивает на 1 → 1007 Добавляет:

1007 => 'новый'

Строка 3:

unset($orders[1005]);

Удаляется:

1005 => 'оплачен'

Строка 4:

$orders[] = 'возврат';

Максимальный ключ — теперь 1007 → следующий: 1008

Добавляется:

1008 => 'возврат'

ИТОГОВЫЙ МАССИВ:

Array (

[1001] => новый

[1002] => новый

[1006] => отправлен

[1003] => доставлен

[1007] => новый

[1008] => возврат

)

Объяснение:

Это как если бы ты вела список заказов на бумаге и каждому присваивала номер. Некоторые номера ты пишешь вручную (1001, 1005), некоторые — просто добавляешь по порядку. Если ты вычеркнешь один номер (1005), то новый заказ получит новый следующий номер, а не "перепрыгнет" на старый.

Выбранный ответ:

Последний вариант:

Array (

[1001] => новый

[1002] => новый

[1006] => отправлен

[1003] => доставлен

[1007] => новый

[1008] => возврат

)

Вопрос 5. Вы разрабатываете функцию process_data, которая должна:

1.     Принимать произвольное количество отдельных аргументов — например: process_data("a", "b", "c").

2.     Принимать массив как единственный аргумент — например: process_data(["a", "b", "c"]).

3.     Выводить все значения через пробел.

Что должна делать функция:

  • Понимать, когда ей передан массив как один аргумент.
  • Понимать, когда переданы отдельные значения.
  • Универсально обрабатывать оба случая.

Анализ вариантов:

Вариант 1:

function process_data() {

$args = func_get_args();

foreach ($args as $arg) {

echo $arg . " ";

}

}

  • Работает только с отдельными аргументами.
  • Не обрабатывает случай, когда передан массив как один аргумент.

Неверно.

Вариант 2:

function process_data(...$args) {

if (count($args) === 1 && is_array($args[0])) {

$args = $args[0];

}

foreach ($args as $arg) {

echo $arg . " ";

}

}

  • ...$args — собирает все аргументы (в том числе массив как один элемент).
  • Если передан единственный массив, он разворачивается.
  • Поддерживает оба случая.

Полностью соответствует требованиям. Верно.

Вариант 3:

function process_data(...$args) {

foreach ($args as $arg) {

echo $arg . " ";

}

}

  • Не разворачивает массив, если он передан как один аргумент.

Неверно.

Вариант 4:

function process_data(array $args) {

foreach ($args as $arg) {

echo $arg . " ";

}

}

  • Работает только если передан массив.
  • При передаче нескольких отдельных аргументов вызовет ошибку.

Неверно.

Вариант 5:

function process_data($args) {

if (is_array($args)) {

foreach ($args as $arg) {

echo $arg . " ";

}

} else {

echo $args . " ";

}

}

  • Работает только с одним аргументом.
  • При передаче двух и более аргументов поведение будет ошибочным.

Неверно.

Объяснение:

Функция как сотрудник на складе. Ему либо приносят коробку с заказами (массив), либо приносят заказы по одному (переменные). Хороший сотрудник должен уметь открыть коробку и разобрать её, или взять каждый заказ по отдельности. Второй вариант функции делает это правильно.

Выбранный ответ:

Вариант 2 — с ...$args и проверкой count($args) === 1 && is_array($args[0]).

Вопрос 6. Вы работаете с PDO и MySQL. Нужно выполнить транзакцию — серию SQL-запросов (INSERT, UPDATE), при этом:

  • Если в любом из запросов произойдёт ошибка, изменения не должны попасть в базу.
  • По умолчанию в PDO включён автокоммит, поэтому его нужно отключить вручную, начав транзакцию.
  • Также необходима обработка ошибок через try-catch.

Что требуется для правильной реализации:

1.     Начать транзакцию с $pdo->beginTransaction().

2.     Выполнить SQL-запросы.

3.     В случае успеха — вызвать $pdo->commit().

4.     В случае исключения — вызвать $pdo->rollBack().

Почему это правильно:

  • Используется PDO-специфичная функция beginTransaction() вместо query("BEGIN") или query("START TRANSACTION").
  • Включена корректная обработка ошибок.
  • commit() вызывается только если все запросы прошли успешно.
  • rollBack() вызывается в catch, если что-то пошло не так.

Объяснение:

Ты кладёшь два товара в корзину. Пока не оплатишь — ничего не зафиксировано.
Если при оплате произойдёт сбой — всё откатится, как будто ты вообще ничего не добавлял.

Выбранный ответ:

Последний вариант (нижний блок с beginTransaction(), try, commit(), rollBack()) — реализует корректную транзакцию с откатом при ошибке:

$pdo->beginTransaction();

try {

$pdo->query("INSERT INTO orders (id, amount) VALUES (10, 100)");

$pdo->query("UPDATE accounts SET balance = balance - 100 WHERE id = 5");

$pdo->commit();

} catch (Exception $e) {

$pdo->rollBack();

}

Вопрос 7. У вас есть класс Car с защищённым свойством $engineStatus и методом startEngine().
Нужно, чтобы вызов startEngine() менял $engineStatus на true (включал двигатель).

class Car {

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. Нужно выбрать ключевое слово, которое должно стоять на месте ? в строке:

class Cat ? Animal {

Цель: сделать так, чтобы класс Cat наследовал поведение класса Animal и мог вызывать метод родителя через parent::speak().

Ключевое слово extends используется в PHP, чтобы объявить наследование между классами:

class Cat extends Animal

Объяснение других вариантов:

  • private — уровень доступа, не используется в объявлении классов.
  • public — тоже модификатор доступа, не относится к наследованию.
  • static — используется для методов и свойств, а не для классов.
  • implements — используется, когда класс реализует интерфейс, а не наследует другой класс.

Объяснение:

Если Animal — это родитель, у которого есть функция «говорить», то Cat — его ребёнок. Чтобы ребёнок мог использовать поведение родителя и немного его изменить, мы должны сказать: «Cat наследует Animal» — и для этого в PHP есть слово extends.

Выбранный ответ:

1.     extends

Вопрос 9. Вы выполняете SQL-запрос на добавление нового пользователя, и возможна ошибка из-за нарушения уникального ограничения (например, email уже существует в БД).

Нужно корректно отловить эту ошибку.

Если вы используете PDO, то при нарушении уникального ключа база данных сгенерирует исключение (если установлен режим ERRMODE_EXCEPTION):

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

И тогда можно корректно отловить ошибку:

try {

$stmt = $pdo->prepare("INSERT INTO users (email) VALUES (?)");

$stmt->execute([$email]);

} catch (PDOException $e) {

if ($e->getCode() == '23000') { // SQLSTATE code for integrity constraint violation

echo "Email уже зарегистрирован.";

} else {

throw $e; // пробросим другие ошибки дальше

}

}

Почему остальные варианты неподходящие:

  • throw внутри SQL-запроса — невозможно. SQL не понимает PHP-операторы.
  • finally { ... } после запроса — не ловит ошибки, просто выполняется всегда. Это не для обработки ошибок.
  • if ($result === false) — работает только при ERRMODE_SILENT, а это не лучший выбор в современных приложениях.
  • $_ERROR_HANDLER — не существует как глобальная переменная в PHP.

Объяснение:

Когда ты отправляешь запрос в базу, это как письмо в банк.
Если база говорит: «Такой email уже есть» — ты должен поймать это сообщение в ловушку (catch) и вежливо ответить пользователю.

Выбранный ответ:

1.     Обернуть выполнение SQL-запроса в блок try и перехватить ошибку в блоке catch.

Вопрос 10. Проанализировать работу кода с пользовательскими исключениями в PHP.

Что делает код:

if ($n === 0) {

new ZeroNumberException("Zero value not allowed");

}

Вот в чём проблема:
Конструктор исключения вызывается, но исключение не выбрасывается, потому что отсутствует ключевое слово throw.

Это значит:

new ZeroNumberException("..."); // создаёт объект, но ничего не происходит

После этого выполнение продолжается и доходит до строки:

return 100 / $n;

При n = 0 произойдёт:

  • Фатальная ошибка: деление на ноль
  • Исключение не было выброшено, значит блок 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

4.     Перевод времени:

15:30:00 UTC - 4 часа = 11:30:00 (EDT)

Результат:

2024-06-16 11:30:00 -04:00

Объяснение:

Всё происходит в UTC, потом добавляется день, и только потом переводится в Нью-Йоркское время. Так что 15:30 UTC +1 день = 16 июня 15:30 UTC → Нью-Йорк: 11:30 EDT.

Выбранный ответ:

1. 2024-06-16 11:30:00 -04:00

Вопрос 12. Данные на сервере выводятся на экран с помощью следующей строки кода: echo$_POST['email']. Какое поле ввода должно быть в форме, чтобы после заполнения формы на сервере данные были доступны в глобальном массиве $_POST с ключом 'email'?

На сервере используется:

echo $_POST['email'];

Значит, в HTML-форме должно быть поле:

<input ... name="email">

Параметр name определяет ключ в массиве $_POST, а не id, type и т. д.

Проверка вариантов:

  1. ❌ <input type="e-mail" id="email" name="e-mail">
    → type неверный (e-mail — невалидный тип).
  2. ❌ <input type="email" id="email" name="mail">
    → name="mail" → попадёт в $_POST['mail'], а не email.
  3. ❌ <input type="email" id="e-mail" name="e-mail">
    → name="e-mail" → $_POST['e-mail'], не email.
  4. ✅ <input type="email" id="e-mail" name="email">
    → name="email" — именно это и нужно. id может быть любым.
  5. ❌ <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. И это уже половина успеха.