From 3c0f2006c06fdc73c569f2d0b7be08ccb0289ea7 Mon Sep 17 00:00:00 2001 From: Adrian Date: Sat, 21 Jan 2017 14:05:13 +0100 Subject: [PATCH 1/8] Always return the same instance of a const Implement EnumManager to keep track of all instances --- src/Enum.php | 25 +++++++++++++++++++++---- src/EnumManager.php | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/EnumTest.php | 11 +++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/EnumManager.php diff --git a/src/Enum.php b/src/Enum.php index 140e722..1fce2f7 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -165,6 +165,21 @@ public static function search($value) return array_search($value, static::toArray(), true); } + /** + * Returns Enum by key + * + * @return static + */ + public static function fromKey($name) + { + $array = static::toArray(); + if (array_key_exists($name, $array)) { + return EnumManager::get(new static($array[$name])); + } + + return null; + } + /** * Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant * @@ -176,11 +191,13 @@ public static function search($value) */ public static function __callStatic($name, $arguments) { - $array = static::toArray(); - if (isset($array[$name])) { - return new static($array[$name]); + $result = static::fromKey($name); + + if ($result === null) { + $msg = "No static method or enum constant '$name' in class " . get_called_class(); + throw new \BadMethodCallException($msg); } - throw new \BadMethodCallException("No static method or enum constant '$name' in class " . get_called_class()); + return $result; } } diff --git a/src/EnumManager.php b/src/EnumManager.php new file mode 100644 index 0000000..100c873 --- /dev/null +++ b/src/EnumManager.php @@ -0,0 +1,43 @@ +getName(); + $name = $enum->getKey(); + + if (isset(self::$instances[$class][$name])) { + return self::$instances[$class][$name]; + } + + self::$instances[$class][$name] = $enum; + return $enum; + } +} diff --git a/tests/EnumTest.php b/tests/EnumTest.php index c468017..9339f0f 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -225,6 +225,17 @@ public function testEquals() $this->assertTrue($foo->equals($anotherFoo)); } + /** + * __callStatic() + */ + public function testSameInstance() + { + $foo1 = EnumFixture::FOO(); + $foo2 = EnumFixture::FOO(); + + $this->assertSame($foo1, $foo2); + } + /** * equals() */ From 0b290791e5eb515547f1bb681c7b66c17acad6c6 Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 25 Jan 2017 22:31:45 +0100 Subject: [PATCH 2/8] Syntactic fixes --- src/Enum.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Enum.php b/src/Enum.php index 1fce2f7..b7fd24c 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -6,6 +6,9 @@ namespace MyCLabs\Enum; +use BadMethodCallException; +use UnexpectedValueException; + /** * Base Enum class * @@ -36,12 +39,12 @@ abstract class Enum * * @param mixed $value * - * @throws \UnexpectedValueException if incompatible type is given. + * @throws UnexpectedValueException if incompatible type is given. */ public function __construct($value) { - if (!$this->isValid($value)) { - throw new \UnexpectedValueException("Value '$value' is not part of the enum " . get_called_class()); + if (!static::isValid($value)) { + throw new UnexpectedValueException("Value '$value' is not part of the enum " . get_called_class()); } $this->value = $value; @@ -187,7 +190,7 @@ public static function fromKey($name) * @param array $arguments * * @return static - * @throws \BadMethodCallException + * @throws BadMethodCallException */ public static function __callStatic($name, $arguments) { @@ -195,7 +198,7 @@ public static function __callStatic($name, $arguments) if ($result === null) { $msg = "No static method or enum constant '$name' in class " . get_called_class(); - throw new \BadMethodCallException($msg); + throw new BadMethodCallException($msg); } return $result; From 1534d31c497f3e8e6e42a868198b5a54d09b431f Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 25 Jan 2017 22:50:29 +0100 Subject: [PATCH 3/8] Make constructor private and use EnumManager for all static methods --- src/Enum.php | 55 +++++++++++++++++++++++--------------------- src/EnumManager.php | 12 ++++++++++ tests/EnumTest.php | 56 +++++++++++++++++++-------------------------- 3 files changed, 64 insertions(+), 59 deletions(-) diff --git a/src/Enum.php b/src/Enum.php index b7fd24c..62b331a 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -7,7 +7,6 @@ namespace MyCLabs\Enum; use BadMethodCallException; -use UnexpectedValueException; /** * Base Enum class @@ -21,32 +20,27 @@ abstract class Enum { /** - * Enum value + * Enum name * - * @var mixed + * @var string */ - protected $value; + private $name; /** - * Store existing constants in a static cache per object. + * Enum value * - * @var array + * @var mixed */ - protected static $cache = array(); + private $value; /** * Creates a new value of some type * * @param mixed $value - * - * @throws UnexpectedValueException if incompatible type is given. */ - public function __construct($value) + final private function __construct($name, $value) { - if (!static::isValid($value)) { - throw new UnexpectedValueException("Value '$value' is not part of the enum " . get_called_class()); - } - + $this->name = $name; $this->value = $value; } @@ -65,7 +59,7 @@ public function getValue() */ public function getKey() { - return static::search($this->value); + return $this->name; } /** @@ -107,8 +101,8 @@ public static function values() { $values = array(); - foreach (static::toArray() as $key => $value) { - $values[$key] = new static($value); + foreach (static::toArray() as $name => $value) { + $values[$name] = EnumManager::get(new static($name, $value)); } return $values; @@ -121,13 +115,7 @@ public static function values() */ public static function toArray() { - $class = get_called_class(); - if (!array_key_exists($class, static::$cache)) { - $reflection = new \ReflectionClass($class); - static::$cache[$class] = $reflection->getConstants(); - } - - return static::$cache[$class]; + return EnumManager::constants(new static(null, null)); } /** @@ -168,6 +156,21 @@ public static function search($value) return array_search($value, static::toArray(), true); } + /** + * Returns Enum by value + * + * @return static + */ + public static function fromValue($value) + { + $name = static::search($value); + if ($name === false) { + return null; + } + + return EnumManager::get(new static($name, $value)); + } + /** * Returns Enum by key * @@ -176,8 +179,8 @@ public static function search($value) public static function fromKey($name) { $array = static::toArray(); - if (array_key_exists($name, $array)) { - return EnumManager::get(new static($array[$name])); + if (isset($array[$name]) || array_key_exists($name, $array)) { + return EnumManager::get(new static($name, $array[$name])); } return null; diff --git a/src/EnumManager.php b/src/EnumManager.php index 100c873..e61065c 100644 --- a/src/EnumManager.php +++ b/src/EnumManager.php @@ -40,4 +40,16 @@ public static function get(Enum $enum) self::$instances[$class][$name] = $enum; return $enum; } + + /** + * Returns all possible values as an array + * + * @return array Constant name in key, constant value in value + */ + public static function constants(Enum $enum) + { + $reflection = new ReflectionObject($enum); + $result = $reflection->getConstants(); + return $result; + } } diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 9339f0f..eed36e9 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -18,13 +18,13 @@ class EnumTest extends \PHPUnit\Framework\TestCase */ public function testGetValue() { - $value = new EnumFixture(EnumFixture::FOO); + $value = EnumFixture::FOO(); $this->assertEquals(EnumFixture::FOO, $value->getValue()); - $value = new EnumFixture(EnumFixture::BAR); + $value = EnumFixture::BAR(); $this->assertEquals(EnumFixture::BAR, $value->getValue()); - $value = new EnumFixture(EnumFixture::NUMBER); + $value = EnumFixture::NUMBER(); $this->assertEquals(EnumFixture::NUMBER, $value->getValue()); } @@ -33,21 +33,11 @@ public function testGetValue() */ public function testGetKey() { - $value = new EnumFixture(EnumFixture::FOO); + $value = EnumFixture::FOO(); $this->assertEquals('FOO', $value->getKey()); $this->assertNotEquals('BA', $value->getKey()); } - /** - * @dataProvider invalidValueProvider - * @expectedException UnexpectedValueException - * @expectedExceptionMessage is not part of the enum MyCLabs\Tests\Enum\EnumFixture - */ - public function testCreatingEnumWithInvalidValue($value) - { - new EnumFixture($value); - } - /** * Contains values not existing in EnumFixture * @return array @@ -70,9 +60,9 @@ public function testToString($expected, $enumObject) public function toStringProvider() { return array( - array(EnumFixture::FOO, new EnumFixture(EnumFixture::FOO)), - array(EnumFixture::BAR, new EnumFixture(EnumFixture::BAR)), - array((string) EnumFixture::NUMBER, new EnumFixture(EnumFixture::NUMBER)), + array(EnumFixture::FOO, EnumFixture::FOO()), + array(EnumFixture::BAR, EnumFixture::BAR()), + array((string) EnumFixture::NUMBER, EnumFixture::NUMBER()), ); } @@ -102,13 +92,13 @@ public function testValues() { $values = EnumFixture::values(); $expectedValues = array( - "FOO" => new EnumFixture(EnumFixture::FOO), - "BAR" => new EnumFixture(EnumFixture::BAR), - "NUMBER" => new EnumFixture(EnumFixture::NUMBER), - "PROBLEMATIC_NUMBER" => new EnumFixture(EnumFixture::PROBLEMATIC_NUMBER), - "PROBLEMATIC_NULL" => new EnumFixture(EnumFixture::PROBLEMATIC_NULL), - "PROBLEMATIC_EMPTY_STRING" => new EnumFixture(EnumFixture::PROBLEMATIC_EMPTY_STRING), - "PROBLEMATIC_BOOLEAN_FALSE" => new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE), + "FOO" => EnumFixture::FOO(), + "BAR" => EnumFixture::BAR(), + "NUMBER" => EnumFixture::NUMBER(), + "PROBLEMATIC_NUMBER" => EnumFixture::PROBLEMATIC_NUMBER(), + "PROBLEMATIC_NULL" => EnumFixture::PROBLEMATIC_NULL(), + "PROBLEMATIC_EMPTY_STRING" => EnumFixture::PROBLEMATIC_EMPTY_STRING(), + "PROBLEMATIC_BOOLEAN_FALSE" => EnumFixture::PROBLEMATIC_BOOLEAN_FALSE(), ); $this->assertEquals($expectedValues, $values); @@ -138,9 +128,9 @@ public function testToArray() */ public function testStaticAccess() { - $this->assertEquals(new EnumFixture(EnumFixture::FOO), EnumFixture::FOO()); - $this->assertEquals(new EnumFixture(EnumFixture::BAR), EnumFixture::BAR()); - $this->assertEquals(new EnumFixture(EnumFixture::NUMBER), EnumFixture::NUMBER()); + $this->assertEquals(EnumFixture::FOO(), EnumFixture::FOO()); + $this->assertEquals(EnumFixture::BAR(), EnumFixture::BAR()); + $this->assertEquals(EnumFixture::NUMBER(), EnumFixture::NUMBER()); } /** @@ -216,9 +206,9 @@ public function searchProvider() { */ public function testEquals() { - $foo = new EnumFixture(EnumFixture::FOO); - $number = new EnumFixture(EnumFixture::NUMBER); - $anotherFoo = new EnumFixture(EnumFixture::FOO); + $foo = EnumFixture::FOO(); + $number = EnumFixture::NUMBER(); + $anotherFoo = EnumFixture::FOO(); $this->assertTrue($foo->equals($foo)); $this->assertFalse($foo->equals($number)); @@ -241,9 +231,9 @@ public function testSameInstance() */ public function testEqualsComparesProblematicValuesProperly() { - $false = new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE); - $emptyString = new EnumFixture(EnumFixture::PROBLEMATIC_EMPTY_STRING); - $null = new EnumFixture(EnumFixture::PROBLEMATIC_NULL); + $false = EnumFixture::PROBLEMATIC_BOOLEAN_FALSE(); + $emptyString = EnumFixture::PROBLEMATIC_EMPTY_STRING(); + $null = EnumFixture::PROBLEMATIC_NULL(); $this->assertTrue($false->equals($false)); $this->assertFalse($false->equals($emptyString)); From 55f268e40c9a2550b11f8c5d05d9272d146519e5 Mon Sep 17 00:00:00 2001 From: Adrian Date: Mon, 30 Jan 2017 18:53:21 +0100 Subject: [PATCH 4/8] Trigger notice when Enum instance already exists during unserialize --- src/Enum.php | 11 +++++++++++ tests/EnumTest.php | 24 ++++++++++++++++++++++++ tests/UnserializeFixture.php | 19 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 tests/UnserializeFixture.php diff --git a/src/Enum.php b/src/Enum.php index 62b331a..64ba46b 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -70,6 +70,17 @@ public function __toString() return (string)$this->value; } + /** + * Register object in cache and trigger a notice if it already exists. + */ + public function __wakeup() + { + $enum = EnumManager::get($this); + if ($enum !== $this) { + trigger_error("Enum is already initialized", E_USER_NOTICE); + } + } + /** * Compares one Enum with another. * diff --git a/tests/EnumTest.php b/tests/EnumTest.php index eed36e9..4a686c2 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -248,4 +248,28 @@ public function testEqualsConflictValues() { $this->assertFalse(EnumFixture::FOO()->equals(EnumConflict::FOO())); } + + /** + * __wakeup() + */ + public function testUnserialize() + { + $ser = 'O:37:"MyCLabs\Tests\Enum\UnserializeFixture":2:{' + . 's:23:"#MyCLabs\Enum\Enum#name";s:4:"ONCE";' + . 's:24:"#MyCLabs\Enum\Enum#value";s:2:"OK";}'; + $once = unserialize(strtr($ser, "#", "\0")); + + $this->assertSame($once, UnserializeFixture::ONCE()); + } + + /** + * @expectedException \PHPUnit_Framework_Error_Notice + */ + public function testUnserializeError() + { + $ser = 'O:30:"MyCLabs\Tests\Enum\EnumFixture":2:{' + . 's:23:"#MyCLabs\Enum\Enum#name";s:3:"FOO";' + . 's:24:"#MyCLabs\Enum\Enum#value";s:3:"foo";}'; + $foo = unserialize(strtr($ser, "#", "\0")); + } } diff --git a/tests/UnserializeFixture.php b/tests/UnserializeFixture.php new file mode 100644 index 0000000..473f148 --- /dev/null +++ b/tests/UnserializeFixture.php @@ -0,0 +1,19 @@ + Date: Mon, 30 Jan 2017 19:03:54 +0100 Subject: [PATCH 5/8] Update Readme --- README.md | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index d0c3001..95cfbe9 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,6 @@ class Action extends Enum ## Usage ```php -$action = new Action(Action::VIEW); - -// or $action = Action::VIEW(); ``` @@ -58,9 +55,22 @@ function setAction(Action $action) { } ``` +Each Enum instance for a given key is a singleton, so you can use: + +```php +function setAction(Action $action) { + if ($action === Action::VIEW()) { + // + } +} +``` + +**Note** that this is not true, if you `unserialize()` Enums. +In case another Enum instance already exists, +an `E_USER_NOTICE` is triggered. + ## Documentation -- `__construct()` The constructor checks that the value exist in the enum - `__toString()` You can `echo $myValue`, it will display the enum value (value of the constant) - `getValue()` Returns the current value of the enum - `getKey()` Returns the key of the current value on Enum @@ -74,6 +84,8 @@ Static methods: - `isValid()` Check if tested value is valid on enum set - `isValidKey()` Check if tested key is valid on enum set - `search()` Return key for searched value +- `fromKey()` Return Enum instance for the given key +- `fromValue()` Return Enum instance for the given value ### Static methods @@ -91,23 +103,8 @@ $action = Action::EDIT(); Static method helpers are implemented using [`__callStatic()`](http://www.php.net/manual/en/language.oop5.overloading.php#object.callstatic). -If you care about IDE autocompletion, you can either implement the static methods yourself: - -```php -class Action extends Enum -{ - const VIEW = 'view'; - - /** - * @return Action - */ - public static function VIEW() { - return new Action(self::VIEW); - } -} -``` - -or you can use phpdoc (this is supported in PhpStorm for example): +If you care about IDE autocompletion, +you can use phpdoc (this is supported in PhpStorm for example): ```php /** From 7b4706210226f70826d0fdff8c3f965101b8fd80 Mon Sep 17 00:00:00 2001 From: Adrian Date: Tue, 31 Jan 2017 00:22:42 +0100 Subject: [PATCH 6/8] Add more tests --- tests/EnumTest.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 4a686c2..efd9934 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -249,6 +249,30 @@ public function testEqualsConflictValues() $this->assertFalse(EnumFixture::FOO()->equals(EnumConflict::FOO())); } + /** + * fromKey() + */ + public function testFromKey() + { + $number = EnumFixture::BAR(); + $fromValue = EnumFixture::fromKey('BAR'); + + $this->assertSame($number, $fromValue); + } + + /** + * fromValue() + */ + public function testFromValue() + { + $enum = EnumFixture::NUMBER(); + $number = EnumFixture::fromValue(42); + $inexistant = EnumFixture::fromValue('inexistant'); + + $this->assertSame($enum, $number); + $this->assertSame(null, $inexistant); + } + /** * __wakeup() */ From 55e1060de0a23db08339083eb193a4d4ad467fbc Mon Sep 17 00:00:00 2001 From: Adrian Date: Sat, 19 Aug 2017 23:57:37 +0200 Subject: [PATCH 7/8] Remove equals method --- src/Enum.php | 12 ------------ tests/EnumTest.php | 37 ------------------------------------- 2 files changed, 49 deletions(-) diff --git a/src/Enum.php b/src/Enum.php index 64ba46b..eee1b6c 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -81,18 +81,6 @@ public function __wakeup() } } - /** - * Compares one Enum with another. - * - * This method is final, for more information read https://github.com/myclabs/php-enum/issues/4 - * - * @return bool True if Enums are equal, false if not equal - */ - final public function equals(Enum $enum) - { - return $this->getValue() === $enum->getValue() && get_called_class() == get_class($enum); - } - /** * Returns the names (keys) of all constants in the Enum class * diff --git a/tests/EnumTest.php b/tests/EnumTest.php index efd9934..817f8ca 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -201,20 +201,6 @@ public function searchProvider() { ); } - /** - * equals() - */ - public function testEquals() - { - $foo = EnumFixture::FOO(); - $number = EnumFixture::NUMBER(); - $anotherFoo = EnumFixture::FOO(); - - $this->assertTrue($foo->equals($foo)); - $this->assertFalse($foo->equals($number)); - $this->assertTrue($foo->equals($anotherFoo)); - } - /** * __callStatic() */ @@ -226,29 +212,6 @@ public function testSameInstance() $this->assertSame($foo1, $foo2); } - /** - * equals() - */ - public function testEqualsComparesProblematicValuesProperly() - { - $false = EnumFixture::PROBLEMATIC_BOOLEAN_FALSE(); - $emptyString = EnumFixture::PROBLEMATIC_EMPTY_STRING(); - $null = EnumFixture::PROBLEMATIC_NULL(); - - $this->assertTrue($false->equals($false)); - $this->assertFalse($false->equals($emptyString)); - $this->assertFalse($emptyString->equals($null)); - $this->assertFalse($null->equals($false)); - } - - /** - * equals() - */ - public function testEqualsConflictValues() - { - $this->assertFalse(EnumFixture::FOO()->equals(EnumConflict::FOO())); - } - /** * fromKey() */ From 63f974d1cd5bce0a7eace61cb7763df9115b022c Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 30 Aug 2017 23:28:37 +0200 Subject: [PATCH 8/8] Fix compatibility for different PHPUnit versions --- tests/EnumTest.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 817f8ca..396ec02 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -250,13 +250,23 @@ public function testUnserialize() } /** - * @expectedException \PHPUnit_Framework_Error_Notice + * __wakeup() */ public function testUnserializeError() { + $triggered = false; + set_error_handler(function () use (&$triggered) { + $triggered = true; + return true; + }, E_USER_NOTICE); + $ser = 'O:30:"MyCLabs\Tests\Enum\EnumFixture":2:{' . 's:23:"#MyCLabs\Enum\Enum#name";s:3:"FOO";' . 's:24:"#MyCLabs\Enum\Enum#value";s:3:"foo";}'; $foo = unserialize(strtr($ser, "#", "\0")); + + restore_error_handler(); + + $this->assertTrue($triggered); } }