From 0eec70536330f6e4b99a8e5de9733875e5228a08 Mon Sep 17 00:00:00 2001 From: Alexandru Patranescu Date: Sat, 27 Feb 2021 07:30:40 +0200 Subject: [PATCH 1/3] Modify the enum implementation targeting PHP 8.0 to be compatible with new Enumeration introduced in PHP 8.1: https://wiki.php.net/rfc/enumerations --- composer.json | 4 +- src/Enum.php | 279 +++++++----- static-analysis/EnumIsPure.php | 2 - tests/EnumConflict.php | 2 +- tests/EnumFixture.php | 39 -- tests/EnumTest.php | 407 ++++++++++-------- tests/IntEnumFixture.php | 26 ++ ...numFixture.php => NotFinalEnumFixture.php} | 7 +- tests/StringEnumFixture.php | 26 ++ 9 files changed, 464 insertions(+), 328 deletions(-) delete mode 100644 tests/EnumFixture.php create mode 100644 tests/IntEnumFixture.php rename tests/{InheritedEnumFixture.php => NotFinalEnumFixture.php} (56%) create mode 100644 tests/StringEnumFixture.php diff --git a/composer.json b/composer.json index 73caf14..09febfd 100644 --- a/composer.json +++ b/composer.json @@ -22,12 +22,12 @@ } }, "require": { - "php": "^7.3 || ^8.0", + "php": "^8.0", "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^4.5.1" + "vimeo/psalm": "^4.6.2" } } diff --git a/src/Enum.php b/src/Enum.php index a16aa23..67f3676 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -14,8 +14,8 @@ * @author Matthieu Napoli * @author Daniel Costa * @author Mirosław Filip + * @author Alexandru Pătrănescu * - * @psalm-template T * @psalm-immutable * @psalm-consistent-constructor */ @@ -23,104 +23,91 @@ abstract class Enum implements \JsonSerializable { /** * Enum value - * - * @var mixed - * @psalm-var T */ - protected $value; + private int|string $value; /** * Enum key, the constant name - * - * @var string */ - private $key; + private string $key; /** * Store existing constants in a static cache per object. * - * - * @var array - * @psalm-var array> + * @psalm-var array> */ - protected static $cache = []; + private static array $cache = []; /** - * Cache of instances of the Enum class + * Store existing constants in a static cache per object. * - * @var array - * @psalm-var array> + * @psalm-var array> */ - protected static $instances = []; + private static array $reverseCache = []; /** - * Creates a new value of some type + * Store type of value, int or string. null value is for empty enums * - * @psalm-pure - * @param mixed $value - * - * @psalm-param T $value - * @throws \UnexpectedValueException if incompatible type is given. + * @psalm-var array */ - public function __construct($value) - { - if ($value instanceof static) { - /** @psalm-var T */ - $value = $value->getValue(); - } + private static array $typeCache = []; - $this->key = static::assertValidValueReturningKey($value); + /** + * Cache of instances of the Enum class + * + * @psalm-var array> + */ + private static array $instances = []; - /** @psalm-var T */ + final private function __construct(string $key, int|string $value) + { + $this->key = $key; $this->value = $value; } - public function __wakeup() + /** + * @psalm-pure + */ + private static function getInstance(string $key, int|string $value): static { - if ($this->key === null) { - $this->key = static::search($this->value); + if (!isset(self::$instances[static::class][$key])) { + return self::$instances[static::class][$key] = new static($key, $value); } + + return clone self::$instances[static::class][$key]; } /** - * @param mixed $value + * @param int|string $value * @return static - * @psalm-return static */ - public static function from($value): self + final public static function from(int|string $value): static + { + return self::tryFrom($value) ?? throw new \UnexpectedValueException("Value '{$value}' is not part of the enum " . static::class); + } + + final public static function tryFrom(int|string $value): ?static { - $key = static::assertValidValueReturningKey($value); + $key = self::search($value); - return self::__callStatic($key, []); + if ($key === false) { + return null; + } + + return self::getInstance($key, $value); } - /** - * @psalm-pure - * @return mixed - * @psalm-return T - */ - public function getValue() + final public function getValue(): int|string { return $this->value; } - /** - * Returns the enum key (i.e. the constant name). - * - * @psalm-pure - * @return string - */ - public function getKey() + final public function getKey(): string { return $this->key; } - /** - * @psalm-pure - * @psalm-suppress InvalidCast - * @return string - */ - public function __toString() + final public function __toString(): string { return (string)$this->value; } @@ -130,14 +117,13 @@ public function __toString() * Returns false if an argument is an object of different class or not an object. * * This method is final, for more information read https://github.com/myclabs/php-enum/issues/4 - * - * @psalm-pure - * @psalm-param mixed $variable + * @param static $variable * @return bool */ - final public function equals($variable = null): bool + final public function equals(self $variable): bool { - return $variable instanceof self + return $variable instanceof static + && $this->getKey() === $variable->getKey() && $this->getValue() === $variable->getValue() && static::class === \get_class($variable); } @@ -147,11 +133,10 @@ final public function equals($variable = null): bool * * @psalm-pure * @psalm-return list - * @return array */ - public static function keys() + public static function keys(): array { - return \array_keys(static::toArray()); + return \array_keys(self::toArray()); } /** @@ -161,13 +146,12 @@ public static function keys() * @psalm-return array * @return static[] Constant name in key, Enum instance in value */ - public static function values() + public static function values(): array { - $values = array(); + $values = []; - /** @psalm-var T $value */ - foreach (static::toArray() as $key => $value) { - $values[$key] = new static($value); + foreach (self::toArray() as $key => $value) { + $values[$key] = self::getInstance($key, $value); } return $values; @@ -179,76 +163,139 @@ public static function values() * @psalm-pure * @psalm-suppress ImpureStaticProperty * - * @psalm-return array + * @psalm-return array * @return array Constant name in key, constant value in value */ - public static function toArray() + final public static function toArray(): array { - $class = static::class; + if (!isset(self::$cache[static::class])) { + self::computeCache(); + } + + return self::$cache[static::class]; + } - if (!isset(static::$cache[$class])) { - /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ - $reflection = new \ReflectionClass($class); - /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ - static::$cache[$class] = $reflection->getConstants(); + /** + * @psalm-pure + * @psalm-suppress ImpureStaticProperty + * + * @psalm-return array + * @return string[] Constant value in key, constant name in value + */ + private static function toReverseArray(): array + { + if (!isset(self::$reverseCache[static::class])) { + self::computeCache(); } - return static::$cache[$class]; + return self::$reverseCache[static::class]; } /** - * Check if is valid enum value + * @psalm-pure + * @psalm-suppress ImpureStaticProperty * - * @param $value - * @psalm-param mixed $value + * @psalm-return 'int'|'string'|'empty' + */ + private static function getType(): string + { + if (!isset(self::$typeCache[static::class])) { + self::computeCache(); + } + + return self::$typeCache[static::class]; + } + + /** * @psalm-pure - * @psalm-assert-if-true T $value - * @return bool + * @psalm-suppress ImpureStaticProperty + * + * Compute cached values for the class using reflection */ - public static function isValid($value) + private static function computeCache(): void { - return \in_array($value, static::toArray(), true); + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + $reflection = new \ReflectionClass(static::class); + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + $constantsDefinition = $reflection->getConstants(); + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + if (!$reflection->isFinal()) { + throw new \ParseError("Class " . static::class . " is not declared final"); + } + + $type = null; + $reverseConstantsDefinition = []; + /** @psalm-assert array $constantsDefinition */ + foreach ($constantsDefinition as $key => $value) { + if (is_int($value)) { + if (!isset($type)) { + $type = 'int'; + } elseif ($type !== 'int') { + throw new \ParseError("Value for constant '{$key}' in class " . static::class . " is not int, even if previous value is int"); + } + } elseif (is_string($value)) { + if (!isset($type)) { + $type = 'string'; + } elseif ($type !== 'string') { + throw new \ParseError("Value for constant '{$key}' in class " . static::class . " is not string, even if previous value is string"); + } + } else { + throw new \ParseError("Value for constant '{$key}' is not int or string."); + } + + if (\array_key_exists($value, $reverseConstantsDefinition)) { + throw new \ParseError("Value '{$value}' is duplicated in the enum definition of class " . static::class); + } + + $reverseConstantsDefinition[$value] = $key; + } + + /** @psalm-suppress MixedPropertyTypeCoercion assert exists already of array on $constantsDefinition */ + self::$cache[static::class] = $constantsDefinition; + self::$reverseCache[static::class] = $reverseConstantsDefinition; + self::$typeCache[static::class] = $type ?? 'empty'; } /** - * Asserts valid enum value + * Check if is valid enum value * + * @param int|string $value * @psalm-pure - * @psalm-assert T $value + * @return bool */ - public static function assertValidValue($value): void + public static function isValid(int|string $value): bool { - self::assertValidValueReturningKey($value); + $reverseArray = self::toReverseArray(); + + return isset($reverseArray[$value]); } /** * Asserts valid enum value * * @psalm-pure - * @psalm-assert T $value + * @param int|string $value */ - private static function assertValidValueReturningKey($value): string + public static function assertValidValue(int|string $value): void { - if (false === ($key = static::search($value))) { + if (!self::isValid($value)) { throw new \UnexpectedValueException("Value '$value' is not part of the enum " . static::class); } - - return $key; } /** * Check if is valid enum key * - * @param $key + * @param string $key * @psalm-param string $key * @psalm-pure * @return bool */ - public static function isValidKey($key) + final public static function isValidKey(string $key): bool { - $array = static::toArray(); + $array = self::toArray(); - return isset($array[$key]) || \array_key_exists($key, $array); + return isset($array[$key]); } /** @@ -260,16 +307,30 @@ public static function isValidKey($key) * @psalm-pure * @return string|false */ - public static function search($value) + final public static function search(int|string $value): string|false { - return \array_search($value, static::toArray(), true); + $type = self::getType(); + if ( + ($type === 'int' && !is_int($value)) + || + ($type === 'string' && !is_string($value)) + || + ($type === 'empty') + ) { + return false; + } + + $reverseArray = self::toReverseArray(); + + /** @psalm-suppress MixedArrayOffset this is already validated at this point */ + return $reverseArray[$value] ?? false; } /** * Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant * * @param string $name - * @param array $arguments + * @param array $arguments * * @return static * @throws \BadMethodCallException @@ -278,16 +339,12 @@ public static function search($value) */ public static function __callStatic($name, $arguments) { - $class = static::class; - if (!isset(self::$instances[$class][$name])) { - $array = static::toArray(); - if (!isset($array[$name]) && !\array_key_exists($name, $array)) { - $message = "No static method or enum constant '$name' in class " . static::class; - throw new \BadMethodCallException($message); - } - return self::$instances[$class][$name] = new static($array[$name]); + $array = self::toArray(); + if (!isset($array[$name])) { + $message = "No static method or enum constant '$name' in class " . static::class; + throw new \BadMethodCallException($message); } - return clone self::$instances[$class][$name]; + return self::getInstance($name, $array[$name]); } /** diff --git a/static-analysis/EnumIsPure.php b/static-analysis/EnumIsPure.php index 5875fd8..21400f6 100644 --- a/static-analysis/EnumIsPure.php +++ b/static-analysis/EnumIsPure.php @@ -11,8 +11,6 @@ * @method static PureEnum C() * * @psalm-immutable - * @psalm-template T of 'A'|'B' - * @template-extends Enum */ final class PureEnum extends Enum { diff --git a/tests/EnumConflict.php b/tests/EnumConflict.php index edeef37..b322dd0 100644 --- a/tests/EnumConflict.php +++ b/tests/EnumConflict.php @@ -16,7 +16,7 @@ * @author Daniel Costa * @author Mirosław Filip */ -class EnumConflict extends Enum +final class EnumConflict extends Enum { const FOO = "foo"; const BAR = "bar"; diff --git a/tests/EnumFixture.php b/tests/EnumFixture.php deleted file mode 100644 index d04b890..0000000 --- a/tests/EnumFixture.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @author Mirosław Filip - */ -class EnumFixture extends Enum -{ - const FOO = "foo"; - const BAR = "bar"; - const NUMBER = 42; - - /** - * Values that are known to cause problems when used with soft typing - */ - const PROBLEMATIC_NUMBER = 0; - const PROBLEMATIC_NULL = null; - const PROBLEMATIC_EMPTY_STRING = ''; - const PROBLEMATIC_BOOLEAN_FALSE = false; -} diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 6fcc5b1..b327b28 100755 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -18,14 +18,23 @@ class EnumTest extends \PHPUnit\Framework\TestCase */ public function testGetValue() { - $value = new EnumFixture(EnumFixture::FOO); - $this->assertEquals(EnumFixture::FOO, $value->getValue()); + $value = StringEnumFixture::FOO(); + self::assertEquals(StringEnumFixture::FOO, $value->getValue()); - $value = new EnumFixture(EnumFixture::BAR); - $this->assertEquals(EnumFixture::BAR, $value->getValue()); + $value = StringEnumFixture::BAR(); + self::assertEquals(StringEnumFixture::BAR, $value->getValue()); - $value = new EnumFixture(EnumFixture::NUMBER); - $this->assertEquals(EnumFixture::NUMBER, $value->getValue()); + $value = StringEnumFixture::EMPTY(); + self::assertEquals(StringEnumFixture::EMPTY, $value->getValue()); + + $value = IntEnumFixture::FIRST(); + self::assertEquals(IntEnumFixture::FIRST, $value->getValue()); + + $value = IntEnumFixture::SECOND(); + self::assertEquals(IntEnumFixture::SECOND, $value->getValue()); + + $value = IntEnumFixture::THIRD(); + self::assertEquals(IntEnumFixture::THIRD, $value->getValue()); } /** @@ -33,49 +42,58 @@ public function testGetValue() */ public function testGetKey() { - $value = new EnumFixture(EnumFixture::FOO); - $this->assertEquals('FOO', $value->getKey()); - $this->assertNotEquals('BA', $value->getKey()); + $value = StringEnumFixture::FOO(); + self::assertEquals('FOO', $value->getKey()); + self::assertNotEquals('BA', $value->getKey()); } - /** @dataProvider invalidValueProvider */ - public function testCreatingEnumWithInvalidValue($value) + /** + * @dataProvider invalidValueProviderForString + * @param mixed $value + */ + public function testFailToCreateStringEnumWithInvalidValueThroughNamedConstructor($value): void { $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('is not part of the enum ' . EnumFixture::class); + $this->expectExceptionMessage('is not part of the enum MyCLabs\Tests\Enum\StringEnumFixture'); - new EnumFixture($value); + StringEnumFixture::from($value); } /** - * @dataProvider invalidValueProvider - * @param mixed $value + * Contains values not existing in EnumFixture + * @return array */ - public function testFailToCreateEnumWithInvalidValueThroughNamedConstructor($value): void + public function invalidValueProviderForString() { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('is not part of the enum MyCLabs\Tests\Enum\EnumFixture'); - - EnumFixture::from($value); + return array( + "string" => array('test'), + "int" => array(1234), + "int0" => array(0), + ); } - public function testFailToCreateEnumWithEnumItselfThroughNamedConstructor(): void + /** + * @dataProvider invalidValueProviderForInt + * @param mixed $value + */ + public function testFailToCreateIntEnumWithInvalidValueThroughNamedConstructor($value): void { $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage("Value 'foo' is not part of the enum " . EnumFixture::class); + $this->expectExceptionMessage('is not part of the enum MyCLabs\Tests\Enum\IntEnumFixture'); - EnumFixture::from(EnumFixture::FOO()); + IntEnumFixture::from($value); } /** * Contains values not existing in EnumFixture * @return array */ - public function invalidValueProvider() + public function invalidValueProviderForInt() { return array( "string" => array('test'), "int" => array(1234), + "emptystring" => array(''), ); } @@ -85,73 +103,107 @@ public function invalidValueProvider() */ public function testToString($expected, $enumObject) { - $this->assertSame($expected, (string) $enumObject); + self::assertSame($expected, (string) $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(StringEnumFixture::FOO, StringEnumFixture::FOO()), + array(StringEnumFixture::BAR, StringEnumFixture::BAR()), + array((string) IntEnumFixture::SECOND, IntEnumFixture::SECOND()), + array((string) IntEnumFixture::FIRST, IntEnumFixture::FIRST()), ); } /** * keys() */ - public function testKeys() + public function testStringKeys() { - $values = EnumFixture::keys(); + $values = StringEnumFixture::keys(); $expectedValues = array( "FOO", "BAR", - "NUMBER", - "PROBLEMATIC_NUMBER", - "PROBLEMATIC_NULL", - "PROBLEMATIC_EMPTY_STRING", - "PROBLEMATIC_BOOLEAN_FALSE", + "EMPTY", + ); + + self::assertSame($expectedValues, $values); + } + + /** + * keys() + */ + public function testIntKeys() + { + $values = IntEnumFixture::keys(); + $expectedValues = array( + "FIRST", + "SECOND", + "THIRD", ); - $this->assertSame($expectedValues, $values); + self::assertSame($expectedValues, $values); } /** * values() */ - public function testValues() + public function testStringValues() { - $values = EnumFixture::values(); + $values = StringEnumFixture::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" => StringEnumFixture::FOO(), + "BAR" => StringEnumFixture::BAR(), + "EMPTY" => StringEnumFixture::EMPTY(), ); - $this->assertEquals($expectedValues, $values); + self::assertEquals($expectedValues, $values); + } + + /** + * values() + */ + public function testIntValues() + { + $values = IntEnumFixture::values(); + $expectedValues = array( + "FIRST" => IntEnumFixture::FIRST(), + "SECOND" => IntEnumFixture::SECOND(), + "THIRD" => IntEnumFixture::THIRD(), + ); + + self::assertEquals($expectedValues, $values); + } + + /** + * toArray() + */ + public function testStringEnumToArray() + { + $values = StringEnumFixture::toArray(); + $expectedValues = array( + "FOO" => StringEnumFixture::FOO, + "BAR" => StringEnumFixture::BAR, + "EMPTY" => StringEnumFixture::EMPTY, + ); + + self::assertSame($expectedValues, $values); } /** * toArray() */ - public function testToArray() + public function testIntEnumToArray() { - $values = EnumFixture::toArray(); + $values = IntEnumFixture::toArray(); $expectedValues = array( - "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, + "FIRST" => IntEnumFixture::FIRST, + "SECOND" => IntEnumFixture::SECOND, + "THIRD" => IntEnumFixture::THIRD, ); - $this->assertSame($expectedValues, $values); + self::assertSame($expectedValues, $values); } /** @@ -159,45 +211,75 @@ 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->assertNotSame(EnumFixture::NUMBER(), EnumFixture::NUMBER()); + self::assertEquals(StringEnumFixture::from(StringEnumFixture::FOO), StringEnumFixture::FOO()); + self::assertEquals(StringEnumFixture::from(StringEnumFixture::BAR), StringEnumFixture::BAR()); + self::assertEquals(StringEnumFixture::from(StringEnumFixture::EMPTY), StringEnumFixture::EMPTY()); + self::assertNotSame(StringEnumFixture::FOO(), StringEnumFixture::FOO()); + + self::assertEquals(IntEnumFixture::from(IntEnumFixture::FIRST), IntEnumFixture::FIRST()); + self::assertEquals(IntEnumFixture::from(IntEnumFixture::SECOND), IntEnumFixture::SECOND()); + self::assertEquals(IntEnumFixture::from(IntEnumFixture::THIRD), IntEnumFixture::THIRD()); + self::assertNotSame(IntEnumFixture::FIRST(), IntEnumFixture::FIRST()); + } public function testBadStaticAccess() { $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('No static method or enum constant \'UNKNOWN\' in class ' . EnumFixture::class); + $this->expectExceptionMessage('No static method or enum constant \'UNKNOWN\' in class ' . StringEnumFixture::class); - EnumFixture::UNKNOWN(); + StringEnumFixture::UNKNOWN(); } /** * isValid() - * @dataProvider isValidProvider + * @dataProvider isValidStringEnumProvider */ - public function testIsValid($value, $isValid) + public function testIsValidStringEnum($value, $isValid) { - $this->assertSame($isValid, EnumFixture::isValid($value)); + self::assertSame($isValid, StringEnumFixture::isValid($value)); } - public function isValidProvider() + public function isValidStringEnumProvider() { return [ /** * Valid values */ ['foo', true], - [42, true], - [null, true], - [0, true], ['', true], - [false, true], /** * Invalid values */ - ['baz', false] + ['baz', false], + [0, false], + [1, false], + ]; + } + + /** + * isValid() + * @dataProvider isValidIntEnumProvider + */ + public function testIsValidIntEnum($value, $isValid) + { + self::assertSame($isValid, IntEnumFixture::isValid($value)); + } + + public function isValidIntEnumProvider() + { + return [ + /** + * Valid values + */ + [0, true], + [1, true], + /** + * Invalid values + */ + ['baz', false], + [42, false], + ['', false], ]; } @@ -206,66 +288,72 @@ public function isValidProvider() */ public function testIsValidKey() { - $this->assertTrue(EnumFixture::isValidKey('FOO')); - $this->assertFalse(EnumFixture::isValidKey('BAZ')); - $this->assertTrue(EnumFixture::isValidKey('PROBLEMATIC_NULL')); + self::assertTrue(StringEnumFixture::isValidKey('FOO')); + self::assertFalse(StringEnumFixture::isValidKey('BAZ')); + + self::assertTrue(IntEnumFixture::isValidKey('FIRST')); + self::assertFalse(IntEnumFixture::isValidKey('FOURTH')); } /** * search() - * @see https://github.com/myclabs/php-enum/issues/13 - * @dataProvider searchProvider + * @dataProvider searchStringEnumProvider */ - public function testSearch($value, $expected) + public function testStringEnumSearch($value, $expected) { - $this->assertSame($expected, EnumFixture::search($value)); + self::assertSame($expected, StringEnumFixture::search($value)); } - public function searchProvider() + public function searchStringEnumProvider() { return array( array('foo', 'FOO'), - array(0, 'PROBLEMATIC_NUMBER'), - array(null, 'PROBLEMATIC_NULL'), - array('', 'PROBLEMATIC_EMPTY_STRING'), - array(false, 'PROBLEMATIC_BOOLEAN_FALSE'), + array('', 'EMPTY'), array('bar I do not exist', false), - array(array(), false), + array(0, false), + array(42, false), ); } /** - * equals() + * search() + * @dataProvider searchIntEnumProvider */ - public function testEquals() + public function testIntEnumSearch($value, $expected) { - $foo = new EnumFixture(EnumFixture::FOO); - $number = new EnumFixture(EnumFixture::NUMBER); - $anotherFoo = new EnumFixture(EnumFixture::FOO); - $objectOfDifferentClass = new \stdClass(); - $notAnObject = 'foo'; + self::assertSame($expected, IntEnumFixture::search($value)); + } - $this->assertTrue($foo->equals($foo)); - $this->assertFalse($foo->equals($number)); - $this->assertTrue($foo->equals($anotherFoo)); - $this->assertFalse($foo->equals(null)); - $this->assertFalse($foo->equals($objectOfDifferentClass)); - $this->assertFalse($foo->equals($notAnObject)); + public function searchIntEnumProvider() + { + return array( + array(0, 'FIRST'), + array(1, 'SECOND'), + array(42, false), + array('', false), + ); } /** * equals() */ - public function testEqualsComparesProblematicValuesProperly() + public function testEquals() { - $false = new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE); - $emptyString = new EnumFixture(EnumFixture::PROBLEMATIC_EMPTY_STRING); - $null = new EnumFixture(EnumFixture::PROBLEMATIC_NULL); - - $this->assertTrue($false->equals($false)); - $this->assertFalse($false->equals($emptyString)); - $this->assertFalse($emptyString->equals($null)); - $this->assertFalse($null->equals($false)); + $foo = StringEnumFixture::from(StringEnumFixture::FOO); + $anotherFoo = StringEnumFixture::from(StringEnumFixture::FOO); + $empty = StringEnumFixture::from(StringEnumFixture::EMPTY); + $first = IntEnumFixture::from(IntEnumFixture::FIRST); + $second = IntEnumFixture::from(IntEnumFixture::SECOND); + $secondTwice = IntEnumFixture::from(IntEnumFixture::SECOND); + + self::assertTrue($foo->equals($foo)); + self::assertFalse($foo->equals($empty)); + self::assertTrue($foo->equals($anotherFoo)); + self::assertFalse($foo->equals($first)); + self::assertFalse($empty->equals($first)); + self::assertFalse($first->equals($empty)); + self::assertFalse($first->equals($second)); + self::assertTrue($second->equals($secondTwice)); } /** @@ -273,7 +361,7 @@ public function testEqualsComparesProblematicValuesProperly() */ public function testEqualsConflictValues() { - $this->assertFalse(EnumFixture::FOO()->equals(EnumConflict::FOO())); + self::assertFalse(StringEnumFixture::FOO()->equals(EnumConflict::FOO())); } /** @@ -281,104 +369,81 @@ public function testEqualsConflictValues() */ public function testJsonSerialize() { - $this->assertJsonEqualsJson('"foo"', json_encode(new EnumFixture(EnumFixture::FOO))); - $this->assertJsonEqualsJson('"bar"', json_encode(new EnumFixture(EnumFixture::BAR))); - $this->assertJsonEqualsJson('42', json_encode(new EnumFixture(EnumFixture::NUMBER))); - $this->assertJsonEqualsJson('0', json_encode(new EnumFixture(EnumFixture::PROBLEMATIC_NUMBER))); - $this->assertJsonEqualsJson('null', json_encode(new EnumFixture(EnumFixture::PROBLEMATIC_NULL))); - $this->assertJsonEqualsJson('""', json_encode(new EnumFixture(EnumFixture::PROBLEMATIC_EMPTY_STRING))); - $this->assertJsonEqualsJson('false', json_encode(new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE))); - } - - public function testNullableEnum() - { - $this->assertNull(EnumFixture::PROBLEMATIC_NULL()->getValue()); - $this->assertNull((new EnumFixture(EnumFixture::PROBLEMATIC_NULL))->getValue()); - $this->assertNull((new EnumFixture(EnumFixture::PROBLEMATIC_NULL))->jsonSerialize()); + self::assertJsonEqualsJson('"foo"', json_encode(StringEnumFixture::from(StringEnumFixture::FOO))); + self::assertJsonEqualsJson('"bar"', json_encode(StringEnumFixture::from(StringEnumFixture::BAR))); + self::assertJsonEqualsJson('""', json_encode(StringEnumFixture::from(StringEnumFixture::EMPTY))); + self::assertJsonEqualsJson('0', json_encode(IntEnumFixture::from(IntEnumFixture::FIRST))); + self::assertJsonEqualsJson('1', json_encode(IntEnumFixture::from(IntEnumFixture::SECOND))); + self::assertJsonEqualsJson('2', json_encode(IntEnumFixture::from(IntEnumFixture::THIRD))); } - public function testBooleanEnum() + private static function assertJsonEqualsJson(string $json1, string $json2): void { - $this->assertFalse(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE()->getValue()); - $this->assertFalse((new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE))->jsonSerialize()); - } - - public function testConstructWithSameEnumArgument() - { - $enum = new EnumFixture(EnumFixture::FOO); - - $enveloped = new EnumFixture($enum); - - $this->assertEquals($enum, $enveloped); - } - - private function assertJsonEqualsJson($json1, $json2) - { - $this->assertJsonStringEqualsJsonString($json1, $json2); + self::assertJsonStringEqualsJsonString($json1, $json2); } public function testSerialize() { // split string for Pretty CI: "Line exceeds 120 characters" - $bin = '4f3a33303a224d79434c6162735c54657374735c456e756d5c456e756d4669787'. - '4757265223a323a7b733a383a22002a0076616c7565223b733a333a22666f6f223b73'. + $bin = '4f3a33363a224d79434c6162735c54657374735c456e756d5c537472696e67456e756d4669787'. + '4757265223a323a7b733a32343a22004d79434c6162735c456e756d5c456e756d0076616c7565223b733a333a22666f6f223b73'. '3a32323a22004d79434c6162735c456e756d5c456e756d006b6579223b733a333a22464f4f223b7d'; - $this->assertEquals($bin, bin2hex(serialize(EnumFixture::FOO()))); - } - - public function testUnserializeVersionWithoutKey() - { - // split string for Pretty CI: "Line exceeds 120 characters" - $bin = '4f3a33303a224d79434c6162735c54657374735c456e756d5c456e756d4669787'. - '4757265223a313a7b733a383a22002a0076616c7565223b733a333a22666f6f223b7d'; - - /* @var $value EnumFixture */ - $value = unserialize(pack('H*', $bin)); - - $this->assertEquals(EnumFixture::FOO, $value->getValue()); - $this->assertTrue(EnumFixture::FOO()->equals($value)); - $this->assertTrue(EnumFixture::FOO() == $value); + self::assertEquals($bin, bin2hex(serialize(StringEnumFixture::FOO()))); } public function testUnserialize() { // split string for Pretty CI: "Line exceeds 120 characters" - $bin = '4f3a33303a224d79434c6162735c54657374735c456e756d5c456e756d4669787'. - '4757265223a323a7b733a383a22002a0076616c7565223b733a333a22666f6f223b73'. + $bin = '4f3a33363a224d79434c6162735c54657374735c456e756d5c537472696e67456e756d4669787'. + '4757265223a323a7b733a32343a22004d79434c6162735c456e756d5c456e756d0076616c7565223b733a333a22666f6f223b73'. '3a32323a22004d79434c6162735c456e756d5c456e756d006b6579223b733a333a22464f4f223b7d'; - /* @var $value EnumFixture */ + /* @var $value StringEnumFixture */ $value = unserialize(pack('H*', $bin)); - $this->assertEquals(EnumFixture::FOO, $value->getValue()); - $this->assertTrue(EnumFixture::FOO()->equals($value)); - $this->assertTrue(EnumFixture::FOO() == $value); + self::assertEquals(StringEnumFixture::FOO, $value->getValue()); + self::assertTrue(StringEnumFixture::FOO()->equals($value)); + self::assertTrue(StringEnumFixture::FOO() == $value); } /** * @see https://github.com/myclabs/php-enum/issues/95 */ - public function testEnumValuesInheritance() + public function testEnumMustBeFinal() { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage("Value 'value' is not part of the enum MyCLabs\Tests\Enum\EnumFixture"); - $inheritedEnumFixture = InheritedEnumFixture::VALUE(); - new EnumFixture($inheritedEnumFixture); + $this->expectException(\ParseError::class); + $this->expectExceptionMessage("Class MyCLabs\Tests\Enum\NotFinalEnumFixture is not declared final"); + NotFinalEnumFixture::VALUE(); + } + + /** + * @dataProvider isValidStringEnumProvider + */ + public function testAssertValidValueStringEnum($value, $isValid): void + { + if (!$isValid) { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage("Value '$value' is not part of the enum " . StringEnumFixture::class); + } + + StringEnumFixture::assertValidValue($value); + + self::assertTrue(StringEnumFixture::isValid($value)); } /** - * @dataProvider isValidProvider + * @dataProvider isValidIntEnumProvider */ - public function testAssertValidValue($value, $isValid): void + public function testAssertValidValueIntEnum($value, $isValid): void { if (!$isValid) { $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage("Value '$value' is not part of the enum " . EnumFixture::class); + $this->expectExceptionMessage("Value '$value' is not part of the enum " . IntEnumFixture::class); } - EnumFixture::assertValidValue($value); + IntEnumFixture::assertValidValue($value); - self::assertTrue(EnumFixture::isValid($value)); + self::assertTrue(IntEnumFixture::isValid($value)); } } diff --git a/tests/IntEnumFixture.php b/tests/IntEnumFixture.php new file mode 100644 index 0000000..dcbf6a6 --- /dev/null +++ b/tests/IntEnumFixture.php @@ -0,0 +1,26 @@ + + * @author Mirosław Filip + */ +final class IntEnumFixture extends Enum +{ + const FIRST = 0; + const SECOND = 1; + const THIRD = 2; +} diff --git a/tests/InheritedEnumFixture.php b/tests/NotFinalEnumFixture.php similarity index 56% rename from tests/InheritedEnumFixture.php rename to tests/NotFinalEnumFixture.php index 301f9bb..5652407 100644 --- a/tests/InheritedEnumFixture.php +++ b/tests/NotFinalEnumFixture.php @@ -1,14 +1,17 @@ + * @author Mirosław Filip + */ +final class StringEnumFixture extends Enum +{ + const FOO = "foo"; + const BAR = "bar"; + const EMPTY = ""; +} From 6101df048bfd4656c90e86df4f1032751a8d926b Mon Sep 17 00:00:00 2001 From: Alexandru Patranescu Date: Mon, 1 Mar 2021 18:39:03 +0200 Subject: [PATCH 2/3] Modify the enum implementation targeting PHP 7.3 to be compatible with new Enumeration introduced in PHP 8.1 --- composer.json | 2 +- src/Enum.php | 222 +++++++++------------- static-analysis/EnumIsPure.php | 2 + tests/EnumFixture.php | 39 ++++ tests/EnumTest.php | 331 +++++++++++---------------------- tests/IntEnumFixture.php | 26 --- tests/NotFinalEnumFixture.php | 1 - tests/StringEnumFixture.php | 26 --- 8 files changed, 236 insertions(+), 413 deletions(-) create mode 100644 tests/EnumFixture.php delete mode 100644 tests/IntEnumFixture.php delete mode 100644 tests/StringEnumFixture.php diff --git a/composer.json b/composer.json index 09febfd..924f924 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } }, "require": { - "php": "^8.0", + "php": "^7.3 || ^8.0", "ext-json": "*" }, "require-dev": { diff --git a/src/Enum.php b/src/Enum.php index 67f3676..fec5138 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -16,6 +16,7 @@ * @author Mirosław Filip * @author Alexandru Pătrănescu * + * @psalm-template T * @psalm-immutable * @psalm-consistent-constructor */ @@ -23,52 +24,60 @@ abstract class Enum implements \JsonSerializable { /** * Enum value + * + * @var mixed + * @psalm-var T */ - private int|string $value; + private $value; /** * Enum key, the constant name - */ - private string $key; - - /** - * Store existing constants in a static cache per object. * - * @psalm-var array> + * @var string */ - private static array $cache = []; + private $key; /** * Store existing constants in a static cache per object. * - * @psalm-var array> - */ - private static array $reverseCache = []; - - /** - * Store type of value, int or string. null value is for empty enums * - * @psalm-var array + * @var array + * @psalm-var array> */ - private static array $typeCache = []; + private static $cache = []; /** * Cache of instances of the Enum class * + * @var array * @psalm-var array> */ - private static array $instances = []; + private static $instances = []; - final private function __construct(string $key, int|string $value) + /** + * Creates a new value of some type + * + * @psalm-pure + * @param string $key + * @param mixed $value + * + * @psalm-param T $value + */ + final private function __construct(string $key, $value) { $this->key = $key; $this->value = $value; } /** + * The single place where the instance is created, other than unserialize + * * @psalm-pure + * @param string $key + * @param mixed $value + * @return static */ - private static function getInstance(string $key, int|string $value): static + private static function getInstance(string $key, $value): self { if (!isset(self::$instances[static::class][$key])) { return self::$instances[static::class][$key] = new static($key, $value); @@ -78,15 +87,27 @@ private static function getInstance(string $key, int|string $value): static } /** - * @param int|string $value + * @param mixed $value * @return static + * @psalm-return static */ - final public static function from(int|string $value): static + final public static function from($value): self { - return self::tryFrom($value) ?? throw new \UnexpectedValueException("Value '{$value}' is not part of the enum " . static::class); + $key = self::search($value); + + if ($key === false) { + throw new \UnexpectedValueException("Value '{$value}' is not part of the enum " . static::class); + } + + return self::getInstance($key, $value); } - final public static function tryFrom(int|string $value): ?static + /** + * @param mixed $value + * @return static|null + * @psalm-return static|null + */ + final public static function tryFrom($value): ?self { $key = self::search($value); @@ -97,16 +118,32 @@ final public static function tryFrom(int|string $value): ?static return self::getInstance($key, $value); } - final public function getValue(): int|string + /** + * @psalm-pure + * @return mixed + * @psalm-return T + */ + final public function getValue() { return $this->value; } + /** + * Returns the enum key (i.e. the constant name). + * + * @psalm-pure + * @return string + */ final public function getKey(): string { return $this->key; } + /** + * @psalm-pure + * @psalm-suppress InvalidCast + * @return string + */ final public function __toString(): string { return (string)$this->value; @@ -134,7 +171,7 @@ final public function equals(self $variable): bool * @psalm-pure * @psalm-return list */ - public static function keys(): array + final public static function keys(): array { return \array_keys(self::toArray()); } @@ -146,7 +183,7 @@ public static function keys(): array * @psalm-return array * @return static[] Constant name in key, Enum instance in value */ - public static function values(): array + final public static function values(): array { $values = []; @@ -163,120 +200,49 @@ public static function values(): array * @psalm-pure * @psalm-suppress ImpureStaticProperty * - * @psalm-return array + * @psalm-return array * @return array Constant name in key, constant value in value */ final public static function toArray(): array { - if (!isset(self::$cache[static::class])) { - self::computeCache(); - } - - return self::$cache[static::class]; - } - - /** - * @psalm-pure - * @psalm-suppress ImpureStaticProperty - * - * @psalm-return array - * @return string[] Constant value in key, constant name in value - */ - private static function toReverseArray(): array - { - if (!isset(self::$reverseCache[static::class])) { - self::computeCache(); - } - - return self::$reverseCache[static::class]; - } - - /** - * @psalm-pure - * @psalm-suppress ImpureStaticProperty - * - * @psalm-return 'int'|'string'|'empty' - */ - private static function getType(): string - { - if (!isset(self::$typeCache[static::class])) { - self::computeCache(); - } - - return self::$typeCache[static::class]; - } - - /** - * @psalm-pure - * @psalm-suppress ImpureStaticProperty - * - * Compute cached values for the class using reflection - */ - private static function computeCache(): void - { - /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ - $reflection = new \ReflectionClass(static::class); - /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ - $constantsDefinition = $reflection->getConstants(); - /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ - if (!$reflection->isFinal()) { - throw new \ParseError("Class " . static::class . " is not declared final"); - } - - $type = null; - $reverseConstantsDefinition = []; - /** @psalm-assert array $constantsDefinition */ - foreach ($constantsDefinition as $key => $value) { - if (is_int($value)) { - if (!isset($type)) { - $type = 'int'; - } elseif ($type !== 'int') { - throw new \ParseError("Value for constant '{$key}' in class " . static::class . " is not int, even if previous value is int"); - } - } elseif (is_string($value)) { - if (!isset($type)) { - $type = 'string'; - } elseif ($type !== 'string') { - throw new \ParseError("Value for constant '{$key}' in class " . static::class . " is not string, even if previous value is string"); - } - } else { - throw new \ParseError("Value for constant '{$key}' is not int or string."); - } - - if (\array_key_exists($value, $reverseConstantsDefinition)) { - throw new \ParseError("Value '{$value}' is duplicated in the enum definition of class " . static::class); + $class = static::class; + + if (!isset(self::$cache[$class])) { + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + $reflection = new \ReflectionClass($class); + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + if (!$reflection->isFinal()) { + throw new \ParseError("Class " . $class . " is not declared final"); } - - $reverseConstantsDefinition[$value] = $key; + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + self::$cache[$class] = $reflection->getConstants(); } - /** @psalm-suppress MixedPropertyTypeCoercion assert exists already of array on $constantsDefinition */ - self::$cache[static::class] = $constantsDefinition; - self::$reverseCache[static::class] = $reverseConstantsDefinition; - self::$typeCache[static::class] = $type ?? 'empty'; + return self::$cache[$class]; } /** * Check if is valid enum value * - * @param int|string $value + * @param $value + * @psalm-param mixed $value * @psalm-pure + * @psalm-assert-if-true T $value * @return bool */ - public static function isValid(int|string $value): bool + final public static function isValid($value): bool { - $reverseArray = self::toReverseArray(); - - return isset($reverseArray[$value]); + return \in_array($value, static::toArray(), true); } /** * Asserts valid enum value * * @psalm-pure - * @param int|string $value + * @psalm-assert T $value + * @param mixed $value */ - public static function assertValidValue(int|string $value): void + final public static function assertValidValue($value): void { if (!self::isValid($value)) { throw new \UnexpectedValueException("Value '$value' is not part of the enum " . static::class); @@ -295,7 +261,7 @@ final public static function isValidKey(string $key): bool { $array = self::toArray(); - return isset($array[$key]); + return isset($array[$key]) || \array_key_exists($key, $array); } /** @@ -307,23 +273,9 @@ final public static function isValidKey(string $key): bool * @psalm-pure * @return string|false */ - final public static function search(int|string $value): string|false + final public static function search($value) { - $type = self::getType(); - if ( - ($type === 'int' && !is_int($value)) - || - ($type === 'string' && !is_string($value)) - || - ($type === 'empty') - ) { - return false; - } - - $reverseArray = self::toReverseArray(); - - /** @psalm-suppress MixedArrayOffset this is already validated at this point */ - return $reverseArray[$value] ?? false; + return \array_search($value, static::toArray(), true); } /** @@ -340,7 +292,7 @@ final public static function search(int|string $value): string|false public static function __callStatic($name, $arguments) { $array = self::toArray(); - if (!isset($array[$name])) { + if (!isset($array[$name]) && !\array_key_exists($name, $array)) { $message = "No static method or enum constant '$name' in class " . static::class; throw new \BadMethodCallException($message); } diff --git a/static-analysis/EnumIsPure.php b/static-analysis/EnumIsPure.php index 21400f6..5875fd8 100644 --- a/static-analysis/EnumIsPure.php +++ b/static-analysis/EnumIsPure.php @@ -11,6 +11,8 @@ * @method static PureEnum C() * * @psalm-immutable + * @psalm-template T of 'A'|'B' + * @template-extends Enum */ final class PureEnum extends Enum { diff --git a/tests/EnumFixture.php b/tests/EnumFixture.php new file mode 100644 index 0000000..2b2fdef --- /dev/null +++ b/tests/EnumFixture.php @@ -0,0 +1,39 @@ + + * @author Mirosław Filip + */ +final class EnumFixture extends Enum +{ + const FOO = "foo"; + const BAR = "bar"; + const NUMBER = 42; + + /** + * Values that are known to cause problems when used with soft typing + */ + const PROBLEMATIC_NUMBER = 0; + const PROBLEMATIC_NULL = null; + const PROBLEMATIC_EMPTY_STRING = ''; + const PROBLEMATIC_BOOLEAN_FALSE = false; +} diff --git a/tests/EnumTest.php b/tests/EnumTest.php index b327b28..8031706 100755 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -18,23 +18,14 @@ class EnumTest extends \PHPUnit\Framework\TestCase */ public function testGetValue() { - $value = StringEnumFixture::FOO(); - self::assertEquals(StringEnumFixture::FOO, $value->getValue()); + $value = EnumFixture::FOO(); + self::assertEquals(EnumFixture::FOO, $value->getValue()); - $value = StringEnumFixture::BAR(); - self::assertEquals(StringEnumFixture::BAR, $value->getValue()); + $value = EnumFixture::BAR(); + self::assertEquals(EnumFixture::BAR, $value->getValue()); - $value = StringEnumFixture::EMPTY(); - self::assertEquals(StringEnumFixture::EMPTY, $value->getValue()); - - $value = IntEnumFixture::FIRST(); - self::assertEquals(IntEnumFixture::FIRST, $value->getValue()); - - $value = IntEnumFixture::SECOND(); - self::assertEquals(IntEnumFixture::SECOND, $value->getValue()); - - $value = IntEnumFixture::THIRD(); - self::assertEquals(IntEnumFixture::THIRD, $value->getValue()); + $value = EnumFixture::NUMBER(); + self::assertEquals(EnumFixture::NUMBER, $value->getValue()); } /** @@ -42,58 +33,32 @@ public function testGetValue() */ public function testGetKey() { - $value = StringEnumFixture::FOO(); + $value = EnumFixture::FOO(); self::assertEquals('FOO', $value->getKey()); self::assertNotEquals('BA', $value->getKey()); } /** - * @dataProvider invalidValueProviderForString + * @dataProvider invalidValueProvider * @param mixed $value */ - public function testFailToCreateStringEnumWithInvalidValueThroughNamedConstructor($value): void + public function testFailToCreateEnumWithInvalidValueThroughNamedConstructor($value): void { $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('is not part of the enum MyCLabs\Tests\Enum\StringEnumFixture'); + $this->expectExceptionMessage('is not part of the enum MyCLabs\Tests\Enum\EnumFixture'); - StringEnumFixture::from($value); + EnumFixture::from($value); } /** * Contains values not existing in EnumFixture * @return array */ - public function invalidValueProviderForString() + public function invalidValueProvider() { return array( "string" => array('test'), "int" => array(1234), - "int0" => array(0), - ); - } - - /** - * @dataProvider invalidValueProviderForInt - * @param mixed $value - */ - public function testFailToCreateIntEnumWithInvalidValueThroughNamedConstructor($value): void - { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('is not part of the enum MyCLabs\Tests\Enum\IntEnumFixture'); - - IntEnumFixture::from($value); - } - - /** - * Contains values not existing in EnumFixture - * @return array - */ - public function invalidValueProviderForInt() - { - return array( - "string" => array('test'), - "int" => array(1234), - "emptystring" => array(''), ); } @@ -109,10 +74,9 @@ public function testToString($expected, $enumObject) public function toStringProvider() { return array( - array(StringEnumFixture::FOO, StringEnumFixture::FOO()), - array(StringEnumFixture::BAR, StringEnumFixture::BAR()), - array((string) IntEnumFixture::SECOND, IntEnumFixture::SECOND()), - array((string) IntEnumFixture::FIRST, IntEnumFixture::FIRST()), + array(EnumFixture::FOO, EnumFixture::FOO()), + array(EnumFixture::BAR, EnumFixture::BAR()), + array((string) EnumFixture::NUMBER, EnumFixture::NUMBER()), ); } @@ -121,26 +85,15 @@ public function toStringProvider() */ public function testStringKeys() { - $values = StringEnumFixture::keys(); + $values = EnumFixture::keys(); $expectedValues = array( "FOO", "BAR", - "EMPTY", - ); - - self::assertSame($expectedValues, $values); - } - - /** - * keys() - */ - public function testIntKeys() - { - $values = IntEnumFixture::keys(); - $expectedValues = array( - "FIRST", - "SECOND", - "THIRD", + "NUMBER", + "PROBLEMATIC_NUMBER", + "PROBLEMATIC_NULL", + "PROBLEMATIC_EMPTY_STRING", + "PROBLEMATIC_BOOLEAN_FALSE", ); self::assertSame($expectedValues, $values); @@ -151,26 +104,15 @@ public function testIntKeys() */ public function testStringValues() { - $values = StringEnumFixture::values(); + $values = EnumFixture::values(); $expectedValues = array( - "FOO" => StringEnumFixture::FOO(), - "BAR" => StringEnumFixture::BAR(), - "EMPTY" => StringEnumFixture::EMPTY(), - ); - - self::assertEquals($expectedValues, $values); - } - - /** - * values() - */ - public function testIntValues() - { - $values = IntEnumFixture::values(); - $expectedValues = array( - "FIRST" => IntEnumFixture::FIRST(), - "SECOND" => IntEnumFixture::SECOND(), - "THIRD" => IntEnumFixture::THIRD(), + "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(), ); self::assertEquals($expectedValues, $values); @@ -181,26 +123,15 @@ public function testIntValues() */ public function testStringEnumToArray() { - $values = StringEnumFixture::toArray(); + $values = EnumFixture::toArray(); $expectedValues = array( - "FOO" => StringEnumFixture::FOO, - "BAR" => StringEnumFixture::BAR, - "EMPTY" => StringEnumFixture::EMPTY, - ); - - self::assertSame($expectedValues, $values); - } - - /** - * toArray() - */ - public function testIntEnumToArray() - { - $values = IntEnumFixture::toArray(); - $expectedValues = array( - "FIRST" => IntEnumFixture::FIRST, - "SECOND" => IntEnumFixture::SECOND, - "THIRD" => IntEnumFixture::THIRD, + "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, ); self::assertSame($expectedValues, $values); @@ -211,75 +142,45 @@ public function testIntEnumToArray() */ public function testStaticAccess() { - self::assertEquals(StringEnumFixture::from(StringEnumFixture::FOO), StringEnumFixture::FOO()); - self::assertEquals(StringEnumFixture::from(StringEnumFixture::BAR), StringEnumFixture::BAR()); - self::assertEquals(StringEnumFixture::from(StringEnumFixture::EMPTY), StringEnumFixture::EMPTY()); - self::assertNotSame(StringEnumFixture::FOO(), StringEnumFixture::FOO()); - - self::assertEquals(IntEnumFixture::from(IntEnumFixture::FIRST), IntEnumFixture::FIRST()); - self::assertEquals(IntEnumFixture::from(IntEnumFixture::SECOND), IntEnumFixture::SECOND()); - self::assertEquals(IntEnumFixture::from(IntEnumFixture::THIRD), IntEnumFixture::THIRD()); - self::assertNotSame(IntEnumFixture::FIRST(), IntEnumFixture::FIRST()); - + $this->assertEquals(EnumFixture::from(EnumFixture::FOO), EnumFixture::FOO()); + $this->assertEquals(EnumFixture::from(EnumFixture::BAR), EnumFixture::BAR()); + $this->assertEquals(EnumFixture::from(EnumFixture::NUMBER), EnumFixture::NUMBER()); + $this->assertNotSame(EnumFixture::NUMBER(), EnumFixture::NUMBER()); } public function testBadStaticAccess() { $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('No static method or enum constant \'UNKNOWN\' in class ' . StringEnumFixture::class); + $this->expectExceptionMessage('No static method or enum constant \'UNKNOWN\' in class ' . EnumFixture::class); - StringEnumFixture::UNKNOWN(); + EnumFixture::UNKNOWN(); } /** * isValid() - * @dataProvider isValidStringEnumProvider + * @dataProvider isValidProvider */ - public function testIsValidStringEnum($value, $isValid) + public function testIsValid($value, $isValid) { - self::assertSame($isValid, StringEnumFixture::isValid($value)); + self::assertSame($isValid, EnumFixture::isValid($value)); } - public function isValidStringEnumProvider() + public function isValidProvider() { return [ /** * Valid values */ ['foo', true], - ['', true], - /** - * Invalid values - */ - ['baz', false], - [0, false], - [1, false], - ]; - } - - /** - * isValid() - * @dataProvider isValidIntEnumProvider - */ - public function testIsValidIntEnum($value, $isValid) - { - self::assertSame($isValid, IntEnumFixture::isValid($value)); - } - - public function isValidIntEnumProvider() - { - return [ - /** - * Valid values - */ + [42, true], + [null, true], [0, true], - [1, true], + ['', true], + [false, true], /** * Invalid values */ - ['baz', false], - [42, false], - ['', false], + ['baz', false] ]; } @@ -288,72 +189,61 @@ public function isValidIntEnumProvider() */ public function testIsValidKey() { - self::assertTrue(StringEnumFixture::isValidKey('FOO')); - self::assertFalse(StringEnumFixture::isValidKey('BAZ')); - - self::assertTrue(IntEnumFixture::isValidKey('FIRST')); - self::assertFalse(IntEnumFixture::isValidKey('FOURTH')); + self::assertTrue(EnumFixture::isValidKey('FOO')); + self::assertFalse(EnumFixture::isValidKey('BAZ')); + self::assertTrue(EnumFixture::isValidKey('PROBLEMATIC_NULL')); } /** * search() - * @dataProvider searchStringEnumProvider + * @see https://github.com/myclabs/php-enum/issues/13 + * @dataProvider searchProvider */ - public function testStringEnumSearch($value, $expected) + public function testSearch($value, $expected) { - self::assertSame($expected, StringEnumFixture::search($value)); + self::assertSame($expected, EnumFixture::search($value)); } - public function searchStringEnumProvider() + public function searchProvider() { return array( array('foo', 'FOO'), - array('', 'EMPTY'), + array(0, 'PROBLEMATIC_NUMBER'), + array(null, 'PROBLEMATIC_NULL'), + array('', 'PROBLEMATIC_EMPTY_STRING'), + array(false, 'PROBLEMATIC_BOOLEAN_FALSE'), array('bar I do not exist', false), - array(0, false), - array(42, false), + array(array(), false), ); } /** - * search() - * @dataProvider searchIntEnumProvider + * equals() */ - public function testIntEnumSearch($value, $expected) + public function testEquals() { - self::assertSame($expected, IntEnumFixture::search($value)); - } + $foo = EnumFixture::from(EnumFixture::FOO); + $number = EnumFixture::from(EnumFixture::NUMBER); + $anotherFoo = EnumFixture::from(EnumFixture::FOO); - public function searchIntEnumProvider() - { - return array( - array(0, 'FIRST'), - array(1, 'SECOND'), - array(42, false), - array('', false), - ); + self::assertTrue($foo->equals($foo)); + self::assertFalse($foo->equals($number)); + self::assertTrue($foo->equals($anotherFoo)); } /** * equals() */ - public function testEquals() + public function testEqualsComparesProblematicValuesProperly() { - $foo = StringEnumFixture::from(StringEnumFixture::FOO); - $anotherFoo = StringEnumFixture::from(StringEnumFixture::FOO); - $empty = StringEnumFixture::from(StringEnumFixture::EMPTY); - $first = IntEnumFixture::from(IntEnumFixture::FIRST); - $second = IntEnumFixture::from(IntEnumFixture::SECOND); - $secondTwice = IntEnumFixture::from(IntEnumFixture::SECOND); + $false = EnumFixture::from(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE); + $emptyString = EnumFixture::from(EnumFixture::PROBLEMATIC_EMPTY_STRING); + $null = EnumFixture::from(EnumFixture::PROBLEMATIC_NULL); - self::assertTrue($foo->equals($foo)); - self::assertFalse($foo->equals($empty)); - self::assertTrue($foo->equals($anotherFoo)); - self::assertFalse($foo->equals($first)); - self::assertFalse($empty->equals($first)); - self::assertFalse($first->equals($empty)); - self::assertFalse($first->equals($second)); - self::assertTrue($second->equals($secondTwice)); + self::assertTrue($false->equals($false)); + self::assertFalse($false->equals($emptyString)); + self::assertFalse($emptyString->equals($null)); + self::assertFalse($null->equals($false)); } /** @@ -361,7 +251,7 @@ public function testEquals() */ public function testEqualsConflictValues() { - self::assertFalse(StringEnumFixture::FOO()->equals(EnumConflict::FOO())); + self::assertFalse(EnumFixture::FOO()->equals(EnumConflict::FOO())); } /** @@ -369,12 +259,20 @@ public function testEqualsConflictValues() */ public function testJsonSerialize() { - self::assertJsonEqualsJson('"foo"', json_encode(StringEnumFixture::from(StringEnumFixture::FOO))); - self::assertJsonEqualsJson('"bar"', json_encode(StringEnumFixture::from(StringEnumFixture::BAR))); - self::assertJsonEqualsJson('""', json_encode(StringEnumFixture::from(StringEnumFixture::EMPTY))); - self::assertJsonEqualsJson('0', json_encode(IntEnumFixture::from(IntEnumFixture::FIRST))); - self::assertJsonEqualsJson('1', json_encode(IntEnumFixture::from(IntEnumFixture::SECOND))); - self::assertJsonEqualsJson('2', json_encode(IntEnumFixture::from(IntEnumFixture::THIRD))); + self::assertJsonEqualsJson('"foo"', json_encode(EnumFixture::from(EnumFixture::FOO))); + self::assertJsonEqualsJson('"bar"', json_encode(EnumFixture::from(EnumFixture::BAR))); + self::assertJsonEqualsJson('42', json_encode(EnumFixture::from(EnumFixture::NUMBER))); + self::assertJsonEqualsJson('0', json_encode(EnumFixture::from(EnumFixture::PROBLEMATIC_NUMBER))); + self::assertJsonEqualsJson('null', json_encode(EnumFixture::from(EnumFixture::PROBLEMATIC_NULL))); + self::assertJsonEqualsJson('""', json_encode(EnumFixture::from(EnumFixture::PROBLEMATIC_EMPTY_STRING))); + self::assertJsonEqualsJson('false', json_encode(EnumFixture::from(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE))); + } + + public function testNullableEnum() + { + self::assertNull(EnumFixture::PROBLEMATIC_NULL()->getValue()); + self::assertNull((EnumFixture::from(EnumFixture::PROBLEMATIC_NULL))->getValue()); + self::assertNull((EnumFixture::from(EnumFixture::PROBLEMATIC_NULL))->jsonSerialize()); } private static function assertJsonEqualsJson(string $json1, string $json2): void @@ -385,26 +283,26 @@ private static function assertJsonEqualsJson(string $json1, string $json2): void public function testSerialize() { // split string for Pretty CI: "Line exceeds 120 characters" - $bin = '4f3a33363a224d79434c6162735c54657374735c456e756d5c537472696e67456e756d4669787'. + $bin = '4f3a33303a224d79434c6162735c54657374735c456e756d5c456e756d4669787'. '4757265223a323a7b733a32343a22004d79434c6162735c456e756d5c456e756d0076616c7565223b733a333a22666f6f223b73'. '3a32323a22004d79434c6162735c456e756d5c456e756d006b6579223b733a333a22464f4f223b7d'; - self::assertEquals($bin, bin2hex(serialize(StringEnumFixture::FOO()))); + self::assertEquals($bin, bin2hex(serialize(EnumFixture::FOO()))); } public function testUnserialize() { // split string for Pretty CI: "Line exceeds 120 characters" - $bin = '4f3a33363a224d79434c6162735c54657374735c456e756d5c537472696e67456e756d4669787'. + $bin = '4f3a33303a224d79434c6162735c54657374735c456e756d5c456e756d4669787'. '4757265223a323a7b733a32343a22004d79434c6162735c456e756d5c456e756d0076616c7565223b733a333a22666f6f223b73'. '3a32323a22004d79434c6162735c456e756d5c456e756d006b6579223b733a333a22464f4f223b7d'; - /* @var $value StringEnumFixture */ + /* @var $value EnumFixture */ $value = unserialize(pack('H*', $bin)); - self::assertEquals(StringEnumFixture::FOO, $value->getValue()); - self::assertTrue(StringEnumFixture::FOO()->equals($value)); - self::assertTrue(StringEnumFixture::FOO() == $value); + self::assertEquals(EnumFixture::FOO, $value->getValue()); + self::assertTrue(EnumFixture::FOO()->equals($value)); + self::assertTrue(EnumFixture::FOO() == $value); } /** @@ -418,32 +316,17 @@ public function testEnumMustBeFinal() } /** - * @dataProvider isValidStringEnumProvider + * @dataProvider isValidProvider */ public function testAssertValidValueStringEnum($value, $isValid): void { if (!$isValid) { $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage("Value '$value' is not part of the enum " . StringEnumFixture::class); - } - - StringEnumFixture::assertValidValue($value); - - self::assertTrue(StringEnumFixture::isValid($value)); - } - - /** - * @dataProvider isValidIntEnumProvider - */ - public function testAssertValidValueIntEnum($value, $isValid): void - { - if (!$isValid) { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage("Value '$value' is not part of the enum " . IntEnumFixture::class); + $this->expectExceptionMessage("Value '$value' is not part of the enum " . EnumFixture::class); } - IntEnumFixture::assertValidValue($value); + EnumFixture::assertValidValue($value); - self::assertTrue(IntEnumFixture::isValid($value)); + self::assertTrue(EnumFixture::isValid($value)); } } diff --git a/tests/IntEnumFixture.php b/tests/IntEnumFixture.php deleted file mode 100644 index dcbf6a6..0000000 --- a/tests/IntEnumFixture.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @author Mirosław Filip - */ -final class IntEnumFixture extends Enum -{ - const FIRST = 0; - const SECOND = 1; - const THIRD = 2; -} diff --git a/tests/NotFinalEnumFixture.php b/tests/NotFinalEnumFixture.php index 5652407..68270ec 100644 --- a/tests/NotFinalEnumFixture.php +++ b/tests/NotFinalEnumFixture.php @@ -1,6 +1,5 @@ - * @author Mirosław Filip - */ -final class StringEnumFixture extends Enum -{ - const FOO = "foo"; - const BAR = "bar"; - const EMPTY = ""; -} From 001c5af9176b529c03c580a04d6492d8c5e1c723 Mon Sep 17 00:00:00 2001 From: Alexandru Patranescu Date: Mon, 1 Mar 2021 18:58:50 +0200 Subject: [PATCH 3/3] update documentation, removing reference to construct usage --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 1e4d1ff..1a2e00c 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,6 @@ $action = Action::VIEW(); $action = Action::$key(); // or with a dynamic value: $action = Action::from($value); -// or -$action = new Action($value); ``` As you can see, static methods are automatically implemented to provide quick access to an enum value. @@ -67,7 +65,6 @@ function setAction(Action $action) { ## 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 @@ -111,7 +108,7 @@ final class Action extends Enum * @return Action */ public static function VIEW() { - return new Action(self::VIEW); + return self::from(self::VIEW); } } ```