Senior: питання на співбесіді з теми «Enums»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
Enum у коді змінюється легко, а от рядки, які вже лежать у базі, - ні. Саме тут зʼявляються помилки після деплою.
Найнебезпечніше - перейменувати кейс:
// було
case Middle = 'middle';
// стало
case Mid = 'mid';
Код збереться, а всі наявні рядки зі значенням middle перестануть кастуватися: ValueError: "middle" is not a valid backing value. Впаде не міграція, а звичайна сторінка.
Правильний порядок для перейменування:
- Додати новий кейс, лишивши старий.
- Міграцією перевести дані:
Vacancy::where('level', 'middle')->update(['level' => 'mid']). - Наступним релізом прибрати старий кейс.
Додати новий кейс - безпечно, якщо колонка varchar. Але коли в базі використано нативний тип enum, потрібна ще й міграція самої колонки, а Schema::table()->change() для нативних enum працює не в усіх драйверах - подекуди доводиться писати DB::statement().
Тому колонку під enum майже завжди роблять string: перелік живе в PHP, база зберігає рядок, і зміни не потребують ALTER на великій таблиці.
Захист від падіння на невідомому значенні:
// null замість винятку, коли в базі щось несподіване
$level = VacancyLevel::tryFrom($vacancy->getRawOriginal('level'));
І ще одне: якщо enum використовується у валідації через Rule::enum(), видалений кейс одразу зробить старі збережені записи невалідними при редагуванні - про це згадують уже після скарг користувачів.
Значення з запиту, бази, черги чи стороннього API - це рядок або число, яке може не відповідати жодному варіанту.
Status::from($value)- повертає варіант або кидаєValueError, якщо такого немає.Status::tryFrom($value)- повертає варіант абоnull.
$status = Status::tryFrom($request->input('status'))
?? throw ValidationException::withMessages(['status' => 'Невідомий статус']);
Коли що:
from- коли невідоме значення означає баг чи пошкоджені дані, і правильна реакція - голосно впасти: значення з власної бази, з внутрішньої черги.tryFrom- на межі з зовнішнім світом, де невідоме значення - нормальна ситуація, яку треба обробити: введення користувача, вебхуки, сторонні API.
На межі застосунку - валідація, а не винятки:
$request->validate([
'status' => ['required', Rule::enum(Status::class)],
]);
$status = $request->enum('status', Status::class);
Rule::enum можна ще обмежити: ->only([Status::Draft, Status::Published]) чи ->except(...).
Підводні камені:
- Тип значення:
tryFrom('1')дляint-backed enum підstrict_types-TypeError, а неnull. Значення з запиту спершу приводять до потрібного типу. - Видалений варіант при наявних даних у базі: після деплою
from()при читанні моделі кидатимеValueErrorна старих рядках. Спершу мігрують дані, потім прибирають варіант. - Зовнішні API додають нові значення без попередження. Код, що робить
from()на їхній відповіді, падає в продакшені наступного ранку.tryFromплюс логування невідомого значення - надійніше.
Статус замовлення, заявки чи статті рідко може змінитися на будь-який інший: оплачене не стає новим, відправлене не скасовується. Ці правила - машина станів, і enum - зручне місце, щоб описати її в одному місці.
enum OrderStatus: string
{
case New = 'new';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
/** @return list<self> */
public function allowedTransitions(): array
{
return match ($this) {
self::New => [self::Paid, self::Cancelled],
self::Paid => [self::Shipped, self::Cancelled],
self::Shipped, self::Cancelled => [],
};
}
public function canTransitionTo(self $next): bool
{
return in_array($next, $this->allowedTransitions(), true);
}
}
Застосування в моделі:
public function transitionTo(OrderStatus $next): void
{
if (! $this->status->canTransitionTo($next)) {
throw new InvalidStateTransition($this->status, $next);
}
$this->update(['status' => $next]);
event(new OrderStatusChanged($this, $next));
}
Що це дає:
- Правила переходів - в одному місці, а не розкидані
if-ами по контролерах. matchбезdefaultзмушує описати переходи для кожного нового статусу.- Легко тестувати: таблиця «з якого - в який - дозволено».
- UI може показувати лише доступні дії:
$order->status->allowedTransitions().
Коли enum уже замало: переходи залежать від даних (оплатити можна, лише якщо сума збігається), потрібні дії при вході й виході зі стану, історія переходів, паралельні стани. Тоді - окремі класи станів (патерн State) чи бібліотеки на кшталт spatie/laravel-model-states, а для довгих бізнес-процесів - workflow-рушії.
Конкурентність: перевірка й оновлення мають бути атомарними - UPDATE ... SET status = 'paid' WHERE id = ? AND status = 'new' чи блокування рядка, інакше два паралельні запити обидва «побачать» статус New.