Проблема
Как известно, в PHP нет встроенного типа перечислений, и в проектах со сложной предметной областью этот факт создает множество проблем. Когда в очередном Symfony-проекте появилась необходимость в перечислениях, было решено создать свою реализацию.
От перечислений требовалась гибкость и возможность использования в разных компонентах приложения. Задачи, которые должны были решать перечисления, следующие:
- иметь возможность получить список значений перечислениях
- интеграция с Doctrine для использования перечисления в качестве типа поля
- интеграция с Form для использования перечислений как поле в форме для выбора нужного элемента
- интеграция с Twig для перевода значений перечисления
Есть несколько реализаций перечислений, например, myclabs/php-enum, иногда довольно странных, в том числе — SplEnum. Но при интеграции их с другими частями приложения (doctrine, twig) возникают проблемы, особенно при использовании Doctrine.
Особенность системы типов Doctrine состоит в том, что все типы должны наследоваться от класса Type, который имеет private final конструктор. Т.е. мы не можем наследоваться от него и перегрузить конструктор, чтобы он принимал значение перечисления. Тем не менее, эту проблему удалось обойти, хоть и несколько нестандартным способом.
Реализация
Enum — базовый класс перечислений
<?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
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, который генерирует прокси-классы для перечислений.
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/
Добавить комментарий