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