Enum в PHP

от автора

Проблема

Как известно, в PHP нет встроенного типа перечислений, и в проектах со сложной предметной областью этот факт создает множество проблем. Когда в очередном Symfony-проекте появилась необходимость в перечислениях, было решено создать свою реализацию.

От перечислений требовалась гибкость и возможность использования в разных компонентах приложения. Задачи, которые должны были решать перечисления, следующие:

  • иметь возможность получить список значений перечислениях
  • интеграция с Doctrine для использования перечисления в качестве типа поля
  • интеграция с Form для использования перечислений как поле в форме для выбора нужного элемента
  • интеграция с Twig для перевода значений перечисления


Есть несколько реализаций перечислений, например, myclabs/php-enum, иногда довольно странных, в том числе — SplEnum. Но при интеграции их с другими частями приложения (doctrine, twig) возникают проблемы, особенно при использовании Doctrine.

Особенность системы типов Doctrine состоит в том, что все типы должны наследоваться от класса Type, который имеет private final конструктор. Т.е. мы не можем наследоваться от него и перегрузить конструктор, чтобы он принимал значение перечисления. Тем не менее, эту проблему удалось обойти, хоть и несколько нестандартным способом.

Реализация

Enum — базовый класс перечислений

Enum.php

<?php  namespace AppBundle\System\Component\Enum;  use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Platforms\PostgreSqlPlatform; use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Platforms\AbstractPlatform;  class Enum {     private static $values = [];     private static $valueMap = [];      private $value;      public function __construct($value)     {         $this->value = $value;     }      public function getValue()     {         return $this->value;     }      public function __toString()     {         return $this->value;     }       /**      * @return Enum[]      * @throws \Exception      */     public static function getValues()     {         $className = get_called_class();         if (!array_key_exists($className, self::$values)) {             throw new \Exception(sprintf("Enum is not initialized, enum=%s", $className));         }         return self::$values[$className];     }      public static function getEnumObject($value)     {         if (empty($value)) {             return null;         }         $className = get_called_class();         return self::$valueMap[$className][$value];     }      public static function init()     {         $className = get_called_class();         $class = new \ReflectionClass($className);          if (array_key_exists($className, self::$values)) {             throw new \Exception(sprintf("Enum has been already initialized, enum=%s", $className));         }         self::$values[$className] = [];         self::$valueMap[$className] = [];           /** @var Enum[] $enumFields */         $enumFields = array_filter($class->getStaticProperties(), function ($property) {             return $property instanceof Enum;         });         if (count($enumFields) == 0) {             throw new \Exception(sprintf("Enum has not values, enum=%s", $className));         }          foreach ($enumFields as $property) {             if (array_key_exists($property->getValue(), self::$valueMap[$className])) {                 throw new \Exception(sprintf("Duplicate enum value %s from enum %s", $property->getValue(), $className));             }              self::$values[$className][] = $property;             self::$valueMap[$className][$property->getValue()] = $property;         }     }  }

Конкретный Enum может выглядеть так:

class Format extends Enum {     public static $WEB;     public static $GOST; }  Format::$WEB = new Format('web'); Format::$GOST = new Format('gost'); Format::init(); 

К сожалению, в php нельзя использовать выражения для статических полей, поэтому создание объектов приходится выносить за пределы класса.

Интеграция с Doctrine

Благодаря закрытому конструктору, Enum не может наследоваться наследуется от Type доктрины. Но как же сделать, чтобы перечисления были Type-ми? Ответ пришел в процессе изучения того, как Doctrine создает прокси-классы для сущностей. На каждую сущность Doctrine генерирует прокси-класс, который наследуется от класса сущности, в котором реализует lazy loading и все остальное. Ну и мы поступим так же — на каждый класс-Еnum будем создавать прокси-класс, который наследуется от Type и реализует логику, нужную для определения типа. Эти классы затем можно сохранить в кэш и подгружать при необходимости.

DoctrineEnumAbstractType, в котором реализована базовая логика Type

DoctrineEnumAbstractType.php

class DoctrineEnumAbstractType extends Type {     /** @var Enum $enum */     protected static $enumClass = null;      public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)     {         $enum = static::$enumClass;         $values = implode(             ", ",             array_map(function (Enum $enum) {                 return "'" . $enum->getValue() . "'";             }, $enum::getValues()));          if ($platform instanceof MysqlPlatform) {             return sprintf('ENUM(%s)', $values);         } elseif ($platform instanceof SqlitePlatform) {             return sprintf('TEXT CHECK(%s IN (%s))', $fieldDeclaration['name'], $values);         } elseif ($platform instanceof PostgreSqlPlatform) {             return sprintf('VARCHAR(255) CHECK(%s IN (%s))', $fieldDeclaration['name'], $values);         } else {             throw new \Exception(sprintf("Sorry, platform %s currently not supported enums", $platform->getName()));         }      }      public function getName()     {         $enum = static::$enumClass;         return (new \ReflectionClass($enum))->getShortName();     }      public function convertToPHPValue($value, AbstractPlatform $platform)     {         $enum = static::$enumClass;         return $enum::getEnumObject($value);     }      public function convertToDatabaseValue($enum, AbstractPlatform $platform)     {         /** @var Enum $enum */         return $enum->getValue();     }      public function requiresSQLCommentHint(AbstractPlatform $platform)     {         return true;     }  } 

DoctrineEnumProxyClassGenerator, который генерирует прокси-классы для перечислений.

DoctrineEnumProxyClassGenerator.php

class DoctrineEnumProxyClassGenerator {     public function proxyClassName($enumClass)     {         $enumClassName = (new \ReflectionClass($enumClass))->getShortName();         return $enumClassName . 'DoctrineEnum';     }      public function proxyClassFullName($namespace, $enumClass) {         return $namespace . '\\' . $this->proxyClassName($enumClass);     }      public function generateProxyClass($enumClass, $namespace)     {         $proxyClassTemplate = <<<EOF <?php  namespace <namespace>;          class <proxyClassName> extends \<proxyClassBase>  {     protected static \$enumClass = '\<enumClass>'; } EOF;         $placeholders = [             'namespace'      => $namespace,             'proxyClassName' => self::proxyClassName($enumClass),             'proxyClassBase' => DoctrineEnumAbstractType::class,             'enumClass'      => $enumClass,         ];          return $this->generateCode($proxyClassTemplate, $placeholders);     }      private function generateCode($classTemplate, array $placeholders)     {         $placeholderNames = array_map(function ($placeholderName) {             return '<' . $placeholderName . '>';         }, array_keys($placeholders));         $placeHolderValues = array_values($placeholders);          return str_replace($placeholderNames, $placeHolderValues, $classTemplate);     } } 

На каждое перечисление ProxyClassGenerator генерирует прокси-класс, который затем можно использовать в Doctrine, чтобы поля сущностей были настоящими перечислениями.

Заключение

В результате мы получили Enum, который может быть использован с разными компонентами Symfony-приложения — Doctrine, Form, Twig. Надеюсь, что эта реализация может кому-нибудь или вдохновит на поиск новых решений.
ссылка на оригинал статьи https://habrahabr.ru/post/314114/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *