├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── UPGRADING.md ├── composer.json ├── psalm.xml.dist └── src ├── Enum.php ├── EnumDefinition.php ├── Exceptions ├── DuplicateLabelsException.php ├── DuplicateValuesException.php ├── UnknownEnumMethod.php └── UnknownEnumProperty.php ├── Faker └── FakerEnumProvider.php └── Phpunit └── EnumAssertions.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `enum` will be documented in this file 4 | 5 | ## 3.13.0 - 2022-04-22 6 | 7 | - Drop PHP 7.4 support 8 | - Add `Stringable` interface to enum class - [#116](https://github.com/spatie/enum/pull/116) 9 | 10 | ## 3.12.0 - 2022-02-05 11 | 12 | - Add support for `isset()` calls to enum `->value` und `->label` - [#109](https://github.com/spatie/enum/pull/109) 13 | 14 | ## 3.12.0 - 2022-02-05 15 | 16 | - Add support for `isset()` calls to enum `->value` und `->label` - [#109](https://github.com/spatie/enum/pull/109) 17 | 18 | ## 3.11.1 - 2021-11-25 19 | 20 | - Change attribute for PHP 8.1 compatibility 21 | 22 | ## 3.11.0 - 2021-11-25 23 | 24 | - Added support for PHP 8.1 25 | 26 | ## 3.10.0 - 2021-10-20 27 | 28 | - Fix `from()` and `tryFrom()` methods to do PHP type-juggling (string to integer) - [#108](https://github.com/spatie/enum/pull/108) 29 | - Fix `tryFrom()` method to catch scalar type-errors - [#105](https://github.com/spatie/enum/pull/105) 30 | 31 | ## 3.9.0 - 2021-05-10 32 | 33 | - Add `from()` and `tryFrom()` methods to get closer to PHP8.1 native enums - [#94](https://github.com/spatie/enum/pull/94) 34 | - Deprecate `make()` in favor of `from()` method - [#94](https://github.com/spatie/enum/pull/94) 35 | - Add flyweight pattern to return the same enum instance for every call - [#94](https://github.com/spatie/enum/pull/94) 36 | 37 | ## 3.8.0 - 2021-04-12 38 | 39 | - Add `cases()` method to retrieve all instances of the enum - [#79](https://github.com/spatie/enum/pull/79) 40 | 41 | ## 3.7.2 - 2021-03-10 42 | 43 | - Fix problem with `@readonly` annotation and annotation parsing libraries - [#92](https://github.com/spatie/enum/pull/92) 44 | 45 | ## 3.7.1 - 2021-02-12 46 | 47 | - Add description to PHPUnit assertion methods - [#88](https://github.com/spatie/enum/pull/88) 48 | 49 | ## 3.7.0 - 2021-01-13 50 | 51 | - Add ability to use a `Closure` as value/label map - [#87](https://github.com/spatie/enum/pull/87) 52 | 53 | ## 3.6.4 - 2021-01-13 54 | 55 | - Add psalm annotations to seal and lock internals - [#85](https://github.com/spatie/enum/pull/85) 56 | 57 | ## 3.6.3 - 2021-01-12 58 | 59 | - Fix extra whitespaces in enum definition doc-blocks - [#86](https://github.com/spatie/enum/pull/86) 60 | 61 | ## 3.6.2 - 2020-12-17 62 | 63 | - Fix issue with enums not callable within enum classes - [#82](https://github.com/spatie/enum/pull/82) 64 | 65 | ## 3.6.0 - 2020-11-26 66 | 67 | - Add `assertIsEnumValue()` and `assertIsEnumLabel()` to `\Spatie\Enum\Phpunit\EnumAssertions` - [#80](https://github.com/spatie/enum/pull/80) 68 | 69 | ## 3.5.1 - 2020-11-19 70 | 71 | - Fix `\Spatie\Enum\Enum` php-doc `@property-read` annotations - [#78](https://github.com/spatie/enum/pull/78) 72 | 73 | ## 3.5.0 - 2020-10-22 74 | 75 | - Add [Faker](https://github.com/fzaninotto/Faker) provider to generate random enum instances, values and labels `\Spatie\Enum\Faker\FakerEnumProvider` - [#74](https://github.com/spatie/enum/pull/74) 76 | 77 | ## 3.4.0 - 2020-10-22 78 | 79 | - Add `\Spatie\Enum\Enum::toValues()` and `\Spatie\Enum\Enum::toLabels()` methods - [#72](https://github.com/spatie/enum/pull/72) 80 | 81 | ## 3.3.0 - 2020-10-21 82 | 83 | - Add `\Spatie\Enum\Phpunit\EnumAssertions` with a default set of assertions - [#71](https://github.com/spatie/enum/pull/71) 84 | 85 | ## 3.2.0 - 2020-10-08 86 | 87 | - Support PHP ^8.0 88 | 89 | ## 3.1.2 - 2020-09-28 90 | 91 | - Don't cast value to string when serializing to JSON - [#68](https://github.com/spatie/enum/pull/68) 92 | 93 | ## 3.1.1 - 2020-08-28 94 | 95 | - Throw `TypeError` if value passed to `Enum` construct is not `string` or `integer` 96 | 97 | ## 3.1.0 - 2020-08-28 98 | 99 | [#64](https://github.com/spatie/enum/pull/64) 100 | 101 | - Add missing type-hints and doc-blocks 102 | - Fix unique values and labels 103 | - Flag `EnumDefinition` as internal 104 | 105 | ## 3.0.0 - 2020-07-22 106 | 107 | - A complete overhaul of the package, all details are discussed in [the PR](https://github.com/spatie/enum/pull/56) 108 | 109 | ## 2.3.8 - 2020-07-17 110 | 111 | - Fix for static `isXyz($value)` magic methods tto return `false` on invalid value - follow up fix to [v2.3.3](#233---2019-09-25) - [#62](https://github.com/spatie/enum/pull/62) 112 | 113 | ## 2.3.7 - 2020-06-30 114 | 115 | - Fix internal usage of `toArray()` to allow custom array representations [#58](https://github.com/spatie/enum/pull/58) 116 | 117 | ## 2.3.6 - 2020-03-11 118 | 119 | - Fix the name if it's matched but isn't the same [#50](https://github.com/spatie/enum/pull/50) 120 | 121 | ## 2.3.5 - 2020-02-11 122 | 123 | - Fix for `isEqual()` and `isAny()` method doc-tags to accept `mixed` values 124 | 125 | ## 2.3.4 - 2020-01-17 126 | 127 | - Fix for static method call passed to `__call` within the context of an object 128 | 129 | ## 2.3.3 - 2019-09-25 130 | 131 | - Allow passing invalid string values to `isEqual()` [#39](https://github.com/spatie/enum/pull/39) 132 | 133 | ## 2.3.1 - 2019-08-19 134 | 135 | - Fix `protected` method calls to allow overrides [#37](https://github.com/spatie/enum/pull/37) 136 | 137 | ## 2.3.0 - 2019-08-05 138 | 139 | - Make `\Spatie\Enum\Enumerable::isValidIndex/Name/Value()` methods public [#36](https://github.com/spatie/enum/pull/36) 140 | - > Please note that this could be breaking for custom implementations of the `\Spatie\Enum\Enumerable` interface. 141 | - 142 | - 143 | - 144 | 145 | ## 2.2.0 - 2019-07-18 146 | 147 | - Add `\Spatie\Enum\Enum::getAll()` method [#33](https://github.com/spatie/enum/pull/33) 148 | 149 | ## 2.1.2 - 2019-05-07 150 | 151 | - Fix calling public non-static methods [#32](https://github.com/spatie/enum/pull/32) 152 | 153 | ## 2.1.1 - 2019-04-18 154 | 155 | - Fix overriden existing public static methods like `Enum::toArray()` [#29](https://github.com/spatie/enum/pull/29) 156 | 157 | ## 2.1.0 - 2019-04-17 158 | 159 | - Add enum map index and value `Enum::MAP_INDEX` and `Enum::MAP_VALUE` [#25](https://github.com/spatie/enum/pull/25) 160 | 161 | ## 2.0.1 - 2019-04-08 162 | 163 | - Improved static analysis support for `::make` 164 | 165 | ## 2.0.0 - 2019-04-01 166 | 167 | A full major rework of the `Enum` class - we try to list all changes, for more details you can check out the [PR](https://github.com/spatie/enum/pull/18) and the [Issue](https://github.com/spatie/enum/issues/10). 168 | 169 | - Add `\Spatie\Enum\Enumerable` interface 170 | - Add `\Spatie\Enum\Exceptions\DuplicatedIndexException`, `\Spatie\Enum\Exceptions\DuplicatedValueException`, `\Spatie\Enum\Exceptions\InvalidIndexException` and `\Spatie\Enum\Exceptions\InvalidValueException` exceptions 171 | - Add `\Spatie\Enum\Enum-&gt;getIndex()` method 172 | - Add `\Spatie\Enum\Enum::getIndices()` method 173 | - Add `\Spatie\Enum\Enum-&gt;getValue()` method 174 | - Add `\Spatie\Enum\Enum::getValues()` method 175 | - Rename `\Spatie\Enum\Enum::from()` to `\Spatie\Enum\Enum::make()` 176 | - Rename `\Spatie\Enum\Enum::equals()` to `\Spatie\Enum\Enum::isEqual()` 177 | - Rename `\Spatie\Enum\Enum::isOneOf()` to `\Spatie\Enum\Enum::isAny()` 178 | - Change `\Spatie\Enum\Enum-&gt;__construct()` signature and responsibility - only take index & value and validate them 179 | - Change `\Spatie\Enum\Enum::toArray()` return value instead of an array of `value =&gt; name` it returns `value =&gt; index` 180 | - Drop recursive `\Spatie\Enum\Enum::make()` support from inside of an unstatic method 181 | - Drop `\Spatie\Enum\Enum::$map` in favor of `\Spatie\Enum\Enum-&gt;getIndex()`and `\Spatie\Enum\Enum-&gt;getValue()` 182 | - Update all methods have strict type checks: `index: int` and `value: string` 183 | - Update all methods are compatible with all required types: index, value, name or instance of Enum 184 | 185 | ## 1.1.0 - 2019-03-18 186 | 187 | - Add support for is\* checks 188 | 189 | ## 1.0.2 - 2019-03-18 190 | 191 | - Support case insensitive enum values (#13) 192 | 193 | ## 1.0.0 - 2019-02-08 194 | 195 | - initial release 196 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enum 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/enum.svg?style=flat-square)](https://packagist.org/packages/spatie/enum) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/enum/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/spatie/enum/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/spatie/enum/php-cs-fixer.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/spatie/enum/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/enum.svg?style=flat-square)](https://packagist.org/packages/spatie/enum) 7 | 8 | > :warning: With the introduction of **Enums** in PHP 8.1, this package is now considered obsolete. For new projects, we recommend using **native enums** instead of this package. Learn more about native enums here: https://stitcher.io/blog/php-enums 9 | 10 | --- 11 | 12 | This package offers strongly typed enums in PHP. In this package, enums are always objects, never constant values on their own. This allows for proper static analysis and refactoring in IDEs. 13 | 14 | Here's how enums are created with this package: 15 | 16 | ```php 17 | use \Spatie\Enum\Enum; 18 | 19 | /** 20 | * @method static self draft() 21 | * @method static self published() 22 | * @method static self archived() 23 | */ 24 | class StatusEnum extends Enum 25 | { 26 | } 27 | ``` 28 | 29 | And this is how they are used: 30 | 31 | ```php 32 | public function setStatus(StatusEnum $status): void 33 | { 34 | $this->status = $status; 35 | } 36 | 37 | // ... 38 | 39 | $class->setStatus(StatusEnum::draft()); 40 | ``` 41 | 42 | ## Support us 43 | 44 | [](https://spatie.be/github-ad-click/enum) 45 | 46 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 47 | 48 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 49 | 50 | ## Installation 51 | 52 | You can install the package via composer: 53 | 54 | ```bash 55 | composer require spatie/enum 56 | ``` 57 | 58 | ## Usage 59 | 60 | This is how an enum can be defined. 61 | 62 | ```php 63 | /** 64 | * @method static self draft() 65 | * @method static self published() 66 | * @method static self archived() 67 | */ 68 | class StatusEnum extends Enum 69 | { 70 | } 71 | ``` 72 | 73 | This is how they are used: 74 | 75 | ```php 76 | public function setStatus(StatusEnum $status) 77 | { 78 | $this->status = $status; 79 | } 80 | 81 | // ... 82 | 83 | $class->setStatus(StatusEnum::draft()); 84 | ``` 85 | 86 | | Autocompletion | Refactoring | 87 | | ---------------------------- | ------------------------ | 88 | | ![](./docs/autocomplete.gif) | ![](./docs/refactor.gif) | 89 | 90 | ### Creating an enum from a value 91 | 92 | ```php 93 | $status = StatusEnum::from('draft'); 94 | ``` 95 | 96 | When an enum value doesn't exist, you'll get a `BadMethodCallException`. If you would prefer not catching an exception, you can use: 97 | 98 | ```php 99 | $status = StatusEnum::tryFrom('draft'); 100 | ``` 101 | 102 | When an enum value doesn't exist in this case, `$status` will be `null`. 103 | 104 | The only time you want to construct an enum from a value is when unserializing them from eg. a database. 105 | 106 | If you want to get the value of an enum to store it, you can do this: 107 | 108 | ```php 109 | $status->value; 110 | ``` 111 | 112 | Note that `value` is a read-only property, it cannot be changed. 113 | 114 | ### Enum values 115 | 116 | By default, the enum value is its method name. You can however override it, for example if you want to store enums as integers in a database, instead of using their method name. 117 | 118 | ```php 119 | /** 120 | * @method static self draft() 121 | * @method static self published() 122 | * @method static self archived() 123 | */ 124 | class StatusEnum extends Enum 125 | { 126 | protected static function values(): array 127 | { 128 | return [ 129 | 'draft' => 1, 130 | 'published' => 2, 131 | 'archived' => 3, 132 | ]; 133 | } 134 | } 135 | ``` 136 | 137 | An enum value doesn't have to be a `string`, as you can see in the example it can also be an `int`. 138 | 139 | Note that you don't need to override all values. Rather, you only need to override the ones that you want to be different from the default. 140 | 141 | If you have a logic that should be applied to all method names to get the value, like lowercase them, you can return a `Closure`. 142 | 143 | ```php 144 | /** 145 | * @method static self DRAFT() 146 | * @method static self PUBLISHED() 147 | * @method static self ARCHIVED() 148 | */ 149 | class StatusEnum extends Enum 150 | { 151 | protected static function values(): Closure 152 | { 153 | return function(string $name): string|int { 154 | return mb_strtolower($name); 155 | }; 156 | } 157 | } 158 | ``` 159 | 160 | ### Enum labels 161 | 162 | Enums can be given a label, you can do this by overriding the `labels` method. 163 | 164 | ```php 165 | /** 166 | * @method static self draft() 167 | * @method static self published() 168 | * @method static self archived() 169 | */ 170 | class StatusEnum extends Enum 171 | { 172 | protected static function labels(): array 173 | { 174 | return [ 175 | 'draft' => 'my draft label', 176 | ]; 177 | } 178 | } 179 | ``` 180 | 181 | Note that you don't need to override all labels, the default label will be the enum's value. 182 | 183 | If you have a logic that should be applied to all method names to get the label, like lowercase them, you can return a `Closure` as in the value example. 184 | 185 | You can access an enum's label like so: 186 | 187 | ```php 188 | $status->label; 189 | ``` 190 | 191 | Note that `label` is a read-only property, it cannot be changed. 192 | 193 | ### Comparing enums 194 | 195 | Enums can be compared using the `equals` method: 196 | 197 | ```php 198 | $status->equals(StatusEnum::draft()); 199 | ``` 200 | 201 | You can pass several enums to the `equals` method, it will return `true` if the current enum equals one of the given values. 202 | 203 | ```php 204 | $status->equals(StatusEnum::draft(), StatusEnum::archived()); 205 | ``` 206 | 207 | ### Phpunit Assertions 208 | 209 | This package provides an abstract class `Spatie\Enum\Phpunit\EnumAssertions` with some basic/common assertions if you have to test enums. 210 | 211 | ```php 212 | use Spatie\Enum\Phpunit\EnumAssertions; 213 | 214 | EnumAssertions::assertIsEnum($post->status); // checks if actual extends Enum::class 215 | EnumAssertions::assertIsEnumValue(StatusEnum::class, 'draft'); // checks if actual is a value of given enum 216 | EnumAssertions::assertIsEnumLabel(StatusEnum::class, 'draft'); // checks if actual is a label of given enum 217 | EnumAssertions::assertEqualsEnum(StatusEnum::draft(), 'draft'); // checks if actual (transformed to enum) equals expected 218 | EnumAssertions::assertSameEnum(StatusEnum::draft(), $post->status); // checks if actual is same as expected 219 | EnumAssertions::assertSameEnumValue(StatusEnum::draft(), 1); // checks if actual is same value as expected 220 | EnumAssertions::assertSameEnumLabel(StatusEnum::draft(), 'draft'); // checks if actual is same label as expected 221 | ``` 222 | 223 | ### Faker Provider 224 | 225 | Possibly you are using [faker](https://github.com/FakerPHP/Faker) and want to generate random enums. 226 | Because doing so with default faker is a lot of copy'n'paste we've got you covered with a faker provider `Spatie\Enum\Faker\FakerEnumProvider`. 227 | 228 | ```php 229 | use Spatie\Enum\Faker\FakerEnumProvider; 230 | use Faker\Generator as Faker; 231 | 232 | /** @var Faker|FakerEnumProvider $faker */ 233 | $faker = new Faker(); 234 | $faker->addProvider(new FakerEnumProvider($faker)); 235 | 236 | $enum = $faker->randomEnum(StatusEnum::class); 237 | $value = $faker->randomEnumValue(StatusEnum::class); 238 | $label = $faker->randomEnumLabel(StatusEnum::class); 239 | ``` 240 | 241 | ## Testing 242 | 243 | ```bash 244 | composer test 245 | ``` 246 | 247 | ## Changelog 248 | 249 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 250 | 251 | ## Contributing 252 | 253 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 254 | 255 | ### Security 256 | 257 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 258 | 259 | ## Postcardware 260 | 261 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 262 | 263 | Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. 264 | 265 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 266 | 267 | ## Credits 268 | 269 | - [Brent Roose](https://github.com/brendt) 270 | - [All Contributors](../../contributors) 271 | 272 | ## License 273 | 274 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 275 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | ## To v3 2 | 3 | v3 is a major simplification of the package, and focuses only on the core enum functionality. The base enum implementation isn't changed, but there are changes to value/index and label/name mappings. 4 | 5 | - The enum `index` is removed. Enum values can be remapped by implementing the `values` method. Please refer to the [README](./README.md) for more information. 6 | - What used to be called `value` in v2 is now simply called `label`. Labels can be mapped like values, by implementing the `labels` method. Please refer to the [README](./README.md) for more information. 7 | - The `isEqual` method has been renamed to `equals`, and now only accept `Enum` objects, no more raw strings or ints. 8 | - You can now also pass multiple enums to the `equals` method. Please refer to the [README](./README.md) for more information. 9 | - Enum specific methods aren't supported anymore. If you want that kind of functionality, please look at [`spatie/laravel-model-states`](https://github.com/spatie/laravel-model-states), or the state pattern in general. 10 | - Static equal methods are removed 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/enum", 3 | "description": "PHP Enums", 4 | "license": "MIT", 5 | "keywords": [ 6 | "spatie", 7 | "enum", 8 | "enumerable" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Brent Roose", 13 | "email": "brent@spatie.be", 14 | "homepage": "https://spatie.be", 15 | "role": "Developer" 16 | }, 17 | { 18 | "name": "Tom Witkowski", 19 | "email": "dev@gummibeer.de", 20 | "homepage": "https://gummibeer.de", 21 | "role": "Developer" 22 | } 23 | ], 24 | "homepage": "https://github.com/spatie/enum", 25 | "support": { 26 | "issues": "https://github.com/spatie/enum/issues", 27 | "source": "https://github.com/spatie/enum", 28 | "docs": "https://docs.spatie.be/enum" 29 | }, 30 | "require": { 31 | "php": "^8.0", 32 | "ext-json": "*" 33 | }, 34 | "require-dev": { 35 | "fakerphp/faker": "^1.9.1", 36 | "larapack/dd": "^1.1", 37 | "pestphp/pest": "^1.22", 38 | "phpunit/phpunit": "^9.0", 39 | "vimeo/psalm": "^4.3" 40 | }, 41 | "suggest": { 42 | "fakerphp/faker": "To use the enum faker provider", 43 | "phpunit/phpunit": "To use the enum assertions" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Spatie\\Enum\\": "src" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Spatie\\Enum\\Tests\\": "tests" 53 | } 54 | }, 55 | "config": { 56 | "sort-packages": true, 57 | "allow-plugins": { 58 | "pestphp/pest-plugin": true 59 | } 60 | }, 61 | "scripts": { 62 | "psalm": "vendor/bin/psalm -c psalm.xml --show-info=true", 63 | "test": "vendor/bin/pest" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Enum.php: -------------------------------------------------------------------------------- 1 | > */ 35 | private static array $definitionCache = []; 36 | 37 | /** @psalm-var array> */ 38 | private static array $instances = []; 39 | 40 | /** 41 | * @return static[] 42 | */ 43 | public static function cases(): array 44 | { 45 | $instances = array_map( 46 | fn (EnumDefinition $definition): Enum => static::from($definition->value), 47 | static::resolveDefinition() 48 | ); 49 | 50 | return array_values($instances); 51 | } 52 | 53 | /** 54 | * @return string[] 55 | * @psalm-return array 56 | */ 57 | public static function toArray(): array 58 | { 59 | $array = []; 60 | 61 | foreach (static::resolveDefinition() as $definition) { 62 | $array[$definition->value] = $definition->label; 63 | } 64 | 65 | return $array; 66 | } 67 | 68 | /** 69 | * @return string[]|int[] 70 | */ 71 | public static function toValues(): array 72 | { 73 | return array_keys(static::toArray()); 74 | } 75 | 76 | /** 77 | * @return string[] 78 | */ 79 | public static function toLabels(): array 80 | { 81 | return array_values(static::toArray()); 82 | } 83 | 84 | /** 85 | * @param string|int $value 86 | * 87 | * @return static 88 | * @deprecated Use `from()` instead 89 | */ 90 | public static function make($value): Enum 91 | { 92 | return static::from($value); 93 | } 94 | 95 | /** 96 | * @param string|int $value 97 | * 98 | * @return static 99 | */ 100 | final public static function from($value): Enum 101 | { 102 | $enum = new static($value); 103 | 104 | if (!isset(self::$instances[static::class][$enum->value])) { 105 | self::$instances[static::class][$enum->value] = $enum; 106 | } 107 | 108 | return self::$instances[static::class][$enum->value]; 109 | } 110 | 111 | /** 112 | * @param string|int|mixed $value 113 | * 114 | * @return static|null 115 | */ 116 | final public static function tryFrom($value): ?Enum 117 | { 118 | try { 119 | return static::from($value); 120 | } catch (BadMethodCallException $exception) { 121 | return null; 122 | } catch (TypeError $exception) { 123 | if ( 124 | $value === null 125 | || is_scalar($value) 126 | || (is_object($value) && method_exists($value, '__toString')) 127 | ) { 128 | return null; 129 | } 130 | 131 | throw $exception; 132 | } 133 | } 134 | 135 | /** 136 | * @param string|int $value 137 | * 138 | * @internal 139 | */ 140 | public function __construct($value) 141 | { 142 | if (is_object($value) && method_exists($value, '__toString')) { 143 | $value = (string)$value; 144 | } 145 | 146 | if (! (is_int($value) || is_string($value))) { 147 | $enumClass = static::class; 148 | 149 | throw new TypeError("Only string and integer are allowed values for enum {$enumClass}."); 150 | } 151 | 152 | $definition = $this->findDefinition($value); 153 | 154 | if ($definition === null) { 155 | $enumClass = static::class; 156 | 157 | throw new BadMethodCallException("There's no value {$value} defined for enum {$enumClass}, consider adding it in the docblock definition."); 158 | } 159 | 160 | $this->value = $definition->value; 161 | $this->label = $definition->label; 162 | } 163 | 164 | /** 165 | * @param string $name 166 | * 167 | * @return int|string 168 | * 169 | * @throws UnknownEnumProperty 170 | */ 171 | public function __get(string $name) 172 | { 173 | if ($name === 'label') { 174 | return $this->label; 175 | } 176 | 177 | if ($name === 'value') { 178 | return $this->value; 179 | } 180 | 181 | throw UnknownEnumProperty::new(static::class, $name); 182 | } 183 | 184 | /** 185 | * @param string $name 186 | * 187 | * @return bool 188 | */ 189 | public function __isset(string $name): bool 190 | { 191 | return $name === 'label' || $name === 'value'; 192 | } 193 | 194 | /** 195 | * @param string $name 196 | * @param array $arguments 197 | * 198 | * @return static 199 | */ 200 | public static function __callStatic(string $name, array $arguments) 201 | { 202 | return static::from($name); 203 | } 204 | 205 | /** 206 | * @param string $name 207 | * @param array $arguments 208 | * 209 | * @return bool|mixed 210 | * 211 | * @throws UnknownEnumMethod 212 | */ 213 | public function __call(string $name, array $arguments) 214 | { 215 | if (strpos($name, 'is') === 0) { 216 | $other = static::from(substr($name, 2)); 217 | 218 | return $this->equals($other); 219 | } 220 | 221 | return self::__callStatic($name, $arguments); 222 | } 223 | 224 | public function equals(Enum ...$others): bool 225 | { 226 | foreach ($others as $other) { 227 | if ( 228 | get_class($this) === get_class($other) 229 | && $this->value === $other->value 230 | ) { 231 | return true; 232 | } 233 | } 234 | 235 | return false; 236 | } 237 | 238 | /** 239 | * @return string[]|int[]|Closure 240 | * @psalm-return array | Closure(string):(int|string) 241 | */ 242 | protected static function values() 243 | { 244 | return []; 245 | } 246 | 247 | /** 248 | * @return string[]|Closure 249 | * @psalm-return array | Closure(string):string 250 | */ 251 | protected static function labels() 252 | { 253 | return []; 254 | } 255 | 256 | /** 257 | * @param string|int $input 258 | * 259 | * @return \Spatie\Enum\EnumDefinition|null 260 | */ 261 | private function findDefinition($input): ?EnumDefinition 262 | { 263 | foreach (static::resolveDefinition() as $definition) { 264 | if ($definition->equals($input)) { 265 | return $definition; 266 | } 267 | } 268 | 269 | return null; 270 | } 271 | 272 | /** 273 | * @return \Spatie\Enum\EnumDefinition[] 274 | */ 275 | private static function resolveDefinition(): array 276 | { 277 | $className = static::class; 278 | 279 | if (static::$definitionCache[$className] ?? null) { 280 | return static::$definitionCache[$className]; 281 | } 282 | 283 | $reflectionClass = new ReflectionClass($className); 284 | 285 | $docComment = $reflectionClass->getDocComment(); 286 | 287 | preg_match_all('/@method\s+static\s+self\s+([\w_]+)\(\)/', $docComment, $matches); 288 | 289 | $definition = []; 290 | 291 | $valueMap = static::values(); 292 | 293 | if ($valueMap instanceof Closure) { 294 | $valueMap = array_map($valueMap, array_combine($matches[1], $matches[1])); 295 | } 296 | 297 | $labelMap = static::labels(); 298 | 299 | if ($labelMap instanceof Closure) { 300 | $labelMap = array_map($labelMap, array_combine($matches[1], $matches[1])); 301 | } 302 | 303 | foreach ($matches[1] as $methodName) { 304 | $value = $valueMap[$methodName] = $valueMap[$methodName] ?? $methodName; 305 | 306 | $label = $labelMap[$methodName] = $labelMap[$methodName] ?? $methodName; 307 | 308 | $definition[$methodName] = new EnumDefinition($methodName, $value, $label); 309 | } 310 | 311 | if (self::arrayHasDuplicates($valueMap)) { 312 | throw new DuplicateValuesException(static::class); 313 | } 314 | 315 | if (self::arrayHasDuplicates($labelMap)) { 316 | throw new DuplicateLabelsException(static::class); 317 | } 318 | 319 | return static::$definitionCache[$className] ??= $definition; 320 | } 321 | 322 | private static function arrayHasDuplicates(array $array): bool 323 | { 324 | return count($array) > count(array_unique($array)); 325 | } 326 | 327 | #[\ReturnTypeWillChange] 328 | public function jsonSerialize() 329 | { 330 | return $this->value; 331 | } 332 | 333 | public function __toString(): string 334 | { 335 | return (string) $this->value; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/EnumDefinition.php: -------------------------------------------------------------------------------- 1 | methodName = strtolower($methodName); 27 | $this->value = $value; 28 | $this->label = $label; 29 | } 30 | 31 | /** 32 | * @param string|int $input 33 | * 34 | * @return bool 35 | */ 36 | public function equals($input): bool 37 | { 38 | if ($this->value === $input) { 39 | return true; 40 | } 41 | 42 | if (is_string($input) && is_int($this->value) && $input === (string)$this->value) { 43 | return true; 44 | } 45 | 46 | if (is_string($input) && $this->methodName === strtolower($input)) { 47 | return true; 48 | } 49 | 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Exceptions/DuplicateLabelsException.php: -------------------------------------------------------------------------------- 1 | $enum 33 | * @param string|mixed $actual 34 | * @param string|null $message 35 | */ 36 | public static function assertIsEnumValue(string $enum, $actual, ?string $message = null): void 37 | { 38 | if (! is_subclass_of($enum, Enum::class)) { 39 | throw new InvalidArgumentException(sprintf('The `$enum` argument has to be a FQCN to an `%s` class', Enum::class)); 40 | } 41 | 42 | PHPUnit::assertContains( 43 | $actual, 44 | forward_static_call([$enum, 'toValues']), 45 | $message ?? '' 46 | ); 47 | } 48 | 49 | /** 50 | * Checks if actual is a label of the given enum class name. 51 | * 52 | * @param string $enum 53 | * @psalm-param class-string<\Spatie\Enum\Enum> $enum 54 | * @param string|mixed $actual 55 | * @param string|null $message 56 | */ 57 | public static function assertIsEnumLabel(string $enum, $actual, ?string $message = null): void 58 | { 59 | if (! is_subclass_of($enum, Enum::class)) { 60 | throw new InvalidArgumentException(sprintf('The `$enum` argument has to be a FQCN to an `%s` class', Enum::class)); 61 | } 62 | 63 | PHPUnit::assertContains( 64 | $actual, 65 | forward_static_call([$enum, 'toLabels']), 66 | $message ?? '' 67 | ); 68 | } 69 | 70 | /** 71 | * Checks if actual (after being transformed to enum) equals expected. 72 | * 73 | * @param Enum $expected 74 | * @param Enum|string|int|mixed $actual 75 | * @param string|null $message 76 | */ 77 | public static function assertEqualsEnum(Enum $expected, $actual, ?string $message = null): void 78 | { 79 | try { 80 | $enum = static::asEnum($actual, $expected); 81 | 82 | PHPUnit::assertTrue( 83 | $expected->equals($enum), 84 | $message ?? '' 85 | ); 86 | } catch (TypeError $ex) { 87 | PHPUnit::assertTrue(false, $ex->getMessage()); 88 | } catch (BadMethodCallException $ex) { 89 | PHPUnit::assertTrue(false, $ex->getMessage()); 90 | } 91 | } 92 | 93 | /** 94 | * Checks if actual equals expected. 95 | * 96 | * @param Enum $expected 97 | * @param Enum|mixed $actual 98 | * @param string|null $message 99 | */ 100 | public static function assertSameEnum(Enum $expected, $actual, ?string $message = null): void 101 | { 102 | static::assertIsEnum($actual); 103 | 104 | PHPUnit::assertTrue( 105 | $expected->equals($actual), 106 | $message ?? '' 107 | ); 108 | } 109 | 110 | /** 111 | * Checks if actual equals the value of expected. 112 | * 113 | * @param Enum $expected 114 | * @param string|int|mixed $actual 115 | * @param string|null $message 116 | */ 117 | public static function assertSameEnumValue(Enum $expected, $actual, ?string $message = null): void 118 | { 119 | PHPUnit::assertSame( 120 | $expected->value, 121 | $actual, 122 | $message ?? '' 123 | ); 124 | } 125 | 126 | /** 127 | * Checks if actual equals the label of expected. 128 | * 129 | * @param Enum $expected 130 | * @param string|mixed $actual 131 | * @param string|null $message 132 | */ 133 | public static function assertSameEnumLabel(Enum $expected, $actual, ?string $message = null): void 134 | { 135 | PHPUnit::assertSame( 136 | $expected->label, 137 | $actual, 138 | $message ?? '' 139 | ); 140 | } 141 | 142 | /** 143 | * @param int|string|Enum $value 144 | * @param Enum $enum 145 | * 146 | * @return Enum 147 | * 148 | * @throws TypeError 149 | * @throws BadMethodCallException 150 | * 151 | * @see \Spatie\Enum\Enum::make() 152 | */ 153 | protected static function asEnum($value, Enum $enum): Enum 154 | { 155 | if ($value instanceof Enum) { 156 | return $value; 157 | } 158 | 159 | return forward_static_call( 160 | [get_class($enum), 'make'], 161 | $value 162 | ); 163 | } 164 | } 165 | --------------------------------------------------------------------------------