Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді з PHP

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

100 питань

  • final class - від класу не можна успадкуватися.
  • final public function - метод не можна перевизначити в нащадку.
  • final const (PHP 8.1) - константу не можна перевизначити.
final class InvoiceNumberGenerator
{
    public function next(): string { /* ... */ }
}

class Report extends InvoiceNumberGenerator {}  // Fatal error

Чому final за замовчуванням:

  • Успадкування - найсильніший зв'язок. Нащадок залежить від усіх деталей предка. Будь-яка зміна в класі може зламати чужого нащадка, про якого автор навіть не знає.
  • Свобода змінювати клас. Якщо клас final, його внутрішню будову (protected-методи, порядок викликів) можна змінювати вільно: зовні доступний лише публічний API.
  • Свідомі рішення. Прибрати final, коли з'явилася реальна потреба в успадкуванні, - одна правка. Повернути final, коли на клас уже успадковуються, - ламаюча зміна.
  • Підштовхує до композиції й інтерфейсів замість ієрархій.

Типове заперечення - «а як мокати в тестах?». Мокати слід інтерфейси, а не конкретні класи. Якщо клас треба підмінити, - він реалізує інтерфейс, і тест підставляє іншу реалізацію. Для зовнішніх final-класів бібліотек є обхідні шляхи (пакет dg/bypass-finals), але це сигнал щодо дизайну.

Де final не ставлять: базові класи, призначені для успадкування (Model, Controller, абстрактні класи), і класи, які фреймворк проксує чи розширює під час виконання (наприклад, моделі Doctrine з лінивими проксі).

Докладніше в документації: Ключове слово final

PHP використовує бібліотеку PCRE: функції preg_* приймають шаблон з роздільниками (/.../, ~...~, #...#) і модифікаторами після них.

preg_match('/^\d{4}-\d{2}-\d{2}$/', $date);                  // 1, 0 або false при помилці
preg_match('/(?<year>\d{4})-(?<month>\d{2})/', $s, $m);      // $m['year'], $m['month']
preg_match_all('/#(\w+)/u', $text, $matches);               // усі хештеги
preg_replace('/\s+/', ' ', $text);                          // стиснути пробіли
preg_replace_callback('/\d+/', fn ($m) => $m[0] * 2, $s);
preg_split('/[,;]\s*/', $list);

Модифікатор u вмикає режим UTF-8:

  • шаблон і рядок обробляються як UTF-8, а не як байти;
  • . відповідає одному символу, а не байту;
  • працюють Unicode-класи: \p{L} (будь-яка літера), \p{Lu} (велика), \p{Cyrillic}.

Без u шаблон /^.{3}$/ для «кіт» не спрацює (там 6 байтів), а \w не вважає кирилицю літерами.

preg_match('/^[\p{L}\s\'-]+$/u', "Мар'яна Іваненко");   // 1

Інші корисні модифікатори: i - без урахування регістру, m - ^/$ для кожного рядка, s - . охоплює й переведення рядка, x - пробіли й коментарі в шаблоні для читабельності.

Пастки:

  • Помилка ≠ збіг: preg_match повертає false при помилці (зламаний UTF-8 у рядку з u, перевищення ліміту backtracking). Причина - у preg_last_error_msg().
  • Катастрофічний backtracking: шаблони на кшталт (a+)+$ на зловмисному введенні виконуються експоненційно довго (ReDoS). Уникайте вкладених квантифікаторів.
  • Екранування введення користувача в шаблоні - через preg_quote($input, '/').
  • Для простих випадків швидші й зрозуміліші str_contains, str_starts_with, str_replace.

Докладніше в документації: preg_match

Деструктуризація - розпакування масиву в окремі змінні (короткий синтаксис замість list()):

[$first, $second] = [10, 20];
[, $second] = [10, 20];                         // пропустити елемент
['id' => $id, 'name' => $name] = $user;         // за ключами
[$a, [$b, $c]] = [1, [2, 3]];                   // вкладене
[$a, $b] = [$b, $a];                            // обмін значеннями

foreach ($rows as ['id' => $id, 'email' => $email]) {
    // ...
}

Якщо ключа немає, змінна отримає null з попередженням.

Spread-оператор ... розпаковує масив у місці виклику чи в літералі масиву:

$all = [...$defaults, ...$overrides];   // склеїти масиви
$merged = [...['a' => 1], ...['a' => 2, 'b' => 3]];   // ['a' => 2, 'b' => 3] (PHP 8.1+)

function sum(int ...$numbers): int        // збір аргументів (variadic)
{
    return array_sum($numbers);
}

sum(...[1, 2, 3]);                        // передача масиву як аргументів

Що варто знати:

  • Рядкові ключі в spread дозволені з PHP 8.1 і поводяться як array_merge: пізніший ключ перемагає. Числові ключі перенумеровуються.
  • Spread ітератора: [...$generator] працює з будь-яким Traversable.
  • Іменовані аргументи зі spread: create(...['name' => 'Оля', 'email' => '...']) (PHP 8.1+) - ключі масиву стають іменами параметрів.
  • Не для великих масивів у циклі: $result = [...$result, $item] у циклі щоразу копіює весь масив - квадратична складність. Для накопичення - $result[] = $item.

Докладніше в документації: Деструктуризація масивів

У PHP є дві системи повідомлень про проблеми: винятки (throw) і помилки рушія - E_WARNING, E_NOTICE, E_DEPRECATED. Попередження за замовчуванням лише пишеться в лог, а виконання продовжується - часто з неправильними даними:

$content = file_get_contents('/missing.txt');   // Warning, $content === false
$data = json_decode($content);                  // працюємо далі з false

set_error_handler перехоплює такі помилки. Найпоширеніший прийом - перетворити їх на ErrorException:

set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    if (! (error_reporting() & $severity)) {
        return false;   // помилку придушено через @ чи налаштування
    }

    throw new ErrorException($message, 0, $severity, $file, $line);
});

Тепер file_get_contents для неіснуючого файлу кидає виняток, і код не піде далі з false.

Laravel робить це за вас: обробник помилок фреймворку (HandleExceptions) перетворює попередження й notice на ErrorException. Тому в Laravel-застосунку «Undefined array key» - це виняток зі стек-трейсом, а не рядок у лозі. Застарілі (E_DEPRECATED) Laravel на винятки не перетворює, а пише в окремий канал логів, якщо його налаштовано (LOG_DEPRECATIONS_CHANNEL).

Що варто знати:

  • Фатальні помилки (E_ERROR, вичерпання пам'яті) set_error_handler не перехоплює. Для них - register_shutdown_function() з error_get_last().
  • Оператор @ придушує попередження, але приховує й справжні проблеми. Краще перевіряти результат функції явно.
  • Сучасні функції часто вже кидають винятки: json_decode(..., flags: JSON_THROW_ON_ERROR), random_int(), більшість методів класів SPL і intl.

Докладніше в документації: set_error_handler

?-> (PHP 8.0) звертається до методу чи властивості, лише якщо об'єкт ліворуч не null. Якщо він null, весь ланцюжок повертає null і далі не виконується (short-circuit).

// До PHP 8
$country = null;
if ($session !== null) {
    $user = $session->user();
    if ($user !== null && $user->address !== null) {
        $country = $user->address->country;
    }
}

// PHP 8
$country = $session?->user()?->address?->country;

Short-circuit важливий: якщо $session - null, метод user() навіть не викликається, а аргументи далі по ланцюжку не обчислюються.

Обмеження:

  • Лише читання. $user?->name = 'Оля' - помилка компіляції: присвоювати через ?-> не можна.
  • Не працює з масивами: $data?['key'] такого синтаксису немає. Для масивів - ??.
  • Не для посилань: foo($obj?->prop) у параметр за посиланням не передати.
  • null - не «помилка». Якщо об'єкт мав бути там завжди, ?-> ховає баг: замість зрозумілого винятку код тихо працює з null.

Разом з ?? дає значення за замовчуванням:

$city = $order->customer?->address?->city ?? 'Не вказано';

Порада: ?-> - для справді необов'язкових зв'язків (у користувача може не бути профілю). Довгі ланцюжки ?-> - запах: код занадто багато знає про внутрішню будову інших об'єктів, і варто подумати про метод на кшталт $order->shippingCity().

Докладніше в документації: Nullsafe-оператор

Головне правило - ніколи не тримати в пам'яті весь файл і не накопичувати результати. Читати рядок за рядком і одразу обробляти.

Читання потоком:

function rows(string $path): Generator
{
    $file = new SplFileObject($path);
    $file->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);

    $header = null;
    foreach ($file as $row) {
        if ($header === null) {
            $header = $row;
            continue;
        }
        yield array_combine($header, $row);
    }
}

Генератор віддає по одному рядку, тож пам'ять стала незалежно від розміру файлу.

Запис у базу - порціями:

$batch = [];
foreach (rows('import.csv') as $row) {
    $batch[] = ['email' => $row['email'], 'name' => $row['name']];

    if (count($batch) === 1000) {
        DB::table('subscribers')->insert($batch);
        $batch = [];
    }
}
if ($batch !== []) {
    DB::table('subscribers')->insert($batch);
}

Вставка по одному рядку - мільйон запитів; порціями по 1000 - тисяча. Масова вставка через query builder ще й не створює моделей і не запускає подій.

Що ще враховувати:

  • Кодування й роздільник: CSV з Excel часто в Windows-1251 і з ;. Перекодування на льоту - потоковим фільтром (php://filter/read=convert.iconv.windows-1251/utf-8/resource=...).
  • Логування запитів вимкнене: DB::enableQueryLog() у довгому імпорті накопичує всі запити в пам'яті.
  • Помилки в окремих рядках не мають зупиняти весь імпорт - збирати їх у звіт.
  • Перервність: запам'ятовувати номер обробленого рядка, щоб продовжити після збою, а не починати спочатку.
  • Черга: великий імпорт з веб-інтерфейсу - завдання в черзі, а не синхронний запит.

У Laravel те саме зручно робити через LazyCollection::make(fn () => yield from rows(...))->chunk(1000).

Докладніше в документації: SplFileObject

PHP-FPM (FastCGI Process Manager) - менеджер процесів, що виконують PHP. Веб-сервер (Nginx, Caddy) передає йому запити по FastCGI, а FPM роздає їх пулу воркерів. Один воркер обробляє один запит за раз.

Режими менеджера процесів (pm):

  • static - фіксована кількість воркерів (pm.max_children). Передбачувано, найкраще для виділених серверів.
  • dynamic - кількість змінюється між pm.min_spare_servers і pm.max_children, стартує з pm.start_servers. Типовий вибір.
  • ondemand - воркери створюються лише під запити й завершуються після простою. Економить пам'ять на малонавантажених сайтах, але перший запит повільніший.

Як порахувати pm.max_children: обмеження - пам'ять.

max_children ≈ (пам'ять для PHP) / (середня пам'ять одного воркера)

Сервер з 4 ГБ, з яких 1 ГБ потрібно базі й системі, і воркери по ~60 МБ: (3072 / 60) ≈ 50. Реальне споживання воркера дивляться в ps чи статусній сторінці FPM під навантаженням.

Що буде при помилці:

  • Замало воркерів: запити стають у чергу, час відповіді росте, у лозі FPM - server reached pm.max_children setting.
  • Забагато: під навантаженням пам'ять закінчується, сервер іде в swap чи ядро вбиває процеси - гірше, ніж черга.

Корисні налаштування:

  • pm.max_requests - перезапускати воркер після N запитів, щоб обмежити наслідки витоків пам'яті.
  • request_terminate_timeout - вбивати завислий запит.
  • pm.status_path - сторінка статусу: активні й вільні воркери, довжина черги.
  • slowlog з request_slowlog_timeout - стек-трейси повільних запитів.

Альтернатива - довгоживучі сервери (FrankenPHP, RoadRunner, Swoole через Laravel Octane): фреймворк завантажується один раз, але з'являються проблеми стану між запитами.

Докладніше в документації: Налаштування PHP-FPM

За замовчуванням Composer шукає пакети на Packagist. Інші джерела додають у секцію repositories.

Git-репозиторій (приватний пакет):

"repositories": [
    { "type": "vcs", "url": "git@github.com:acme/billing-sdk.git" }
],
"require": {
    "acme/billing-sdk": "^2.1"
}

Composer читає теги репозиторію як версії. Доступ - через SSH-ключ або токен у auth.json (composer config --global github-oauth.github.com <token>), а в CI - через змінну COMPOSER_AUTH.

Локальна папка (розробка пакета поруч із застосунком):

"repositories": [
    { "type": "path", "url": "../packages/billing-sdk", "options": { "symlink": true } }
]

Зміни в пакеті одразу видно в застосунку, без публікації нових версій. Для монорепозиторіїв - "url": "packages/*".

Приватний реєстр: для кількох пакетів і великої команди зручніше Private Packagist, Satis (статичний реєстр) або реєстр пакетів GitLab/GitHub - ними керують як Packagist, з версіями й правами доступу.

Що варто знати:

  • Порядок важливий: репозиторії з repositories перевіряються перед Packagist. Пакет з тією ж назвою з вашого репозиторію матиме пріоритет - цим користуються для тимчасових форків з виправленням ("type": "vcs" на свій форк і версія dev-fix-branch).
  • Форк - тимчасовий захід. Виправлення варто віддати в оригінальний пакет і повернутися на нього, інакше ви самі підтримуєте форк.
  • path-репозиторій на проді зазвичай не використовують: локальний шлях там може не існувати.

Докладніше в документації: Репозиторії Composer

PER Coding Style - стандарт форматування PHP-коду від PHP-FIG, наступник PSR-12 (а той - PSR-2). Описує відступи (4 пробіли), розташування дужок, пробіли навколо операторів, порядок use, оформлення сигнатур, нові конструкції мови (enum, match, атрибути, property hooks).

Навіщо єдиний стиль: код-рев'ю обговорює суть, а не пробіли; diff-и не засмічуються переформатуванням; новий розробник читає код без звикання до особистих звичок кожного автора.

Інструменти, що форматують автоматично:

  • Laravel Pint - обгортка над PHP-CS-Fixer з пресетом Laravel (є й пресет per). У Laravel-проєктах - стандарт.
vendor/bin/pint            # виправити
vendor/bin/pint --test     # лише перевірити (для CI)
vendor/bin/pint --dirty    # лише змінені файли
  • PHP-CS-Fixer - гнучке налаштування правил.
  • PHP_CodeSniffer (phpcs/phpcbf) - перевірка й виправлення за стандартами.

Як впровадити, щоб працювало:

  1. Один конфіг у репозиторії (pint.json) - а не налаштування в IDE кожного розробника.
  2. Перевірка в CI - злиття неможливе, якщо стиль порушено.
  3. Автоформатування при збереженні в IDE чи pre-commit хук - щоб CI рідко падав.
  4. Одноразове переформатування всього коду окремим комітом, а його хеш - у .git-blame-ignore-revs, щоб git blame не показував цей коміт як автора кожного рядка.

Форматування ≠ якість коду: стиль ловить пробіли й дужки, а помилки логіки й типів - статичний аналіз (PHPStan/Larastan) і тести.

Докладніше в документації: PER Coding Style

Так. Enum у PHP - повноцінний тип: він може мати методи (звичайні й статичні), константи, реалізовувати інтерфейси й використовувати трейти (без властивостей).

interface HasLabel
{
    public function label(): string;
}

enum OrderStatus: string implements HasLabel
{
    case New = 'new';
    case Paid = 'paid';
    case Shipped = 'shipped';

    public const DEFAULT = self::New;

    public function label(): string
    {
        return match ($this) {
            self::New => 'Нове',
            self::Paid => 'Оплачене',
            self::Shipped => 'Відправлене',
        };
    }

    public function isFinal(): bool
    {
        return $this === self::Shipped;
    }

    public static function forSelect(): array
    {
        return array_combine(
            array_column(self::cases(), 'value'),
            array_map(fn (self $s) => $s->label(), self::cases()),
        );
    }
}

$order->status->label();       // 'Оплачене'
OrderStatus::DEFAULT;          // OrderStatus::New

Навіщо це: вся поведінка, пов'язана з переліком, живе в одному місці. Замість if ($status === 'paid' || $status === 'shipped'), розкиданих по коду, - $status->isFinal(). Додали новий варіант - match без default змусить оновити кожен метод.

Інтерфейси дозволяють використовувати різні enum однаково: Filament і багато бібліотек розпізнають інтерфейси на кшталт HasLabel, HasColor, HasIcon і самі показують мітку, колір і іконку.

Обмеження:

  • Немає властивостей (стану) - лише константи.
  • Не можна успадкувати один enum від іншого.
  • Трейти - лише без властивостей.
  • Магічні методи заборонені, крім __call, __callStatic і __invoke.

Докладніше в документації: Методи перелічень

Варіант enum - це об'єкт, а ключем масиву в PHP може бути лише int чи string. Спроба дає помилку:

$limits = [Plan::Free => 3, Plan::Pro => 100];
// TypeError: Cannot access offset of type Plan on array

Варіанти:

  • ->value backed enum як ключ:
$limits = [Plan::Free->value => 3, Plan::Pro->value => 100];
$limits[$user->plan->value];

Просто, але втрачається тип: ключ знову рядок, і опечатка в ключі не ловиться.

  • Метод на самому enum - найчастіше найкращий варіант:
enum Plan: string
{
    case Free = 'free';
    case Pro = 'pro';

    public function projectLimit(): int
    {
        return match ($this) {
            self::Free => 3,
            self::Pro => 100,
        };
    }
}

Знання про ліміт живе разом із варіантами, а новий варіант без гілки в match не пройде непомітно.

  • SplObjectStorage чи WeakMap - коли потрібна справжня мапа «об'єкт → значення», наприклад, для підрахунку: $counts[$status] ??= 0 з WeakMap працює з enum як ключами.

Ще про порівняння: < і > між варіантами не мають сенсу - результат завжди false, бо порядку між об'єктами немає. Якщо порядок потрібен, - метод order(): int чи порівняння ->value для числових backed enum. Рівність перевіряють через ===: варіанти - синглтони.

Докладніше в документації: Відмінності перелічень від об'єктів

Простір імен - префікс до назв класів, функцій і констант, щоб різні бібліотеки могли мати класи з однаковими іменами: App\Models\User і Laravel\Socialite\Two\User - різні класи.

namespace App\Services\Billing;

use App\Models\Order;
use Illuminate\Support\Facades\Log;
use Stripe\Invoice as StripeInvoice;   // псевдонім, щоб не конфліктувати з нашим Invoice

final class Invoice
{
    public function sync(Order $order, StripeInvoice $remote): void { /* ... */ }
}

use не підключає файл і нічого не завантажує - лише створює коротке ім'я для повного. Клас завантажить автозавантажувач у момент першого використання.

Як PHP розв'язує ім'я класу:

  • Повне ім'я з \ на початку - як є: \DateTime.
  • Ім'я з use - підставляється повне.
  • Інакше - до імені додається поточний простір імен: new DateTime() у App\Services шукатиме App\Services\DateTime. Звідси класична помилка «Class App\Services\DateTime not found» - потрібен use DateTime; чи \DateTime.

Функції й константи розв'язуються інакше: якщо в поточному просторі функції немає, PHP шукає глобальну (strlen працює без \). Явний \strlen() чи use function strlen; трохи пришвидшує виклик, бо рушій не перевіряє простір імен, і дозволяє OPcache оптимізувати вбудовані функції.

Простори імен і PSR-4: простір імен відповідає каталогу (App\Services\Billing → app/Services/Billing), а ім'я класу - файлу. На цьому збігу тримається автозавантаження.

::class дає повне ім'я класу рядком: Invoice::class → 'App\Services\Billing\Invoice' - так посилаються на класи в конфігурації без помилок у рядках.

Докладніше в документації: Простори імен

Одинарні лапки - рядок «як є». Змінні не підставляються, з екранувань працюють лише \' і \\.

'Привіт, $name\n';   // буквально: Привіт, $name\n

Подвійні лапки - з інтерполяцією змінних і екрануваннями (\n, \t, \u{1F600}, \$):

"Привіт, $name\n";
"Сума: {$order->total} грн";          // складні вирази - у фігурних дужках
"Перший: {$items[0]['title']}";

Heredoc - багаторядковий рядок з інтерполяцією, як подвійні лапки:

$html = <<<HTML
    <p>Привіт, {$user->name}</p>
    <p>Замовлень: {$count}</p>
    HTML;

Nowdoc - багаторядковий без інтерполяції, як одинарні лапки (мітка в лапках):

$template = <<<'SQL'
    SELECT * FROM users WHERE email = :email
    SQL;

З PHP 7.3 закривальну мітку можна відступати - відступ прибирається з усіх рядків, що зручно для коду всередині класів.

Що варто знати:

  • Інтерполяція ${name} застаріла з PHP 8.2 - лише {$name}.
  • Швидкодія однакова: історична порада «одинарні лапки швидші» сьогодні не має значення - рядки компілюються один раз і кешуються в OPcache.
  • Виклики методів і функцій у рядку не працюють без змінної: "{$user->name()}" - так, "{strtoupper($name)}" - ні.
  • Nowdoc зручний для SQL, шаблонів і регулярних виразів, де $ має бути буквальним.
  • Інтерполяція - не для HTML і SQL з даними користувача: екранування (htmlspecialchars) і прив'язка параметрів обов'язкові незалежно від способу складання рядка.

Докладніше в документації: Рядки

Суперглобальні змінні доступні в будь-якому місці коду без global:

  • $_GET, $_POST - параметри рядка запиту й тіла форми;
  • $_COOKIE, $_FILES, $_REQUEST (суміш перших трьох);
  • $_SERVER - заголовки, метод, шлях, IP, змінні середовища сервера;
  • $_SESSION, $_ENV, $GLOBALS.

Чому не брати дані напряму:

  • Усе - від користувача і все - рядки чи масиви. $_GET['id'] може бути '5', '5 OR 1=1', масивом ['x'] (?id[]=x) чи взагалі відсутнім. Без валідації це шлях до SQL-ін'єкцій, XSS і помилок типів.
  • Глобальний стан. Код, що читає $_POST у глибині сервісу, неможливо протестувати без підробки глобальних змінних і неможливо перевикористати в консольній команді чи черзі.
  • $_REQUEST змішує джерела, і порядок пріоритету залежить від налаштувань (request_order) - незрозуміло, звідки прийшло значення.
  • $_SERVER теж від клієнта: HTTP_HOST, HTTP_X_FORWARDED_FOR, будь-які HTTP_* - це заголовки, які можна підробити.

У фреймворку - об'єкт запиту і валідація:

public function update(Request $request, Post $post)
{
    $data = $request->validate([
        'title' => ['required', 'string', 'max:200'],
        'published' => ['boolean'],
    ]);

    $post->update($data);
}

Запит передається явно (його легко підробити в тесті), дані проходять перевірку, а $request->ip() враховує налаштування довірених проксі замість сирого заголовка.

У довгоживучих серверах (Octane) суперглобальні змінні й зовсім ненадійні: фреймворк сам формує об'єкт запиту для кожного запиту.

Докладніше в документації: Суперглобальні змінні

Атрибути (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 не безкоштовне - фреймворки кешують результат (кеш маршрутів, скомпільований контейнер).

Докладніше в документації: Атрибути

Питання з реальних технічних співбесід - 100 питань у 9 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 32 Middle 35 Senior 33

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії