Питання на співбесіді: PHP 8+
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
10 питань
Іменовані аргументи (PHP 8.0) дозволяють передавати аргументи за іменем параметра, а не лише за позицією.
// Позиційно: що означає true? а 0?
htmlspecialchars($text, ENT_QUOTES, 'UTF-8', false);
// Іменовано
htmlspecialchars($text, double_encode: false);
str_pad(string: 'x', length: 5, pad_type: STR_PAD_LEFT);
Переваги:
- Читабельність для булевих прапорців і «магічних» чисел:
new Invoice(paid: true, sendEmail: false)замістьnew Invoice(true, false). - Пропуск необов'язкових параметрів: передати лише останній, не перелічуючи значення за замовчуванням для всіх попередніх.
- Будь-який порядок: іменовані аргументи можна передавати не в порядку оголошення.
- Разом з constructor promotion дають зручні value objects і DTO:
new Address(city: 'Київ', street: 'Хрещатик').
Правила:
- Спершу позиційні, потім іменовані:
f(1, b: 2)- так;f(a: 1, 2)- ні. - Один параметр не можна передати двічі.
- Невідоме ім'я - помилка.
- Для variadic-параметрів (
...$args) іменовані аргументи збираються в масив з рядковими ключами.
Застереження для авторів бібліотек: з іменованими аргументами назва параметра стає частиною публічного API. Перейменування $str на $string у новій версії зламає код, що викликав функцію іменовано. Тому в бібліотеках назви параметрів змінюють так само обережно, як і назви методів.
First-class callable syntax (PHP 8.1) створює замикання (Closure) з функції чи методу, записавши (...) замість аргументів.
$length = strlen(...);
$length('привіт'); // 12
$format = $this->formatPrice(...); // метод об'єкта
$create = User::create(...); // статичний метод
array_map(trim(...), $lines);
array_filter($users, $this->isActive(...));
Чим краще за старі способи передати функцію:
array_map('trim', $lines); // рядок
array_filter($users, [$this, 'isActive']); // масив
array_filter($users, fn ($u) => $this->isActive($u)); // обгортка
- Перевіряється одразу: якщо методу не існує чи він недоступний, помилка виникне в момент створення, а не десь пізніше при виклику.
- Рефакторинг і IDE: «перейти до визначення» і перейменування методу працюють - на відміну від імені в рядку
'isActive'. - Видимість:
$this->privateMethod(...)усередині класу створює замикання, яке можна передати назовні й викликати, - а[$this, 'privateMethod']поза класом не спрацює. - Статичний аналіз бачить сигнатуру й перевіряє типи.
Обмеження: не працює з nullsafe ($obj?->method(...) - помилка) і з new Foo(...).
PHP 8.5 доповнив це оператором конвеєра |>, який передає значення в callable праворуч: $text |> trim(...) |> strtoupper(...).
PHP 8.0 (листопад 2020) - один з найбільших релізів мови. Головне:
Мова:
- Іменовані аргументи -
f(limit: 10). - Constructor property promotion - властивості в параметрах конструктора.
- Union-типи -
int|string, плюс типmixedі тип поверненняstatic. match- вираз зі строгим порівнянням і без провалювання.- Nullsafe-оператор
?->. - Атрибути -
#[Route('/users')]замість анотацій у PHPDoc. throwяк вираз -$user ?? throw new NotFound().catchбез змінної -catch (TimeoutException).
Суворіша поведінка:
- Порівняння числа з рядком:
0 == 'abc'теперfalse. - Помилки замість попереджень: багато внутрішніх функцій кидають
TypeErrorчиValueErrorпри неправильних аргументах, а арифметика з нечисловим рядком -TypeError. - Узгоджені сигнатури вбудованих функцій і перевірка сумісності методів при успадкуванні.
Нові функції: str_contains(), str_starts_with(), str_ends_with(), get_debug_type(), fdiv().
Продуктивність: JIT-компілятор (для обчислювальних задач; типовому веб-застосунку дає небагато) і загальні оптимізації рушія.
Чому це важливо на співбесіді: більшість сучасного PHP-коду спирається саме на ці можливості, а суворіші правила PHP 8 - часта причина проблем при оновленні старих застосунків з PHP 7.
Атрибути (PHP 8.0) - структуровані метадані, прикріплені до класів, методів, властивостей, параметрів, функцій і констант. Синтаксис #[...]. На відміну від анотацій у PHPDoc, атрибути - частина мови: перевіряються синтаксично, мають класи й автодоповнення.
Оголошення атрибута:
#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
public function __construct(
public string $path,
public string $method = 'GET',
) {}
}
Використання:
final class UserController
{
#[Route('/users', method: 'GET')]
public function index(): Response { /* ... */ }
}
Читання через Reflection:
foreach ((new ReflectionClass(UserController::class))->getMethods() as $method) {
foreach ($method->getAttributes(Route::class) as $attribute) {
$route = $attribute->newInstance(); // створює об'єкт Route з аргументами
$router->add($route->method, $route->path, [$method->class, $method->name]);
}
}
Важливо: атрибут сам нічого не робить. Це лише дані, які хтось має прочитати. Об'єкт атрибута створюється лише при newInstance().
Вбудовані атрибути PHP: #[\Override] (8.3) - перевірка, що метод справді перевизначає батьківський; #[\SensitiveParameter] (8.2) - приховати значення параметра в стек-трейсах (паролі); #[\Deprecated] (8.4); #[\NoDiscard] (8.5) - попередження, якщо результат функції проігноровано; #[\AllowDynamicProperties].
У Laravel: атрибути контейнера (#[Config('app.name')], #[CurrentUser], #[Storage('s3')]), атрибути моделей (#[ObservedBy], #[ScopedBy]), атрибути Livewire (#[Url], #[Locked], #[Computed]).
Продуктивність: читання через Reflection не безкоштовне - фреймворки кешують результат (кеш маршрутів, скомпільований контейнер).
PHP 8.4 (листопад 2024) - головні зміни:
Property hooks - логіка читання й запису властивості без геттерів і сеттерів:
public string $email {
set => mb_strtolower($value);
}
Асиметрична видимість - різні права на читання й запис: public private(set) string $status.
new без дужок у ланцюжку:
$name = new Profile($user)->displayName(); // раніше: (new Profile($user))->displayName()
Нові функції масивів: array_find(), array_find_key(), array_any(), array_all():
$admin = array_find($users, fn (User $u) => $u->isAdmin());
$allPaid = array_all($orders, fn (Order $o) => $o->isPaid());
Лінива ініціалізація об'єктів (lazy objects) - вбудована підтримка ghost- і proxy-об'єктів, що ініціалізуються при першому зверненні. Корисно для ORM і контейнерів.
#[\Deprecated] - позначати власні функції й методи застарілими, і PHP сам видаватиме E_USER_DEPRECATED при виклику.
Інше:
- Новий DOM API з підтримкою HTML5 (
Dom\HTMLDocument). mb_trim(),mb_ucfirst(),mb_lcfirst().- BCMath отримав об'єктний API (
BcMath\Number) з перевантаженням операторів. request_parse_body()- розбір тіла дляPUT/PATCH.
Застарілим стало: неявно nullable параметри - function f(Type $x = null) тепер видає попередження; правильно ?Type $x = null. Це одна з найчастіших змін при оновленні старого коду.
PHP 8.5 (листопад 2025) - головні нововведення:
Оператор конвеєра |> - передає значення ліворуч у callable праворуч, без проміжних змінних і вкладених викликів:
$slug = $title
|> trim(...)
|> mb_strtolower(...)
|> (fn (string $s) => preg_replace('/\s+/', '-', $s));
// замість
$slug = preg_replace('/\s+/', '-', mb_strtolower(trim($title)));
Кожен крок має приймати один параметр, тому функції з кількома аргументами загортають у стрілкову функцію.
clone з новими значеннями - зручні «withers» для незмінних об'єктів, зокрема з readonly-властивостями:
public function withAmount(int $amount): static
{
return clone($this, ['amount' => $amount]);
}
#[\NoDiscard] - PHP попередить, якщо результат функції проігноровано. Захищає від помилок на кшталт виклику методу незмінного об'єкта без присвоєння результату.
URI extension - вбудований розбір і нормалізація URL за RFC 3986 і стандартом WHATWG (Uri\Rfc3986\Uri, Uri\WhatWg\Url) замість неточного parse_url().
Інше:
array_first()іarray_last()- перший і останній елемент без маніпуляцій з внутрішнім вказівником масиву.- Замикання й first-class callables у константних виразах (наприклад, у значеннях атрибутів і параметрів за замовчуванням).
- Стек-трейси для фатальних помилок.
- Постійні cURL share handles - повторне використання з'єднань і DNS-кешу між запитами.
Як оновлюватися: спершу прогнати тести й статичний аналіз на новій версії в CI, переглянути список застарілого в посібнику з міграції, і лише потім оновлювати прод.
Типізовані константи класу (PHP 8.3) - тип можна вказати й для константи:
interface HasVersion
{
const string VERSION = '1.0';
}
class Api implements HasVersion
{
const string VERSION = '2.0'; // ок
// const VERSION = 2; // помилка: константа має бути string
}
Раніше нащадок чи клас, що реалізує інтерфейс, міг перевизначити константу значенням будь-якого типу, і код, що на неї покладався, отримував сюрприз.
#[\Override] - позначає метод, який має перевизначати метод батьківського класу чи інтерфейсу. Якщо такого методу в предка немає, PHP видасть помилку:
class BaseController
{
protected function authorizeRequest(): void {}
}
class UserController extends BaseController
{
#[\Override]
protected function authorizeRequest(): void {} // ок
#[\Override]
protected function authoriseRequest(): void {} // помилка: опечатка, нічого не перевизначено
}
Навіщо #[\Override]:
- Ловить опечатки в назві перевизначеного методу - без атрибута такий метод просто ніколи не викликається фреймворком.
- Ловить зміни в батьківському класі: якщо бібліотека перейменувала чи видалила метод, усі «перевизначення» в нащадках одразу дадуть помилку замість тихого мертвого коду.
- Документує намір для читача.
Ще з PHP 8.3: json_validate() (перевірка без розбору всього документа в пам'ять), динамічне звернення до констант класу (Foo::{$name}), повторна ініціалізація readonly-властивостей у __clone, нові методи Random\Randomizer, mb_str_pad().
Лінивий об'єкт створюється одразу, але його справжня ініціалізація (дорогий конструктор, запит до бази, з'єднання) відкладається до моменту, коли об'єкт справді знадобиться. PHP 8.4 зробив це вбудованою можливістю мови через Reflection.
Два види:
- Ghost - сам об'єкт потрібного класу, який ініціалізується «на місці» при першому зверненні до його стану:
$reflector = new ReflectionClass(Connection::class);
$connection = $reflector->newLazyGhost(function (Connection $object): void {
$object->__construct(config('database.dsn')); // підключення лише тепер
});
// ... з'єднання ще не встановлено
$connection->query('SELECT 1'); // перше звернення до властивостей - ініціалізація
- Proxy - об'єкт-замісник, який при першому зверненні створює справжній екземпляр через фабрику й далі перенаправляє йому всі звернення:
$service = $reflector->newLazyProxy(fn () => $container->make(HeavyService::class));
Важливий нюанс: ініціалізацію запускає звернення до властивостей об'єкта. Виклик методу, який не чіпає стан, лінивий об'єкт не ініціалізує.
Де це корисно:
- Контейнери залежностей: сервіс, впроваджений у контролер, але не використаний у цьому запиті, не створюється взагалі.
- ORM: зв'язана сутність завантажується з бази лише при зверненні до неї (так Doctrine давно робила через згенеровані класи-проксі - тепер це вміє мова).
- Важкі клієнти: з'єднання з Redis, S3, сторонніми API, яке потрібне лише в частині запитів.
Чим краще за рукописні проксі: не треба генерувати код класів-проксі, об'єкт лишається того самого класу (працюють instanceof і типи), а final-класи теж можна зробити лінивими.
Обмеження: ініціалізація може відбутися в несподіваний момент (наприклад, при var_dump чи серіалізації), а помилка в ініціалізаторі виникне там, де об'єкт уперше використали, а не там, де його створили.
Оновлення PHP - це не зміна однієї цифри в Dockerfile. Ламаються зазвичай не нові можливості, а зміни поведінки й залежності.
1. Прочитати посібник з міграції на php.net для кожної версії між поточною й цільовою: несумісні зміни, застаріле, змінені функції.
2. Перевірити залежності:
composer why-not php 8.5
composer outdated --direct
Пакет, що не підтримує нову версію PHP, - найчастіший блокер. Оновити, знайти заміну чи дочекатися релізу.
3. Автоматичні інструменти:
- Rector - автоматично переписує код під нову версію (набори правил
UpgradeToPhp84тощо) і прибирає застарілі конструкції. - PHPStan/Psalm з налаштованою цільовою версією PHP.
- PHPCompatibility для PHP_CodeSniffer - знаходить несумісний код.
4. Тести на обох версіях. Матриця в CI: поточна й нова версія PHP. Поки обидві зелені, можна оновлюватися поступово.
5. Застарілі попередження - як помилки в тестах. Deprecation сьогодні - помилка в наступній мажорній версії. PHPUnit/Pest можна налаштувати так, щоб E_DEPRECATED валили тест.
6. Поступове розгортання: спершу стейджинг, потім частина продакшн-трафіку (канарка), моніторинг помилок і швидкодії, і лише потім усе.
Типові проблеми при оновленні:
- PHP 8.0: порівняння чисел з рядками,
TypeErrorзамість попереджень у вбудованих функціях. - PHP 8.1: передача
nullу параметри вбудованих функцій, що не приймають null (deprecated). - PHP 8.2: динамічні властивості й інтерполяція
${var}- deprecated. - PHP 8.4: неявно nullable параметри - deprecated.
Не відкладати надовго: кожна версія PHP отримує виправлення безпеки обмежений час. Оновлюватися щороку на одну версію значно легше, ніж стрибати через три.
Застарілі (deprecated) можливості ще працюють, але видають попередження й будуть прибрані в наступній мажорній версії. Найпоширеніші в реальному коді:
Динамічні властивості (PHP 8.2):
$user = new User();
$user->temporaryFlag = true; // Deprecated: Creation of dynamic property
Оголосіть властивість явно. Для класів, яким це справді потрібно, - #[\AllowDynamicProperties]. stdClass і класи з __get/__set не зачіпаються.
Інтерполяція ${var} у рядках (8.2):
"Привіт, ${name}"; // застаріло
"Привіт, {$name}"; // правильно
Неявно nullable параметри (8.4):
function find(Criteria $criteria = null) {} // застаріло
function find(?Criteria $criteria = null) {} // правильно
null у параметрах вбудованих функцій, що не приймають null (8.1):
strlen(null); // Deprecated
trim($request->input('name')); // якщо поле відсутнє - null
trim($request->input('name') ?? '');
Інше:
utf8_encode()/utf8_decode()(8.2) →mb_convert_encoding().- Часткова підтримка callable-рядків на кшталт
"self::method"(8.2). - Неявне приведення
floatз дробовою частиною доintу ключах і операціях (8.1). E_STRICTяк константа (8.4).
Як знайти все одразу:
error_reporting(E_ALL)у тестах і перетворення deprecation на помилки тестів.- Laravel пише застарілі в окремий канал логів (
LOG_DEPRECATIONS_CHANNEL) - увімкнути його на стейджингу й подивитися, що сиплеться під реальним трафіком. - Rector автоматично виправляє більшість із цього списку.
Чим раніше прибрати застаріле, тим дешевше наступне оновлення PHP.