├── .gitignore
├── .gitlab-ci.yml
├── LICENSE
├── README.md
├── composer.json
├── phpstan.neon
├── src
├── AutoInstances.php
├── Enum.php
├── Internal
│ ├── ConsistencyChecker.php
│ ├── InstanceRegister.php
│ └── Meta.php
└── exceptions.php
└── tests
├── Basic
├── accessing-scalar-value.phpt
├── autoinstances.phpt
├── equals.phpt
├── non-strict-value-comparison.phpt
├── stateMachine.phpt
└── strict-value-comparison.phpt
├── Consistency
├── finalAndAbstractCheck.phpt
├── methodAnnotations.missing.phpt
└── missingInstance.phpt
├── Example
├── AddingBehaviourToEnum
│ ├── readme.md
│ ├── step-1.phpt
│ ├── step-2.phpt
│ └── step-3.phpt
├── LoyaltyProgramExample
│ └── example.phpt
├── MigratingLegacyCode
│ ├── readme.md
│ ├── step0.phpt
│ ├── step1.phpt
│ ├── step2.phpt
│ └── step3.phpt
└── OrderState
│ ├── readme.md
│ ├── refactoring-1.phpt
│ ├── refactoring-2.phpt
│ ├── refactoring-3.phpt
│ ├── refactoring-4.phpt
│ └── refactoring-5.phpt
├── Reflection
├── constantNames.inherited.phpt
├── constantNames.phpt
└── getAvailableValues.phpt
├── Regression
├── access-to-non-existing-value-using-static-method-should-throw-an-exception.phpt
├── forgotten-constructor-call.phpt
├── fromScalar-non-existing-values-should-throw-exception.phpt
├── full-classes-as-values.phpt
├── loose-comparison-across-types.phpt
└── mixed-key-type-test.phpt
├── bootstrap.php
└── php-windows.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 | .idea
4 | **/output/*
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: grifart/php8.1-with-all-modules-and-various-tools
2 |
3 | # STAGES
4 |
5 | stages:
6 | - build
7 | - test
8 |
9 |
10 |
11 | # BUILDS
12 |
13 | # composer
14 |
15 | .build-composer-template: &build-composer-template
16 | stage: build
17 |
18 | artifacts:
19 | expire_in: 2 hours
20 | name: "${CI_BUILD_REF_NAME}_${CI_BUILD_NAME}"
21 | paths:
22 | - vendor
23 |
24 | build.composer.dev:
25 | <<: *build-composer-template
26 |
27 | script:
28 | - composer install --no-interaction --ansi
29 |
30 | build.composer:
31 | <<: *build-composer-template
32 |
33 | script:
34 | - composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --ansi
35 |
36 |
37 |
38 |
39 | # TESTS
40 |
41 |
42 | # php lint
43 |
44 | test.php-syntax-check:
45 | stage: test
46 |
47 | script:
48 | - composer require php-parallel-lint/php-parallel-lint
49 | - vendor/bin/parallel-lint src
50 |
51 |
52 | # php stan
53 |
54 | test.phpstan:
55 | stage: test
56 |
57 | dependencies:
58 | - build.composer.dev
59 |
60 | script:
61 | - composer run phpstan
62 |
63 |
64 | # tests
65 |
66 | .test.tests: &test-tests
67 | stage: test
68 |
69 | dependencies: []
70 | needs: []
71 |
72 | before_script:
73 | - composer install --no-interaction --ansi
74 |
75 | script:
76 | - composer run test
77 |
78 | artifacts:
79 | expire_in: 15 minutes
80 | paths:
81 | - log
82 | - src # can contain assertion diffs
83 | when: on_failure
84 |
85 |
86 | test.tests.php72:
87 | <<: *test-tests
88 | image: grifart/php7.2-with-gulp-and-all-php-modules
89 |
90 | test.tests.php72.oldDeps:
91 | <<: *test-tests
92 | before_script:
93 | - composer update --prefer-lowest --no-interaction --ansi
94 | image: grifart/php7.2-with-gulp-and-all-php-modules
95 |
96 |
97 |
98 | test.tests.php73:
99 | <<: *test-tests
100 | image: grifart/php7.3-with-gulp-and-all-php-modules
101 |
102 | test.tests.php73.oldDeps:
103 | <<: *test-tests
104 | before_script:
105 | - composer update --prefer-lowest --no-interaction --ansi
106 | image: grifart/php7.3-with-gulp-and-all-php-modules
107 |
108 |
109 |
110 | test.tests.php74:
111 | <<: *test-tests
112 | image: grifart/php7.4-with-gulp-and-all-php-modules
113 |
114 | test.tests.php74.oldDeps:
115 | <<: *test-tests
116 | before_script:
117 | - composer update --prefer-lowest --no-interaction --ansi
118 | image: grifart/php7.4-with-gulp-and-all-php-modules
119 |
120 |
121 |
122 | test.tests.php80:
123 | <<: *test-tests
124 | image: grifart/php8.0-with-all-modules-and-various-tools
125 |
126 | test.tests.php80.oldDeps:
127 | <<: *test-tests
128 | before_script:
129 | - composer update --prefer-lowest --no-interaction --ansi
130 | image: grifart/php8.0-with-all-modules-and-various-tools
131 |
132 |
133 |
134 | test.tests.php81:
135 | <<: *test-tests
136 | image: grifart/php8.1-with-all-modules-and-various-tools
137 |
138 | test.tests.php81.oldDeps:
139 | <<: *test-tests
140 | before_script:
141 | - composer update --prefer-lowest --no-interaction --ansi
142 | image: grifart/php8.1-with-all-modules-and-various-tools
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 GRIFART spol. s r.o.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # grifart/enum
2 |
3 | Enumeration value object. Enumerate values and behaviour with type-safety.
4 |
5 | Repositories [gitlab.grifart.cz](https://gitlab.grifart.cz/jkuchar1/grifart-enum)
6 | and [github.com](https://github.com/grifart/enum).
7 | [](https://gitlab.grifart.cz/jkuchar1/grifart-enum/commits/master)
8 |
9 | Sponsored by [grifart.com](https://grifart.com).
10 |
11 | Do you like video 📺 instead of reading? Here is one in [english](http://bit.ly/better-enum-for-php) and one in [czech](http://bit.ly/lepsi-enum-pro-php).
12 |
13 | ## Introduction
14 |
15 | Enums represent predefined set of values. The available values are defined statically by each enum class. Each value is represented by an instance of this class in a flyweight manner.
16 |
17 | - This enum allows you to add individual behaviour for every enum value (as in Java). This allows you to transform your `switch`es/`if`s into more readable composition. (see example bellow)
18 | - Checks enum annotations if phpdoc-declared methods are properly declared (will generate docblock for you when not specified or incorrect)
19 | - `===`, `==` and usage of `switch`es is supported
20 | - string or integer scalar keys are supported
21 | - Easily access scalar value of enum `DayOfWeek::MONDAY()->toScalar()` or `(string) DayOfWeek::MONDAY()`
22 |
23 | Also includes:
24 |
25 | - It is type safe. By annotating your enumeration type, you are guaranteed that there will be no other values then you declared. `function translateTo(DayOfWeek $day) { ...`
26 | - You can get a list of all the possible values `Enum::getAvailableValues()`
27 |
28 | ## Installation
29 |
30 | ```bash
31 | composer require grifart/enum
32 | ```
33 |
34 | This library uses [**semantic versioning 2.0**](https://semver.org/spec/v2.0.0.html).
35 | You can safely use `^` constrain in you `composer.json`.
36 |
37 | ## Requirements
38 |
39 | This library requires PHP 7.1 and later.
40 |
41 | ## Project status & release process
42 |
43 | While this library is still under development, it is well tested and should be stable enough to use in production environments.
44 |
45 | The current releases are numbered 0.x.y. When a non-breaking change is introduced (adding new methods, optimizing existing code, etc.), y is incremented.
46 |
47 | When a breaking change is introduced, a new 0.x version cycle is always started.
48 |
49 | It is therefore safe to lock your project to a given release cycle, such as 0.1.*.
50 |
51 | If you need to upgrade to a newer release cycle, check the release history for a list of changes introduced by each further 0.x.0 version.
52 |
53 | ## Overview
54 |
55 | ### Static methods
56 |
57 | - fromScalar() - returns enum instance (value) for given scalar
58 | - getAvailableValues() - returns all values for given type
59 | - provideInstances() - implement to return enum instances or automatically implemented by `Grifart\Enum\AutoInstances` trait.
60 |
61 | ### Instance methods
62 |
63 | - toScalar() - return scalar value identifier
64 | - equals() - returns true if the same enum value is passed
65 | - scalarEquals() - returns true if passed scalar value is equal to current value
66 |
67 | ### Simplest enumeration
68 |
69 | ```php
70 | /**
71 | * @method static DayOfWeek MONDAY()
72 | * @method static DayOfWeek TUESDAY()
73 | */
74 | final class DayOfWeek extends \Grifart\Enum\Enum
75 | {
76 | use Grifart\Enum\AutoInstances;
77 | private const MONDAY = 'monday';
78 | private const TUESDAY = 'tuesday';
79 | }
80 |
81 | $monday = DayOfWeek::MONDAY();
82 | function process(DayOfWeek $day): void { /* ... */ }
83 | ````
84 |
85 | ### Values with behaviour
86 |
87 | This way conditions can be replaced by composition.
88 |
89 | This example originally comes from loyalty program domain, [continue to full code sample with context](tests/Example/LoyaltyProgramExample/example.phpt).
90 |
91 | ```php
92 | /**
93 | * @method static ExpirationType ASSIGNMENT()
94 | * @method static ExpirationType FIXED()
95 | */
96 | abstract class ExpirationType extends \Grifart\Enum\Enum
97 | {
98 | protected const ASSIGNMENT = 'assignment';
99 | protected const FIXED = 'fixed';
100 |
101 | abstract public function computeExpiration(Offer $offer): \DateTimeImmutable;
102 |
103 | protected static function provideInstances() : array {
104 | return [
105 | new class(self::ASSIGNMENT) extends ExpirationType {
106 | public function computeExpiration(Offer $offer): \DateTimeImmutable {
107 | return /* behaviour A */;
108 | }
109 | },
110 | new class(self::FIXED) extends ExpirationType {
111 | public function computeExpiration(Offer $offer): \DateTimeImmutable {
112 | return /* behaviour B */;
113 | }
114 | },
115 | ];
116 | }
117 | }
118 | ````
119 |
120 | ### Migrating from class constants [[source code](tests/Example/MigratingLegacyCode/readme.md)]
121 |
122 | This guide show how to migrate from classes with constants to `\Grifart\Enum` in few simple steps. [Continue to example](tests/Example/MigratingLegacyCode/readme.md)
123 |
124 | ### Adding behaviour to values [[source code](tests/Example/AddingBehaviourToEnum/readme.md)]
125 |
126 | This guide show how to slowly add behaviour to enum values. Step by step. [Continue to example](tests/Example/AddingBehaviourToEnum/readme.md)
127 |
128 | ### Complex showcase: order lifecycle tracking [[source code](tests/Example/OrderState/readme.md)]
129 |
130 | This example contains 5 ways of implementing order state. [Continue to example](tests/Example/OrderState/readme.md).
131 |
132 | ## Big thanks
133 |
134 | - [David Grudl](https://github.com/dg) for making [Nette Tester](https://github.com/nette/tester)
135 | - [Ondřej Mirtes](https://github.com/ondrejmirtes) for making [PHP Stan](https://github.com/phpstan/phpstan).
136 |
137 | ## More reading
138 |
139 | - [consistence/consistence enum](https://github.com/consistence/consistence/blob/master/docs/Enum/enums.md)
140 | - [myclabs/php-enum](https://github.com/myclabs/php-enum)
141 | - [Java enum](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html) (see planet example)
142 | - [great talk about "crafting wicked domain models" from Jimmy Bogard](https://vimeo.com/43598193)
143 | - [more my notes on DDD topic](https://gitlab.grifart.cz/jkuchar1/eventsourcing-cqrs-simple-app/blob/master/README.md)
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grifart/enum",
3 | "description": "Provides bullet proof enums with behaviours.",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Jan Kuchař",
9 | "email": "honza.kuchar@grifart.cz"
10 | }
11 | ],
12 |
13 | "scripts": {
14 | "verify": [
15 | "@phpstan",
16 | "@test"
17 | ],
18 | "phpstan": "vendor/bin/phpstan analyze -c phpstan.neon --error-format compact --no-interaction --ansi --no-progress -- src",
19 | "test": "vendor/bin/tester tests --colors 1"
20 | },
21 |
22 | "require": {
23 | "php": ">=7.2.0"
24 | },
25 | "autoload": {
26 | "psr-4": {
27 | "Grifart\\Enum\\": "src"
28 | },
29 | "classmap": [
30 | "src/exceptions.php"
31 | ]
32 | },
33 |
34 | "require-dev": {
35 | "nette/tester": "~2.4.2",
36 | "phpstan/phpstan": "~1.6.9",
37 | "phpstan/phpstan-strict-rules": "~1.2.3",
38 | "grifart/phpstan-oneline": "~v0.4.2"
39 | },
40 | "autoload-dev": {
41 | "files": [
42 | "src/exceptions.php"
43 | ]
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - vendor/phpstan/phpstan-strict-rules/rules.neon
3 | - vendor/grifart/phpstan-oneline/config.neon
4 |
5 | parameters:
6 | level: 9
7 | ignoreErrors:
8 |
--------------------------------------------------------------------------------
/src/AutoInstances.php:
--------------------------------------------------------------------------------
1 | getValues();
35 | }
36 |
37 | /**
38 | * @return array
39 | */
40 | protected static function getConstantToScalar(): array
41 | {
42 | try {
43 | /** @var array $constants */
44 | $constants = (new \ReflectionClass(static::class))->getConstants();
45 | return $constants;
46 | } catch (\ReflectionException $e) {
47 | throw new ReflectionFailedException($e);
48 | }
49 | }
50 |
51 | /**
52 | * Builds enumeration from its scalar value.
53 | * @param TScalarValue $scalar
54 | * @return static
55 | * @throws MissingValueDeclarationException if there is no value for given scalar
56 | */
57 | public static function fromScalar($scalar): Enum
58 | {
59 | return self::getMeta()->getValueForScalar($scalar);
60 | }
61 |
62 | /**
63 | * Provides access to values using ::CONSTANT_NAME() interface.
64 | * @param array{} $arguments And empty array, arguments not used.
65 | * @return static
66 | * @throws MissingValueDeclarationException
67 | */
68 | public static function __callStatic(string $constantName, array $arguments): Enum
69 | {
70 | $value = self::getMeta(FALSE)->getValueForConstantName($constantName);
71 | if($value === NULL) {
72 | throw new \Error('Call to undefined method ' . static::class . '::' . $constantName . '(). Please check that you have provided constant, annotation and value.');
73 | }
74 | return $value;
75 | }
76 |
77 | /**
78 | * @return Meta
79 | */
80 | private static function getMeta(bool $checkIfAccessingRootDirectly = true): Meta
81 | {
82 | $rootClass = self::getRootClass();
83 | if ($checkIfAccessingRootDirectly && $rootClass !== static::class) {
84 | throw new UsageException(
85 | 'You have accessed static enum method on non-root class '
86 | . "('$rootClass' is a root class)"
87 | );
88 | }
89 |
90 | return InstanceRegister::get(
91 | $rootClass,
92 | function () use ($rootClass): Meta {
93 | /** @var Meta $meta */
94 | $meta = Meta::from(
95 | $rootClass,
96 | static::getConstantToScalar(),
97 | static::provideInstances()
98 | );
99 | return $meta;
100 | }
101 | );
102 | }
103 |
104 | /**
105 | * @return class-string
106 | */
107 | private static function getRootClass(): string
108 | {
109 | try {
110 | $rootClassName = (new \ReflectionClass(static::class))
111 | ->getMethod('provideInstances')
112 | ->getDeclaringClass()
113 | ->getName();
114 | /** @var class-string $rootClassName */
115 | return $rootClassName;
116 |
117 | } catch (\ReflectionException $e) {
118 | throw new ReflectionFailedException($e);
119 | }
120 | }
121 |
122 |
123 |
124 | // -------- INSTANCE IMPLEMENTATION ---------
125 |
126 | /** @var ?TScalarValue */
127 | private $scalarValue;
128 |
129 | /**
130 | * @param TScalarValue $scalarValue
131 | */
132 | protected function __construct($scalarValue)
133 | {
134 | $this->scalarValue = $scalarValue;
135 | }
136 |
137 | /**
138 | * Returns scalar representation of enum value.
139 | * @return TScalarValue
140 | */
141 | public function toScalar()
142 | {
143 | if ($this->scalarValue === NULL) {
144 | $rootClassName = self::getRootClass();
145 | throw new UsageException(
146 | "Parent constructor has not been called while constructing one of {$rootClassName} enum values."
147 | );
148 | }
149 |
150 | return $this->scalarValue;
151 | }
152 |
153 | public function __toString(): string
154 | {
155 | // as enum does not allow mixed key types (all must be int or all string),
156 | // we can safely convert integers to strings without worrying introducing
157 | // value conflicts
158 | return (string) $this->toScalar();
159 | }
160 |
161 | /**
162 | * Retrieves constant name that is used to access enum value.
163 | *
164 | * @internal Do not depend on this values, as it can change anytime. This value can be
165 | * subject of refactorings of user-defined enums.
166 | */
167 | public function getConstantName(): string
168 | {
169 | return $this::getMeta(FALSE)->getConstantNameForScalar(
170 | $this->toScalar()
171 | );
172 | }
173 |
174 | /**
175 | * @param Enum $that the other object we are comparing to
176 | * @return bool if current value equals to the other value
177 | * If value is non-enum value, returns false (as they are also not equal).
178 | */
179 | public function equals(Enum $that): bool
180 | {
181 | return $this === $that;
182 | }
183 |
184 | /**
185 | * @param TScalarValue $theOtherScalarValue
186 | * @return bool true if current scalar representation of value equals to given scalar value
187 | */
188 | public function scalarEquals($theOtherScalarValue): bool
189 | {
190 | return $this->toScalar() === $theOtherScalarValue;
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/Internal/ConsistencyChecker.php:
--------------------------------------------------------------------------------
1 | $enumMeta
15 | */
16 | public static function checkAnnotations(Meta $enumMeta): void
17 | {
18 | self::checkCallStaticAnnotations($enumMeta);
19 | self::checkAllInstancesProvided($enumMeta);
20 | self::checkAbstractAndFinal($enumMeta);
21 | }
22 |
23 | /**
24 | * @param Meta $enumMeta
25 | */
26 | private static function checkCallStaticAnnotations(Meta $enumMeta): void
27 | {
28 | $enumReflection = $enumMeta->getClassReflection();
29 |
30 | $docBlock = $enumReflection->getDocComment();
31 | $className = $enumReflection->getShortName();
32 | if ($docBlock === false) {
33 | $docBlock = '';
34 | }
35 |
36 | $missingAnnotations = [];
37 | foreach ($enumMeta->getConstantNames() as $constantName) {
38 | $desiredAnnotation = "@method static $className $constantName()";
39 | if (stripos($docBlock, $desiredAnnotation) === false) {
40 | $missingAnnotations[] = $desiredAnnotation;
41 | }
42 | }
43 |
44 | if (\count($missingAnnotations) !== 0) {
45 | $properDoc = "/**\n * " . implode("\n * ", $missingAnnotations) . "\n */\n";
46 | throw new UsageException("You have forgotten to add @method annotations for enum '{$enumReflection->getName()}'. Documentation block should contain: \n$properDoc");
47 | }
48 | // todo: @method annotations without constants
49 | }
50 |
51 | /**
52 | * @param Meta $enumMeta
53 | */
54 | private static function checkAllInstancesProvided(Meta $enumMeta): void
55 | {
56 | // todo: instances without constants
57 |
58 | foreach ($enumMeta->getScalarValues() as $scalarValue) {
59 | if (!$enumMeta->hasValueForScalar($scalarValue)) {
60 | $constantName = $enumMeta->getConstantNameForScalar($scalarValue);
61 | throw new UsageException("You have forgotten to provide instance for $constantName.");
62 | }
63 | }
64 | }
65 |
66 | /**
67 | * @param Meta $enumMeta
68 | */
69 | private static function checkAbstractAndFinal(Meta $enumMeta): void
70 | {
71 | $enumReflection = $enumMeta->getClassReflection();
72 |
73 | if (!$enumReflection->isFinal() && !$enumReflection->isAbstract()) {
74 | throw new UsageException(
75 | "Enum root class must be either abstract or final.\n"
76 | . "Final is used when one type is enough for all enum instance values.\n"
77 | . 'Abstract is used when enum values are always instances of child classes of enum root class.'
78 | );
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Internal/InstanceRegister.php:
--------------------------------------------------------------------------------
1 | [] */
13 | private static $instances = [];
14 |
15 | /**
16 | * @template TEnum of Enum
17 | * @param class-string $enumClass
18 | * @param callable():Meta $registrar
19 | * @return Meta
20 | */
21 | public static function get(string $enumClass, callable $registrar): Meta
22 | {
23 | if (!isset(self::$instances[$enumClass])) {
24 | self::register($enumClass, $registrar());
25 | }
26 | /** @var Meta $meta */
27 | $meta = self::$instances[$enumClass];
28 | return $meta;
29 | }
30 |
31 | /**
32 | * @template TEnum of \Grifart\Enum\Enum
33 | * @param class-string $className
34 | * @param Meta $meta
35 | * @return void
36 | */
37 | public static function register(string $className, Meta $meta): void
38 | {
39 | \assert($meta->getClass() === $className, 'Provided Meta object is for different enum class that was originally registered.');
40 |
41 | // check consistency of enum when assertions are enabled (typically non-production code)
42 | // @phpstan-ignore-next-line as "Call to function assert() with true will always evaluate to true." is intentional
43 | assert(
44 | (function () use ($meta): bool {
45 | ConsistencyChecker::checkAnnotations($meta);
46 | return true;
47 | })()
48 | );
49 | self::$instances[$className] = $meta;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Internal/Meta.php:
--------------------------------------------------------------------------------
1 | */
18 | private $class;
19 |
20 | /** @var array */
21 | private $constantToScalar;
22 |
23 | /** @var array */
24 | private $scalarToValue;
25 |
26 | /**
27 | * @param class-string $class
28 | * @param array $constantToScalar
29 | * @param TEnum[] $values
30 | */
31 | private function __construct(string $class, array $constantToScalar, array $values)
32 | {
33 | $this->class = $class;
34 | $this->constantToScalar = $constantToScalar;
35 | $this->scalarToValue = $this->buildScalarToValueMapping($values); // requires constantToScalar to be already set!
36 | }
37 |
38 | /**
39 | * @param Enum[] $values
40 | * @phpstan-param TEnum[] $values
41 | * @return array
42 | */
43 | private function buildScalarToValueMapping(array $values): array {
44 | $scalarToValues = [];
45 |
46 | // check type of all scalar values
47 | $keyType = null;
48 | foreach($values as $value) {
49 | $scalar = $value->toScalar();
50 | if ($keyType === NULL) {
51 | $keyType = \gettype($scalar);
52 | }
53 | if ($keyType !== \gettype($scalar)) {
54 | throw new UsageException('Mixed types of scalar value. Keys must either all string or all int.');
55 | }
56 | }
57 |
58 | foreach($values as $value) {
59 | $scalar = $value->toScalar();
60 |
61 |
62 |
63 | if (isset($scalarToValues[$scalar])) {
64 | throw new UsageException('You have provided duplicated scalar values.');
65 | }
66 | if(!$this->hasConstantForScalar($scalar)) {
67 | throw new UsageException("Provided instance contains scalar value '$scalar'. But no corresponding constant was found.");
68 | }
69 | $scalarToValues[$scalar] = $value;
70 |
71 | }
72 | return $scalarToValues;
73 | }
74 |
75 | /**
76 | * @param class-string $class
77 | * @param array $constantToScalar
78 | * @param TEnum[] $values
79 | * @return self
80 | */
81 | public static function from(string $class, array $constantToScalar, array $values): self
82 | {
83 | return new self($class, $constantToScalar, $values);
84 | }
85 |
86 | /**
87 | * @return class-string
88 | */
89 | public function getClass(): string
90 | {
91 | return $this->class;
92 | }
93 |
94 | /**
95 | * @return \ReflectionClass
96 | */
97 | public function getClassReflection(): \ReflectionClass
98 | {
99 | try {
100 | return new \ReflectionClass($this->getClass());
101 | } catch (\ReflectionException $e) {
102 | throw new ReflectionFailedException($e);
103 | }
104 | }
105 |
106 | /**
107 | * @return TConstantName[]
108 | */
109 | public function getConstantNames(): array
110 | {
111 | return \array_keys($this->constantToScalar);
112 | }
113 |
114 | /**
115 | * @return array
116 | */
117 | public function getScalarValues(): array
118 | {
119 | return \array_values($this->constantToScalar);
120 | }
121 |
122 | /**
123 | * @return TEnum[]
124 | */
125 | public function getValues(): array
126 | {
127 | return \array_values($this->scalarToValue);
128 | }
129 |
130 | /**
131 | * @param TConstantName $constantName
132 | * @return ?TEnum
133 | * @throws MissingValueDeclarationException
134 | */
135 | public function getValueForConstantName($constantName): ?Enum
136 | {
137 | if(!isset($this->constantToScalar[$constantName])) {
138 | return NULL;
139 | }
140 | $scalar = $this->constantToScalar[$constantName];
141 | return $this->getValueForScalar($scalar);
142 | }
143 |
144 | /**
145 | * @param TScalarValue $scalarValue
146 | */
147 | public function hasValueForScalar($scalarValue): bool
148 | {
149 | return isset($this->scalarToValue[$scalarValue]);
150 | }
151 |
152 | /**
153 | * @param TScalarValue $scalarValue
154 | */
155 | public function getConstantNameForScalar($scalarValue): string
156 | {
157 | $result = \array_search($scalarValue, $this->constantToScalar, true);
158 | if ($result === false) {
159 | throw new UsageException("Could not find constant name for $scalarValue.");
160 | }
161 | return $result;
162 | }
163 |
164 | /**
165 | * @return TScalarValue
166 | */
167 | public function getScalarForValue(Enum $enum)
168 | {
169 | $result = \array_search($enum, $this->scalarToValue, true);
170 | if ($result === false) {
171 | throw new UsageException("Could not find scalar for given instance.");
172 | }
173 | return $result;
174 | }
175 |
176 | /**
177 | * @param TScalarValue $scalar
178 | * @return TEnum
179 | * @throws MissingValueDeclarationException if there is no value for given scalar
180 | */
181 | public function getValueForScalar($scalar): Enum
182 | {
183 | if (!isset($this->scalarToValue[$scalar])) {
184 | throw new MissingValueDeclarationException("There is no value for enum '{$this->class}' and scalar value '$scalar'.");
185 | }
186 | return $this->scalarToValue[$scalar];
187 | }
188 |
189 | /**
190 | * @param TScalarValue $scalar
191 | */
192 | private function hasConstantForScalar($scalar): bool
193 | {
194 | return \in_array($scalar, $this->constantToScalar, TRUE);
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/src/exceptions.php:
--------------------------------------------------------------------------------
1 | toScalar());
19 | \Tester\Assert::same('value1', (string) EnumString::VALUE1());
20 |
21 | \Tester\Assert::same('value2', EnumString::VALUE2()->toScalar());
22 | \Tester\Assert::same('value2', (string) EnumString::VALUE2());
23 |
24 |
25 | /**
26 | * @method static EnumInt VALUE1()
27 | * @method static EnumInt VALUE2()
28 | */
29 | final class EnumInt extends \Grifart\Enum\Enum
30 | {
31 | use Grifart\Enum\AutoInstances;
32 |
33 | protected const VALUE1 = 1;
34 | protected const VALUE2 = 2;
35 | }
36 |
37 | \Tester\Assert::same(1, EnumInt::VALUE1()->toScalar());
38 | \Tester\Assert::same('1', (string) EnumInt::VALUE1());
39 |
40 | \Tester\Assert::same(2, EnumInt::VALUE2()->toScalar());
41 | \Tester\Assert::same('2', (string) EnumInt::VALUE2());
42 |
--------------------------------------------------------------------------------
/tests/Basic/autoinstances.phpt:
--------------------------------------------------------------------------------
1 | toScalar());
27 | \Tester\Assert::same(OrderState::ACTIVE(), OrderState::fromScalar('active'));
28 |
--------------------------------------------------------------------------------
/tests/Basic/equals.phpt:
--------------------------------------------------------------------------------
1 | equals(EqualsState::NEW()));
18 | \Tester\Assert::false(EqualsState::NEW()->equals(EqualsState::ACTIVE()));
19 |
20 | \Tester\Assert::true(EqualsState::NEW()->scalarEquals('new'));
21 | \Tester\Assert::false(EqualsState::NEW()->scalarEquals('active'));
22 |
--------------------------------------------------------------------------------
/tests/Basic/non-strict-value-comparison.phpt:
--------------------------------------------------------------------------------
1 | canDoTransitionTo(StateMachine::STATE_B()));
44 | \Tester\Assert::true(StateMachine::STATE_B()->canDoTransitionTo(StateMachine::STATE_A()));
45 | \Tester\Assert::false(StateMachine::STATE_A()->canDoTransitionTo(StateMachine::STATE_A()));
46 | \Tester\Assert::false(StateMachine::STATE_B()->canDoTransitionTo(StateMachine::STATE_B()));
47 |
48 |
--------------------------------------------------------------------------------
/tests/Basic/strict-value-comparison.phpt:
--------------------------------------------------------------------------------
1 | nextDay();
80 | ```
81 |
82 | Public API now looks much better! Asking monday, what is the next day and it knows answer. ✅
83 |
84 | However I'm still worried about code of `nextDay()` function.
85 |
86 | - ❌ There are still ugly `if`s which are hard to read.
87 | - ❌ I have to worry about case when **someone adds new value** to this enum and forgets to update `nextDay()` method.
88 |
89 | Both of these can be solved by **using composition** instead of just value and switches.
90 |
91 | What if every value of enum would be separate class? Then we can write behaviour for each day individually making code very simple. And if someone adds new value, type-system will force him to add all required behaviour. And `grifart/enum` makes this easy, you do not have declare separate class for every value, just use anonymous classes.
92 |
93 | ```php
94 | /**
95 | * @method static DayOfWeek MONDAY()
96 | * @method static DayOfWeek TUESDAY()
97 | */
98 | abstract class DayOfWeek extends \Grifart\Enum\Enum
99 | {
100 |
101 | protected const MONDAY = 'monday';
102 | protected const TUESDAY = 'tuesday';
103 | // ...
104 |
105 | abstract public function nextDay(): self;
106 |
107 |
108 | /** @return static[] */
109 | protected static function provideInstances(): array
110 | {
111 | return [
112 | new class(self::MONDAY) extends DayOfWeek
113 | {
114 | public function nextDay(): DayOfWeek
115 | {
116 | return DayOfWeek::TUESDAY();
117 | }
118 | },
119 |
120 | new class(self::TUESDAY) extends DayOfWeek
121 | {
122 | public function nextDay(): DayOfWeek
123 | {
124 | return DayOfWeek::WEDNESDAY();
125 | }
126 | },
127 | ];
128 | }
129 | }
130 | ```
131 |
132 | Now type-system knows that every enum value must have method `nextDay()` with return type of self ✅. Please note that this is completely internal thing - **public API haven't changed**! And we got rid of all `if`s and `switch`es ✅.
133 |
134 | This approach is very useful when one wants to implement anything state-machine related (see tests for more examples, they are simple and easy to read).
135 |
--------------------------------------------------------------------------------
/tests/Example/AddingBehaviourToEnum/step-1.phpt:
--------------------------------------------------------------------------------
1 | nextDay()
37 | );
--------------------------------------------------------------------------------
/tests/Example/AddingBehaviourToEnum/step-3.phpt:
--------------------------------------------------------------------------------
1 | nextDay()
45 | );
--------------------------------------------------------------------------------
/tests/Example/LoyaltyProgramExample/example.phpt:
--------------------------------------------------------------------------------
1 | assignedAt()
61 | ->modify('+' . $offer->type()->daysValid() . ' days');
62 | }
63 | },
64 | new class(self::FIXED) extends ExpirationType {
65 | public function computeExpiration(Offer $offer): \DateTimeImmutable {
66 | $beginDate = $offer->type()->beginDate();
67 | \assert($beginDate !== NULL);
68 | return $beginDate->modify('+' . $offer->type()->daysValid() . ' days');
69 | }
70 | },
71 | ];
72 | }
73 | }
74 |
75 |
76 | // just checking if it compiles
77 | Assert::type(ExpirationType::class, ExpirationType::ASSIGNMENT());
--------------------------------------------------------------------------------
/tests/Example/MigratingLegacyCode/readme.md:
--------------------------------------------------------------------------------
1 | # Migrating legacy code to `\Grifart\Enum`
2 |
3 | This is step-by-step guide how to migrate you legacy code to `\Grifart\Enum`.
4 |
5 | We will start with non-type safe enum represented by class with constants. [[full source code](step0.phpt)]
6 |
7 | ```php
8 | class OrderState {
9 | public const NEW = 'new';
10 | public const PROCESSING = 'processing';
11 | }
12 | ```
13 |
14 | Our business logic is this:
15 |
16 | ```php
17 | $result = '';
18 | switch ($state) {
19 | // your business logic
20 | case OrderState::NEW:
21 | $result = 'new';
22 | break;
23 | case OrderState::PROCESSING:
24 | $result = 'processing';
25 | break;
26 | }
27 | ```
28 |
29 | ## Step 1: add new type-safe API [[source](step1.phpt)]
30 |
31 | This is done by
32 |
33 | - extending `\Grifart\Enum\Enum` class
34 | - by automatically implementing enum values by including `use \Grifart\Enum\AutoInstances;` trait
35 | - and by adding magic methods annotations
36 |
37 | There is not backward incompatible change introduced. And now you can use new APIs!
38 |
39 |
40 | ```php
41 | /**
42 | * @method static OrderState NEW()
43 | * @method static OrderState PROCESSING()
44 | */
45 | class OrderState extends \Grifart\Enum\Enum {
46 | use \Grifart\Enum\AutoInstances;
47 | public const NEW = 'new';
48 | public const PROCESSING = 'processing';
49 | }
50 | ```
51 |
52 | ## Step 2: Migrating existing code to new API [[source](step2.phpt)]
53 |
54 | Migrating old code to new API is usually easy, just add parenthesis `()` when you access value.
55 |
56 | ```php
57 | $state = OrderState::NEW();
58 |
59 | $result = '';
60 | switch ($state) {
61 | // your business logic
62 | case OrderState::NEW():
63 | $result = 'new';
64 | break;
65 | case OrderState::PROCESSING():
66 | $result = 'processing';
67 | break;
68 | }
69 |
70 | Assert::same('new', $result);
71 | ```
72 |
73 | Please note, that you will need to handle some cases manually as `OrderState::NEW()` returns object, enum instance, not a string.
74 |
75 | #### Removing old API
76 |
77 | So when you are finally ready to remove old API, just change constant visibility to `private`.
78 |
79 | ```php
80 | /**
81 | * @method static OrderState NEW()
82 | * @method static OrderState PROCESSING()
83 | */
84 | class OrderState extends \Grifart\Enum\Enum {
85 | use \Grifart\Enum\AutoInstances;
86 | private const NEW = 'new';
87 | private const PROCESSING = 'processing';
88 | }
89 | ```
90 |
91 | ## Step 3: Enjoy new features [[source](step3.phpt)]
92 |
93 | Now, when you decided that you what to move your business logic inside enum declaration. You are now free to do so. And there are many more options, see other examples.
94 |
95 |
96 |
--------------------------------------------------------------------------------
/tests/Example/MigratingLegacyCode/step0.phpt:
--------------------------------------------------------------------------------
1 | doBusinessLogic();
34 | Assert::same('new', $result);
35 |
--------------------------------------------------------------------------------
/tests/Example/OrderState/readme.md:
--------------------------------------------------------------------------------
1 | # Order state example
2 |
3 | In order state example I would like to demonstrate that there are more then one solution of domain problem of order state which can transition into another states.
4 |
5 | ## 1. Class constants
6 |
7 | [source code](refactoring-1.phpt)
8 |
9 | There are public constants on class and you should figure out that you should put them into `canDoTransition()` method. There is nothing on type-level that helps you with that. Please note that all logic is in `OrderService`.
10 |
11 | ## 2. Dumb type-safe enum
12 |
13 | [source code](refactoring-2.phpt)
14 |
15 | This test shows usage of explicitly-declared dumb-enum.
16 |
17 | I have explicitly declared type for `OrderState`. It is not possible anymore to pass non-sense values into `OrderService`. That is because `OrderState` enum provides no interface for creating non-sense values. So they simply cannot exists.
18 |
19 | All logic has been kept in `OrderService`. We still need to handle cas when someone added new value to enum, which we do not count with. (the exception in default case).
20 |
21 | ## 3. Logic moved into enum
22 |
23 | [source code](refactoring-3.phpt)
24 |
25 | Here I have moved `OrderService::canDoTransition()` method into enum itself.
26 |
27 | Nice thing is that we do not need anymore external service for asking `OrderState`-related questions.
28 |
29 | Remaining problem is that there are still lot of ifs and we still need to handle case where someone adds new value into enum which we do not count with.
30 |
31 | ## 4. Separate instance for each value
32 |
33 | [source code](refactoring-4.phpt)
34 |
35 | When there is behaviour same for all values of enum, it can be safely placed on enum class. Behaviour can be parametrized by providing necessary information in enum-value constructor.
36 |
37 | ## 5. Separate class implementation for each value
38 |
39 | [source code](refactoring-5.phpt)
40 |
41 | Now, new domain requirement:
42 |
43 | > I would like to remove person who has been assigned to work on order, when order changes state to cancelled or finished.
44 |
45 | 1. I have rewritten each value as separate class (as behaviour is different for different values)
46 | 2. I have implemented doTransition() on enum parent class as it is only proper way of changing enum value
47 | 3. I have added `onActivation(Order $order)` method, which is called whenever state transition occurs.
48 | 3. I have overridden `onActivation()` on enum values with desired behaviour.
49 |
--------------------------------------------------------------------------------
/tests/Example/OrderState/refactoring-1.phpt:
--------------------------------------------------------------------------------
1 | canDoTransition(
53 | OrderService::STATE_RECEIVED,
54 | OrderService::STATE_PROCESSING
55 | )
56 | );
57 | Assert::true(
58 | $orderService->canDoTransition(
59 | OrderService::STATE_PROCESSING,
60 | OrderService::STATE_FINISHED
61 | )
62 | );
63 |
64 | // Cancellation order flow
65 | Assert::true(
66 | $orderService->canDoTransition(
67 | OrderService::STATE_RECEIVED,
68 | OrderService::STATE_CANCELLED
69 | )
70 | );
71 |
72 | // Reflexivity test
73 | Assert::true(
74 | $orderService->canDoTransition(
75 | OrderService::STATE_CANCELLED,
76 | OrderService::STATE_CANCELLED
77 | )
78 | );
79 |
80 |
81 |
82 |
83 | // --- NEGATIVE TESTS ---
84 |
85 | // Invalid order flow
86 | Assert::false(
87 | $orderService->canDoTransition(
88 | OrderService::STATE_RECEIVED,
89 | OrderService::STATE_FINISHED
90 | )
91 | );
92 | Assert::false(
93 | $orderService->canDoTransition(
94 | OrderService::STATE_PROCESSING,
95 | OrderService::STATE_CANCELLED
96 | )
97 | );
98 | Assert::false(
99 | $orderService->canDoTransition(
100 | OrderService::STATE_FINISHED,
101 | OrderService::STATE_CANCELLED
102 | )
103 | );
104 |
105 | // check for completely invalid arguments
106 | Assert::exception(function () use ($orderService) {
107 | $orderService->canDoTransition('invalid', 'non-existing');
108 | }, \LogicException::class);
109 |
110 |
--------------------------------------------------------------------------------
/tests/Example/OrderState/refactoring-2.phpt:
--------------------------------------------------------------------------------
1 | canDoTransition(
68 | OrderState::RECEIVED(),
69 | OrderState::PROCESSING()
70 | )
71 | );
72 | Assert::true(
73 | $orderService->canDoTransition(
74 | OrderState::PROCESSING(),
75 | OrderState::FINISHED()
76 | )
77 | );
78 |
79 | // Cancellation order flow
80 | Assert::true(
81 | $orderService->canDoTransition(
82 | OrderState::RECEIVED(),
83 | OrderState::CANCELLED()
84 | )
85 | );
86 |
87 | // Reflexivity test
88 | Assert::false(
89 | $orderService->canDoTransition(
90 | OrderState::CANCELLED(),
91 | OrderState::CANCELLED()
92 | )
93 | );
94 |
95 |
96 |
97 |
98 | // --- NEGATIVE TESTS ---
99 |
100 | // Invalid order flow
101 | Assert::false(
102 | $orderService->canDoTransition(
103 | OrderState::RECEIVED(),
104 | OrderState::FINISHED()
105 | )
106 | );
107 | Assert::false(
108 | $orderService->canDoTransition(
109 | OrderState::PROCESSING(),
110 | OrderState::CANCELLED()
111 | )
112 | );
113 | Assert::false(
114 | $orderService->canDoTransition(
115 | OrderState::FINISHED(),
116 | OrderState::CANCELLED()
117 | )
118 | );
119 |
120 |
--------------------------------------------------------------------------------
/tests/Example/OrderState/refactoring-3.phpt:
--------------------------------------------------------------------------------
1 | canDoTransition(
62 | OrderState::PROCESSING()
63 | )
64 | );
65 | Assert::true(
66 | OrderState::PROCESSING()->canDoTransition(
67 | OrderState::FINISHED()
68 | )
69 | );
70 |
71 | // Cancellation order flow
72 | Assert::true(
73 | OrderState::RECEIVED()->canDoTransition(
74 | OrderState::CANCELLED()
75 | )
76 | );
77 |
78 | // Non-reflexivity test
79 | Assert::false(
80 | OrderState::CANCELLED()->canDoTransition(
81 | OrderState::CANCELLED()
82 | )
83 | );
84 |
85 |
86 |
87 |
88 | // --- NEGATIVE TESTS ---
89 |
90 | // Invalid order flow
91 | Assert::false(
92 | OrderState::RECEIVED()->canDoTransition(
93 | OrderState::FINISHED()
94 | )
95 | );
96 | Assert::false(
97 | OrderState::PROCESSING()->canDoTransition(
98 | OrderState::CANCELLED()
99 | )
100 | );
101 | Assert::false(
102 | OrderState::FINISHED()->canDoTransition(
103 | OrderState::CANCELLED()
104 | )
105 | );
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | $state1 = OrderState::RECEIVED();
116 | $state2 = OrderState::RECEIVED();
117 | Assert::true($state1 === $state2);
118 |
119 | $state3 = OrderState::PROCESSING();
120 | Assert::true($state1 !== $state3);
121 | Assert::true($state2 !== $state3);
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/tests/Example/OrderState/refactoring-4.phpt:
--------------------------------------------------------------------------------
1 | nextAllowedStates = $nextAllowedStates;
45 | }
46 |
47 | public function canDoTransition(OrderState $nextState): bool
48 | {
49 | return \in_array($nextState->toScalar(), $this->nextAllowedStates, TRUE);
50 | }
51 |
52 |
53 | /** @return self[] */
54 | final protected static function provideInstances(): array
55 | {
56 | // please not that we cannot reference self::PREPARING()
57 | // as it returns class instance, and this will call provideInstances()
58 | // again and you will get infinite loop.
59 |
60 | return [
61 | new self(self::RECEIVED, [self::PROCESSING, self::CANCELLED]),
62 | new self(self::PROCESSING, [self::FINISHED]),
63 | new self(self::FINISHED, []),
64 | new self(self::CANCELLED, []),
65 | ];
66 | }
67 | }
68 |
69 |
70 |
71 | // Standard order flow:
72 | Assert::true(
73 | OrderState::RECEIVED()->canDoTransition(
74 | OrderState::PROCESSING()
75 | )
76 | );
77 | Assert::true(
78 | OrderState::PROCESSING()->canDoTransition(
79 | OrderState::FINISHED()
80 | )
81 | );
82 |
83 | // Cancellation order flow
84 | Assert::true(
85 | OrderState::RECEIVED()->canDoTransition(
86 | OrderState::CANCELLED()
87 | )
88 | );
89 |
90 | // Non-reflexivity test
91 | Assert::false(
92 | OrderState::CANCELLED()->canDoTransition(
93 | OrderState::CANCELLED()
94 | )
95 | );
96 |
97 |
98 |
99 | // --- NEGATIVE TESTS ---
100 |
101 | // Invalid order flow
102 | Assert::false(
103 | OrderState::RECEIVED()->canDoTransition(
104 | OrderState::FINISHED()
105 | )
106 | );
107 | Assert::false(
108 | OrderState::PROCESSING()->canDoTransition(
109 | OrderState::CANCELLED()
110 | )
111 | );
112 | Assert::false(
113 | OrderState::FINISHED()->canDoTransition(
114 | OrderState::CANCELLED()
115 | )
116 | );
117 |
118 |
--------------------------------------------------------------------------------
/tests/Example/OrderState/refactoring-5.phpt:
--------------------------------------------------------------------------------
1 | state = OrderState::RECEIVED();
25 | }
26 |
27 | public function unassignEmployee(): void {
28 | $this->employee = null;
29 | }
30 |
31 | public function getEmployee(): ?string {
32 | return $this->employee;
33 | }
34 |
35 | /**
36 | * @throws InvalidTransitionException
37 | */
38 | public function changeState(OrderState $desiredState): void
39 | {
40 | $this->state =
41 | $this->state->doTransition($this, $desiredState);
42 | }
43 | }
44 |
45 | /**
46 | * @method static OrderState RECEIVED()
47 | * @method static OrderState PROCESSING()
48 | * @method static OrderState FINISHED()
49 | * @method static OrderState CANCELLED()
50 | */
51 | abstract class OrderState extends Enum {
52 |
53 | protected const
54 | RECEIVED = 'received',
55 | PROCESSING = 'processing',
56 | FINISHED = 'finished',
57 |
58 | CANCELLED = 'cancelled'; // domain logic: can be cancelled before preparation is started
59 |
60 | /**
61 | * @throws InvalidTransitionException
62 | */
63 | final public function doTransition(Order $order, OrderState $desiredState): self
64 | {
65 | if ($desiredState !== $this && !$this->canDoTransition($desiredState)) {
66 | throw new InvalidTransitionException();
67 | }
68 | $desiredState->onActivation($order);
69 | return $desiredState;
70 | }
71 |
72 |
73 | abstract public function canDoTransition(OrderState $nextState): bool;
74 |
75 | protected function onActivation(Order $order): void { /* override me */}
76 |
77 |
78 | /** @return self[] */
79 | final protected static function provideInstances(): array
80 | {
81 | return [
82 | new class(self::RECEIVED) extends OrderState {
83 |
84 | public function canDoTransition(OrderState $nextState): bool
85 | {
86 | return $nextState === $this::PROCESSING() || $nextState === $this::CANCELLED();
87 | }
88 |
89 | },
90 |
91 |
92 | new class(self::PROCESSING) extends OrderState {
93 | public function canDoTransition(OrderState $nextState): bool
94 | {
95 | return $nextState === $this::FINISHED();
96 | }
97 | },
98 |
99 |
100 | new class(self::FINISHED) extends OrderState {
101 |
102 | public function canDoTransition(OrderState $nextState): bool
103 | {
104 | return FALSE;
105 | }
106 |
107 | protected function onActivation(Order $order): void
108 | {
109 | $order->unassignEmployee();
110 | }
111 |
112 | },
113 |
114 |
115 | new class(self::CANCELLED) extends OrderState {
116 |
117 | public function canDoTransition(OrderState $nextState): bool
118 | {
119 | return FALSE;
120 | }
121 |
122 | protected function onActivation(Order $order): void
123 | {
124 | $order->unassignEmployee();
125 | }
126 | },
127 | ];
128 | }
129 | }
130 |
131 | // Standard order flow:
132 | (function() {
133 | $order = new Order();
134 | Assert::same('employee', $order->getEmployee());
135 | $order->changeState(OrderState::PROCESSING());
136 | Assert::same('employee', $order->getEmployee());
137 | $order->changeState(OrderState::FINISHED());
138 | Assert::null($order->getEmployee());
139 | })();
140 |
141 | // Cancellation order flow
142 | (function() {
143 | $order = new Order();
144 | Assert::same('employee', $order->getEmployee());
145 | $order->changeState(OrderState::CANCELLED());
146 | Assert::null($order->getEmployee());
147 | })();
148 |
149 |
150 | // --- NEGATIVE TESTS ---
151 |
152 | // Invalid order flow
153 | Assert::exception(function () {
154 | $order = new Order();
155 | $order->changeState(OrderState::FINISHED()); // not allowed
156 | }, InvalidTransitionException::class);
157 |
158 | Assert::exception(function () {
159 | $order = new Order();
160 | $order->changeState(OrderState::PROCESSING());
161 | $order->changeState(OrderState::CANCELLED()); // not allowed
162 | }, InvalidTransitionException::class);
163 |
164 | Assert::exception(function () {
165 | $order = new Order();
166 | $order->changeState(OrderState::PROCESSING());
167 | $order->changeState(OrderState::FINISHED());
168 |
169 | $order->changeState(OrderState::CANCELLED()); // not allowed
170 | }, InvalidTransitionException::class);
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/tests/Reflection/constantNames.inherited.phpt:
--------------------------------------------------------------------------------
1 | toScalar());
33 | \Tester\Assert::same('active', ReflectionConstantNames2::ACTIVE()->toScalar());
34 |
--------------------------------------------------------------------------------
/tests/Reflection/constantNames.phpt:
--------------------------------------------------------------------------------
1 | getConstantName());
18 | \Tester\Assert::same('ACTIVE', ReflectionConstantNames::ACTIVE()->getConstantName());
19 |
--------------------------------------------------------------------------------
/tests/Reflection/getAvailableValues.phpt:
--------------------------------------------------------------------------------
1 | toScalar(), 1);
32 | Assert::equal(FullClassesAsValuesEnum::VALUE2()->toScalar(), 2);
33 |
34 | Assert::type(Value1::class, FullClassesAsValuesEnum::VALUE1());
35 | Assert::type(Value2::class, FullClassesAsValuesEnum::VALUE2());
36 |
37 | Assert::same(
38 | [
39 | FullClassesAsValuesEnum::VALUE1(),
40 | FullClassesAsValuesEnum::VALUE2()
41 | ],
42 | FullClassesAsValuesEnum::getAvailableValues()
43 | );
44 |
45 | Assert::same(FullClassesAsValuesEnum::VALUE1(), FullClassesAsValuesEnum::fromScalar(1));
46 |
47 |
48 | // ## Wrong usage & edge-cases
49 |
50 | // wrong usage:
51 | $expectNonRootAccess = function(callable $fn) {
52 | Assert::exception(
53 | $fn,
54 | UsageException::class,
55 | "You have accessed static enum method on non-root class ('TestFullClasses\\FullClassesAsValuesEnum' is a root class)"
56 | );
57 | };
58 | $expectNonRootAccess(function () {
59 | Value1::getAvailableValues();
60 | });
61 | $expectNonRootAccess(function () {
62 | Value1::fromScalar('1');
63 | });
64 | //$expectNonRootAccess(function () {
65 | // Value1::VALUE1();
66 | //});
67 | //$expectNonRootAccess(function () {
68 | // Value1::VALUE2();
69 | //});
70 | Assert::type(Value1::class, Value1::VALUE1());
71 | Assert::type(Value1::class, Value2::VALUE1());
72 | Assert::type(Value2::class, Value1::VALUE2());
73 | Assert::type(Value2::class, Value2::VALUE2());
74 |
75 | // valid edge case: this is valid and accesses registers the same way as calls above
76 | Assert::same(
77 | 'VALUE1',
78 | FullClassesAsValuesEnum::VALUE1()
79 | ->getConstantName()
80 | );
81 |
82 |
--------------------------------------------------------------------------------
/tests/Regression/loose-comparison-across-types.phpt:
--------------------------------------------------------------------------------
1 | toScalar() === Enum2::VALUE()->toScalar());
26 |
27 | // everything same, but type of value is different
28 | \Tester\Assert::false(Enum1::VALUE() === Enum2::VALUE());
29 | /** @noinspection PhpNonStrictObjectEqualityInspection TypeUnsafeComparisonInspection */
30 | \Tester\Assert::false(Enum1::VALUE() == Enum2::VALUE());
31 |
--------------------------------------------------------------------------------
/tests/Regression/mixed-key-type-test.phpt:
--------------------------------------------------------------------------------
1 |