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

Middle: питання на співбесіді з теми «Enums»

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

3 питання

Backed enum кастується в моделі, і колонка починає повертати обʼєкт замість рядка:

enum VacancyLevel: string
{
    case Junior = 'junior';
    case Middle = 'middle';
    case Senior = 'senior';

    public function label(): string
    {
        return match ($this) {
            self::Junior => 'Junior',
            self::Middle => 'Middle',
            self::Senior => 'Senior',
        };
    }
}

class Vacancy extends Model
{
    protected function casts(): array
    {
        return ['level' => VacancyLevel::class];
    }
}

Тепер $vacancy->level - це enum, а не рядок:

$vacancy->level->label();
$vacancy->level === VacancyLevel::Senior;
$vacancy->update(['level' => VacancyLevel::Middle]);   // у базу піде 'middle'

Переваги над константами класу:

  • Обмежена множина. Значення поза переліком не існує, тоді як константа не заважає передати будь-який рядок.
  • Типізація. function assign(VacancyLevel $level) не прийме випадковий рядок, і редактор підкаже варіанти.
  • Поведінка поруч зі значенням. Enum має методи, тож підпис, колір чи іконка живуть там само, а не в розкиданих match по шаблонах.
  • match без default підсвітить пропущений випадок, коли додасте новий кейс.

Дві практичні деталі. У валідації є правило Rule::enum(VacancyLevel::class). А tryFrom() повертає null замість винятку - саме він потрібен, коли значення приходить від користувача чи з URL.

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

Так. 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. Рівність перевіряють через ===: варіанти - синглтони.

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