Enum в PHP

6yka

Повелитель тьмы
Проверенный
Веселый флудер
Мастер реакций
Любитель реакций
Знаток письма высшего ранга
Знаток великого письма
Знаток письма
Знаток Lineage2
Старожил I степени
Победитель в номинации 2018
Победитель в номинации 2017
Медаль за активность на Форуме
За веру и верность форуму
Сообщения
1 730
Розыгрыши
0
Репутация
1 616
Реакции
1 213
Баллы
1 758
Как известно, в 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. Надеюсь, что эта реализация поможет кому-нибудь или вдохновит на поиск новых решений.


Автор статьи Bayer
 
Последнее редактирование модератором:

Назад
Сверху Снизу