Last active
March 2, 2017 13:15
-
-
Save shabbyrobe/beeaa1cd2a5348d5a0ff to your computer and use it in GitHub Desktop.
Freenum - PHP Enum class
This file contains 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 | |
/** | |
* 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 <[email protected]> | |
* | |
* 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