Avatar ·

Реализация перечислений (Enum) в PHP: как правильно сделать?

Всем привет! Я новичок в PHP (пишу около года), и столкнулся с проблемой. В проекте нужно хранить статусы заказов: "новый", "в обработке", "отправлен", "доставлен". Сейчас я использую обычные константы: ```php define('STATUS_NEW', 1); define('STATUS_PROCESSING', 2); define('STATUS_SHIPPED', 3); define('STATUS_DELIVERED', 4); ``` Но это неудобно: во-первых, я могу случайно передать в функцию любое число вместо статуса, и PHP не выдаст ошибку. Во-вторых, если нужно добавить описание или метод для статуса (например, "можно ли отменить заказ?"), приходится писать отдельные функции. В-третьих, при выводе списка статусов в выпадающем списке формы я путаюсь с ключами и значениями. Я слышал, что в PHP 8.1 появились нативные enum, но проект работает на PHP 7.4, и обновиться пока нельзя. Как правильно реализовать перечисления в старых версиях PHP? Нужно что-то надёжное, с проверкой типов и возможностью добавлять методы. Буду благодарен за примеры кода! <<>>

Правильная реализация Enum в PHP (для версий до 8.1)

Привет! Ты абсолютно прав — константы через define() — это устаревший и небезопасный подход. Даже в PHP 7.4 можно сделать полноценные перечисления через паттерн ValueObject. Вот проверенное решение, которое используется в крупных проектах (Symfony, Laravel).

Базовый класс Enum

<?php
abstract class Enum
{
    protected static array $cache = [];
    protected $value;
    
    final public function __construct($value)
    {
        if (!in_array($value, static::values(), true)) {
            throw new \InvalidArgumentException(
                sprintf('Недопустимое значение "%s" для перечисления %s', 
                    $value, static::class)
            );
        }
        $this->value = $value;
    }

    final public function getValue()
    {
        return $this->value;
    }

    final public static function values(): array
    {
        $class = static::class;
        if (!isset(static::$cache[$class])) {
            $ref = new \ReflectionClass($class);
            static::$cache[$class] = array_values($ref->getConstants());
        }
        return static::$cache[$class];
    }

    final public static function isValid($value): bool
    {
        return in_array($value, static::values(), true);
    }

    final public function __toString(): string
    {
        return (string) $this->value;
    }
}

Пример конкретного перечисления

<?php
class OrderStatus extends Enum
{
    const NEW = 'new';
    const PROCESSING = 'processing';
    const SHIPPED = 'shipped';
    const DELIVERED = 'delivered';
    const CANCELLED = 'cancelled';

    // Дополнительные методы
    public function isActive(): bool
    {
        return in_array($this->value, [
            self::NEW,
            self::PROCESSING,
            self::SHIPPED
        ], true);
    }

    public function label(): string
    {
        $labels = [
            self::NEW => 'Новый',
            self::PROCESSING => 'В обработке',
            self::SHIPPED => 'Отправлен',
            self::DELIVERED => 'Доставлен',
            self::CANCELLED => 'Отменён',
        ];
        return $labels[$this->value] ?? $this->value;
    }

    // Статические методы для удобства
    public static function new(): self
    {
        return new self(self::NEW);
    }

    public static function processing(): self
    {
        return new self(self::PROCESSING);
    }
}

Использование в коде

<?php
// Создание
$status = new OrderStatus(OrderStatus::NEW);
$status = OrderStatus::new(); // более читаемо

// Проверка типов (строгая)
function processOrder(OrderStatus $status): void {
    if ($status->isActive()) {
        echo "Заказ активен: " . $status->label();
    }
}

// Получение всех значений для формы
$statuses = OrderStatus::values(); // ['new','processing',...]

// Безопасное приведение из БД
$dbValue = 'shipped';
$status = new OrderStatus($dbValue); // выбросит исключение, если значение невалидно

Преимущества этого подхода

  • Type-hinting — функция принимает только объект OrderStatus, а не любое число/строку
  • Самодокументирование — IDE подсказывает доступные константы
  • Безопасность — конструктор выбрасывает исключение при невалидном значении
  • Расширяемость — можно добавлять методы (label, isActive, canCancel)
  • Сериализация — легко сохранить в БД через getValue() и восстановить через конструктор

Совет по миграции на PHP 8.1+

Когда обновитесь, замените наследование от вашего Enum на нативный enum — структура останется похожей:

<?php
enum OrderStatus: string {
    case NEW = 'new';
    case PROCESSING = 'processing';
    // ...методы остаются почти теми же
}
<<>>

Альтернативный подход: через SplEnum (устарел) или библиотеки

В PHP 7.x существовал встроенный класс SplEnum, но он был объявлен устаревшим в PHP 8.0 и удалён в PHP 8.1. Поэтому использовать его не рекомендую — при обновлении сломается код.

Если не хотите писать свой класс, можно использовать готовые библиотеки:

  • myclabs/php-enum — популярная библиотека, работает начиная с PHP 7.4. Просто наследуете свой класс от MyCLabs\Enum\Enum и объявляете константы. Поддерживает type-hinting, методы values(), isValid().
  • spatie/enum — более современная библиотека от создателей Laravel-пакетов. Поддерживает магические методы (например, OrderStatus::NEW()), строгую типизацию, можно добавлять произвольные методы.

Пример с spatie/enum:

<?php
use Spatie\Enum\Enum;

/**
 * @method static self NEW()
 * @method static self PROCESSING()
 */
class OrderStatus extends Enum
{
    protected static function values(): array
    {
        return [
            'NEW' => 'new',
            'PROCESSING' => 'processing',
        ];
    }
}

// Использование
$status = OrderStatus::NEW();
echo $status->value; // 'new'

Библиотеки удобны тем, что сразу дают готовую реализацию с тестами и документацией. Но если проект небольшой, проще написать свой класс Enum — так вы будете полностью контролировать поведение и не зависеть от сторонних зависимостей.