Avatar ·

Как правильно реализовать перечисления (Enum) в PHP без встроенной поддержки?

Всем привет! Я начинающий PHP-разработчик, и столкнулся с проблемой. В других языках (Java, C#) есть удобная конструкция enum, которая позволяет задать фиксированный набор констант. Например, статусы заказа: "новый", "в обработке", "отправлен", "доставлен".

В PHP я не нашел встроенного enum. Пытался использовать обычные константы класса:

class OrderStatus {
    const NEW = 'new';
    const PROCESSING = 'processing';
    const SHIPPED = 'shipped';
    const DELIVERED = 'delivered';
}

$status = OrderStatus::NEW;

Но это неудобно. Я не могу проверить, что переменная $status содержит только одно из допустимых значений. Можно случайно присвоить $status = 'invalid_status', и никакой ошибки не будет. Также нет возможности добавить методы к каждому значению (например, получить описание статуса на русском).

Как правильно организовать перечисления в PHP? Есть ли какие-то паттерны или библиотеки? Хочется получить типобезопасное решение с возможностью добавлять поведение.

<<>>

Решение: абстрактный класс-перечисление с магическими методами

В PHP нет нативной поддержки enum до версии 8.1, но вы можете создать гибкий и типобезопасный класс-перечисление. Вот проверенный паттерн, который используют в крупных проектах:

abstract class Enum {
    private static $cache = [];
    private $value;
    private $key;

    protected function __construct($value) {
        $this->value = $value;
        $this->key = array_search($value, static::getConstants(), true);
    }

    final public static function __callStatic($method, $args) {
        $constants = static::getConstants();
        if (isset($constants[$method])) {
            return new static($constants[$method]);
        }
        throw new \InvalidArgumentException("Недопустимый метод: $method");
    }

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

    public function getKey() {
        return $this->key;
    }

    public function equals($enum) {
        return $this->value === $enum->getValue();
    }

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

    private static function getConstants() {
        $class = get_called_class();
        if (!isset(self::$cache[$class])) {
            $reflection = new \ReflectionClass($class);
            self::$cache[$class] = $reflection->getConstants();
        }
        return self::$cache[$class];
    }
}

// Пример использования
class OrderStatus extends Enum {
    const NEW = 'new';
    const PROCESSING = 'processing';
    const SHIPPED = 'shipped';
    const DELIVERED = 'delivered';
}

// Создание через статический вызов
$status = OrderStatus::NEW();
echo $status->getValue(); // 'new'

// Сравнение
if ($status->equals(OrderStatus::NEW())) {
    echo "Статус: новый";
}

// Проверка типа
function processOrder(OrderStatus $status) {
    // Типобезопасно: только OrderStatus
}

Добавление поведения

Вы можете расширить функциональность, добавив методы в конкретный класс:

class OrderStatus extends Enum {
    const NEW = 'new';
    const PROCESSING = 'processing';
    const SHIPPED = 'shipped';
    const DELIVERED = 'delivered';

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

    public function canCancel(): bool {
        return in_array($this->getValue(), [self::NEW, self::PROCESSING], true);
    }
}

$status = OrderStatus::SHIPPED();
echo $status->getLabel(); // 'Отправлен'
echo $status->canCancel() ? 'Можно отменить' : 'Нельзя отменить'; // 'Нельзя отменить'

Почему это работает?

  • Типобезопасность: функция принимает только OrderStatus, а не любую строку
  • Гарантия валидности: экземпляр создаётся только через статические методы, соответствующие константам
  • Гибкость: легко добавить методы, связанные с каждым значением
  • Производительность: кэширование констант через Reflection

Этот паттерн используется в фреймворках (например, Symfony ChoiceType) и в продакшн-проектах. Он решает проблему "магических строк" и делает код самодокументируемым.

<<>>

Альтернативный подход: использование SplEnum (устарел) и PHP 8.1 нативных Enum

Если вы используете PHP 8.1 и выше, задача решается встроенной конструкцией enum:

enum OrderStatus: string {
    case NEW = 'new';
    case PROCESSING = 'processing';
    case SHIPPED = 'shipped';
    case DELIVERED = 'delivered';

    public function getLabel(): string {
        return match($this) {
            self::NEW => 'Новый заказ',
            self::PROCESSING => 'В обработке',
            self::SHIPPED => 'Отправлен',
            self::DELIVERED => 'Доставлен',
        };
    }
}

$status = OrderStatus::NEW;
echo $status->value; // 'new'
echo $status->getLabel(); // 'Новый заказ'

Для старых версий PHP (7.x) можно использовать SplEnum из PECL, но он не рекомендуется к применению из-за проблем с производительностью и отсутствием поддержки в современных фреймворках.

Рекомендация: если проект на PHP 8.1+, используйте нативные enum. Если на более старой версии — абстрактный класс из первого ответа. Избегайте SplEnum и простых констант без проверок.