- PVSM.RU - https://www.pvsm.ru -
Как известно, в PHP нет встроенного типа перечислений, и в проектах со сложной предметной областью этот факт создает множество проблем. Когда в очередном Symfony-проекте появилась необходимость в перечислениях, было решено создать свою реализацию.
От перечислений требовалась гибкость и возможность использования в разных компонентах приложения. Задачи, которые должны были решать перечисления, следующие:
Есть несколько реализаций перечислений, например, myclabs/php-enum [1], иногда довольно странных [2], в том числе — SplEnum [3]. Но при интеграции их с другими частями приложения (doctrine, twig) возникают проблемы, особенно при использовании Doctrine.
Особенность системы типов Doctrine состоит в том, что все типы должны наследоваться от класса Type, который имеет private final конструктор. Т.е. мы не можем наследоваться от него и перегрузить конструктор, чтобы он принимал значение перечисления. Тем не менее, эту проблему удалось обойти, хоть и несколько нестандартным способом.
Enum — базовый класс перечислений
<?php
namespace AppBundleSystemComponentEnum;
use DoctrineDBALPlatformsMySqlPlatform;
use DoctrineDBALPlatformsPostgreSqlPlatform;
use DoctrineDBALPlatformsSqlitePlatform;
use DoctrineDBALTypesType;
use DoctrineDBALPlatformsAbstractPlatform;
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 нельзя использовать выражения для статических полей, поэтому создание объектов приходится выносить за пределы класса.
Благодаря закрытому конструктору, 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. Надеюсь, что эта реализация может кому-нибудь или вдохновит на поиск новых решений.
Автор: lewbor
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/204896
Ссылки в тексте:
[1] myclabs/php-enum: https://github.com/myclabs/php-enum
[2] довольно странных: https://habrahabr.ru/post/38584/
[3] SplEnum: http://php.net/manual/ru/class.splenum.php
[4] Источник: https://habrahabr.ru/post/314114/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.