Last active
October 23, 2017 22:29
-
-
Save zlikavac32/71e387ac91d271a97d4c54593d0cea92 to your computer and use it in GitHub Desktop.
WIP - better enums in PHP (Java inspired)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| declare(strict_types=1); | |
| /** | |
| * Proof of concept for better enums in PHP. Supports type-hinting and per enum abstract method implementation | |
| */ | |
| abstract class Enum | |
| { | |
| private static $existingEnums = []; | |
| private $ordinal = -1; | |
| private $name = ''; | |
| final public function ordinal(): int | |
| { | |
| return $this->ordinal; | |
| } | |
| final public function name(): string | |
| { | |
| return $this->name; | |
| } | |
| final public function __clone() | |
| { | |
| throw new LogicException('Cloning enum object is not allowed'); | |
| } | |
| public function __toString() | |
| { | |
| return $this->name; | |
| } | |
| /** | |
| * @return Iterator|Enum[] | |
| */ | |
| public static function iterator(): Iterator | |
| { | |
| return new ArrayIterator(array_values(self::retrieveCurrentContextEnumerations())); | |
| } | |
| public static function __callStatic($name, $arguments) | |
| { | |
| if (count($arguments) > 0) { | |
| throw new InvalidArgumentException( | |
| sprintf('No argument must be provided when calling %s::%s.', static::class, $name) | |
| ); | |
| } | |
| $objects = self::retrieveCurrentContextEnumerations(); | |
| if (!isset($objects[$name])) { | |
| throw new RuntimeException(sprintf('Enum object %s missing in %s', $name, static::class)); | |
| } | |
| return $objects[$name]; | |
| } | |
| /** | |
| * @return Enum[] | |
| */ | |
| private static function retrieveCurrentContextEnumerations(): array | |
| { | |
| $class = static::class; | |
| if (!isset(self::$existingEnums[$class])) { | |
| self::$existingEnums[$class] = self::discoverEnumerationObjects(); | |
| } | |
| return self::$existingEnums[$class]; | |
| } | |
| private static function discoverEnumerationObjects() | |
| { | |
| /* @var Enum[] $objects */ | |
| $objects = static::createEnumerationObjects(); | |
| if (!is_array($objects)) { | |
| throw new LogicException( | |
| sprintf( | |
| 'Method %s::createEnumerationObjects() must return an array of enumeration objects', | |
| static::class | |
| ) | |
| ); | |
| } | |
| if (count($objects) === 0) { | |
| throw new LogicException(sprintf('Enumeration objects array in class %s can not be empty', static::class)); | |
| } | |
| $i = 0; | |
| foreach ($objects as $alias => $object) { | |
| self::assertValidAlias($alias); | |
| self::assertValidEnumObject($object); | |
| $object->ordinal = $i++; | |
| $object->name = $alias; | |
| } | |
| return $objects; | |
| } | |
| protected static function createEnumerationObjects(): array | |
| { | |
| throw new LogicException( | |
| sprintf( | |
| 'You must provide protected static function createEnumerationObjects(): array method in your enum class %s', | |
| static::class | |
| ) | |
| ); | |
| } | |
| private static function assertValidAlias($alias): void | |
| { | |
| if (!is_string($alias)) { | |
| throw new LogicException(sprintf('Alias %d in enum class %s is not valid alias', $alias, static::class)); | |
| } | |
| //For now we allow anything else although in the future some name patterns may arise | |
| } | |
| private static function assertValidEnumObject($object): void | |
| { | |
| if ($object instanceof Enum) { | |
| return ; | |
| } | |
| if (is_object($object)) { | |
| $resolvedType = 'an instance of ' . get_class($object); | |
| } else { | |
| $resolvedType = gettype($object); | |
| } | |
| throw new LogicException( | |
| sprintf( | |
| 'Enum object in class %s must be an instance of %s (%s received)', | |
| static::class, | |
| Enum::class, | |
| $resolvedType | |
| ) | |
| ); | |
| } | |
| } | |
| /** | |
| * @method static Gender MALE | |
| * @method static Gender FEMALE | |
| */ | |
| abstract class Gender extends Enum | |
| { | |
| /** | |
| * @var string | |
| */ | |
| private $symbol; | |
| public function __construct(string $symbol) | |
| { | |
| $this->symbol = $symbol; | |
| } | |
| protected static function createEnumerationObjects(): array | |
| { | |
| return [ | |
| 'MALE' => new class('M') extends Gender | |
| { | |
| }, | |
| 'FEMALE' => new class('F') extends Gender | |
| { | |
| }, | |
| ]; | |
| } | |
| public function symbol(): string | |
| { | |
| return $this->symbol; | |
| } | |
| } | |
| /** | |
| * @method static YesNo YES | |
| * @method static YesNo NO | |
| */ | |
| abstract class YesNo extends Enum | |
| { | |
| protected static function createEnumerationObjects(): array | |
| { | |
| return [ | |
| 'YES' => new class extends YesNo | |
| { | |
| }, | |
| 'NO' => new class extends YesNo | |
| { | |
| }, | |
| ]; | |
| } | |
| } | |
| /** | |
| * @method static MathematicalOperator PLUS | |
| * @method static MathematicalOperator MINUS | |
| * @method static MathematicalOperator TIMES | |
| * @method static MathematicalOperator DIV | |
| */ | |
| abstract class MathematicalOperator extends Enum | |
| { | |
| protected static function createEnumerationObjects(): array | |
| { | |
| return [ | |
| 'PLUS' => new class extends MathematicalOperator | |
| { | |
| public function apply(float $left, float $right): float | |
| { | |
| return $left + $right; | |
| } | |
| public function __toString() | |
| { | |
| return '+'; | |
| } | |
| }, | |
| 'MINUS' => new class extends MathematicalOperator | |
| { | |
| public function apply(float $left, float $right): float | |
| { | |
| return $left - $right; | |
| } | |
| public function __toString() | |
| { | |
| return '-'; | |
| } | |
| }, | |
| 'TIMES' => new class extends MathematicalOperator | |
| { | |
| public function apply(float $left, float $right): float | |
| { | |
| return $left * $right; | |
| } | |
| public function __toString() | |
| { | |
| return '*'; | |
| } | |
| }, | |
| 'DIV' => new class extends MathematicalOperator | |
| { | |
| public function apply(float $left, float $right): float | |
| { | |
| if (.0 === $right) { | |
| throw new LogicException('Division by zero'); | |
| } | |
| return $left / $right; | |
| } | |
| public function __toString() | |
| { | |
| return '/'; | |
| } | |
| }, | |
| ]; | |
| } | |
| abstract public function apply(float $left, float $right): float; | |
| } | |
| function dumpGender(Gender $gender) | |
| { | |
| var_dump($gender->symbol()); | |
| } | |
| var_dump( | |
| Gender::FEMALE() | |
| ->symbol() | |
| ); | |
| var_dump(Gender::MALE() === Gender::FEMALE()); | |
| var_dump(Gender::MALE() === Gender::MALE()); | |
| dumpGender(Gender::MALE()); | |
| function yesNo(YesNo $yesNo) | |
| { | |
| switch ($yesNo) { | |
| case YesNo::NO(): | |
| var_dump('No'); | |
| break; | |
| case YesNo::YES(): | |
| var_dump('Yes'); | |
| break; | |
| } | |
| } | |
| yesNo(YesNo::NO()); | |
| yesNo(YesNo::YES()); | |
| var_dump( | |
| YesNo::NO() | |
| ->ordinal(), | |
| YesNo::YES() | |
| ->ordinal() | |
| ); | |
| //Will throw exception | |
| //dumpGender(YesNo::NO()); | |
| //Will throw exception | |
| //var_dump(clone YesNo::NO()); | |
| var_dump( | |
| MathematicalOperator::PLUS() | |
| ->apply(10, 20) | |
| ); | |
| /* @var MathematicalOperator $operator */ | |
| foreach (MathematicalOperator::iterator() as $operator) { | |
| $left = 10; | |
| $right = 20; | |
| var_dump( | |
| sprintf( | |
| '%.2lf %s %.2lf = %.2lf', | |
| $left, | |
| (string) $operator, | |
| $right, | |
| $operator->apply( | |
| $left, | |
| $right | |
| ) | |
| ) | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment