diff --git a/.gitattributes b/.gitattributes index 50b8b8a..2140930 100755 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,10 @@ # Auto detect text files and perform LF normalization * text=auto +.gitattributes export-ignore +.github export-ignore +.gitignore export-ignore tests/ export-ignore phpunit.xml export-ignore -.travis.yml export-ignore +psalm.xml export-ignore +static-analysis/ export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9b8c2d6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: mnapoli # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: "packagist/myclabs/php-enum" +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..7863acf --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,46 @@ +name: "CI" + +on: + pull_request: + push: + branches: + - "master" + +jobs: + phpunit: + name: "PHPUnit" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: + - "7.3" + - "7.4" + - "8.0" + - "8.1" + - "8.2" + dependencies: + - "highest" + include: + - dependencies: "lowest" + php-version: "7.3" + + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + with: + fetch-depth: 2 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + ini-values: "zend.assertions=1" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + with: + dependency-versions: "${{ matrix.dependencies }}" + + - name: "Run PHPUnit" + run: "vendor/bin/phpunit" diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml new file mode 100644 index 0000000..017d4a6 --- /dev/null +++ b/.github/workflows/static-analysis.yaml @@ -0,0 +1,31 @@ +name: "Static Analysis" + +on: + pull_request: + push: + branches: + - "master" + +jobs: + static-analysis-psalm: + name: "Static Analysis with Psalm" + runs-on: "ubuntu-20.04" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Psalm + uses: docker://vimeo/psalm-github-actions:4.9.3 + with: + args: --shepherd + composer_ignore_platform_reqs: true + composer_require_dev: true + security_analysis: true + report_file: results.sarif + env: + CHECK_PLATFORM_REQUIREMENTS: "false" + - name: Upload Security Analysis results to GitHub + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: results.sarif diff --git a/.travis.yml b/.travis.yml deleted file mode 100755 index 451832b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: php - -php: - - 5.3 - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - nightly - - hhvm - -matrix: - allow_failures: - - php: hhvm - -before_script: - - composer install --no-interaction - -script: - - vendor/bin/phpunit --coverage-clover=coverage.clover - - vendor/bin/phpcs --standard=PSR2 ./src/ - -after_script: - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover coverage.clover - -# Use Travis' new container-based infrastructure. -# See http://docs.travis-ci.com/user/migrating-from-legacy/#How-can-I-use-container-based-infrastructure%3F -sudo: false - diff --git a/LICENSE b/LICENSE index accd5a0..2a8cf22 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -php-enum - PHP Enum implementation http://github.com/myclabs/php-enum +The MIT License (MIT) -Copyright (C) 2015 My C-Labs +Copyright (c) 2015 My C-Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, diff --git a/README.md b/README.md index d0c3001..2bf98cd 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ # PHP Enum implementation inspired from SplEnum -[![Build Status](https://travis-ci.org/myclabs/php-enum.png?branch=master)](https://travis-ci.org/myclabs/php-enum) -[![Latest Stable Version](https://poser.pugx.org/myclabs/php-enum/version.png)](https://packagist.org/packages/myclabs/php-enum) -[![Total Downloads](https://poser.pugx.org/myclabs/php-enum/downloads.png)](https://packagist.org/packages/myclabs/php-enum) +[![GitHub Actions][GA Image]][GA Link] +[![Latest Stable Version](https://poser.pugx.org/myclabs/php-enum/version.svg)](https://packagist.org/packages/myclabs/php-enum) +[![Total Downloads](https://poser.pugx.org/myclabs/php-enum/downloads.svg)](https://packagist.org/packages/myclabs/php-enum) +[![Psalm Shepherd][Shepherd Image]][Shepherd Link] + +Maintenance for this project is [supported via Tidelift](https://tidelift.com/subscription/pkg/packagist-myclabs-php-enum?utm_source=packagist-myclabs-php-enum&utm_medium=referral&utm_campaign=readme). ## Why? -First, and mainly, `SplEnum` is not integrated to PHP, you have to install it separately. +First, and mainly, `SplEnum` is not integrated to PHP, you have to install the extension separately. Using an enum instead of class constants provides the following advantages: -- You can type-hint: `function setAction(Action $action) {` +- You can use an enum as a parameter type: `function setAction(Action $action) {` +- You can use an enum as a return type: `function getAction() : Action {` - You can enrich the enum with methods (e.g. `format`, `parse`, …) - You can extend the enum to add new values (make your enum `final` to prevent it) - You can get a list of all the possible values (see below) @@ -30,27 +34,32 @@ use MyCLabs\Enum\Enum; /** * Action enum + * + * @extends Enum */ -class Action extends Enum +final class Action extends Enum { - const VIEW = 'view'; - const EDIT = 'edit'; + private const VIEW = 'view'; + private const EDIT = 'edit'; } ``` - ## Usage ```php -$action = new Action(Action::VIEW); +$action = Action::VIEW(); +// or with a dynamic key: +$action = Action::$key(); +// or with a dynamic value: +$action = Action::from($value); // or -$action = Action::VIEW(); +$action = new Action($value); ``` As you can see, static methods are automatically implemented to provide quick access to an enum value. -One advantage over using class constants is to be able to type-hint enum values: +One advantage over using class constants is to be able to use an enum as a parameter type: ```php function setAction(Action $action) { @@ -68,20 +77,22 @@ function setAction(Action $action) { Static methods: +- `from()` Creates an Enum instance, checking that the value exist in the enum - `toArray()` method Returns all possible values as an array (constant name in key, constant value in value) - `keys()` Returns the names (keys) of all constants in the Enum class - `values()` Returns instances of the Enum class of all Enum constants (constant name in key, Enum instance in value) - `isValid()` Check if tested value is valid on enum set - `isValidKey()` Check if tested key is valid on enum set +- `assertValidValue()` Assert the value is valid on enum set, throwing exception otherwise - `search()` Return key for searched value ### Static methods ```php -class Action extends Enum +final class Action extends Enum { - const VIEW = 'view'; - const EDIT = 'edit'; + private const VIEW = 'view'; + private const EDIT = 'edit'; } // Static method: @@ -94,9 +105,9 @@ Static method helpers are implemented using [`__callStatic()`](http://www.php.ne If you care about IDE autocompletion, you can either implement the static methods yourself: ```php -class Action extends Enum +final class Action extends Enum { - const VIEW = 'view'; + private const VIEW = 'view'; /** * @return Action @@ -114,14 +125,72 @@ or you can use phpdoc (this is supported in PhpStorm for example): * @method static Action VIEW() * @method static Action EDIT() */ -class Action extends Enum +final class Action extends Enum +{ + private const VIEW = 'view'; + private const EDIT = 'edit'; +} +``` + +## Native enums and migration +Native enum arrived to PHP in version 8.1: https://www.php.net/enumerations +If your project is running PHP 8.1+ or your library has it as a minimum requirement you should use it instead of this library. + +When migrating from `myclabs/php-enum`, the effort should be small if the usage was in the recommended way: +- private constants +- final classes +- no method overridden + +Changes for migration: +- Class definition should be changed from +```php +/** + * @method static Action VIEW() + * @method static Action EDIT() + */ +final class Action extends Enum +{ + private const VIEW = 'view'; + private const EDIT = 'edit'; +} +``` + to +```php +enum Action: string { - const VIEW = 'view'; - const EDIT = 'edit'; + case VIEW = 'view'; + case EDIT = 'edit'; } ``` +All places where the class was used as a type will continue to work. + +Usages and the change needed: + +| Operation | myclabs/php-enum | native enum | +|----------------------------------------------------------------|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Obtain an instance will change from | `$enumCase = Action::VIEW()` | `$enumCase = Action::VIEW` | +| Create an enum from a backed value | `$enumCase = new Action('view')` | `$enumCase = Action::from('view')` | +| Get the backed value of the enum instance | `$enumCase->getValue()` | `$enumCase->value` | +| Compare two enum instances | `$enumCase1 == $enumCase2`
or
`$enumCase1->equals($enumCase2)` | `$enumCase1 === $enumCase2` | +| Get the key/name of the enum instance | `$enumCase->getKey()` | `$enumCase->name` | +| Get a list of all the possible instances of the enum | `Action::values()` | `Action::cases()` | +| Get a map of possible instances of the enum mapped by name | `Action::values()` | `array_combine(array_map(fn($case) => $case->name, Action::cases()), Action::cases())`
or
`(new ReflectionEnum(Action::class))->getConstants()` | +| Get a list of all possible names of the enum | `Action::keys()` | `array_map(fn($case) => $case->name, Action::cases())` | +| Get a list of all possible backed values of the enum | `Action::toArray()` | `array_map(fn($case) => $case->value, Action::cases())` | +| Get a map of possible backed values of the enum mapped by name | `Action::toArray()` | `array_combine(array_map(fn($case) => $case->name, Action::cases()), array_map(fn($case) => $case->value, Action::cases()))`
or
`array_map(fn($case) => $case->value, (new ReflectionEnum(Action::class))->getConstants()))` | ## Related projects +- [PHP 8.1+ native enum](https://www.php.net/enumerations) - [Doctrine enum mapping](https://github.com/acelaya/doctrine-enum-type) -- [Symfony 2/3 ParamConverter integration](https://github.com/Ex3v/MyCLabsEnumParamConverter) +- [Symfony ParamConverter integration](https://github.com/Ex3v/MyCLabsEnumParamConverter) +- [PHPStan integration](https://github.com/timeweb/phpstan-enum) + + +[GA Image]: https://github.com/myclabs/php-enum/workflows/CI/badge.svg + +[GA Link]: https://github.com/myclabs/php-enum/actions?query=workflow%3A%22CI%22+branch%3Amaster + +[Shepherd Image]: https://shepherd.dev/github/myclabs/php-enum/coverage.svg + +[Shepherd Link]: https://shepherd.dev/github/myclabs/php-enum diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..84fd4e3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +Only the latest stable release is supported. + +## Reporting a Vulnerability + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). + +Tidelift will coordinate the fix and disclosure. diff --git a/composer.json b/composer.json index e9769a3..eab6263 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "library", "description": "PHP Enum implementation", "keywords": ["enum"], - "homepage": "http://github.com/myclabs/php-enum", + "homepage": "https://github.com/myclabs/php-enum", "license": "MIT", "authors": [ { @@ -14,7 +14,10 @@ "autoload": { "psr-4": { "MyCLabs\\Enum\\": "src/" - } + }, + "classmap": [ + "stubs/Stringable.php" + ] }, "autoload-dev": { "psr-4": { @@ -22,10 +25,12 @@ } }, "require": { - "php": ">=5.3" + "php": "^7.3 || ^8.0", + "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^4.8.35|^5.7|^6.0", - "squizlabs/php_codesniffer": "1.*" + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2 || ^5.2" } } diff --git a/phpunit.xml b/phpunit.xml index af5293b..33b8f67 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,20 +1,19 @@ - - + ./tests + + + + src + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..ff06b66 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Enum.php b/src/Enum.php index 140e722..1bd5592 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -14,41 +14,99 @@ * @author Matthieu Napoli * @author Daniel Costa * @author Mirosław Filip + * + * @psalm-template T + * @psalm-immutable + * @psalm-consistent-constructor */ -abstract class Enum +abstract class Enum implements \JsonSerializable, \Stringable { /** * Enum value * * @var mixed + * @psalm-var T */ protected $value; + /** + * Enum key, the constant name + * + * @var string + */ + private $key; + /** * Store existing constants in a static cache per object. * + * + * @var array + * @psalm-var array> + */ + protected static $cache = []; + + /** + * Cache of instances of the Enum class + * * @var array + * @psalm-var array> */ - protected static $cache = array(); + protected static $instances = []; /** * Creates a new value of some type * + * @psalm-pure * @param mixed $value * + * @psalm-param T $value * @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 ($value instanceof static) { + /** @psalm-var T */ + $value = $value->getValue(); } + /** @psalm-suppress ImplicitToStringCast assertValidValueReturningKey returns always a string but psalm has currently an issue here */ + $this->key = static::assertValidValueReturningKey($value); + + /** @psalm-var T */ $this->value = $value; } /** + * This method exists only for the compatibility reason when deserializing a previously serialized version + * that didn't had the key property + */ + public function __wakeup() + { + /** @psalm-suppress DocblockTypeContradiction key can be null when deserializing an enum without the key */ + if ($this->key === null) { + /** + * @psalm-suppress InaccessibleProperty key is not readonly as marked by psalm + * @psalm-suppress PossiblyFalsePropertyAssignmentValue deserializing a case that was removed + */ + $this->key = static::search($this->value); + } + } + + /** + * @param mixed $value + * @return static + */ + public static function from($value): self + { + $key = static::assertValidValueReturningKey($value); + + return self::__callStatic($key, []); + } + + /** + * @psalm-pure * @return mixed + * @psalm-return T */ public function getValue() { @@ -58,14 +116,17 @@ public function getValue() /** * Returns the enum key (i.e. the constant name). * - * @return mixed + * @psalm-pure + * @return string */ public function getKey() { - return static::search($this->value); + return $this->key; } /** + * @psalm-pure + * @psalm-suppress InvalidCast * @return string */ public function __toString() @@ -74,37 +135,48 @@ public function __toString() } /** - * Compares one Enum with another. + * Determines if Enum should be considered equal with the variable passed as a parameter. + * 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 * - * @return bool True if Enums are equal, false if not equal + * @psalm-pure + * @psalm-param mixed $variable + * @return bool */ - final public function equals(Enum $enum) + final public function equals($variable = null): bool { - return $this->getValue() === $enum->getValue() && get_called_class() == get_class($enum); + return $variable instanceof self + && $this->getValue() === $variable->getValue() + && static::class === \get_class($variable); } /** * Returns the names (keys) of all constants in the Enum class * + * @psalm-pure + * @psalm-return list * @return array */ public static function keys() { - return array_keys(static::toArray()); + return \array_keys(static::toArray()); } /** * Returns instances of the Enum class of all Enum constants * + * @psalm-pure + * @psalm-return array * @return static[] Constant name in key, Enum instance in value */ public static function values() { $values = array(); + /** @psalm-var T $value */ foreach (static::toArray() as $key => $value) { + /** @psalm-suppress UnsafeGenericInstantiation */ $values[$key] = new static($value); } @@ -114,13 +186,20 @@ public static function values() /** * Returns all possible values as an array * + * @psalm-pure + * @psalm-suppress ImpureStaticProperty + * + * @psalm-return array * @return array Constant name in key, constant value in value */ public static function toArray() { - $class = get_called_class(); - if (!array_key_exists($class, static::$cache)) { + $class = 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(); } @@ -131,38 +210,72 @@ public static function toArray() * Check if is valid enum value * * @param $value - * + * @psalm-param mixed $value + * @psalm-pure + * @psalm-assert-if-true T $value * @return bool */ public static function isValid($value) { - return in_array($value, static::toArray(), true); + return \in_array($value, static::toArray(), true); + } + + /** + * Asserts valid enum value + * + * @psalm-pure + * @psalm-assert T $value + * @param mixed $value + */ + public static function assertValidValue($value): void + { + self::assertValidValueReturningKey($value); + } + + /** + * Asserts valid enum value + * + * @psalm-pure + * @psalm-assert T $value + * @param mixed $value + * @return string + */ + private static function assertValidValueReturningKey($value): string + { + if (false === ($key = static::search($value))) { + throw new \UnexpectedValueException("Value '$value' is not part of the enum " . static::class); + } + + return $key; } /** * Check if is valid enum key * * @param $key - * + * @psalm-param string $key + * @psalm-pure * @return bool */ public static function isValidKey($key) { $array = static::toArray(); - return isset($array[$key]); + return isset($array[$key]) || \array_key_exists($key, $array); } /** * Return key for value * - * @param $value + * @param mixed $value * - * @return mixed + * @psalm-param mixed $value + * @psalm-pure + * @return string|false */ public static function search($value) { - return array_search($value, static::toArray(), true); + return \array_search($value, static::toArray(), true); } /** @@ -173,14 +286,34 @@ public static function search($value) * * @return static * @throws \BadMethodCallException + * + * @psalm-pure */ public static function __callStatic($name, $arguments) { - $array = static::toArray(); - if (isset($array[$name])) { - return new static($array[$name]); + $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); + } + /** @psalm-suppress UnsafeGenericInstantiation */ + return self::$instances[$class][$name] = new static($array[$name]); } + return clone self::$instances[$class][$name]; + } - throw new \BadMethodCallException("No static method or enum constant '$name' in class " . get_called_class()); + /** + * Specify data which should be serialized to JSON. This method returns data that can be serialized by json_encode() + * natively. + * + * @return mixed + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->getValue(); } } diff --git a/src/PHPUnit/Comparator.php b/src/PHPUnit/Comparator.php new file mode 100644 index 0000000..7c65e4e --- /dev/null +++ b/src/PHPUnit/Comparator.php @@ -0,0 +1,54 @@ +register(new \MyCLabs\Enum\PHPUnit\Comparator()); + */ +final class Comparator extends \SebastianBergmann\Comparator\Comparator +{ + public function accepts($expected, $actual) + { + return $expected instanceof Enum && ( + $actual instanceof Enum || $actual === null + ); + } + + /** + * @param Enum $expected + * @param Enum|null $actual + * + * @return void + */ + public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) + { + if ($expected->equals($actual)) { + return; + } + + throw new ComparisonFailure( + $expected, + $actual, + $this->formatEnum($expected), + $this->formatEnum($actual), + false, + 'Failed asserting that two Enums are equal.' + ); + } + + private function formatEnum(?Enum $enum = null) + { + if ($enum === null) { + return "null"; + } + + return get_class($enum)."::{$enum->getKey()}()"; + } +} diff --git a/static-analysis/EnumIsPure.php b/static-analysis/EnumIsPure.php new file mode 100644 index 0000000..5875fd8 --- /dev/null +++ b/static-analysis/EnumIsPure.php @@ -0,0 +1,33 @@ + + */ +final class PureEnum extends Enum +{ + const A = 'A'; + const C = 'C'; +} + +/** @psalm-pure */ +function enumFetchViaMagicMethodIsPure(): PureEnum +{ + return PureEnum::A(); +} + +/** @psalm-pure */ +function enumFetchViaExplicitMagicCallIsPure(): PureEnum +{ + return PureEnum::__callStatic('A', []); +} diff --git a/stubs/Stringable.php b/stubs/Stringable.php new file mode 100644 index 0000000..4811af7 --- /dev/null +++ b/stubs/Stringable.php @@ -0,0 +1,11 @@ +assertNotEquals('BA', $value->getKey()); } + /** @dataProvider invalidValueProvider */ + public function testCreatingEnumWithInvalidValue($value) + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('is not part of the enum ' . EnumFixture::class); + + new EnumFixture($value); + } + /** * @dataProvider invalidValueProvider - * @expectedException UnexpectedValueException - * @expectedExceptionMessage is not part of the enum MyCLabs\Tests\Enum\EnumFixture + * @param mixed $value */ - public function testCreatingEnumWithInvalidValue($value) + public function testFailToCreateEnumWithInvalidValueThroughNamedConstructor($value): void { - new EnumFixture($value); + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('is not part of the enum MyCLabs\Tests\Enum\EnumFixture'); + + EnumFixture::from($value); + } + + public function testFailToCreateEnumWithEnumItselfThroughNamedConstructor(): void + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage("Value 'foo' is not part of the enum " . EnumFixture::class); + + EnumFixture::from(EnumFixture::FOO()); } /** * Contains values not existing in EnumFixture * @return array */ - public function invalidValueProvider() { + public function invalidValueProvider() + { return array( "string" => array('test'), "int" => array(1234), @@ -68,7 +88,8 @@ public function testToString($expected, $enumObject) $this->assertSame($expected, (string) $enumObject); } - public function toStringProvider() { + public function toStringProvider() + { return array( array(EnumFixture::FOO, new EnumFixture(EnumFixture::FOO)), array(EnumFixture::BAR, new EnumFixture(EnumFixture::BAR)), @@ -141,15 +162,14 @@ 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()); } - /** - * @expectedException \BadMethodCallException - * @expectedExceptionMessage No static method or enum constant 'UNKNOWN' in class - * UnitTest\MyCLabs\Enum\Enum\EnumFixture - */ public function testBadStaticAccess() { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('No static method or enum constant \'UNKNOWN\' in class ' . EnumFixture::class); + EnumFixture::UNKNOWN(); } @@ -162,22 +182,23 @@ public function testIsValid($value, $isValid) $this->assertSame($isValid, EnumFixture::isValid($value)); } - public function isValidProvider() { - return array( + public function isValidProvider() + { + return [ /** * Valid values */ - array('foo', true), - array(42, true), - array(null, true), - array(0, true), - array('', true), - array(false, true), + ['foo', true], + [42, true], + [null, true], + [0, true], + ['', true], + [false, true], /** * Invalid values */ - array('baz', false) - ); + ['baz', false] + ]; } /** @@ -187,6 +208,7 @@ public function testIsValidKey() { $this->assertTrue(EnumFixture::isValidKey('FOO')); $this->assertFalse(EnumFixture::isValidKey('BAZ')); + $this->assertTrue(EnumFixture::isValidKey('PROBLEMATIC_NULL')); } /** @@ -199,7 +221,8 @@ public function testSearch($value, $expected) $this->assertSame($expected, EnumFixture::search($value)); } - public function searchProvider() { + public function searchProvider() + { return array( array('foo', 'FOO'), array(0, 'PROBLEMATIC_NUMBER'), @@ -219,10 +242,15 @@ public function testEquals() $foo = new EnumFixture(EnumFixture::FOO); $number = new EnumFixture(EnumFixture::NUMBER); $anotherFoo = new EnumFixture(EnumFixture::FOO); + $objectOfDifferentClass = new \stdClass(); + $notAnObject = 'foo'; $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)); } /** @@ -247,4 +275,110 @@ public function testEqualsConflictValues() { $this->assertFalse(EnumFixture::FOO()->equals(EnumConflict::FOO())); } + + /** + * jsonSerialize() + */ + 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()); + } + + public function testBooleanEnum() + { + $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); + } + + public function testSerialize() + { + // split string for Pretty CI: "Line exceeds 120 characters" + $bin = '4f3a33303a224d79434c6162735c54657374735c456e756d5c456e756d4669787'. + '4757265223a323a7b733a383a22002a0076616c7565223b733a333a22666f6f223b73'. + '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); + } + + public function testUnserialize() + { + // split string for Pretty CI: "Line exceeds 120 characters" + $bin = '4f3a33303a224d79434c6162735c54657374735c456e756d5c456e756d4669787'. + '4757265223a323a7b733a383a22002a0076616c7565223b733a333a22666f6f223b73'. + '3a32323a22004d79434c6162735c456e756d5c456e756d006b6579223b733a333a22464f4f223b7d'; + + /* @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); + } + + /** + * @see https://github.com/myclabs/php-enum/issues/95 + */ + public function testEnumValuesInheritance() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage("Value 'value' is not part of the enum MyCLabs\Tests\Enum\EnumFixture"); + $inheritedEnumFixture = InheritedEnumFixture::VALUE(); + new EnumFixture($inheritedEnumFixture); + } + + /** + * @dataProvider isValidProvider + */ + public function testAssertValidValue($value, $isValid): void + { + if (!$isValid) { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage("Value '$value' is not part of the enum " . EnumFixture::class); + } + + EnumFixture::assertValidValue($value); + + self::assertTrue(EnumFixture::isValid($value)); + } } diff --git a/tests/InheritedEnumFixture.php b/tests/InheritedEnumFixture.php new file mode 100644 index 0000000..301f9bb --- /dev/null +++ b/tests/InheritedEnumFixture.php @@ -0,0 +1,14 @@ +register(new \MyCLabs\Enum\PHPUnit\Comparator());