Skip to content

Instantly share code, notes, and snippets.

@shabbyrobe
Last active March 2, 2017 13:15
Show Gist options
  • Save shabbyrobe/beeaa1cd2a5348d5a0ff to your computer and use it in GitHub Desktop.
Save shabbyrobe/beeaa1cd2a5348d5a0ff to your computer and use it in GitHub Desktop.
Freenum - PHP Enum class
<?php
/**
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* Copyright (c) 2013 Blake Williams <code@shabbyrobe.org>
*
* https://gist.github.com/shabbyrobe/beeaa1cd2a5348d5a0ff
*/
namespace Freenum;
/**
* Allows you to define an enum of constants, like SplEnum but better (and not
* requiring a C extension).
*
* Put this in your own namespace, feel free to remove the `test()` method (or
* call it from PHPUnit). I usually put it in some variation of `MyProj\Lang`.
*
* You can use it as a bag of constants, iterate by name or id, or use the names
* as instances.
*
* See the `test()` method for a thorough description of rules and uses.
*
*
* Naming
* ------
*
* This class considers the ID to be the final value that the Enum is
* standing in for, and the NAME to be the name of the symbolic constant. Sometimes
* it seems like it's backwards, but I tried it the other way first and this is
* less backwards.
*
*
* Duplicate ids
* -------------
*
* If you wish to specify multiple constants with the same ID, you must
* declare a static method on your class called `allowDuplicateIds()` which returns
* `true`.
*
* When performing a lookup of the NAME by the ID, the first one appearing
* in the class declaration will be used. When retrieving an instance by static method,
* you will always get an instance of the first one.
*
* Duplicate IDs are considered useful for refactoring or code clarity, but you
* don't want to be in a situation where two instances with the same identity
* are incomparable. This assertion should pass:
*
* `assert(MyEnum::PANTS === MyEnum::TROUSERS && MyEnum::PANTS() == MyEnum::TROUSERS());`
*
*
* Floats
* ------
*
* You shouldn't use this class with floating point values for IDs. The reason why
* is illustrated by this snippet::
*
* assert([1.23 => 1, 1.45 => 2] == [1 => 2]); // true. ya rly.
*
*
* Y U NO composer?
* ----------------
*
* Single class or single function libraries are awful. Also, you might disagree with or
* find bugs with this thing. Just put it in your project and hack away. If you find bugs,
* comment on the gist or email me.
*/
abstract class Enum
{
private $id;
private $name;
private static $instances = [];
public static function __callStatic($name, $args)
{
if ($args) {
throw new \BadMethodCallException();
}
$called = get_called_class();
if (!$called::hasName($name)) {
throw new \BadMethodCallException();
}
if (isset(self::$instances[$called][$name])) {
return self::$instances[$called][$name];
} else {
return self::$instances[$called][$name] = new $called($called::findId($name));
}
}
public function __construct($id)
{
$this->id = static::ensureId($id);
$this->name = static::findName($id);
}
public function getId() { return $this->id; }
public function getName() { return $this->name; }
private static $names = [];
private static $ids = [];
public function __toString() { return $this->id.''; }
public static function allowDuplicateIds() { return false; }
public static function ensure($idOrInstance)
{
$c = get_called_class();
return $idOrInstance instanceof $c
? $idOrInstance
: new static(static::ensureId($idOrInstance));
}
public static function hasId($id)
{
$c = get_called_class();
return isset($c::names()[$id]);
}
public static function hasName($name)
{
$c = get_called_class();
return isset($c::ids()[$name]);
}
public static function ensureId($id)
{
$c = get_called_class();
if (!isset($c::names()[$id])) {
throw new \InvalidArgumentException("ID $id does not exist in class $c");
}
return $id;
}
public static function ensureName($name)
{
$c = get_called_class();
if (!isset($c::names()[$name])) {
throw new \InvalidArgumentException("Name $name does not exist in class $c");
}
return $name;
}
public static function ids()
{
$c = get_called_class();
if (!isset(self::$names[$c])) {
$rc = new \ReflectionClass($c);
self::$names[$c] = $rc->getConstants();
}
return self::$names[$c];
}
public static function findName($id)
{
$c = get_called_class();
$ids = static::names();
if (!isset($ids[$id])) {
throw new \InvalidArgumentException();
}
return $ids[$id];
}
public static function findId($name)
{
$c = get_called_class();
$ids = static::ids();
if (!isset($ids[$name])) {
throw new \InvalidArgumentException();
}
return $ids[$name];
}
public static function names()
{
$c = get_called_class();
if (!isset(self::$ids[$c])) {
$out = [];
$found = [];
foreach ($c::ids() as $k=>$v) {
if (!isset($found[$v])) {
$out[$v] = $k;
$found[$v] = true;
}
elseif (!$c::allowDuplicateIds()) {
throw new \UnexpectedValueException("Enum $c does not allow duplicated ids");
}
}
self::$ids[$c] = $out;
}
return self::$ids[$c];
}
public static function test()
{
$assert = function($test) {
if (!$test) throw new \RuntimeException('Assertion failed');
};
$assertException = function($exception, $cb) use ($assert) {
$caught = false;
try {
$cb();
}
catch (\Exception $e) {
if ($e instanceof $exception) {
$caught = true;
} else {
throw new \RuntimeException('Assertion failed', null, $e);
}
}
$assert($caught);
};
standard: {
if (!class_exists(TestEnum::class)) {
eval('namespace '.__NAMESPACE__.'; class TestEnum extends Enum {
const HELLO = 1;
const WORLD = "2";
const YEP = "yep";
}');
}
$h = TestEnum::HELLO();
$assert($h->getId() === 1);
$assert($h->getName() === 'HELLO');
$h = new TestEnum(TestEnum::HELLO);
$assert($h->getId() === 1);
$assert($h->getName() === 'HELLO');
$h = TestEnum::WORLD();
$assert($h->getId() === "2");
$assert($h->getName() === 'WORLD');
$h = TestEnum::YEP();
$assert($h->getId() === "yep");
$assert($h->getName() === 'YEP');
$assert(TestEnum::names() === [1 => 'HELLO', '2' => 'WORLD', 'yep' => 'YEP']);
$assert(TestEnum::ids() === ['HELLO' => 1, 'WORLD' => '2', 'YEP' => 'yep']);
foreach (TestEnum::ids() as $name=>$id) {
$assert(TestEnum::$name()->getId() === $id);
$assert((new TestEnum($id))->getId() === $id);
}
$assert(TestEnum::findName(TestEnum::HELLO) === 'HELLO');
$assert(TestEnum::findId('HELLO') === TestEnum::HELLO);
}
duplicates: {
if (!class_exists(TestEnumDupes::class)) {
eval('namespace '.__NAMESPACE__.'; class TestEnumDupes extends Enum {
const HELLO = 1;
const WORLD = 1;
const PANTS = 2;
static function allowDuplicateIds() { return true; }
}');
}
$h = TestEnumDupes::HELLO();
$assert($h->getId() === 1);
$w = TestEnumDupes::WORLD();
$assert($w->getId() === 1);
$assert($h == $w);
$assert(TestEnumDupes::names() === [1 => 'HELLO', 2 => 'PANTS']);
$assert(TestEnumDupes::ids() === ['HELLO' => 1, 'WORLD' => 1, 'PANTS' => 2]);
}
no_duplicates_allowed: {
if (!class_exists(TestEnumNoDupes::class)) {
eval('namespace '.__NAMESPACE__.'; class TestEnumNoDupes extends Enum {
const HELLO = 1;
const WORLD = 1;
}');
}
$assertException(\UnexpectedValueException::class, function() {
$h = TestEnumNoDupes::HELLO();
});
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment