Skip to content

Instantly share code, notes, and snippets.

@gnoesiboe
Last active July 12, 2021 11:53
Show Gist options
  • Save gnoesiboe/e030850c2fa03aeb529c5332ef184654 to your computer and use it in GitHub Desktop.
Save gnoesiboe/e030850c2fa03aeb529c5332ef184654 to your computer and use it in GitHub Desktop.
[Enum] #utility #valueobject
<?php
declare(strict_types=1);
namespace App\Domain\ValueObject;
use DomainException;
use ReflectionClass;
use ReflectionException;
use UnexpectedValueException;
abstract class Enum
{
private const UNIQUE_IDENTIFIER_SEPARATOR = ':';
protected string $value;
/**
* @var array<class-string<Enum>, string[]>
*/
private static array $constants = [];
/**
* @throws ReflectionException
*/
public function __construct(string $value)
{
$this->setValue($value);
}
public static function fromUniqueIdentifier(string $identifier): Enum
{
[$enumClass, $enumValue] = explode(self::UNIQUE_IDENTIFIER_SEPARATOR, $identifier);
if (!$enumClass || !$enumValue) {
throw new UnexpectedValueException('Expecting decoded values to have class and value properties set');
}
return new $enumClass($enumValue);
}
public function toUniqueIdentifier(): string
{
return get_class($this) . self::UNIQUE_IDENTIFIER_SEPARATOR . $this->value;
}
/**
* @param string $value
*
* @throws ReflectionException
*/
private function setValue(string $value): void
{
$this->validateValue($value);
$this->value = $value;
}
/**
* @throws ReflectionException
*/
protected function validateValue(string $value): void
{
/** @var string[] $supported */
$supported = array_values(
self::extractConstants(get_called_class())
);
if (in_array($value, $supported, true) === false) {
$exception = $this->createInvalidException($value, $supported);
if (!$exception instanceof DomainException) {
throw new UnexpectedValueException('Expecting to throw an instance of: ' . DomainException::class);
}
throw $exception;
}
}
/**
* @param string $supplied
* @param string[] $supported
*
* @return DomainException
*/
protected function createInvalidException(string $supplied, array $supported): DomainException
{
$className = get_class($this);
return new DomainException("$className value '$supplied' not supported. Supported are: " . implode(', ', $supported));
}
/**
* @return string[]
*
* @throws ReflectionException
*/
public static function getSupported(): array
{
return self::extractConstants(get_called_class());
}
/**
* @param class-string<Enum> $class
*
* @return string[]
*
* @throws ReflectionException
*/
private static function extractConstants(string $class): array
{
if (array_key_exists($class, self::$constants) === true) {
return self::$constants[$class];
}
$reflection = new ReflectionClass($class);
/** @var string[] $constants */
$constants = $reflection->getConstants();
self::validateClassConstantValuesAreUnique($constants);
// This is required to make sure that constants of base classes will be the first to be checked
// and then that if it's children
while (($reflection = $reflection->getParentClass()) && $reflection instanceof ReflectionClass && $reflection->name !== __CLASS__) {
$constants = $reflection->getConstants() + $constants;
}
self::$constants[$class] = $constants;
return $constants;
}
/**
* @param string[] $constants
*
* @throws UnexpectedValueException
*/
private static function validateClassConstantValuesAreUnique(array $constants): void
{
// values needs to be unique
$ambiguous = [];
foreach ($constants as $constantValue) {
$nameOccurrenceForValue = array_keys($constants, $constantValue, true);
if (count($nameOccurrenceForValue) > 1) {
$ambiguous[var_export($constantValue, true)] = $nameOccurrenceForValue;
}
}
if (count($ambiguous) > 0) {
throw new UnexpectedValueException(
'The constant values for an Enum need to be unique to be able to extinguish hem'
);
}
}
public function getValue(): string
{
return $this->value;
}
public function isValue(string $value): bool
{
return $this->value === $value;
}
/**
* @param string[] $values
*
* @return bool
*/
public function isOneOfValues(array $values): bool
{
foreach ($values as $value) {
if ($this->isValue($value)) {
return true;
}
}
return false;
}
public function __toString(): string
{
return $this->getValue();
}
}
<?php
declare(strict_types=1);
namespace App\Infrastructure\Doctrine\Type;
use App\Domain\ValueObject\Enum;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use UnexpectedValueException;
/**
* To be able to persist to and retrieve from database using Doctrine.
*/
final class EnumType extends Type
{
private const NAME = 'domain_enum';
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return 'VARCHAR(255)';
}
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
{
if ($value === null) {
return null;
}
if (!$value instanceof Enum) {
throw new UnexpectedValueException('Expecting value to either be null or an instance of: ' . Enum::class);
}
return $value->toUniqueIdentifier();
}
public function convertToPHPValue($value, AbstractPlatform $platform): ?Enum
{
if (!is_string($value) || $value === '') {
return null;
}
return Enum::fromUniqueIdentifier($value);
}
public function getName(): string
{
return self::NAME;
}
}
<?php
declare(strict_types=1);
namespace App\Domain\Entity\Employer;
use App\Domain\ValueObject\AbstractEnum;
final class Status extends AbstractEnum
{
public const OPEN = 'open';
public const IN_PROGRESS = 'in_progress';
public const CLOSED = 'closed';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment