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

Senior: питання на співбесіді з теми «Типи й помилки»

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

4 питання

Мета - щоб код, який використовує модуль, міг зловити саме те, що вміє обробити, і нічого зайвого.

Типова структура:

interface PaymentException extends Throwable {}

final class CardDeclined extends RuntimeException implements PaymentException
{
    public static function forReason(string $reason): self
    {
        return new self("Картку відхилено: {$reason}");
    }
}

final class GatewayUnavailable extends RuntimeException implements PaymentException {}

Принципи:

  • Маркерний інтерфейс модуля (PaymentException) дозволяє зловити все з модуля одним catch, не прив'язуючись до ієрархії класів.
  • Успадкування від SPL-винятків (RuntimeException, InvalidArgumentException, LogicException) зберігає зміст: LogicException - помилка програміста, RuntimeException - обставини під час виконання.
  • Іменовані конструктори (forReason()) тримають формулювання повідомлень в одному місці.
  • Контекст як властивості, а не лише в тексті: $e->orderId можна записати в лог чи показати користувачу без розбору рядка.
  • Ланцюжок причин: загортаючи чужий виняток, передавайте його як previous - інакше в логах загубиться справжня причина.

Чого уникати: винятків для звичайного керування потоком (користувач не знайдений - часто це null, а не виняток) і одного «універсального» AppException на все.

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

  • void (PHP 7.1) - функція нічого не повертає. return; дозволено, return null; - ні.
  • never (PHP 8.1) - функція ніколи не повертає керування: завжди кидає виняток, завершує скрипт чи працює нескінченно.
  • static (PHP 8.0) - повертає екземпляр класу, через який метод викликали (а не того, де він оголошений).
function abortNotFound(): never
{
    throw new NotFoundHttpException();
}

function process(?Order $order): void
{
    $order ?? abortNotFound();
    $order->ship(); // аналізатор знає: тут $order уже не null
}

Навіщо це:

  • never дає статичному аналізатору й IDE зрозуміти, що код після виклику недосяжний. Так звужується тип, і зникають хибні попередження «можливий null».
  • static потрібен для fluent-інтерфейсів і фабрик у базових класах: Builder::where() у нащадку повертає нащадка, а не базовий клас.
  • void чесно документує, що результат не варто використовувати.

Усі три перевіряються рушієм: функція з never, яка все ж завершилася, кине TypeError.

Докладніше в документації: Тип never

PHP 8 переробив правила приведення рядків до чисел, зробивши їх суворішими й передбачуванішими.

Три види рядків:

  • Числовий - лише число, з можливими пробілами на початку й у кінці: '42', ' 3.14 ', '1e3'.
  • З числовим початком - число, а за ним інші символи: '5 apples'.
  • Нечисловий - усе інше: 'abc', ''.

В арифметиці:

'42' + 1;          // 43
'5 apples' + 1;    // 6 і Warning: A non-numeric value encountered
'abc' + 1;         // TypeError

У PHP 7 'abc' + 1 давав 1 з попередженням - помилка тихо перетворювалася на нуль. Тепер це виняток.

У порівнянні ==:

  • два числові рядки порівнюються як числа: '1e3' == '1000' - true, '10' == '010' - true;
  • число з числовим рядком - як числа;
  • число з нечисловим рядком - як рядки: 0 == 'abc' - false у PHP 8 (у PHP 7 - true, бо 'abc' ставав нулем).

Під declare(strict_types=1) скалярні параметри функцій не приводяться взагалі: int $id не прийме '5' - буде TypeError. Арифметичних операторів і порівнянь strict_types не стосується.

Практичні висновки:

  • Дані з форм і запитів - завжди рядки. Приводьте їх явно й валідуйте ((int), filter_var($v, FILTER_VALIDATE_INT), правила валідації Laravel), а не покладайтеся на неявне приведення.
  • === замість ==, особливо для значень від користувача: «магічні хеші» на кшталт '0e123' == '0e456' (обидва - нуль у науковій нотації) досі true.
  • Для перевірки «чи це число» - is_numeric() (приймає числові рядки) чи ctype_digit() (лише цифри, без знаку й крапки).

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

Intersection-тип A&B (PHP 8.1) - значення має відповідати всім перелічених типам одночасно. На практиці - об'єкт, що реалізує кілька інтерфейсів:

function process(Countable&Traversable $items): void
{
    echo count($items);
    foreach ($items as $item) { /* ... */ }
}

Раніше доводилося або обирати один інтерфейс і перевіряти другий вручну (instanceof), або створювати штучний інтерфейс CountableTraversable, який нічого не додає.

DNF-типи (Disjunctive Normal Form, PHP 8.2) - поєднання union і intersection: об'єднання груп, кожна з яких - перетин.

function save((Countable&ArrayAccess)|null $data): void {}
function cache((Stringable&JsonSerializable)|string $value): void {}

Правило DNF: перетини мають бути в дужках, і вони об'єднуються через |. Запис A&(B|C) недозволений - його треба переписати як (A&B)|(A&C).

Обмеження:

  • Перетин - лише з класів і інтерфейсів, не зі скалярів (int&string не має сенсу).
  • Дублювання й надлишкові типи ((A&B)|A) - помилка компіляції.

Коли потрібні:

  • Функція справді покладається на кілька незалежних можливостей об'єкта (лічильність і ітерація, рядкове представлення й серіалізація).
  • Бібліотеки з дрібними інтерфейсами-можливостями (принцип розділення інтерфейсів) - щоб вимагати рівно потрібні, а не один «товстий».
  • Моки в тестах: PHPUnit створює об'єкти-перетини MockObject&Service, і саме такий тип має змінна з моком.

Альтернатива: якщо одна й та сама комбінація інтерфейсів повторюється скрізь, можливо, це окреме поняття домену - і варто оголосити інтерфейс, що розширює обидва.

Докладніше в документації: Система типів PHP