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

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. Впаде не міграція, а звичайна сторінка.

Правильний порядок для перейменування:

  1. Додати новий кейс, лишивши старий.
  2. Міграцією перевести дані: Vacancy::where('level', 'middle')->update(['level' => 'mid']).
  3. Наступним релізом прибрати старий кейс.

Додати новий кейс - безпечно, якщо колонка 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 плюс логування невідомого значення - надійніше.

Докладніше в документації: BackedEnum::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.

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