Проблема Как известно, в PHP нет встроенного типа перечисления, а в проектах со сложными доменами этот факт создает множество проблем.
Когда в другом проекте Symfony возникла необходимость в перечислениях, было решено создать собственную реализацию.
Перечислениям требовалась гибкость и возможность использования в различных компонентах приложения.
Задачи, которые должны были решить трансферы, заключались в следующем:
- иметь возможность получить список значений перечисления
- Интеграция Doctrine для использования enum в качестве типа поля
- интеграция с формой для использования перечислений в качестве поля в форме для выбора нужного элемента
- интеграция с Twig для перевода значений перечислений
Но при их интеграции с другими частями приложения (doctrine, twig) возникают проблемы, особенно при использовании Doctrine. Особенностью системы типов Doctrine является то, что все типы должны наследовать от класса Type, который имеет приватный финальный конструктор.
Те.
мы не можем наследовать от него и перегружать конструктор, чтобы он принимал значение перечисления.
Однако эту проблему удалось обойти, хотя и несколько нетрадиционным способом.
Выполнение
Enum — базовый класс перечисления Перечисление.php
Конкретный 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; } } }
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. Но как мы можем сделать перечисления типами? Ответ пришел после изучения того, как Doctrine создает прокси-классы для сущностей.Для каждой сущности Doctrine генерирует прокси-класс, наследуемый от класса сущности, в котором реализуется отложенная загрузка и всё остальное.
Что ж, мы поступим так же — для каждого класса Enum создадим прокси-класс, который наследуется от 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. Я надеюсь, что эта реализация сможет или вдохновит кого-нибудь на поиск новых решений.Теги: #php #symfony #doctrine #twig #php #symfony
-
Книги Из Серии «Новая Наука»
19 Oct, 24 -
Как Мы Видим Себя И Как Они Видят Нас
19 Oct, 24 -
Краткое Примечание Об Обнаружении Функций
19 Oct, 24