Вы добавляете фильтрацию по  ?status=paid  в эндпоинт, пишете  Status::from($_GET['status'])  — и всё работает. Затем кто-то запрашивает  ?status=PAID  или  ?status=refunded , и страница возвращает ошибку 500, потому что  from()  выбрасывает исключение для любого нераспознанного значения. Все советуют исправление «используйте  tryFrom() », и это верно для перечислений со строковым базовым типом (string-backed enums). Для перечислений с целочисленным базовым типом (int-backed enums) всё не так однозначно, и я проверил, где именно это ломается в PHP 8.3.

Базовое отличие 

Оба метода существуют у перечислений с базовым типом (backed enums).  from()  возвращает кейс или выбрасывает  ValueError .  tryFrom()  возвращает кейс или  null :

 enum Status: string
{
    case Paid    = 'paid';
    case Pending = 'pending';
}

Status::from('paid');        // Status::Paid
Status::from('PAID');        // ValueError: "PAID" is not a valid backing value for enum Status
Status::tryFrom('PAID');     // null
Status::tryFrom('refunded'); // null
Status::tryFrom('');         // null
Status::tryFrom(null);       // null
 

Сопоставление происходит точно и с учетом регистра, поэтому  PAID  — это не  paid . Если вы хотите принимать оба варианта, нормализуйте значение перед поиском с помощью  strtolower() .

Обычный паттерн превращает неверные входные данные в значение по умолчанию или в нормальный ответ 422:

 $status = Status::tryFrom($request->query('status', '')) ?? Status::Pending;

// or reject it explicitly
$status = Status::tryFrom($request->query('status', ''))
    ?? abort(422, 'Unknown status.');
 

Используйте  from() , когда неверное значение является багом в вашем собственном коде, например при чтении значения, которое вы сами записали в базу данных. Ошибка  ValueError  здесь — правильный результат, потому что она означает повреждение данных, и вы хотите узнать об этом громко и сразу.

Подводный камень: tryFrom() всё еще может выбросить исключение 

Название намекает, что он никогда не терпит неудачу. Но это не так. Он возвращает  null  для значения правильного типа, которое не соответствует ни одному кейсу, однако он всё равно проверяет тип параметра. Для энама с int-значением значение из строки запроса является строкой, и PHP приходится его преобразовывать:

 enum Level: int
{
    case Low  = 1;
    case High = 2;
}

Level::tryFrom('2');   // Level::High      (numeric string is coerced)
Level::tryFrom('-1');  // null
Level::tryFrom('abc'); // TypeError: Level::tryFrom(): Argument #1 ($value) must be of type int, string given
Level::tryFrom('');    // TypeError
 

Таким образом,  ?level=abc  приводит к  TypeError , что является перехваченной ошибкой и ответом 500 — именно тем, чего вы пытались избежать. Стоит также знать о еще двух результатах выполнения:

  •   Level::tryFrom('2.5')  возвращает  Level::High  и генерирует предупреждение об устаревании (Implicit conversion from float-string "2.5" to int loses precision). Дробное значение молча усекается до 2.
  •   Level::tryFrom('1e0')  возвращает  Level::Low , так как экспоненциальная форма является допустимой числовой строкой. Это неожиданно, но соответствует стандартным правилам приведения типов в PHP.

При использовании  declare(strict_types=1)  вызов со строкой  '2'  также выбрасывает  TypeError , поскольку неявное приведение типов запрещено. Значения из параметров запроса всегда являются строками, поэтому их нужно конвертировать заранее.

Безопасный хелпер для энамов с int-основой 

Убедитесь, что входные данные являются целым числом, а затем выполните поиск:

 function levelFrom(mixed $value): ?Level
{
    $int = filter_var($value, FILTER_VALIDATE_INT);

    return $int === false ? null : Level::tryFrom($int);
}
 

Я протестировал это на наборе входных данных.  '2'  и  ' 2'  вернули  Level::High .  '2.5' ,  'abc' ,  '' ,  '1e0' ,  null  и  ['2']  — все вернули  null  без каких-либо ошибок и предупреждений об устаревании. Это также закрывает проблему с массивами, которые ломают и строковые энамы:  ?status[]=paid  попадает в ваш код как массив, а  Status::tryFrom(['paid'])  выбрасывает  TypeError: ... must be of type string|int, array given .

В Laravel 

Фреймворк уже обрабатывает большую часть этого, за одним исключением. Правило валидации  Rule::enum()  вызывает  tryFrom()  внутри блока  try / catch (TypeError)  (что видно в  Illuminate\Validation\Rules\Enum ), поэтому некорректное значение приводит к ошибке валидации вместо падения приложения. Сначала выполняйте валидацию, а затем конвертацию:

 use Illuminate\Validation\Rule;

$data = $request->validate([
    'status' => ['sometimes', Rule::enum(Status::class)],
    'level'  => ['sometimes', Rule::enum(Level::class)],
]);

$status = isset($data['status']) ? Status::from($data['status']) : null;
 

После успешного прохождения валидации вызов  from()  безопасен, а сбой означает настоящий баг. Исключением является  $request->enum('level', Level::class) . Судя по исходному коду фреймворка, он вызывает  $enumClass::tryFrom(...)  без блоков  try / catch , поэтому для энама с int-основой я ожидаю выброса  ?level=abc . Я подтвердил поведение PHP выше, но не запускал этот конкретный хелпер в полноценном приложении на Laravel, поэтому протестируйте его перед тем, как полагаться на него. Для энамов со строковым базовым типом всё работает так, как вы и надеетесь. Неявное связывание маршрутов (Implicit route binding) с энамом приводит сегмент URL к строке и также использует  tryFrom() , поэтому неизвестное значение там вернет 404, а не 500.

Краткая справка 


Ситуация Использовать
| Энам со строковым базовым типом, недоверенные данные  |   tryFrom()  со значением по умолчанию или 422
| Энам с int-основой, недоверенные данные  | Проверить с помощью  filter_var(..., FILTER_VALIDATE_INT) , затем  tryFrom() 
| Запрос в Laravel  | Сначала  Rule::enum() , затем  from() 
| Значения, записанные вами лично (база данных, конфиг)  |   from() , чтобы повреждение данных приводило к заметному сбою

Часто задаваемые вопросы 

Работает ли tryFrom() с простыми перечислениями (pure enums)? 

Нет. И  from() , и  tryFrom()  существуют только у перечислений с базовым типом (backed enums). У простого перечисления, не имеющего типа  : string  или  : int , нет ни одного из этих методов.

Стоит ли перехватывать ValueError вместо использования tryFrom()? 

Вы можете это сделать, но  tryFrom()  точнее отражает ваше намерение и избавляет от исключений при обычных некорректных входных данных. К тому же перехват  ValueError  всё равно не покроет случай с  TypeError .

Как принимать входные данные без учета регистра? 

Нормализуйте их перед поиском, например используя  Status::tryFrom(strtolower($value)) , если значения вашего базового типа записаны в нижнем регистре.