diff --git a/CHANGES.md b/CHANGES.md index 024e23d..65d6886 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,16 @@ # Changes History +2.0.2 +----- +Fix the following logic bug: wrong strict comparison of +the identical enum instances. + +2.0.0 +----- +Replace old Enum class by new Java-style Enum class. + +Update predefined enums: Gender, PagingType, PaymentType. + 1.0.9 ----- Add: diff --git a/README.md b/README.md index f4f7a30..4c0bb16 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,91 @@ Designed to be container for repeatedly used set of constants. **Example**: ```php -class Gender extends Saritasa\Enum +class LogicOperations extends Saritasa\Enum { - const MALE = 'Male'; - const FEMALE = 'Female'; + protected const AND = null; + protected const OR = null; + protected const XOR = null; } ``` -then somewere in code: +then somewhere in code: ```php -$allGenders = Gender::getConstants(); -$gender = new Gender($stringValue); // Will throw UnexpectedValueException on unknown value; -function getGenderDependentValue(Gender $gender) { ... } +$operations = LogicOperations::getConstantNames(); // returns ['AND', 'OR', 'XOR'] +... +function getLogicOperationDependentValue(LogicOperations $op) +{ + if ($op === LogicOperations::OR()) { ... } + ... + switch ($op) { + case LogicOperations::AND(): + .... + break; + case LogicOperations::OR(): + ... + break; + case LogicOperations::XOR(): + ... + break; + default: + ... + } +} +... +$xor = LogicOperations::XOR(); +echo getLogicOperationDependentValue($xor); +... +echo $xor; // will display XOR +echo json_encode($xor); // will display "XOR" +... +$foo = LogicOperations::FOO(); // will throw InvalidEnumValueException on unknown value +... +if ($xor == 'XOR') {} // the condition is TRUE because of Enum::toString() ``` + +The enum class body can include methods and other fields (Java style): +```php +class TeamGender extends Saritasa\Enum +{ + protected const MEN = ['Men', false]; + protected const WOMEN = ['Women', false]; + protected const MIXED = ['Co-ed', true]; + + private $name = ''; + private $mixed = false; + + protected function __construct(string $name, bool $mixed) + { + $this->name = $name; + $this->mixed = $mixed; + } + + public function getName(): string + { + return $this->name; + } + + public function getIsMixed(): bool + { + return $this->mixed; + } +} +``` +then somewhere in code: +```php +$genders = Gender::getConstants(); +// returns ['MEN' => ['Men', false], 'WOMEN' => ['Women', false], 'MIXED' => ['Co-ed', true]] +... +echo TeamGender::MEN(); // will display MEN +echo TeamGender::WOMEN('name'); // will display Women +echo TeamGender::WOMEN()->getName(); // will display Women +echo (int)TeamGender::MIXED('isMixed'); // will display 1 +echo (int)TeamGender::MIXED()->getIsMixed(); // will display 1 +``` + +**Note:** It's recommended to make enum constants protected or private (because each enum value +is actually an object and public constants break the encapsulation). However, constant access +modifiers are only available since PHP 7.1 + ### Dto A simple DTO, that can convert associative array to strong typed class with fields and back: diff --git a/src/Enum.php b/src/Enum.php index 0398e12..9b4c918 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -2,7 +2,6 @@ namespace Saritasa; -use ReflectionClass; use Saritasa\Exceptions\InvalidEnumValueException; /** @@ -14,107 +13,181 @@ */ abstract class Enum implements \JsonSerializable { - private static $constCacheArray = null; + /** + * The constants' cache. + * + * @var array + */ + private static $constants = []; + + /** + * The enum instances. + * + * @var array + */ + private static $instances = []; - private $value; + /** + * The name of an enum constant associated with the given enum instance. + * + * @var string + */ + protected $constant = ''; /** - * Enum implementation for PHP, alternative to \SplEnum. + * Returns the class's constants. * - * @param mixed $value String representation of enum value (must be valid enum value or exception will be thrown) + * @return array + * @throws \ReflectionException */ - public function __construct($value) + final public static function getConstants(): array { - if (!static::isValidValue($value)) { - throw new \UnexpectedValueException('Value not a const in enum ' . get_class($this)); + $class = static::class; + if (isset(self::$constants[$class])) { + return self::$constants[$class]; } + return self::$constants[$class] = (new \ReflectionClass($class))->getConstants(); + } - $this->value = $value; + /** + * Returns the available constant names. + * + * @return array + * @throws \ReflectionException + */ + final public static function getConstantNames(): array + { + return array_keys(self::getConstants()); } /** - * Returns scalar value of this enum. + * Checks if the given constant name is in the enum type. * - * @return mixed + * @param string $name + * @return bool + * @throws \ReflectionException */ - public function getValue() + public static function isValidConstantName($name): bool { - return $this->value; + return array_key_exists($name, self::getConstants()); } /** - * Compares given enum value to another value + * Checks if the given value is in the enum type. * - * @param Enum|mixed $value A scalar value or another Enum to compare with - * @return boolean true if values are equal, false otherwise + * @param mixed $value + * @param bool $strict Determines whether to search for identical elements. + * @return bool + * @throws \ReflectionException */ - public function equalsTo($value) + public static function isValidConstantValue($value, $strict = false): bool { - if (is_object($value) && $value instanceof Enum) { - return $value->getValue() == $this->value; - } - return $value == $this->value; + return in_array($value, self::getConstants(), $strict); } /** - * An array of all constants in this enum (keys are constant names). + * Validates the constant name. * - * @return array + * @param string $name The constant name. + * @return void + * @throws InvalidEnumValueException + * @throws \ReflectionException */ - public static function getConstants() : array + public static function validate($name) { - if (self::$constCacheArray == null) { - self::$constCacheArray = []; + if (!static::isValidConstantName($name)) { + throw new InvalidEnumValueException($name, array_keys(self::getConstants())); } - $calledClass = get_called_class(); - if (!array_key_exists($calledClass, self::$constCacheArray)) { - $reflect = new ReflectionClass($calledClass); - self::$constCacheArray[$calledClass] = $reflect->getConstants(); - } - return self::$constCacheArray[$calledClass]; } /** - * Checks if given value is valid for this enum class. + * Returns value by constant name. * - * @param mixed $value A value to check - * @param bool $strict If strict comparison should be used or not - * @return boolean True of value is valid, false otherwise + * @param string $name The constant name. + * @return mixed + * @throws InvalidEnumValueException + * @throws \ReflectionException */ - public static function isValidValue($value, $strict = true) : bool + public static function getConstantValue($name) { - $values = array_values(self::getConstants()); - return in_array($value, $values, $strict); + static::validate($name); + return self::getConstants()[$name]; } /** - * Returns validated value for this enum class or throws exception if not. + * Creates an enum instance that associated with the given enum constant name. * - * @param mixed $value value to be checked - * @param bool $strict If strict comparison should be used or not - * @return mixed validated value + * @param string $name The constant name. + * @param array $arguments + * @return mixed * @throws InvalidEnumValueException + * @throws \BadMethodCallException + * @throws \ReflectionException */ - public static function validate($value, $strict = true) + final public static function __callStatic(string $name, array $arguments) { - if (static::isValidValue($value, $strict) === false) { - throw new InvalidEnumValueException(static::getConstants()); + $value = static::getConstantValue($name); + $value = is_array($value) ? $value : [$value]; + $instance = self::getInstance($name, $value); + $instance->constant = $name; + if ($arguments) { + $method = 'get' . ucfirst(reset($arguments)); + if (method_exists($instance, $method)) { + return $instance->{$method}(...$arguments); + } + throw new \BadMethodCallException("Method $method does not exist."); } - return $value; + return $instance; } /** - * Converts value to a string + * Returns the name of the constant that associated with the current enum instance. * * @return string */ - public function __toString() + public function getConstantName(): string { - return (string)$this->value; + return $this->constant; } - public function jsonSerialize() + /** + * Converts the enum instance to a string. + * + * @return string + */ + public function __toString(): string { - return $this->__toString(); + return $this->getConstantName(); } + + /** + * Returns data which should be serialized to JSON. + * + * @return string + */ + public function jsonSerialize(): string + { + return $this->getConstantName(); + } + + /** + * Creates the enum instance. + * + * @param string $constant + * @param array $value + * @return static + */ + private static function getInstance(string $constant, array $value) + { + $class = static::class; + if (isset(self::$instances[$class][$constant])) { + return self::$instances[$class][$constant]; + } + return self::$instances[$class][$constant] = new $class(...$value); + } + + /** + * Forbids the implicit creation of enum instances without own constructors. + */ + private function __construct() {} } diff --git a/src/Enums/Gender.php b/src/Enums/Gender.php index cb558d4..e649874 100644 --- a/src/Enums/Gender.php +++ b/src/Enums/Gender.php @@ -2,13 +2,13 @@ namespace Saritasa\Enums; -use Saritasa\Enum; +use Saritasa\NamedEnum; /** * Human gender - Male or Female */ -class Gender extends Enum +class Gender extends NamedEnum { const MALE = 'Male'; const FEMALE = 'Female'; -} +} \ No newline at end of file diff --git a/src/Enums/PagingType.php b/src/Enums/PagingType.php index fcd6bae..2c01aa3 100644 --- a/src/Enums/PagingType.php +++ b/src/Enums/PagingType.php @@ -6,7 +6,7 @@ class PagingType extends Enum { - const NONE = 'NONE'; - const PAGINATOR = 'PAGINATOR'; - const CURSOR = 'CURSOR'; + const NONE = null; + const PAGINATOR = null; + const CURSOR = null; } diff --git a/src/Enums/PaymentType.php b/src/Enums/PaymentType.php index 4a93994..42b1e22 100644 --- a/src/Enums/PaymentType.php +++ b/src/Enums/PaymentType.php @@ -2,12 +2,12 @@ namespace Saritasa\Enums; -use Saritasa\Enum; +use Saritasa\NamedEnum; /** * How user pays money */ -class PaymentType extends Enum +class PaymentType extends NamedEnum { const ANDROID_PAY = 'Android Pay'; const APPLE_PAY = 'Apple Pay'; diff --git a/src/Exceptions/InvalidEnumValueException.php b/src/Exceptions/InvalidEnumValueException.php index ba1ac2b..67e5ace 100644 --- a/src/Exceptions/InvalidEnumValueException.php +++ b/src/Exceptions/InvalidEnumValueException.php @@ -9,31 +9,42 @@ */ class InvalidEnumValueException extends InvalidArgumentException { - /* @var array $possibleValues possible values */ - protected $possibleValues; + /** + * The valid enum constants. + * + * @var array $validConstants + */ + protected $validConstants; /** * Thrown, if the argument passed to the function does not match any of the possible values. * - * @param array $possibleValues possible values - * @param string|null $message exception message - * @param int $code http code - * @param Throwable|null $previous previous thrown exception + * @param string $invalidConstant The invalid constant name. + * @param array $validConstants The valid enum constants. + * @param string|null $message The exception message. + * @param int $code The exception code. + * @param Throwable|null $previous The previously thrown exception. */ - public function __construct(array $possibleValues, string $message = null, int $code = 0, Throwable $previous = null) + public function __construct( + string $invalidConstant, + array $validConstants, + string $message = null, + int $code = 0, + Throwable $previous = null) { - $this->possibleValues = $possibleValues; - $message = $message ?? 'Value must be from the list: ' . implode(', ', $possibleValues); + $this->validConstants = $validConstants; + $message = $message ?? "Constant \"$invalidConstant\" does not exist. Valid values are " . + implode(', ', $validConstants); parent::__construct($message, $code, $previous); } /** - * Returns list of possible values. + * Returns the list of the valid enum constants. * * @return array */ - public function getPossibleValues(): array + public function getValidConstants(): array { - return $this->possibleValues; + return $this->validConstants; } } \ No newline at end of file diff --git a/src/NamedEnum.php b/src/NamedEnum.php new file mode 100644 index 0000000..20de09c --- /dev/null +++ b/src/NamedEnum.php @@ -0,0 +1,38 @@ +name = $name; + } + + /** + * Returns the name that associated with the current enum value. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } +} \ No newline at end of file diff --git a/src/ValuedEnum.php b/src/ValuedEnum.php new file mode 100644 index 0000000..65b6256 --- /dev/null +++ b/src/ValuedEnum.php @@ -0,0 +1,42 @@ +value = $value; + } + + /** + * Returns the value that associated with the current enum value. + * + * @return int|float + */ + public function getValue() + { + return $this->value; + } +} \ No newline at end of file diff --git a/tests/EnumTest.php b/tests/EnumTest.php index a02bf94..166da3a 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -4,49 +4,146 @@ use PHPUnit\Framework\TestCase; use Saritasa\Enum; +use Saritasa\Exceptions\InvalidEnumValueException; class EnumTest extends TestCase { - public function testEnumValueParse() + public function testEnumConstructor() { - static::assertTrue(TestEnum::isValidValue('const1')); - static::assertFalse(TestEnum::isValidValue('const3')); - $val1 = new TestEnum('const1'); - - static::assertTrue($val1->equalsTo(TestEnum::CONST1)); - static::assertTrue($val1->equalsTo(new TestEnum('const1'))); - static::assertFalse($val1->equalsTo(TestEnum::CONST2)); - static::assertFalse($val1->equalsTo(new TestEnum('const2'))); + static::assertFalse((new \ReflectionClass(TestEnum1::class))->isInstantiable()); + static::assertFalse((new \ReflectionClass(TestEnum2::class))->isInstantiable()); + static::assertFalse((new \ReflectionClass(TestEnum3::class))->isInstantiable()); } public function testGetConstants() { - $values = TestEnum::getConstants(); - static::assertEquals(2, count($values)); - static::assertContains('const1', $values); - static::assertContains('const2', $values); + static::assertEquals([ + 'CONST1' => null, + 'CONST2' => null + ], TestEnum1::getConstants()); + + static::assertEquals([ + 'CONST1' => null, + 'CONST2' => null + ], TestEnum2::getConstants()); + + static::assertEquals([ + 'CONST1' => [1, 'a', 'Text #1'], + 'CONST2' => [2, 'b', 'Text #2'], + 'CONST3' => [3, 'c', 'Text #3'] + ], TestEnum3::getConstants()); + } + + public function testEnumValueParse() + { + static::assertInstanceOf(TestEnum1::class, TestEnum1::CONST1()); + static::assertInstanceOf(TestEnum1::class, TestEnum1::CONST2()); + static::assertInstanceOf(TestEnum2::class, TestEnum2::CONST2()); + static::assertInstanceOf(TestEnum3::class, TestEnum3::CONST3()); + } + + public function testEnumComparison() + { + static::assertEquals(TestEnum1::CONST1(), TestEnum1::CONST1()); + static::assertEquals(TestEnum3::CONST3(), TestEnum3::CONST3()); + static::assertEquals(TestEnum3::CONST1(), 'CONST1'); + + static::assertNotEquals(TestEnum1::CONST1(), TestEnum2::CONST1()); + static::assertNotEquals(TestEnum1::CONST1(), TestEnum3::CONST1()); + static::assertNotEquals(TestEnum3::CONST1(), TestEnum2::CONST2()); + + static::assertNotSame(TestEnum3::CONST1(), 'CONST1'); + static::assertNotSame(TestEnum1::CONST1(), TestEnum3::CONST1()); + static::assertSame(TestEnum2::CONST2(), TestEnum2::CONST2()); } public function testInvalidValue() { - static::expectException(\UnexpectedValueException::class); - new TestEnum('const3'); + static::expectException(InvalidEnumValueException::class); + TestEnum1::CONST3(); + } + + public function testMethodShortcuts() + { + static::assertEquals('CONST2', TestEnum1::CONST2('constantName')); + static::assertEquals('CONST2', TestEnum2::CONST2('constantName')); + + static::assertEquals(2, TestEnum3::CONST2('id')); + static::assertEquals('a', TestEnum3::CONST1('key')); + static::assertEquals('Text #3', TestEnum3::CONST3('text')); + } + + public function testInvalidMethodShortcut() + { + static::expectException(\BadMethodCallException::class); + TestEnum1::CONST1('id'); } public function testToString() { - $val1 = new TestEnum('const1'); - static::assertEquals(TestEnum::CONST1, strval($val1)); + static::assertEquals('CONST1', (string)TestEnum1::CONST1()); + static::assertEquals('CONST2', (string)TestEnum2::CONST2()); + static::assertEquals('CONST3', (string)TestEnum3::CONST3()); } public function testJsonSerialize() { - $val1 = new TestEnum('const1'); - static::assertEquals(TestEnum::CONST1, strval($val1)); - static::assertEquals(TestEnum::CONST1, $val1->jsonSerialize()); + static::assertEquals('"CONST1"', json_encode(TestEnum1::CONST1())); + static::assertEquals('"CONST2"', json_encode(TestEnum2::CONST2())); + static::assertEquals('"CONST3"', json_encode(TestEnum3::CONST3())); } } -class TestEnum extends Enum { - const CONST1 = 'const1'; - const CONST2 = 'const2'; +/** + * @method static TestEnum1 CONST1(...$params) + * @method static TestEnum1 CONST2(...$params) + */ +class TestEnum1 extends Enum { + const CONST1 = null; + const CONST2 = null; +} + +/** + * @method static TestEnum1 CONST1(...$params) + * @method static TestEnum1 CONST2(...$params) + */ +class TestEnum2 extends Enum { + const CONST1 = null; + const CONST2 = null; +} + +/** + * @method static TestEnum1 CONST1(...$params) + * @method static TestEnum1 CONST2(...$params) + * @method static TestEnum1 CONST3(...$params) + */ +class TestEnum3 extends Enum { + const CONST1 = [1, 'a', 'Text #1']; + const CONST2 = [2, 'b', 'Text #2']; + const CONST3 = [3, 'c', 'Text #3']; + + private $id = 0; + private $key = ''; + private $text = ''; + + protected function __construct(int $id, string $key, string $text) + { + $this->id = $id; + $this->key = $key; + $this->text = $text; + } + + public function getId() + { + return $this->id; + } + + public function getKey() + { + return $this->key; + } + + public function getText() + { + return $this->text; + } } \ No newline at end of file