📜 ⬆️ ⬇️

Enum in PHP

Problem


As you know, there is no built-in type of enumeration in PHP, and in projects with a complex subject area, this fact creates many problems. When in the next Symfony-project there was a need for transfers, it was decided to create its own implementation.

Enumerations required flexibility and use in various components of the application. The tasks that the enumerations should have solved are the following:


There are several implementations of enumerations, for example, myclabs / php-enum , sometimes quite strange , including SplEnum . But when integrating them with other parts of the application (doctrine, twig), problems arise, especially when using Doctrine.
')
A feature of the Doctrine type system is that all types must inherit from the Type class, which has a private final constructor. Those. we cannot inherit from it and overload the constructor to accept the enumeration value. Nevertheless, this problem was circumvented, albeit in a somewhat non-standard way.

Implementation


Enum - base class enums

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; } } } 


A specific Enum might look like this:

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

Unfortunately, php cannot use expressions for static fields, so the creation of objects has to be moved out of class.

Doctrine Integration


Due to the closed constructor, Enum cannot be inherited inherited from Type doctrines. But how to make, that transfers were Type? The answer came in the process of learning how Doctrine creates proxies for entities. For each entity, Doctrine generates a proxy class, which is inherited from the entity class, which implements lazy loading and everything else. Well, we will do the same - for each Enum class we will create a proxy class that inherits from Type and implements the logic needed to determine the type. These classes can then be cached and loaded as needed.

DoctrineEnumAbstractType, which implements the basic logic 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, which generates proxy classes for enums.

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); } } 


For each enumeration, the ProxyClassGenerator generates a proxy class, which can then be used in Doctrine so that the entity fields are real enumerations.

Conclusion


As a result, we got Enum, which can be used with different components of a symfony application - Doctrine, Form, Twig. I hope that this implementation can someone or inspire the search for new solutions.

Source: https://habr.com/ru/post/314114/


All Articles