├── .php-cs-fixer.php ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── UPGRADE.md ├── composer.json ├── extension.neon ├── lang └── en │ └── messages.php ├── phpstan.neon ├── rector-rules.md └── src ├── Attributes └── Description.php ├── Casts └── EnumCast.php ├── Commands ├── EnumAnnotateCommand.php ├── EnumToNativeCommand.php ├── MakeEnumCommand.php └── stubs │ ├── enum.flagged.stub │ └── enum.stub ├── Contracts ├── EnumContract.php └── LocalizedEnum.php ├── Enum.php ├── EnumServiceProvider.php ├── EnumType.php ├── Exceptions ├── InvalidEnumKeyException.php └── InvalidEnumMemberException.php ├── FlaggedEnum.php ├── PHPStan ├── EnumMethodReflection.php ├── EnumMethodsClassReflectionExtension.php └── UniqueValuesRule.php ├── Rector ├── ToNativeImplementationRector.php ├── ToNativeRector.php ├── ToNativeUsagesRector.php ├── implementation.php ├── usages-and-implementation.php └── usages.php ├── Rules ├── Enum.php ├── EnumKey.php └── EnumValue.php └── Traits └── QueriesFlaggedEnums.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->name('*.php') 6 | ->notPath('vendor') 7 | ->notPath('tests/Enums/Annotate') // Generated 8 | ->notPath('tests/Enums/AnnotateFixtures') // Matches laminas/laminas-code 9 | ->ignoreDotFiles(false) 10 | ->ignoreVCS(true); 11 | 12 | return MLL\PhpCsFixerConfig\risky($finder); 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ## 6.12.1 11 | 12 | ### Fixed 13 | 14 | - Avoid false-positive addition of `->value` in `enum:to-native` 15 | 16 | ## 6.12.0 17 | 18 | ### Added 19 | 20 | - Support Laravel 11 21 | - Support Rector 2 22 | 23 | ## 6.11.1 24 | 25 | ### Fixed 26 | 27 | - Fix conversion of `in()` and `notIn()` to native enums when called with non-arrays 28 | 29 | ## 6.11.0 30 | 31 | ### Added 32 | 33 | - Support Laravel 11 34 | 35 | ## 6.10.0 36 | 37 | ### Added 38 | 39 | - Allow Allow installation alongside PHPUnit 11 40 | 41 | ## 6.9.1 42 | 43 | ### Fixed 44 | 45 | - Check if value is `int` or `string` in conversion of `Enum::hasValue()` to native enum 46 | 47 | ## 6.9.0 48 | 49 | ### Added 50 | 51 | - Add conversion of `Enum::hasValue()` to native enum 52 | 53 | ## 6.8.0 54 | 55 | ### Changed 56 | 57 | - Make `php artisan enum:to-native` compatible with rector `0.19` 58 | 59 | ## 6.7.0 60 | 61 | ### Added 62 | 63 | - Add PHPStan rule to detect duplicate enum values 64 | 65 | ## 6.6.4 66 | 67 | ### Fixed 68 | 69 | - Fix conversion of `Enum::fromKey()` to native enum 70 | 71 | ## 6.6.3 72 | 73 | ### Fixed 74 | 75 | - Remove leading backslash in class names passed to `php artisan enum:to-native` 76 | 77 | ## 6.6.2 78 | 79 | ### Fixed 80 | 81 | - Convert single classes in one step with `php artisan enum:to-native` 82 | 83 | ## 6.6.1 84 | 85 | ### Fixed 86 | 87 | - Disable timeout of rector calls in `php artisan enum:to-native` 88 | 89 | ## 6.6.0 90 | 91 | ### Changed 92 | 93 | - Use command `enum:to-native` for simplified one-step conversion of classes that extend `BenSampo\Enum\Enum` to native PHP enums 94 | 95 | ## 6.5.0 96 | 97 | ### Added 98 | 99 | - Add Rector rules for conversion of classes that extend `BenSampo\Enum\Enum` to native PHP enums 100 | 101 | ### Deprecated 102 | 103 | - Deprecate command `enum:to-native` in favor of Rector conversion 104 | 105 | ## 6.4.1 106 | 107 | ### Fixed 108 | 109 | - Ensure validation rules are always added https://github.com/BenSampo/laravel-enum/pull/327 110 | 111 | ## 6.4.0 112 | 113 | ### Added 114 | 115 | - Add command `enum:to-native` to convert a class that extends `BenSampo\Enum\Enum` to a native PHP enum 116 | 117 | ### Fixed 118 | 119 | - Load pipe-string syntax validation translations lazily https://github.com/BenSampo/laravel-enum/pull/324 120 | 121 | ## 6.3.3 122 | 123 | ### Fixed 124 | 125 | - Allow `mixed` in `Enum::hasValue()` 126 | 127 | ## 6.3.2 128 | 129 | ### Fixed 130 | 131 | - Preserve whitespace in PHPDocs when running `enum:annotate` command 132 | 133 | ## 6.3.1 134 | 135 | ### Fixed 136 | 137 | - Mark `Enum::$key` and `Enum::$description` as non-nullable in `Enum` and document they are unset in `FlaggedEnum` 138 | 139 | ## [6.3.0](https://github.com/BenSampo/laravel-enum/compare/v6.2.2...v6.3.0) - 2023-01-31 140 | 141 | ### Added 142 | 143 | - Support Laravel 10 [298](https://github.com/BenSampo/laravel-enum/pull/298) 144 | 145 | ## [6.2.2](https://github.com/BenSampo/laravel-enum/compare/v6.2.1...v6.2.2) - 2023-01-17 146 | 147 | ### Fixed 148 | 149 | - Fix backtrack regexp error and add Windows EOL support to the annotate command [296](https://github.com/BenSampo/laravel-enum/pull/296) 150 | 151 | ## [6.2.1](https://github.com/BenSampo/laravel-enum/compare/v6.2.0...v6.2.1) - 2023-01-12 152 | 153 | ### Fixed 154 | 155 | - Fix running `php artisan enum:annotate` on long enum class [294](https://github.com/BenSampo/laravel-enum/pull/294) 156 | 157 | ## [6.2.0](https://github.com/BenSampo/laravel-enum/compare/v6.1.0...v6.2.0) - 2022-12-07 158 | 159 | ### Changed 160 | 161 | - Open `EnumServiceProvider` for customization [292](https://github.com/BenSampo/laravel-enum/pull/292) 162 | 163 | ## [6.1.0](https://github.com/BenSampo/laravel-enum/compare/v6.0.0...v6.1.0) - 2022-10-26 164 | 165 | ### Changed 166 | 167 | - Eliminate unnecessary abstract class `AbstractAnnotationCommand` [283](https://github.com/BenSampo/laravel-enum/pull/283) 168 | 169 | ### Fixed 170 | 171 | - Provide more accurate type hints in `Enum` and `FlaggedEnum` [283](https://github.com/BenSampo/laravel-enum/pull/283) 172 | - Accept `FlaggedEnum` instances in `QueriesFlaggedEnums` scopes [283](https://github.com/BenSampo/laravel-enum/pull/283) 173 | 174 | ## [6.0.0](https://github.com/BenSampo/laravel-enum/compare/v5.3.1...v6.0.0) - 2022-08-22 175 | 176 | ### Added 177 | 178 | - Allow Description attribute usage on class [270](https://github.com/BenSampo/laravel-enum/pull/270) 179 | - Add generic type `TValue` to `Enum` class 180 | 181 | ### Changed 182 | 183 | - Require composer/class-map-generator over composer/composer [268](https://github.com/BenSampo/laravel-enum/pull/268) 184 | - Use native types whenever possible 185 | - Throw when calling `Enum::getDescription()` with invalid values 186 | - Expect class-string in `InvalidEnumMemberException` constructor 187 | 188 | ### Fixed 189 | 190 | - Leverage late static binding for instantiation methods in PHPStan extension 191 | 192 | ### Removed 193 | 194 | - Remove `Enum::getInstance()` in favor or `Enum::fromValue()` 195 | 196 | ## [5.3.1](https://github.com/BenSampo/laravel-enum/compare/v5.3.0...v5.3.1) - 2022-06-22 197 | 198 | ### Fixed 199 | 200 | - Narrow property type hints [258](https://github.com/BenSampo/laravel-enum/pull/258) 201 | 202 | ## [5.3.0](https://github.com/BenSampo/laravel-enum/compare/v5.2.0...v5.3.0) - 2022-04-08 203 | 204 | ### Fixed 205 | 206 | - Return value for invalid enum case when using the `Description` attribute [264](https://github.com/BenSampo/laravel-enum/pull/264) 207 | 208 | ### Fixed 209 | 210 | - Type-hint `Enum::$key` and `Enum::$description` as `string` 211 | - Type-hint `FlaggedEnum::$value` as `int` 212 | 213 | ## [5.2.0](https://github.com/BenSampo/laravel-enum/compare/v5.1.0...v5.2.0) - 2022-03-11 214 | 215 | ### Fixed 216 | 217 | - Publish language definitions to `lang` directory [254](https://github.com/BenSampo/laravel-enum/pull/254) 218 | 219 | ### Added 220 | 221 | - Restore enum instance from `var_export()` [252](https://github.com/BenSampo/laravel-enum/pull/252) 222 | 223 | ## [5.1.0](https://github.com/BenSampo/laravel-enum/compare/v5.0.0...v5.1.0) - 2022-02-09 224 | 225 | ### Added 226 | 227 | - Ability to define enum case descriptions using `Description` attribute. 228 | 229 | ## [5.0.0](https://github.com/BenSampo/laravel-enum/compare/v4.2.0...v5.0.0) - 2022-02-09 230 | 231 | ### Added 232 | 233 | - Support for Laravel 9 234 | 235 | ### Changed 236 | 237 | - The `annotate` command now uses composer to parse directories for instances of enums instead of `hanneskod/classtools` 238 | 239 | ### Removed 240 | 241 | - Removed old `CastsEnums` trait. Laravel attribute casting should be used now instead. [247](https://github.com/BenSampo/laravel-enum/pull/247) 242 | 243 | ## [4.2.0](https://github.com/BenSampo/laravel-enum/compare/v4.1.0...v4.2.0) - 2022-01-31 244 | 245 | ### Fixed 246 | 247 | - Fix return type on FlaggedEnum flags method [241](https://github.com/BenSampo/laravel-enum/pull/241) 248 | - Suppress deprecated notice on PHP8.1 [236](https://github.com/BenSampo/laravel-enum/pull/236) 249 | 250 | ## [4.1.0](https://github.com/BenSampo/laravel-enum/compare/v4.0.0...v4.1.0) - 2021-11-16 251 | 252 | ### Added 253 | 254 | - Allow package to be installed with PHP 8.1 [#233](https://github.com/BenSampo/laravel-enum/pull/233) 255 | 256 | ### Changed 257 | 258 | - Allow `laminas/laminas-code:^4.0` as a dependency [#233](https://github.com/BenSampo/laravel-enum/pull/233) 259 | 260 | ## [4.0.0](https://github.com/BenSampo/laravel-enum/compare/v3.4.2...v4.0.0) - 2021-11-09 261 | 262 | ### Fixed 263 | 264 | - Fixed validation error message localization when using string validation rules [#227](https://github.com/BenSampo/laravel-enum/pull/227) 265 | 266 | ### Changed 267 | 268 | - Extend the functionality of the `getKeys()` and `getValues()` methods [#223](https://github.com/BenSampo/laravel-enum/pull/223) 269 | 270 | ### Added 271 | 272 | - Added new method `notIn()` to check whether a value is not in an iterable set of values [#232](https://github.com/BenSampo/laravel-enum/pull/232) 273 | 274 | ## [3.4.2](https://github.com/BenSampo/laravel-enum/compare/v3.4.1...v3.4.2) - 2021-09-09 275 | 276 | ### Fixed 277 | 278 | - Fixed broken enums due to wrapping of long constant names in method annotations [#226](https://github.com/BenSampo/laravel-enum/pull/226) 279 | 280 | ## [3.4.1](https://github.com/BenSampo/laravel-enum/compare/v3.4.0...v3.4.1) - 2021-06-17 281 | 282 | ### Fixed 283 | 284 | - Fixed type issued in PHP 7.3 285 | 286 | ## [3.4.0](https://github.com/BenSampo/laravel-enum/compare/v3.3.0...v3.4.0) - 2021-06-17 287 | 288 | ### Added 289 | 290 | - `addAllFlags()` method to flagged enums 291 | - `removeAllFlags()` method to flagged enums 292 | 293 | ### Fixed 294 | 295 | - Fixed coercion of flagged enums when the value represents multiple flags 296 | 297 | ## [3.3.0](https://github.com/BenSampo/laravel-enum/compare/v3.2.0...v3.3.0) - 2021-02-16 298 | 299 | ### Changed 300 | 301 | - Update doctrine/dbal requirement from ^2.9 to ^2.9|^3.0 [#208](https://github.com/BenSampo/laravel-enum/pull/208) 302 | - Allow passing iterables to Enum::in() [#212](https://github.com/BenSampo/laravel-enum/pull/212) 303 | 304 | ### Fixed 305 | 306 | - fix: `$model->getChanges()` triggered due to strict comparison [#187](https://github.com/BenSampo/laravel-enum/pull/187) 307 | - Fixed issue in `getFriendlyKeyName`when uppercase key contains non-alpha characters [#210](https://github.com/BenSampo/laravel-enum/pull/210) 308 | 309 | ## [3.2.0](https://github.com/BenSampo/laravel-enum/compare/v3.1.0...v3.2.0) - 2020-12-15 310 | 311 | ### Added 312 | 313 | - PHP 8.0 support [#203](https://github.com/BenSampo/laravel-enum/pull/203) 314 | 315 | ### Changed 316 | 317 | - Switched from Travis to GitHub Actions 318 | 319 | ## [3.1.0](https://github.com/BenSampo/laravel-enum/compare/v3.0.0...v3.1.0) - 2020-10-22 320 | 321 | ### Added 322 | 323 | - Added trait to query flagged enums using Eloquent [#180](https://github.com/BenSampo/laravel-enum/pull/180) 324 | - Add the option to publish enums stubs [#182](https://github.com/BenSampo/laravel-enum/pull/182) 325 | 326 | ### Changed 327 | 328 | - Improved test equality strictness [#185](https://github.com/BenSampo/laravel-enum/pull/185) 329 | 330 | ### Fixed 331 | 332 | - fix:`toSelectArray` breaking change + document `toArray` change [#184](https://github.com/BenSampo/laravel-enum/pull/184) 333 | 334 | ## [3.0.0](https://github.com/BenSampo/laravel-enum/compare/v2.2.0...v3.0.0) - 2020-08-07 335 | 336 | ### Added 337 | 338 | - Support for Laravel 8 339 | 340 | ### Fixed 341 | 342 | - Model annotation has been removed in favour of `laravel-ide-helper` [#165](https://github.com/BenSampo/laravel-enum/pull/165) 343 | 344 | ## [2.2.0](https://github.com/BenSampo/laravel-enum/compare/v2.1.0...v2.2.0) - 2020-08-30 345 | 346 | ### Fixed 347 | 348 | - Model attributes which use Laravel 7 native casting now return the enum value when serialized. [#162](https://github.com/BenSampo/laravel-enum/issues/162) [#163](https://github.com/BenSampo/laravel-enum/issues/163) 349 | 350 | ### Deprecated 351 | 352 | - `Enum::toArray()` should no longer be called statically, instead use `Enum::asArray()`. 353 | 354 | ## [2.1.0](https://github.com/BenSampo/laravel-enum/compare/v2.0.0...v2.1.0) - 2020-07-24 355 | 356 | ### Fixed 357 | 358 | - Allow returning `null` when using native casting [#152](https://github.com/BenSampo/laravel-enum/pull/152) 359 | 360 | ## [2.0.0](https://github.com/BenSampo/laravel-enum/compare/v1.38.0...v2.0.0) - 2020-07-02 361 | 362 | ### Added 363 | 364 | - Native attribute casting [#131](https://github.com/BenSampo/laravel-enum/pull/131) 365 | 366 | ### Changed 367 | 368 | - Require Laravel 7.5 or higher 369 | - Require PHP 7.2.5 or higher 370 | 371 | ### Deprecated 372 | 373 | - Deprecate legacy attribute casting in favor of native casting 374 | 375 | ## [1.38.0](https://github.com/BenSampo/laravel-enum/compare/v1.37.0...v1.38.0) - 2020-06-07 376 | 377 | ### Fixed 378 | 379 | - Handle calling magic instantiation methods from within instance methods of the Enum [#147](https://github.com/BenSampo/laravel-enum/pull/147) 380 | - Add new instantiation methods `Enum::fromKey()` and `Enum::fromValue()` [#142](https://github.com/BenSampo/laravel-enum/pull/142) 381 | - Fixed issue with localized validation messages [#141](https://github.com/BenSampo/laravel-enum/pull/141) 382 | 383 | ### Deprecated 384 | 385 | - Deprecate `Enum::getInstance()` in favor of `Enum::fromValue()` 386 | 387 | ## [1.37.0](https://github.com/BenSampo/laravel-enum/compare/v1.36.0...v1.37.0) - 2020-04-11 388 | 389 | ### Changed 390 | 391 | - EnumValue validation rule allows multiple flags for FlaggedEnums 392 | 393 | ## [1.36.0](https://github.com/BenSampo/laravel-enum/compare/v1.35...v1.36.0) - 2020-03-22 394 | 395 | ### Changed 396 | 397 | - Validation messages are now pulled from translation files [#134](https://github.com/BenSampo/laravel-enum/pull/134) 398 | 399 | ## [1.35.0](https://github.com/BenSampo/laravel-enum/compare/v1.34...v1.35) - 2020-03-16 400 | 401 | ### Changed 402 | 403 | - Added missing pipe validation syntax for enum instance validation [#132](https://github.com/BenSampo/laravel-enum/pull/132) 404 | 405 | ## [1.34.0](https://github.com/BenSampo/laravel-enum/compare/v1.33...v1.34) - 2020-03-13 406 | 407 | ### Changed 408 | 409 | - Change order of attributes in `BenSampo\Enum\Enum`, to ensure relational comparison (with <,>) uses the $value attribute. (Ref: https://www.php.net/manual/en/language.oop5.object-comparison.php#98725) [#129](https://github.com/BenSampo/laravel-enum/pull/129) 410 | - Fix for Lumen when Facade not set [#123](https://github.com/BenSampo/laravel-enum/pull/123) 411 | 412 | ## [1.33.0](https://github.com/BenSampo/laravel-enum/compare/v1.32...v1.33) - 2020-03-05 413 | 414 | ### Added 415 | 416 | - Add Laravel 7.x compatibility 417 | 418 | ## [1.32.0](https://github.com/BenSampo/laravel-enum/compare/v1.31...v1.32) - 2020-02-11 419 | 420 | ### Added 421 | 422 | - Add tests and make `EnumMethodReflection` return generating constant values for `isInternal`, `isDeprecated`, and 423 | `getDeprecatedDescription` [#121](https://github.com/BenSampo/laravel-enum/pull/121) 424 | 425 | ## [1.31.0](https://github.com/BenSampo/laravel-enum/compare/v1.30...v1.31) - 2020-02-09 426 | 427 | ### Added 428 | 429 | - Add compatibility with PHPStan `0.12.x` [#119](https://github.com/BenSampo/laravel-enum/pull/119) 430 | - Changelog started. 431 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2019 Ben Sampson 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: it 2 | it: fix stan test docs ## Run the commonly used targets 3 | 4 | .PHONY: help 5 | help: ## Displays this list of targets with descriptions 6 | @grep --extended-regexp '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' 7 | 8 | .PHONY: fix 9 | fix: vendor ## Apply automatic code fixes 10 | # TODO fix PHP Fatal error: Class PhpCsFixer\Fixer\Operator\AssignNullCoalescingToCoalesceEqualFixer contains 4 abstract methods and must therefore be declared abstract or implement the remaining methods (PhpCsFixer\Fixer\FixerInterface::isRisky, PhpCsFixer\Fixer\FixerInterface::fix, PhpCsFixer\Fixer\FixerInterface::getName, ...) in /home/bfranke/projects/laravel-enum/vendor/friendsofphp/php-cs-fixer/src/Fixer/Operator/AssignNullCoalescingToCoalesceEqualFixer.php on line 24 11 | #vendor/bin/php-cs-fixer fix 12 | 13 | .PHONY: stan 14 | stan: vendor ## Runs a static analysis with phpstan 15 | vendor/bin/phpstan 16 | 17 | .PHONY: test 18 | test: vendor ## Runs tests with phpunit 19 | vendor/bin/phpunit --testsuite=Tests 20 | vendor/bin/phpunit --testsuite=Rector 21 | 22 | docs: ## Generate documentation 23 | vendor/bin/rule-doc-generator generate src/Rector --output-file=rector-rules.md 24 | 25 | vendor: composer.json 26 | composer validate --strict 27 | composer install 28 | composer normalize 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Laravel Enum

2 |

3 | Packagist Stable Version 4 | Packagist downloads 5 | MIT Software License 6 |

7 | 8 | ## Using this library is no longer recommended 9 | 10 | Using this library is no longer recommended, especially for new projects. 11 | PHP 8.1 supports enums natively. 12 | 13 | See https://github.com/BenSampo/laravel-enum/issues/332. 14 | 15 | ## About Laravel Enum 16 | 17 | Simple, extensible and powerful enumeration implementation for Laravel. 18 | 19 | - Enum key value pairs as class constants 20 | - Full-featured suite of methods 21 | - Enum instantiation 22 | - Flagged/Bitwise enums 23 | - Type hinting 24 | - Attribute casting 25 | - Enum artisan generator 26 | - Validation rules for passing enum key or values as input parameters 27 | - Localization support 28 | - Extendable via Macros 29 | 30 | Created by [Ben Sampson](https://sampo.co.uk) 31 | 32 | ## Jump To 33 | 34 | - [Guide](#guide) 35 | - [Installation](#installation) 36 | - [Migrate to Native PHP Enums](#migrate-to-native-PHP-enums) 37 | - [Enum Library](enum-library.md) 38 | - [Basic Usage](#basic-usage) 39 | - [Enum Definition](#enum-definition) 40 | - [Instantiation](#instantiation) 41 | - [Instance Properties](#instance-properties) 42 | - [Instance Casting](#instance-casting) 43 | - [Instance Equality](#instance-equality) 44 | - [Type Hinting](#type-hinting) 45 | - [Flagged/Bitwise Enum](#flaggedbitwise-enum) 46 | - [Attribute Casting](#attribute-casting) 47 | - [Migrations](#migrations) 48 | - [Validation](#validation) 49 | - [Localization](#localization) 50 | - [Customizing Descriptions](#customizing-descriptions) 51 | - [Customizing Class Description](#customizing-class-description) 52 | - [Customizing Value Descriptions](#customizing-value-descriptions) 53 | - [Extending the Enum Base Class](#extending-the-enum-base-class) 54 | - [Laravel Nova Integration](#laravel-nova-integration) 55 | - [PHPStan Integration](#phpstan-integration) 56 | - [Artisan Command List](#artisan-command-list) 57 | - [Enum Class Reference](#enum-class-reference) 58 | - [Stubs](#stubs) 59 | 60 | ## Documentation for older versions 61 | 62 | You are reading the documentation for `6.x`. 63 | 64 | - If you're using **Laravel 8** please see the [docs for `4.x`](https://github.com/BenSampo/laravel-enum/blob/v4.2.0/README.md). 65 | - If you're using **Laravel 7** please see the [docs for `2.x`](https://github.com/BenSampo/laravel-enum/blob/v2.2.0/README.md). 66 | - If you're using **Laravel 6** or below, please see the [docs for `1.x`](https://github.com/BenSampo/laravel-enum/blob/v1.38.0/README.md). 67 | 68 | Please see the [upgrade guide](UPGRADE.md) for information on how to upgrade to the latest version. 69 | 70 | ## Guide 71 | 72 | I wrote a blog post about using laravel-enum: https://sampo.co.uk/blog/using-enums-in-laravel 73 | 74 | ## Installation 75 | 76 | Requires PHP 8, and Laravel 9 or 10. 77 | 78 | ```sh 79 | composer require bensampo/laravel-enum 80 | ``` 81 | 82 | ## Migrate to Native PHP Enums 83 | 84 | PHP 8.1 supports enums natively. 85 | You can migrate your usages of `BenSampo\Enum\Enum` to native PHP enums using the following steps. 86 | 87 | Make sure you meet the following requirements: 88 | - PHP 8.1 or higher 89 | - Laravel 10 or higher 90 | - Rector 0.17 or higher, your `rector.php` includes all relevant files 91 | - Latest version of this library 92 | 93 | Depending on the size of your project, you may choose to migrate all enums at once, 94 | or migrate just a couple or one enum at a time. 95 | - Convert all enums at once: `php artisan enum:to-native` 96 | - Pass the fully qualified class name of an enum to limit the conversion: `php artisan enum:to-native "App\Enums\UserType"` 97 | 98 | This is necessary if any enums are used during the bootstrap phase of Laravel, 99 | the conversion of their usages interferes with Larastan and prevents a second run of Rector from working. 100 | 101 | Review and validate the code changes for missed edge cases: 102 | - See [Unimplemented](tests/Rector/Unimplemented) 103 | - `Enum::coerce()`: If only values were passed, you can replace it with `tryFrom()`. 104 | If keys or instances could also be passed, you might need additional logic to cover this. 105 | - `Enum::$description` and `Enum::getDescription()`: Implement an alternative. 106 | - try/catch-blocks that handle `BenSampo\Enum\Exceptions\InvalidEnumKeyException` or `BenSampo\Enum\Exceptions\InvalidEnumMemberException`. 107 | Either catch the `ValueError` thrown by native enums, or switch to using `tryFrom()` and handle `null`. 108 | 109 | Once all enums are converted, you can remove your dependency on this library. 110 | 111 | ## Enum Library 112 | 113 | Browse and download from a list of commonly used, community contributed enums. 114 | 115 | [Enum library →](enum-library.md) 116 | 117 | ## Basic Usage 118 | 119 | ### Enum Definition 120 | 121 | You can use the following Artisan command to generate a new enum class: 122 | 123 | ```php 124 | php artisan make:enum UserType 125 | ``` 126 | 127 | Now, you just need to add the possible values your enum can have as constants. 128 | 129 | ```php 130 | key; // SuperAdministrator 202 | $userType->value; // 3 203 | $userType->description; // Super Administrator 204 | ``` 205 | 206 | This is particularly useful if you're passing an enum instance to a blade view. 207 | 208 | ### Instance Casting 209 | 210 | Enum instances can be cast to strings as they implement the `__toString()` magic method. 211 | This also means they can be echoed in blade views, for example. 212 | 213 | ```php 214 | $userType = UserType::fromValue(UserType::SuperAdministrator); 215 | 216 | (string) $userType // '3' 217 | ``` 218 | 219 | ### Instance Equality 220 | 221 | You can check the equality of an instance against any value by passing it to the `is` method. 222 | For convenience, there is also an `isNot` method which is the exact reverse of the `is` method. 223 | 224 | ```php 225 | $admin = UserType::Administrator(); 226 | 227 | $admin->is(UserType::Administrator); // true 228 | $admin->is($admin); // true 229 | $admin->is(UserType::Administrator()); // true 230 | 231 | $admin->is(UserType::Moderator); // false 232 | $admin->is(UserType::Moderator()); // false 233 | $admin->is('random-value'); // false 234 | ``` 235 | 236 | You can also check to see if the instance's value matches against an array of possible values using the `in` method, 237 | and use `notIn` to check if instance value is not in an array of values. 238 | Iterables can also be checked against. 239 | 240 | ```php 241 | $admin = UserType::Administrator(); 242 | 243 | $admin->in([UserType::Moderator, UserType::Administrator]); // true 244 | $admin->in([UserType::Moderator(), UserType::Administrator()]); // true 245 | 246 | $admin->in([UserType::Moderator, UserType::Subscriber]); // false 247 | $admin->in(['random-value']); // false 248 | 249 | $admin->notIn([UserType::Moderator, UserType::Administrator]); // false 250 | $admin->notIn([UserType::Moderator(), UserType::Administrator()]); // false 251 | 252 | $admin->notIn([UserType::Moderator, UserType::Subscriber]); // true 253 | $admin->notIn(['random-value']); // true 254 | ``` 255 | 256 | The instantiated enums are not singletons, rather a new object is created every time. 257 | Thus, strict comparison `===` of different enum instances will always return `false`, no matter the value. 258 | In contrast, loose comparison `==` will depend on the value. 259 | 260 | ```php 261 | $admin = UserType::Administrator(); 262 | 263 | $admin === UserType::Administrator(); // false 264 | UserType::Administrator() === UserType::Administrator(); // false 265 | $admin === UserType::Moderator(); // false 266 | 267 | $admin === $admin; // true 268 | 269 | $admin == UserType::Administrator(); // true 270 | $admin == UserType::Administrator; // true 271 | 272 | $admin == UserType::Moderator(); // false 273 | $admin == UserType::Moderator; // false 274 | ``` 275 | 276 | ### Type Hinting 277 | 278 | One of the benefits of enum instances is that it enables you to use type hinting, as shown below. 279 | 280 | ```php 281 | function canPerformAction(UserType $userType) 282 | { 283 | if ($userType->is(UserType::SuperAdministrator)) { 284 | return true; 285 | } 286 | 287 | return false; 288 | } 289 | 290 | $userType1 = UserType::fromValue(UserType::SuperAdministrator); 291 | $userType2 = UserType::fromValue(UserType::Moderator); 292 | 293 | canPerformAction($userType1); // Returns true 294 | canPerformAction($userType2); // Returns false 295 | ``` 296 | 297 | ## Flagged/Bitwise Enum 298 | 299 | Standard enums represent a single value at a time, but flagged or bitwise enums are capable of of representing multiple values simultaneously. This makes them perfect for when you want to express multiple selections of a limited set of options. A good example of this would be user permissions where there are a limited number of possible permissions but a user can have none, some or all of them. 300 | 301 | You can create a flagged enum using the following artisan command: 302 | 303 | `php artisan make:enum UserPermissions --flagged` 304 | 305 | ### Defining values 306 | 307 | When defining values you must use powers of 2, the easiest way to do this is by using the _shift left_ `<<` operator like so: 308 | 309 | ```php 310 | final class UserPermissions extends FlaggedEnum 311 | { 312 | const ReadComments = 1 << 0; 313 | const WriteComments = 1 << 1; 314 | const EditComments = 1 << 2; 315 | const DeleteComments = 1 << 3; 316 | // The next one would be `1 << 4` and so on... 317 | } 318 | ``` 319 | 320 | ### Defining shortcuts 321 | 322 | You can use the bitwise _or_ `|` to set a shortcut value which represents a given set of values. 323 | 324 | ```php 325 | final class UserPermissions extends FlaggedEnum 326 | { 327 | const ReadComments = 1 << 0; 328 | const WriteComments = 1 << 1; 329 | const EditComments = 1 << 2; 330 | const DeleteComments = 1 << 3; 331 | 332 | // Shortcuts 333 | const Member = self::ReadComments | self::WriteComments; // Read and write. 334 | const Moderator = self::Member | self::EditComments; // All the permissions a Member has, plus Edit. 335 | const Admin = self::Moderator | self::DeleteComments; // All the permissions a Moderator has, plus Delete. 336 | } 337 | ``` 338 | 339 | ### Instantiating a flagged enum 340 | 341 | There are couple of ways to instantiate a flagged enum: 342 | 343 | ```php 344 | // Standard new PHP class, passing the desired enum values as an array of values or array of enum instances 345 | $permissions = new UserPermissions([UserPermissions::ReadComments, UserPermissions::EditComments]); 346 | $permissions = new UserPermissions([UserPermissions::ReadComments(), UserPermissions::EditComments()]); 347 | 348 | // Static flags method, again passing the desired enum values as an array of values or array of enum instances 349 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::EditComments]); 350 | $permissions = UserPermissions::flags([UserPermissions::ReadComments(), UserPermissions::EditComments()]); 351 | ``` 352 | 353 | [Attribute casting](#attribute-casting) works in the same way as single value enums. 354 | 355 | ### Empty flagged enums 356 | 357 | Flagged enums can contain no value at all. Every flagged enum has a pre-defined constant of `None` which is comparable to `0`. 358 | 359 | ```php 360 | UserPermissions::flags([])->value === UserPermissions::None; // True 361 | ``` 362 | 363 | ### Flagged enum methods 364 | 365 | In addition to the standard enum methods, there are a suite of helpful methods available on flagged enums. 366 | 367 | Note: Anywhere where a static property is passed, you can also pass an enum instance. 368 | 369 | #### setFlags(array $flags): Enum 370 | 371 | Set the flags for the enum to the given array of flags. 372 | 373 | ```php 374 | $permissions = UserPermissions::flags([UserPermissions::ReadComments]); 375 | $permissions->flags([UserPermissions::EditComments, UserPermissions::DeleteComments]); // Flags are now: EditComments, DeleteComments. 376 | ``` 377 | 378 | #### addFlag($flag): Enum 379 | 380 | Add the given flag to the enum 381 | 382 | ```php 383 | $permissions = UserPermissions::flags([UserPermissions::ReadComments]); 384 | $permissions->addFlag(UserPermissions::EditComments); // Flags are now: ReadComments, EditComments. 385 | ``` 386 | 387 | #### addFlags(array $flags): Enum 388 | 389 | Add the given flags to the enum 390 | 391 | ```php 392 | $permissions = UserPermissions::flags([UserPermissions::ReadComments]); 393 | $permissions->addFlags([UserPermissions::EditComments, UserPermissions::WriteComments]); // Flags are now: ReadComments, EditComments, WriteComments. 394 | ``` 395 | 396 | #### addAllFlags(): Enum 397 | 398 | Add all flags to the enum 399 | 400 | ```php 401 | $permissions = UserPermissions::flags([UserPermissions::ReadComments]); 402 | $permissions->addAllFlags(); // Enum now has all flags 403 | ``` 404 | 405 | #### removeFlag($flag): Enum 406 | 407 | Remove the given flag from the enum 408 | 409 | ```php 410 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]); 411 | $permissions->removeFlag(UserPermissions::ReadComments); // Flags are now: WriteComments. 412 | ``` 413 | 414 | #### removeFlags(array $flags): Enum 415 | 416 | Remove the given flags from the enum 417 | 418 | ```php 419 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments, UserPermissions::EditComments]); 420 | $permissions->removeFlags([UserPermissions::ReadComments, UserPermissions::WriteComments]); // Flags are now: EditComments. 421 | ``` 422 | 423 | #### removeAllFlags(): Enum 424 | 425 | Remove all flags from the enum 426 | 427 | ```php 428 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]); 429 | $permissions->removeAllFlags(); 430 | ``` 431 | 432 | #### hasFlag($flag): bool 433 | 434 | Check if the enum has the specified flag. 435 | 436 | ```php 437 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]); 438 | $permissions->hasFlag(UserPermissions::ReadComments); // True 439 | $permissions->hasFlag(UserPermissions::EditComments); // False 440 | ``` 441 | 442 | #### hasFlags(array $flags): bool 443 | 444 | Check if the enum has all of the specified flags. 445 | 446 | ```php 447 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]); 448 | $permissions->hasFlags([UserPermissions::ReadComments, UserPermissions::WriteComments]); // True 449 | $permissions->hasFlags([UserPermissions::ReadComments, UserPermissions::EditComments]); // False 450 | ``` 451 | 452 | #### notHasFlag($flag): bool 453 | 454 | Check if the enum does not have the specified flag. 455 | 456 | ```php 457 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]); 458 | $permissions->notHasFlag(UserPermissions::EditComments); // True 459 | $permissions->notHasFlag(UserPermissions::ReadComments); // False 460 | ``` 461 | 462 | #### notHasFlags(array $flags): bool 463 | 464 | Check if the enum doesn't have any of the specified flags. 465 | 466 | ```php 467 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]); 468 | $permissions->notHasFlags([UserPermissions::ReadComments, UserPermissions::EditComments]); // True 469 | $permissions->notHasFlags([UserPermissions::ReadComments, UserPermissions::WriteComments]); // False 470 | ``` 471 | 472 | #### getFlags(): Enum[] 473 | 474 | Return the flags as an array of instances. 475 | 476 | ```php 477 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]); 478 | $permissions->getFlags(); // [UserPermissions::ReadComments(), UserPermissions::WriteComments()]; 479 | ``` 480 | 481 | #### hasMultipleFlags(): bool 482 | 483 | Check if there are multiple flags set on the enum. 484 | 485 | ```php 486 | $permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]); 487 | $permissions->hasMultipleFlags(); // True; 488 | $permissions->removeFlag(UserPermissions::ReadComments)->hasMultipleFlags(); // False 489 | ``` 490 | 491 | #### getBitmask(): int 492 | 493 | Get the bitmask for the enum. 494 | 495 | ```php 496 | UserPermissions::Member()->getBitmask(); // 11; 497 | UserPermissions::Moderator()->getBitmask(); // 111; 498 | UserPermissions::Admin()->getBitmask(); // 1111; 499 | UserPermissions::DeleteComments()->getBitmask(); // 1000; 500 | ``` 501 | 502 | ### Flagged enums in Eloquent queries 503 | 504 | To use flagged enums directly in your Eloquent queries, you may use the `QueriesFlaggedEnums` trait on your model which provides you with the following methods: 505 | 506 | #### hasFlag($column, $flag): Builder 507 | 508 | ```php 509 | User::hasFlag('permissions', UserPermissions::DeleteComments())->get(); 510 | ``` 511 | 512 | #### notHasFlag($column, $flag): Builder 513 | 514 | ```php 515 | User::notHasFlag('permissions', UserPermissions::DeleteComments())->get(); 516 | ``` 517 | 518 | #### hasAllFlags($column, $flags): Builder 519 | 520 | ```php 521 | User::hasAllFlags('permissions', [UserPermissions::EditComment(), UserPermissions::ReadComment()])->get(); 522 | ``` 523 | 524 | #### hasAnyFlags($column, $flags): Builder 525 | 526 | ```php 527 | User::hasAnyFlags('permissions', [UserPermissions::DeleteComments(), UserPermissions::EditComments()])->get(); 528 | ``` 529 | 530 | ## Attribute Casting 531 | 532 | You may cast model attributes to enums using Laravel's built in custom casting. This will cast the attribute to an enum instance when getting and back to the enum value when setting. 533 | Since `Enum::class` implements the `Castable` contract, you just need to specify the classname of the enum: 534 | 535 | ```php 536 | use BenSampo\Enum\Tests\Enums\UserType; 537 | use Illuminate\Database\Eloquent\Model; 538 | 539 | class Example extends Model 540 | { 541 | protected $casts = [ 542 | 'random_flag' => 'boolean', // Example standard laravel cast 543 | 'user_type' => UserType::class, // Example enum cast 544 | ]; 545 | } 546 | ``` 547 | 548 | Now, when you access the `user_type` attribute of your `Example` model, 549 | the underlying value will be returned as a `UserType` enum. 550 | 551 | ```php 552 | $example = Example::first(); 553 | $example->user_type // Instance of UserType 554 | ``` 555 | 556 | Review the [methods and properties available on enum instances](#instantiation) to get the most out of attribute casting. 557 | 558 | You can set the value by either passing the enum value or another enum instance. 559 | 560 | ```php 561 | $example = Example::first(); 562 | 563 | // Set using enum value 564 | $example->user_type = UserType::Moderator; 565 | 566 | // Set using enum instance 567 | $example->user_type = UserType::Moderator(); 568 | ``` 569 | 570 | ### Customising `$model->toArray()` behaviour 571 | 572 | When using `toArray` (or returning model/models from your controller as a response) Laravel will call the `toArray` method on the enum instance. 573 | 574 | By default, this will return only the value in its native type. You may want to also have access to the other properties (key, description), for example to return 575 | to javascript app. 576 | 577 | To customise this behaviour, you can override the `toArray` method on the enum instance. 578 | 579 | ```php 580 | // Example Enum 581 | final class UserType extends Enum 582 | { 583 | const ADMINISTRATOR = 0; 584 | const MODERATOR = 1; 585 | } 586 | 587 | $instance = UserType::Moderator(); 588 | 589 | // Default 590 | public function toArray() 591 | { 592 | return $this->value; 593 | } 594 | // Returns int(1) 595 | 596 | // Return all properties 597 | public function toArray() 598 | { 599 | return $this; 600 | } 601 | // Returns an array of all the properties 602 | // array(3) { 603 | // ["value"]=> 604 | // int(1)" 605 | // ["key"]=> 606 | // string(9) "MODERATOR" 607 | // ["description"]=> 608 | // string(9) "Moderator" 609 | // } 610 | 611 | ``` 612 | 613 | ### Casting underlying native types 614 | 615 | Many databases return everything as strings (for example, an integer may be returned as the string `'1'`). 616 | To reduce friction for users of the library, we use type coercion to figure out the intended value. If you'd prefer to control this, you can override the `parseDatabase` static method on your enum class: 617 | 618 | ```php 619 | final class UserType extends Enum 620 | { 621 | const Administrator = 0; 622 | const Moderator = 1; 623 | 624 | public static function parseDatabase($value) 625 | { 626 | return (int) $value; 627 | } 628 | } 629 | ``` 630 | 631 | Returning `null` from the `parseDatabase` method will cause the attribute on the model to also be `null`. This can be useful if your database stores inconsistent blank values such as empty strings instead of `NULL`. 632 | 633 | ### Model Annotation 634 | 635 | If you're casting attributes on your model to enums, the [laravel-ide-helper](https://github.com/barryvdh/laravel-ide-helper) package can be used to automatically generate property docblocks for you. 636 | 637 | ## Migrations 638 | 639 | ### Recommended 640 | 641 | Because enums enforce consistency at the code level it's not necessary to do so again at the database level, therefore the recommended type for database columns is `string` or `int` depending on your enum values. This means you can add/remove enum values in your code without worrying about your database layer. 642 | 643 | ```php 644 | use App\Enums\UserType; 645 | use Illuminate\Support\Facades\Schema; 646 | use Illuminate\Database\Schema\Blueprint; 647 | use Illuminate\Database\Migrations\Migration; 648 | 649 | class CreateUsersTable extends Migration 650 | { 651 | /** 652 | * Run the migrations. 653 | * 654 | * @return void 655 | */ 656 | public function up(): void 657 | { 658 | Schema::table('users', function (Blueprint $table): void { 659 | $table->bigIncrements('id'); 660 | $table->timestamps(); 661 | $table->string('type') 662 | ->default(UserType::Moderator); 663 | }); 664 | } 665 | } 666 | ``` 667 | 668 | ### Using `enum` column type 669 | 670 | Alternatively you may use `Enum` classes in your migrations to define enum columns. 671 | The enum values must be defined as strings. 672 | 673 | ```php 674 | use App\Enums\UserType; 675 | use Illuminate\Support\Facades\Schema; 676 | use Illuminate\Database\Schema\Blueprint; 677 | use Illuminate\Database\Migrations\Migration; 678 | 679 | class CreateUsersTable extends Migration 680 | { 681 | /** 682 | * Run the migrations. 683 | * 684 | * @return void 685 | */ 686 | public function up(): void 687 | { 688 | Schema::table('users', function (Blueprint $table): void { 689 | $table->bigIncrements('id'); 690 | $table->timestamps(); 691 | $table->enum('type', UserType::getValues()) 692 | ->default(UserType::Moderator); 693 | }); 694 | } 695 | } 696 | ``` 697 | 698 | ## Validation 699 | 700 | ### Array Validation 701 | 702 | #### Enum value 703 | 704 | You may validate that an enum value passed to a controller is a valid value for a given enum by using the `EnumValue` rule. 705 | 706 | ```php 707 | use BenSampo\Enum\Rules\EnumValue; 708 | 709 | public function store(Request $request) 710 | { 711 | $this->validate($request, [ 712 | 'user_type' => ['required', new EnumValue(UserType::class)], 713 | ]); 714 | } 715 | ``` 716 | 717 | By default, type checking is set to strict, but you can bypass this by passing `false` to the optional second parameter of the EnumValue class. 718 | 719 | ```php 720 | new EnumValue(UserType::class, false) // Turn off strict type checking. 721 | ``` 722 | 723 | #### Enum key 724 | 725 | You can also validate on keys using the `EnumKey` rule. This is useful if you're taking the enum key as a URL parameter for sorting or filtering for example. 726 | 727 | ```php 728 | use BenSampo\Enum\Rules\EnumKey; 729 | 730 | public function store(Request $request) 731 | { 732 | $this->validate($request, [ 733 | 'user_type' => ['required', new EnumKey(UserType::class)], 734 | ]); 735 | } 736 | ``` 737 | 738 | #### Enum instance 739 | 740 | Additionally you can validate that a parameter is an instance of a given enum. 741 | 742 | ```php 743 | use BenSampo\Enum\Rules\Enum; 744 | 745 | public function store(Request $request) 746 | { 747 | $this->validate($request, [ 748 | 'user_type' => ['required', new Enum(UserType::class)], 749 | ]); 750 | } 751 | ``` 752 | 753 | ### Pipe Validation 754 | 755 | You can also use the 'pipe' syntax for rules. 756 | 757 | **enum_value**_:enum_class,[strict]_ 758 | **enum_key**_:enum_class_ 759 | **enum**_:enum_class_ 760 | 761 | ```php 762 | 'user_type' => 'required|enum_value:' . UserType::class, 763 | 'user_type' => 'required|enum_key:' . UserType::class, 764 | 'user_type' => 'required|enum:' . UserType::class, 765 | ``` 766 | 767 | ## Localization 768 | 769 | ### Validation messages 770 | 771 | Run the following command to publish the language files to your `lang` folder. 772 | 773 | ``` 774 | php artisan vendor:publish --provider="BenSampo\Enum\EnumServiceProvider" --tag="translations" 775 | ``` 776 | 777 | ### Enum descriptions 778 | 779 | You can translate the strings returned by the `getDescription` method using Laravel's built-in [localization](https://laravel.com/docs/localization) features. 780 | 781 | Add a new `enums.php` keys file for each of your supported languages. In this example there is one for English and one for Spanish. 782 | 783 | ```php 784 | // lang/en/enums.php 785 | [ 792 | UserType::Administrator => 'Administrator', 793 | UserType::SuperAdministrator => 'Super administrator', 794 | ], 795 | 796 | ]; 797 | ``` 798 | 799 | ```php 800 | // lang/es/enums.php 801 | [ 808 | UserType::Administrator => 'Administrador', 809 | UserType::SuperAdministrator => 'Súper administrador', 810 | ], 811 | 812 | ]; 813 | ``` 814 | 815 | Now, you just need to make sure that your enum implements the `LocalizedEnum` interface as demonstrated below: 816 | 817 | ```php 818 | use BenSampo\Enum\Enum; 819 | use BenSampo\Enum\Contracts\LocalizedEnum; 820 | 821 | final class UserType extends Enum implements LocalizedEnum 822 | { 823 | // ... 824 | } 825 | ``` 826 | 827 | The `getDescription` method will now look for the value in your localization files. If a value doesn't exist for a given key, the default description is returned instead. 828 | 829 | ## Customizing descriptions 830 | 831 | ### Customizing class description 832 | 833 | If you'd like to return a custom description for your enum class, add a `Description` attribute to your Enum class: 834 | 835 | ```php 836 | use BenSampo\Enum\Enum; 837 | use BenSampo\Enum\Attributes\Description; 838 | 839 | #[Description('List of available User types')] 840 | final class UserType extends Enum 841 | { 842 | ... 843 | } 844 | ``` 845 | 846 | Calling `UserType::getClassDescription()` now returns `List of available User types` instead of `User type`. 847 | 848 | You may also override the `getClassDescription` method on the base Enum class if you wish to have more control of the description. 849 | 850 | ### Customizing value descriptions 851 | 852 | If you'd like to return a custom description for your enum values, add a `Description` attribute to your Enum constants: 853 | 854 | ```php 855 | use BenSampo\Enum\Enum; 856 | use BenSampo\Enum\Attributes\Description; 857 | 858 | final class UserType extends Enum 859 | { 860 | const Administrator = 'Administrator'; 861 | 862 | #[Description('Super admin')] 863 | const SuperAdministrator = 'SuperAdministrator'; 864 | } 865 | ``` 866 | 867 | Calling `UserType::SuperAdministrator()->description` now returns `Super admin` instead of `Super administrator`. 868 | 869 | You may also override the `getDescription` method on the base Enum class if you wish to have more control of the description. 870 | 871 | ## Extending the Enum Base Class 872 | 873 | The `Enum` base class implements the [Laravel `Macroable`](https://laravel.com/api/9.x/Illuminate/Support/Traits/Macroable.html) trait, meaning it's easy to extend it with your own functions. If you have a function that you often add to each of your enums, you can use a macro. 874 | 875 | Let's say we want to be able to get a flipped version of the enum `asArray` method, we can do this using: 876 | 877 | ```php 878 | Enum::macro('asFlippedArray', function() { 879 | return array_flip(self::asArray()); 880 | }); 881 | ``` 882 | 883 | Now, on each of my enums, I can call it using `UserType::asFlippedArray()`. 884 | 885 | It's best to register the macro inside a service providers' boot method. 886 | 887 | ## Laravel Nova Integration 888 | 889 | Use the [nova-enum-field](https://github.com/simplesquid/nova-enum-field) package by Simple Squid to easily create fields for your Enums in Nova. See their readme for usage. 890 | 891 | ## PHPStan Integration 892 | 893 | If you are using [PHPStan](https://github.com/phpstan/phpstan) for static analysis, enable the extension for: 894 | - proper recognition of the magic instantiation methods 895 | - detection of duplicate enum values 896 | 897 | Use [PHPStan Extension Installer](https://github.com/phpstan/extension-installer) or add the following to your projects `phpstan.neon` includes: 898 | 899 | ```neon 900 | includes: 901 | - vendor/bensampo/laravel-enum/extension.neon 902 | ``` 903 | 904 | ## Artisan Command List 905 | 906 | ### `php artisan make:enum` 907 | 908 | Create a new enum class. Pass `--flagged` as an option to create a flagged enum. 909 | [Find out more](#enum-definition) 910 | 911 | ### `php artisan enum:annotate` 912 | 913 | Generate DocBlock annotations for enum classes. 914 | [Find out more](#instantiation) 915 | 916 | ### `php artisan enum:to-native` 917 | 918 | See [migrate to native PHP enums](#migrate-to-native-php-enums). 919 | 920 | ## Enum Class Reference 921 | 922 | ### static getKeys(mixed $values = null): array 923 | 924 | Returns an array of all or a custom set of the keys for an enum. 925 | 926 | ```php 927 | UserType::getKeys(); // Returns ['Administrator', 'Moderator', 'Subscriber', 'SuperAdministrator'] 928 | UserType::getKeys(UserType::Administrator); // Returns ['Administrator'] 929 | UserType::getKeys(UserType::Administrator, UserType::Moderator); // Returns ['Administrator', 'Moderator'] 930 | UserType::getKeys([UserType::Administrator, UserType::Moderator]); // Returns ['Administrator', 'Moderator'] 931 | ``` 932 | 933 | ### static getValues(mixed $keys = null): array 934 | 935 | Returns an array of all or a custom set of the values for an enum. 936 | 937 | ```php 938 | UserType::getValues(); // Returns [0, 1, 2, 3] 939 | UserType::getValues('Administrator'); // Returns [0] 940 | UserType::getValues('Administrator', 'Moderator'); // Returns [0, 1] 941 | UserType::getValues(['Administrator', 'Moderator']); // Returns [0, 1] 942 | ``` 943 | 944 | ### static getKey(mixed $value): string 945 | 946 | Returns the key for the given enum value. 947 | 948 | ```php 949 | UserType::getKey(1); // Returns 'Moderator' 950 | UserType::getKey(UserType::Moderator); // Returns 'Moderator' 951 | ``` 952 | 953 | ### static getValue(string $key): mixed 954 | 955 | Returns the value for the given enum key. 956 | 957 | ```php 958 | UserType::getValue('Moderator'); // Returns 1 959 | ``` 960 | 961 | ### static hasKey(string $key): bool 962 | 963 | Check if the enum contains a given key. 964 | 965 | ```php 966 | UserType::hasKey('Moderator'); // Returns 'True' 967 | ``` 968 | 969 | ### static hasValue(mixed $value, bool $strict = true): bool 970 | 971 | Check if the enum contains a given value. 972 | 973 | ```php 974 | UserType::hasValue(1); // Returns 'True' 975 | 976 | // It's possible to disable the strict type checking: 977 | UserType::hasValue('1'); // Returns 'False' 978 | UserType::hasValue('1', false); // Returns 'True' 979 | ``` 980 | 981 | ### static getClassDescription(): string 982 | 983 | Returns the class name in sentence case for the enum class. It's possible to [customize the description](#customizing-descriptions) if the guessed description is not appropriate. 984 | 985 | ```php 986 | UserType::getClassDescription(); // Returns 'User type' 987 | ``` 988 | 989 | ### static getDescription(mixed $value): string 990 | 991 | Returns the key in sentence case for the enum value. It's possible to [customize the description](#customizing-descriptions) if the guessed description is not appropriate. 992 | 993 | ```php 994 | UserType::getDescription(3); // Returns 'Super administrator' 995 | UserType::getDescription(UserType::SuperAdministrator); // Returns 'Super administrator' 996 | ``` 997 | 998 | ### static getRandomKey(): string 999 | 1000 | Returns a random key from the enum. Useful for factories. 1001 | 1002 | ```php 1003 | UserType::getRandomKey(); // Returns 'Administrator', 'Moderator', 'Subscriber' or 'SuperAdministrator' 1004 | ``` 1005 | 1006 | ### static getRandomValue(): mixed 1007 | 1008 | Returns a random value from the enum. Useful for factories. 1009 | 1010 | ```php 1011 | UserType::getRandomValue(); // Returns 0, 1, 2 or 3 1012 | ``` 1013 | 1014 | ### static getRandomInstance(): mixed 1015 | 1016 | Returns a random instance of the enum. Useful for factories. 1017 | 1018 | ```php 1019 | UserType::getRandomInstance(); // Returns an instance of UserType with a random value 1020 | ``` 1021 | 1022 | ### static asArray(): array 1023 | 1024 | Returns the enum key value pairs as an associative array. 1025 | 1026 | ```php 1027 | UserType::asArray(); // Returns ['Administrator' => 0, 'Moderator' => 1, 'Subscriber' => 2, 'SuperAdministrator' => 3] 1028 | ``` 1029 | 1030 | ### static asSelectArray(): array 1031 | 1032 | Returns the enum for use in a select as value => description. 1033 | 1034 | ```php 1035 | UserType::asSelectArray(); // Returns [0 => 'Administrator', 1 => 'Moderator', 2 => 'Subscriber', 3 => 'Super administrator'] 1036 | ``` 1037 | 1038 | ### static fromValue(mixed $enumValue): Enum 1039 | 1040 | Returns an instance of the called enum. Read more about [enum instantiation](#instantiation). 1041 | 1042 | ```php 1043 | UserType::fromValue(UserType::Administrator); // Returns instance of Enum with the value set to UserType::Administrator 1044 | ``` 1045 | 1046 | ### static getInstances(): array 1047 | 1048 | Returns an array of all possible instances of the called enum, keyed by the constant names. 1049 | 1050 | ```php 1051 | var_dump(UserType::getInstances()); 1052 | 1053 | array(4) { 1054 | 'Administrator' => 1055 | class BenSampo\Enum\Tests\Enums\UserType#415 (3) { 1056 | public $key => 1057 | string(13) "Administrator" 1058 | public $value => 1059 | int(0) 1060 | public $description => 1061 | string(13) "Administrator" 1062 | } 1063 | 'Moderator' => 1064 | class BenSampo\Enum\Tests\Enums\UserType#396 (3) { 1065 | public $key => 1066 | string(9) "Moderator" 1067 | public $value => 1068 | int(1) 1069 | public $description => 1070 | string(9) "Moderator" 1071 | } 1072 | 'Subscriber' => 1073 | class BenSampo\Enum\Tests\Enums\UserType#393 (3) { 1074 | public $key => 1075 | string(10) "Subscriber" 1076 | public $value => 1077 | int(2) 1078 | public $description => 1079 | string(10) "Subscriber" 1080 | } 1081 | 'SuperAdministrator' => 1082 | class BenSampo\Enum\Tests\Enums\UserType#102 (3) { 1083 | public $key => 1084 | string(18) "SuperAdministrator" 1085 | public $value => 1086 | int(3) 1087 | public $description => 1088 | string(19) "Super administrator" 1089 | } 1090 | } 1091 | ``` 1092 | 1093 | ### static coerce(mixed $enumKeyOrValue): ?Enum 1094 | 1095 | Attempt to instantiate a new Enum using the given key or value. Returns null if the Enum cannot be instantiated. 1096 | 1097 | ```php 1098 | UserType::coerce(0); // Returns instance of UserType with the value set to UserType::Administrator 1099 | UserType::coerce('Administrator'); // Returns instance of UserType with the value set to UserType::Administrator 1100 | UserType::coerce(99); // Returns null (not a valid enum value) 1101 | ``` 1102 | 1103 | ## Stubs 1104 | 1105 | Run the following command to publish the stub files to the `stubs` folder in the root of your application. 1106 | 1107 | ```shell 1108 | php artisan vendor:publish --provider="BenSampo\Enum\EnumServiceProvider" --tag="stubs" 1109 | ``` 1110 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## 6.x 4 | 5 | ### Native types 6 | 7 | The library now uses native types whenever possible. 8 | When you override methods or implement interfaces, you will need to add them. 9 | 10 | ### `Enum::getDescription()` throws 11 | 12 | Instead of returning an empty string `''` on invalid values, 13 | `Enum::getDescription()` will throw an `InvalidEnumMemberException`. 14 | 15 | ### Construct `InvalidEnumMemberException` 16 | 17 | The constructor of `InvalidEnumMemberException` now expects the class name 18 | of an enum instead of an enum instance. 19 | 20 | ## 5.x 21 | 22 | ### Laravel 9 required 23 | 24 | Laravel `9` or higher is required. 25 | 26 | ### PHP 8.0 required 27 | 28 | PHP `8.0` or higher is now required. 29 | 30 | ## 4.x 31 | 32 | ### Review use of Localization features 33 | 34 | You should make sure that any enums using localization features are still translated as expected. 35 | 36 | ## 3.x 37 | 38 | ### Laravel 8 required 39 | 40 | Laravel `8` or higher is required. 41 | 42 | ### PHP 7.3 required 43 | 44 | PHP `7.3` or higher is now required. 45 | 46 | ## 2.x 47 | 48 | ### Laravel 7.5 required 49 | 50 | Laravel `7.5` or higher is required for the new native attribute casting. 51 | 52 | ### PHP 7.2 required 53 | 54 | PHP `7.2.5` or higher is now required. 55 | 56 | ### Switch to native casting 57 | 58 | You should update your models to use Laravel 7 native casting. Remove the trait and 59 | move the casts from `$enumCasts` to `$casts`. 60 | 61 | Trait based casting is still present, but is now deprecated and will be removed in the next major version. 62 | 63 | ```diff 64 | --use BenSampo\Enum\Traits\CastsEnums; 65 | 66 | class MyModel extends Model 67 | { 68 | - use CastsEnums; 69 | 70 | - protected $enumCasts = [ 71 | + protected $casts = [ 72 | 'foo' => Foo::class, 73 | ]; 74 | ``` 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bensampo/laravel-enum", 3 | "description": "Simple, extensible and powerful enumeration implementation for Laravel.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "bensampo", 8 | "enum", 9 | "laravel", 10 | "package", 11 | "validation" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Ben Sampson", 16 | "homepage": "https://sampo.co.uk", 17 | "role": "Developer" 18 | }, 19 | { 20 | "name": "Benedikt Franke", 21 | "homepage": "https://franke.tech", 22 | "role": "Developer" 23 | } 24 | ], 25 | "homepage": "https://github.com/bensampo/laravel-enum", 26 | "require": { 27 | "php": "^8", 28 | "composer/class-map-generator": "^1", 29 | "illuminate/contracts": "^9 || ^10 || ^11 || ^12", 30 | "illuminate/support": "^9 || ^10 || ^11 || ^12", 31 | "laminas/laminas-code": "^3.4 || ^4", 32 | "nikic/php-parser": "^4.13.2 || ^5" 33 | }, 34 | "require-dev": { 35 | "doctrine/dbal": "^3.9.4", 36 | "ergebnis/composer-normalize": "^2.45", 37 | "larastan/larastan": "^2.9.14 || ^3.1", 38 | "mll-lab/php-cs-fixer-config": "^5.10", 39 | "mockery/mockery": "^1.6.12", 40 | "orchestra/testbench": "^7.6.1 || ^8.33 || ^9.11 || ^10", 41 | "phpstan/extension-installer": "^1.4.3", 42 | "phpstan/phpstan": "^1.12.19 || ^2.1.6", 43 | "phpstan/phpstan-mockery": "^1.1.3 || ^2", 44 | "phpstan/phpstan-phpunit": "^1.4.2 || ^2.0.4", 45 | "phpunit/phpunit": "^9.5.21 || ^10.5.45 || ^11.5.10 || ^12.0.5", 46 | "rector/rector": "^1.2.10 || ^2.0.9", 47 | "symplify/rule-doc-generator": "^11.2 || ^12.2.5" 48 | }, 49 | "minimum-stability": "dev", 50 | "prefer-stable": true, 51 | "autoload": { 52 | "psr-4": { 53 | "BenSampo\\Enum\\": "src" 54 | } 55 | }, 56 | "autoload-dev": { 57 | "psr-4": { 58 | "BenSampo\\Enum\\Tests\\": "tests" 59 | } 60 | }, 61 | "config": { 62 | "allow-plugins": { 63 | "ergebnis/composer-normalize": true, 64 | "phpstan/extension-installer": true 65 | }, 66 | "sort-packages": true 67 | }, 68 | "extra": { 69 | "laravel": { 70 | "providers": [ 71 | "BenSampo\\Enum\\EnumServiceProvider" 72 | ] 73 | }, 74 | "phpstan": { 75 | "includes": [ 76 | "extension.neon" 77 | ] 78 | } 79 | }, 80 | "scripts": { 81 | "post-autoload-dump": [ 82 | "@php vendor/bin/testbench package:discover" 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | services: 2 | - class: BenSampo\Enum\PHPStan\EnumMethodsClassReflectionExtension 3 | tags: 4 | - phpstan.broker.methodsClassReflectionExtension 5 | - class: BenSampo\Enum\PHPStan\UniqueValuesRule 6 | tags: 7 | - phpstan.rules.rule 8 | -------------------------------------------------------------------------------- /lang/en/messages.php: -------------------------------------------------------------------------------- 1 | 'The value you have provided is not a valid enum instance.', 5 | 'enum_value' => 'The value you have entered is invalid.', 6 | 'enum_key' => 'The key you have entered is invalid.', 7 | ]; 8 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - extension.neon 3 | parameters: 4 | level: 6 # TODO level up to max 5 | paths: 6 | - src 7 | - tests 8 | checkOctaneCompatibility: true 9 | reportUnmatchedIgnoredErrors: false # As long as we support multiple Laravel versions at once, there will be some dead spots 10 | treatPhpDocTypesAsCertain: false 11 | noEnvCallsOutsideOfConfig: false 12 | ignoreErrors: 13 | - '#Unsafe usage of new static.*#' # This is a library, so it should be extendable 14 | # The Process API is only available in newer Laravel versions 15 | - '#unknown class Illuminate\\Support\\Facades\\Process#' 16 | - '#unknown class Illuminate\\Process#' 17 | - '#invalid type Illuminate\\Process#' 18 | - '#^Attribute class PHPUnit\\Framework\\Attributes\\DataProvider does not exist\.$#' # Only available with newer PHPUnit versions 19 | excludePaths: 20 | - tests/PHPStan/Fixtures 21 | # Install https://plugins.jetbrains.com/plugin/7677-awesome-console to make those links clickable 22 | editorUrl: '%%relFile%%:%%line%%' 23 | editorUrlTitle: '%%relFile%%:%%line%%' 24 | -------------------------------------------------------------------------------- /rector-rules.md: -------------------------------------------------------------------------------- 1 | # 2 Rules Overview 2 | 3 | ## ToNativeImplementationRector 4 | 5 | Convert usages of `BenSampo\Enum\Enum` to native PHP enums 6 | 7 | :wrench: **configure it!** 8 | 9 | - class: [`BenSampo\Enum\Rector\ToNativeImplementationRector`](src/Rector/ToNativeImplementationRector.php) 10 | 11 | ```diff 12 | -/** 13 | - * @method static static ADMIN() 14 | - * @method static static MEMBER() 15 | - * 16 | - * @extends Enum 17 | - */ 18 | -class UserType extends Enum 19 | +enum UserType : int 20 | { 21 | - const ADMIN = 1; 22 | - const MEMBER = 2; 23 | + case ADMIN = 1; 24 | + case MEMBER = 2; 25 | } 26 | ``` 27 | 28 |
29 | 30 | ## ToNativeUsagesRector 31 | 32 | Convert usages of `BenSampo\Enum\Enum` to native PHP enums 33 | 34 | :wrench: **configure it!** 35 | 36 | - class: [`BenSampo\Enum\Rector\ToNativeUsagesRector`](src/Rector/ToNativeUsagesRector.php) 37 | 38 | ```diff 39 | -$user = UserType::ADMIN(); 40 | -$user->is(UserType::ADMIN); 41 | +$user = UserType::ADMIN; 42 | +$user === UserType::ADMIN; 43 | ``` 44 | 45 |
46 | -------------------------------------------------------------------------------- /src/Attributes/Description.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | // @phpstan-ignore-next-line CastsAttributes is only sometimes generic 12 | class EnumCast implements CastsAttributes 13 | { 14 | public function __construct( 15 | protected string $enumClass 16 | ) {} 17 | 18 | /** 19 | * @template TValue 20 | * 21 | * @param TValue $value 22 | * @param array $attributes 23 | * 24 | * @return Enum|null 25 | */ 26 | public function get($model, string $key, $value, array $attributes): ?Enum 27 | { 28 | return $this->castEnum($value); 29 | } 30 | 31 | /** 32 | * @param array $attributes 33 | * 34 | * @return array 35 | */ 36 | public function set($model, string $key, $value, array $attributes): array 37 | { 38 | $value = $this->castEnum($value); 39 | 40 | return [$key => $this->enumClass::serializeDatabase($value)]; 41 | } 42 | 43 | /** 44 | * @template TValue 45 | * 46 | * @param TValue $value 47 | * 48 | * @return Enum|null 49 | */ 50 | protected function castEnum(mixed $value): ?Enum 51 | { 52 | if ($value === null || $value instanceof $this->enumClass) { 53 | return $value; 54 | } 55 | 56 | $value = $this->getCastableValue($value); 57 | 58 | if ($value === null) { 59 | return null; 60 | } 61 | 62 | return $this->enumClass::fromValue($value); 63 | } 64 | 65 | protected function getCastableValue(mixed $value): mixed 66 | { 67 | // If the enum has overridden the `parseDatabase` method, use it to get the cast value 68 | $value = $this->enumClass::parseDatabase($value); 69 | 70 | if ($value === null) { 71 | return null; 72 | } 73 | 74 | // If the value exists in the enum (using strict type checking) return it 75 | if ($this->enumClass::hasValue($value)) { 76 | return $value; 77 | } 78 | 79 | // Find the value in the enum that the incoming value can be coerced to 80 | foreach ($this->enumClass::getValues() as $enumValue) { 81 | if ($value == $enumValue) { 82 | return $enumValue; 83 | } 84 | } 85 | 86 | // Fall back to trying to construct it directly (will result in an error since it doesn't exist) 87 | return $value; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Commands/EnumAnnotateCommand.php: -------------------------------------------------------------------------------- 1 | > */ 25 | protected function getArguments(): array 26 | { 27 | return [ 28 | ['class', InputArgument::OPTIONAL, 'The class name to generate annotations for'], 29 | ]; 30 | } 31 | 32 | /** @return array> */ 33 | protected function getOptions(): array 34 | { 35 | return [ 36 | ['folder', null, InputOption::VALUE_OPTIONAL, 'The folder to scan for classes to annotate'], 37 | ]; 38 | } 39 | 40 | public function handle(Filesystem $filesystem): int 41 | { 42 | $this->filesystem = $filesystem; 43 | 44 | $class = $this->argument('class'); 45 | if ($class) { 46 | $this->annotateClass($class); 47 | 48 | return 0; 49 | } 50 | 51 | $this->annotateFolder(); 52 | 53 | return 0; 54 | } 55 | 56 | protected function annotateClass(string $className): void 57 | { 58 | if (! is_subclass_of($className, Enum::class)) { 59 | $parentClass = Enum::class; 60 | throw new \InvalidArgumentException("The given class {$className} must be an instance of {$parentClass}."); 61 | } 62 | 63 | $reflection = new \ReflectionClass($className); 64 | $this->annotate($reflection); 65 | } 66 | 67 | protected function annotateFolder(): void 68 | { 69 | foreach (ClassMapGenerator::createMap($this->searchDirectory()) as $class => $_) { 70 | $reflection = new \ReflectionClass($class); 71 | 72 | if ($reflection->isSubclassOf(Enum::class)) { 73 | $this->annotate($reflection); 74 | } 75 | } 76 | } 77 | 78 | /** @param \ReflectionClass<*> $reflectionClass */ 79 | protected function annotate(\ReflectionClass $reflectionClass): void 80 | { 81 | $docBlock = $this->getDocBlock($reflectionClass); 82 | $shortName = $reflectionClass->getShortName(); 83 | $fileName = $reflectionClass->getFileName(); 84 | $contents = $this->filesystem->get($fileName); 85 | 86 | $classDeclaration = "class {$shortName}"; 87 | 88 | if ($reflectionClass->isFinal()) { 89 | $classDeclaration = "final {$classDeclaration}"; 90 | } elseif ($reflectionClass->isAbstract()) { 91 | $classDeclaration = "abstract {$classDeclaration}"; 92 | } 93 | 94 | // Remove existing docblock 95 | $quotedClassDeclaration = preg_quote($classDeclaration); 96 | $contents = preg_replace( 97 | "#\\r?\\n?\/\*[\s\S]*?\*\/(\\r?\\n)?{$quotedClassDeclaration}#ms", 98 | "\$1{$classDeclaration}", 99 | $contents, 100 | ); 101 | 102 | // Make sure we don't replace too much 103 | $contents = substr_replace( 104 | $contents, 105 | "{$docBlock->generate()}{$classDeclaration}", 106 | strpos($contents, $classDeclaration), 107 | strlen($classDeclaration), 108 | ); 109 | 110 | $this->filesystem->put($fileName, $contents); 111 | $this->info("Wrote new phpDocBlock to {$fileName}."); 112 | } 113 | 114 | /** @param \ReflectionClass<*> $reflectionClass */ 115 | protected function getDocBlock(\ReflectionClass $reflectionClass): DocBlockGenerator 116 | { 117 | $docBlock = DocBlockGenerator::fromArray([]) 118 | ->setWordWrap(false); 119 | 120 | $originalDocBlock = null; 121 | 122 | $docComment = $reflectionClass->getDocComment(); 123 | if ($docComment) { 124 | $docBlockReflection = new DocBlockReflection(ltrim($docComment)); 125 | $originalDocBlock = DocBlockGenerator::fromReflection($docBlockReflection); 126 | 127 | $docBlock->setLongDescription($this->getDocblockWithoutTags($docBlockReflection)); 128 | } 129 | 130 | $docBlock->setTags($this->getDocblockTags( 131 | $originalDocBlock, 132 | $reflectionClass 133 | )); 134 | 135 | return $docBlock; 136 | } 137 | 138 | protected function getDocblockWithoutTags(DocBlockReflection $docBlockReflection): string 139 | { 140 | $docBlockContents = $docBlockReflection->getContents(); 141 | // We can remove all tags here, as we add them back in with getDocblockTags 142 | $withoutTags = preg_replace('/@.*$/m', '', $docBlockContents); 143 | 144 | return trim($withoutTags); 145 | } 146 | 147 | /** 148 | * @param \ReflectionClass<*> $reflectionClass 149 | * 150 | * @return array<\Laminas\Code\Generator\DocBlock\Tag\TagInterface> 151 | */ 152 | protected function getDocblockTags(?DocBlockGenerator $originalDocblock, \ReflectionClass $reflectionClass): array 153 | { 154 | $constants = $reflectionClass->getConstants(); 155 | $constantKeys = array_keys($constants); 156 | 157 | $tags = array_map( 158 | static fn (mixed $value, string $constantName): MethodTag => new MethodTag($constantName, ['static'], null, true), 159 | $constants, 160 | $constantKeys, 161 | ); 162 | 163 | if ($originalDocblock) { 164 | $tags = array_merge( 165 | $tags, 166 | array_filter($originalDocblock->getTags(), fn (TagInterface $tag): bool => ! $tag instanceof MethodTag 167 | || ! in_array($tag->getMethodName(), $constantKeys, true)) 168 | ); 169 | } 170 | 171 | return $tags; 172 | } 173 | 174 | protected function searchDirectory(): string 175 | { 176 | return $this->option('folder') 177 | ?? app_path('Enums'); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Commands/EnumToNativeCommand.php: -------------------------------------------------------------------------------- 1 | > */ 20 | protected function getArguments(): array 21 | { 22 | return [ 23 | ['class', InputArgument::OPTIONAL, 'The class name to convert'], 24 | ]; 25 | } 26 | 27 | public function handle(): int 28 | { 29 | $class = $this->argument('class'); 30 | if ($class) { 31 | $class = ltrim($class, '\\'); 32 | } 33 | 34 | $env = [ 35 | self::TO_NATIVE_CLASS_ENV => $class ?? Enum::class, 36 | self::BASE_RECTOR_CONFIG_PATH_ENV => base_path('rector.php'), 37 | ]; 38 | $withPipedOutput = function (string $type, string $output): void { 39 | echo $output; 40 | }; 41 | $run = fn (string $command) => Process::env($env) 42 | ->timeout(0) // Unlimited, rector can take quite a while 43 | ->run($command, $withPipedOutput); 44 | 45 | if ($class) { 46 | if (! class_exists($class)) { 47 | $this->error("Class does not exist: {$class}."); 48 | 49 | return 1; 50 | } 51 | 52 | // If a specific class is given, we can do both conversion steps at once 53 | // since the usages can still be recognized by the class name. 54 | $usagesAndImplementationConfig = realpath(__DIR__ . '/../Rector/usages-and-implementation.php'); 55 | $convertUsagesAndImplementation = "vendor/bin/rector process --clear-cache --config={$usagesAndImplementationConfig}"; 56 | $this->info("Converting {$class}, running: {$convertUsagesAndImplementation}"); 57 | $run($convertUsagesAndImplementation); 58 | } else { 59 | // If not, we have to do two steps to avoid partial conversion, 60 | // since the usages conversion relies on the enums extending BenSampo\Enum\Enum. 61 | $usagesConfig = realpath(__DIR__ . '/../Rector/usages.php'); 62 | $convertUsages = "vendor/bin/rector process --clear-cache --config={$usagesConfig}"; 63 | $this->info("Converting usages, running: {$convertUsages}"); 64 | $run($convertUsages); 65 | 66 | $implementationConfig = realpath(__DIR__ . '/../Rector/implementation.php'); 67 | $convertImplementation = "vendor/bin/rector process --clear-cache --config={$implementationConfig}"; 68 | $this->info("Converting implementation, running: {$convertImplementation}"); 69 | $run($convertImplementation); 70 | } 71 | 72 | return 0; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Commands/MakeEnumCommand.php: -------------------------------------------------------------------------------- 1 | option('flagged') 19 | ? $this->resolveStubPath('/stubs/enum.flagged.stub') 20 | : $this->resolveStubPath('/stubs/enum.stub'); 21 | } 22 | 23 | protected function resolveStubPath(string $stub): string 24 | { 25 | return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) 26 | ? $customPath 27 | : __DIR__ . $stub; 28 | } 29 | 30 | protected function getDefaultNamespace($rootNamespace): string 31 | { 32 | return "{$rootNamespace}\Enums"; 33 | } 34 | 35 | /** @return array> */ 36 | protected function getOptions(): array 37 | { 38 | return [ 39 | ['flagged', 'f', InputOption::VALUE_NONE, 'Generate a flagged enum'], 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/stubs/enum.flagged.stub: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | abstract class Enum implements EnumContract, Castable, Arrayable, \JsonSerializable 23 | { 24 | use Macroable { 25 | __callStatic as macroCallStatic; 26 | __call as macroCall; 27 | } 28 | 29 | /** 30 | * The value of one of the enum members. 31 | * 32 | * @var TValue 33 | */ 34 | public $value; 35 | 36 | /** The key of one of the enum members. */ 37 | public string $key; 38 | 39 | /** The description of one of the enum members. */ 40 | public string $description; 41 | 42 | /** 43 | * Caches reflections of enum subclasses. 44 | * 45 | * @var array, \ReflectionClass> 46 | */ 47 | protected static array $reflectionCache = []; 48 | 49 | /** 50 | * Construct an Enum instance. 51 | * 52 | * @param TValue $enumValue 53 | * 54 | * @throws \BenSampo\Enum\Exceptions\InvalidEnumMemberException 55 | */ 56 | public function __construct(mixed $enumValue) 57 | { 58 | if (! static::hasValue($enumValue)) { 59 | throw new InvalidEnumMemberException($enumValue, static::class); 60 | } 61 | 62 | $this->value = $enumValue; 63 | $this->key = static::getKey($enumValue); 64 | $this->description = static::getDescription($enumValue); 65 | } 66 | 67 | /** 68 | * Restores an enum instance exported by var_export(). 69 | * 70 | * @param array{value: TValue, key: string, description: string} $enum 71 | */ 72 | public static function __set_state(array $enum): static 73 | { 74 | return new static($enum['value']); 75 | } 76 | 77 | /** 78 | * Make a new instance from an enum value. 79 | * 80 | * @param TValue $enumValue 81 | */ 82 | public static function fromValue(mixed $enumValue): static 83 | { 84 | if ($enumValue instanceof static) { 85 | return $enumValue; 86 | } 87 | 88 | return new static($enumValue); // @phpstan-ignore return.type (generic variance) 89 | } 90 | 91 | /** 92 | * Returns a reflection of the enum subclass. 93 | * 94 | * @return \ReflectionClass 95 | */ 96 | protected static function getReflection(): \ReflectionClass 97 | { 98 | $class = static::class; 99 | 100 | return static::$reflectionCache[$class] ??= new \ReflectionClass($class); 101 | } 102 | 103 | /** 104 | * Make an enum instance from a given key. 105 | * 106 | * @throws \BenSampo\Enum\Exceptions\InvalidEnumKeyException 107 | */ 108 | public static function fromKey(string $key): static 109 | { 110 | if (static::hasKey($key)) { 111 | $enumValue = static::getValue($key); 112 | 113 | return new static($enumValue); 114 | } 115 | 116 | throw new InvalidEnumKeyException($key, static::class); 117 | } 118 | 119 | /** 120 | * Attempt to instantiate an enum by calling the enum key as a static method. 121 | * 122 | * This function defers to the macroable __callStatic function if a macro is found using the static method called. 123 | * 124 | * @param array $parameters 125 | */ 126 | public static function __callStatic(string $method, array $parameters): mixed 127 | { 128 | if (static::hasMacro($method)) { 129 | return static::macroCallStatic($method, $parameters); 130 | } 131 | 132 | return static::fromKey($method); 133 | } 134 | 135 | /** 136 | * Delegate magic method calls to macro's or the static call. 137 | * 138 | * While it is not typical to use the magic instantiation dynamically, it may happen 139 | * incidentally when calling the instantiation in an instance method of itself. 140 | * Even when using the `static::KEY()` syntax, PHP still interprets this is a call to 141 | * an instance method when it happens inside an instance method of the same class. 142 | * 143 | * @param string $method 144 | * @param array $parameters 145 | */ 146 | public function __call($method, $parameters): mixed 147 | { 148 | if (static::hasMacro($method)) { 149 | return $this->macroCall($method, $parameters); 150 | } 151 | 152 | return self::__callStatic($method, $parameters); 153 | } 154 | 155 | /** Checks if this instance is equal to the given enum instance or value. */ 156 | public function is(mixed $enumValue): bool 157 | { 158 | if ($enumValue instanceof static) { 159 | return $this->value === $enumValue->value; 160 | } 161 | 162 | return $this->value === $enumValue; 163 | } 164 | 165 | /** Checks if this instance is not equal to the given enum instance or value. */ 166 | public function isNot(mixed $enumValue): bool 167 | { 168 | return ! $this->is($enumValue); 169 | } 170 | 171 | /** 172 | * Checks if a matching enum instance or value is in the given values. 173 | * 174 | * @param iterable $values 175 | */ 176 | public function in(iterable $values): bool 177 | { 178 | foreach ($values as $value) { 179 | if ($this->is($value)) { 180 | return true; 181 | } 182 | } 183 | 184 | return false; 185 | } 186 | 187 | /** 188 | * Checks if a matching enum instance or value is not in the given values. 189 | * 190 | * @param iterable $values 191 | */ 192 | public function notIn(iterable $values): bool 193 | { 194 | foreach ($values as $value) { 195 | if ($this->is($value)) { 196 | return false; 197 | } 198 | } 199 | 200 | return true; 201 | } 202 | 203 | /** 204 | * Return instances of all the contained values. 205 | * 206 | * @return array 207 | */ 208 | public static function getInstances(): array 209 | { 210 | return array_map( 211 | static fn (mixed $constantValue): self => new static($constantValue), 212 | static::getConstants() 213 | ); 214 | } 215 | 216 | /** Attempt to instantiate a new Enum using the given key or value. */ 217 | public static function coerce(mixed $enumKeyOrValue): ?static 218 | { 219 | if ($enumKeyOrValue === null) { 220 | return null; 221 | } 222 | 223 | if ($enumKeyOrValue instanceof static) { 224 | return $enumKeyOrValue; 225 | } 226 | 227 | if (static::hasValue($enumKeyOrValue)) { 228 | return static::fromValue($enumKeyOrValue); 229 | } 230 | 231 | if (is_string($enumKeyOrValue) && static::hasKey($enumKeyOrValue)) { 232 | $enumValue = static::getValue($enumKeyOrValue); 233 | 234 | return new static($enumValue); 235 | } 236 | 237 | return null; 238 | } 239 | 240 | /** 241 | * Get all constants defined on the class. 242 | * 243 | * @return array 244 | */ 245 | protected static function getConstants(): array 246 | { 247 | return self::getReflection()->getConstants(); 248 | } 249 | 250 | /** 251 | * Get all or a custom set of the enum keys. 252 | * 253 | * @param TValue|array|null $values 254 | * 255 | * @return array 256 | */ 257 | public static function getKeys(mixed $values = null): array 258 | { 259 | if ($values === null) { 260 | return array_keys(static::getConstants()); 261 | } 262 | 263 | return array_map( 264 | [static::class, 'getKey'], 265 | is_array($values) ? $values : func_get_args(), 266 | ); 267 | } 268 | 269 | /** 270 | * Get all or a custom set of the enum values. 271 | * 272 | * @param string|array|null $keys 273 | * 274 | * @return array 275 | */ 276 | public static function getValues(string|array|null $keys = null): array 277 | { 278 | if ($keys === null) { 279 | return array_values(static::getConstants()); 280 | } 281 | 282 | return array_map( 283 | [static::class, 'getValue'], 284 | is_array($keys) ? $keys : func_get_args(), 285 | ); 286 | } 287 | 288 | /** 289 | * Get the key for a single enum value. 290 | * 291 | * @param TValue $value 292 | */ 293 | public static function getKey(mixed $value): string 294 | { 295 | return array_search($value, static::getConstants(), true) 296 | ?: throw new InvalidEnumMemberException($value, static::class); 297 | } 298 | 299 | /** 300 | * Get the value for a single enum key. 301 | * 302 | * @return TValue 303 | */ 304 | public static function getValue(string $key): mixed 305 | { 306 | return static::getConstants()[$key]; 307 | } 308 | 309 | /** 310 | * Get the description for an enum value. 311 | * 312 | * @param TValue $value 313 | */ 314 | public static function getDescription(mixed $value): string 315 | { 316 | return 317 | static::getLocalizedDescription($value) 318 | ?? static::getAttributeDescription($value) 319 | ?? static::getFriendlyName(static::getKey($value)); 320 | } 321 | 322 | /** 323 | * Get the localized description of a value. 324 | * 325 | * This works only if localization is enabled 326 | * for the enum and if the key exists in the lang file. 327 | * 328 | * @param TValue $value 329 | */ 330 | protected static function getLocalizedDescription(mixed $value): ?string 331 | { 332 | if (static::isLocalizable()) { 333 | $localizedStringKey = static::getLocalizationKey() . '.' . $value; 334 | 335 | if (Lang::has($localizedStringKey)) { 336 | return __($localizedStringKey); 337 | } 338 | } 339 | 340 | return null; 341 | } 342 | 343 | /** 344 | * Get the description of a value from its PHP attribute. 345 | * 346 | * @param TValue $value 347 | */ 348 | protected static function getAttributeDescription(mixed $value): ?string 349 | { 350 | $reflection = self::getReflection(); 351 | $constantName = static::getKey($value); 352 | $constReflection = $reflection->getReflectionConstant($constantName); 353 | if ($constReflection === false) { 354 | return null; 355 | } 356 | 357 | $descriptionAttributes = $constReflection->getAttributes(Description::class); 358 | 359 | return match (count($descriptionAttributes)) { 360 | 0 => null, 361 | 1 => $descriptionAttributes[0]->newInstance()->description, 362 | default => throw new \Exception('You cannot use more than 1 description attribute on ' . class_basename(static::class) . '::' . $constantName), 363 | }; 364 | } 365 | 366 | /** 367 | * Get the description of the enum class. 368 | * Default to Enum class short name. 369 | */ 370 | public static function getClassDescription(): string 371 | { 372 | return static::getClassAttributeDescription() 373 | ?? static::getFriendlyName(self::getReflection()->getShortName()); 374 | } 375 | 376 | protected static function getClassAttributeDescription(): ?string 377 | { 378 | $reflection = self::getReflection(); 379 | 380 | $descriptionAttributes = $reflection->getAttributes(Description::class); 381 | 382 | return match (count($descriptionAttributes)) { 383 | 0 => null, 384 | 1 => $descriptionAttributes[0]->newInstance()->description, 385 | default => throw new \Exception('You cannot use more than 1 description attribute on ' . class_basename(static::class)) 386 | }; 387 | } 388 | 389 | /** Get a random key from the enum. */ 390 | public static function getRandomKey(): string 391 | { 392 | $keys = static::getKeys(); 393 | 394 | return $keys[array_rand($keys)]; 395 | } 396 | 397 | /** 398 | * Get a random value from the enum. 399 | * 400 | * @return TValue 401 | */ 402 | public static function getRandomValue(): mixed 403 | { 404 | $values = static::getValues(); 405 | 406 | return $values[array_rand($values)]; 407 | } 408 | 409 | /** Get a random instance of the enum. */ 410 | public static function getRandomInstance(): static 411 | { 412 | return new static(static::getRandomValue()); 413 | } 414 | 415 | /** 416 | * Return the enum as an array. 417 | * 418 | * @return array 419 | */ 420 | public static function asArray(): array 421 | { 422 | return static::getConstants(); 423 | } 424 | 425 | /** 426 | * Get the enum as an array formatted for a select. 427 | * 428 | * @return array 429 | */ 430 | public static function asSelectArray(): array 431 | { 432 | $array = static::asArray(); 433 | $selectArray = []; 434 | 435 | foreach ($array as $value) { 436 | $selectArray[$value] = static::getDescription($value); 437 | } 438 | 439 | return $selectArray; 440 | } 441 | 442 | /** 443 | * @deprecated use self::asSelectArray() 444 | * 445 | * @return array 446 | */ 447 | public static function toSelectArray(): array 448 | { 449 | return self::asSelectArray(); 450 | } 451 | 452 | /** Check that the enum contains a specific key. */ 453 | public static function hasKey(string $key): bool 454 | { 455 | return in_array($key, static::getKeys(), true); 456 | } 457 | 458 | /** Check that the enum contains a specific value. */ 459 | public static function hasValue(mixed $value, bool $strict = true): bool 460 | { 461 | $validValues = static::getValues(); 462 | 463 | if ($strict) { 464 | return in_array($value, $validValues, true); 465 | } 466 | 467 | return in_array((string) $value, array_map('strval', $validValues), true); 468 | } 469 | 470 | /** Transform the name into a friendly, formatted version. */ 471 | protected static function getFriendlyName(string $name): string 472 | { 473 | if (ctype_upper(preg_replace('/[^a-zA-Z]/', '', $name))) { 474 | $name = strtolower($name); 475 | } 476 | 477 | return ucfirst(str_replace('_', ' ', Str::snake($name))); 478 | } 479 | 480 | /** Check that the enum implements the LocalizedEnum interface. */ 481 | protected static function isLocalizable(): bool 482 | { 483 | return isset(class_implements(static::class)[LocalizedEnum::class]); 484 | } 485 | 486 | /** Get the default localization key. */ 487 | public static function getLocalizationKey(): string 488 | { 489 | return 'enums.' . static::class; 490 | } 491 | 492 | /** 493 | * Cast values loaded from the database before constructing an enum from them. 494 | * 495 | * You may need to overwrite this when using string values that are returned 496 | * from a raw database query or a similar data source. 497 | * 498 | * @param mixed $value A raw value that may have any native type 499 | * 500 | * @return TValue|null The value cast into the type this enum expects or null 501 | */ 502 | public static function parseDatabase(mixed $value): mixed 503 | { 504 | return $value; 505 | } 506 | 507 | /** 508 | * Transform value from the enum instance before it's persisted to the database. 509 | * 510 | * You may need to overwrite this when using a database that expects a different 511 | * type to that used internally in your enum. 512 | * 513 | * @param TValue $value A value of the type this enum expects 514 | * 515 | * @return mixed The value cast into the type the database expects 516 | */ 517 | public static function serializeDatabase(mixed $value): mixed 518 | { 519 | if ($value instanceof self) { 520 | return $value->value; 521 | } 522 | 523 | return $value; 524 | } 525 | 526 | /** 527 | * Get the name of the caster class to use when casting from / to this cast target. 528 | * 529 | * @param array $arguments 530 | */ 531 | public static function castUsing(array $arguments): EnumCast 532 | { 533 | return new EnumCast(static::class); 534 | } 535 | 536 | /** 537 | * Return a plain representation of the enum. 538 | * 539 | * This method is not meant to be called directly, but rather be called 540 | * by Laravel through a recursive serialization with @see \Illuminate\Contracts\Support\Arrayable. 541 | * Thus, it returns a value meant to be included in a plain array. 542 | * 543 | * @return TValue 544 | */ 545 | public function toArray(): mixed 546 | { 547 | return $this->value; 548 | } 549 | 550 | /** 551 | * Return a JSON-serializable representation of the enum. 552 | * 553 | * @return TValue 554 | */ 555 | public function jsonSerialize(): mixed 556 | { 557 | return $this->value; 558 | } 559 | 560 | /** Return a string representation of the enum. */ 561 | public function __toString(): string 562 | { 563 | return (string) $this->value; 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /src/EnumServiceProvider.php: -------------------------------------------------------------------------------- 1 | bootCommands(); 20 | $this->bootValidationTranslation(); 21 | $this->bootValidators(); 22 | $this->bootDoctrineType(); 23 | } 24 | 25 | protected function bootCommands(): void 26 | { 27 | $this->publishes([ 28 | __DIR__ . '/Commands/stubs' => $this->app->basePath('stubs'), 29 | ], 'stubs'); 30 | 31 | if ($this->app->runningInConsole()) { 32 | $this->commands([ 33 | EnumAnnotateCommand::class, 34 | EnumToNativeCommand::class, 35 | MakeEnumCommand::class, 36 | ]); 37 | } 38 | } 39 | 40 | protected function bootValidators(): void 41 | { 42 | $this->app->extend(ValidationFactory::class, function (ValidationFactory $validationFactory): ValidationFactory { 43 | $validationFactory->extend('enum_key', function (string $attribute, $value, array $parameters, $validator): bool { 44 | $enum = $parameters[0] ?? null; 45 | 46 | return (new EnumKey($enum))->passes($attribute, $value); 47 | }, __('laravelEnum::messages.enum_key')); 48 | 49 | $validationFactory->extend('enum_value', function (string $attribute, $value, array $parameters, $validator): bool { 50 | $enum = $parameters[0] ?? null; 51 | $strict = $parameters[1] ?? null; 52 | 53 | if (! $strict) { 54 | return (new EnumValue($enum))->passes($attribute, $value); 55 | } 56 | $strict = (bool) json_decode(strtolower($strict)); 57 | 58 | return (new EnumValue($enum, $strict))->passes($attribute, $value); 59 | }, __('laravelEnum::messages.enum_value')); 60 | 61 | $validationFactory->extend('enum', function (string $attribute, $value, array $parameters, $validator): bool { 62 | $enum = $parameters[0] ?? null; 63 | 64 | return (new Enum($enum))->passes($attribute, $value); 65 | }, __('laravelEnum::messages.enum')); 66 | 67 | return $validationFactory; 68 | }); 69 | } 70 | 71 | protected function bootDoctrineType(): void 72 | { 73 | // Not included by default in Laravel 74 | if (class_exists('Doctrine\DBAL\Types\Type')) { 75 | if (! Type::hasType(EnumType::ENUM)) { 76 | Type::addType(EnumType::ENUM, EnumType::class); 77 | } 78 | } 79 | } 80 | 81 | protected function bootValidationTranslation(): void 82 | { 83 | $this->publishes([ 84 | __DIR__ . '/../lang' => lang_path('vendor/laravelEnum'), 85 | ], 'translations'); 86 | 87 | $this->loadTranslationsFrom(__DIR__ . '/../lang', 'laravelEnum'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/EnumType.php: -------------------------------------------------------------------------------- 1 | change() on an enum column definition when using migrations. 10 | */ 11 | class EnumType extends Type 12 | { 13 | public const ENUM = 'enum'; 14 | 15 | public function getSQLDeclaration(array $column, AbstractPlatform $platform): string 16 | { 17 | $values = implode( 18 | ',', 19 | array_map( 20 | fn (string $value): string => "'{$value}'", 21 | $column['allowed'] 22 | ) 23 | ); 24 | 25 | return "ENUM({$values})"; 26 | } 27 | 28 | public function getName(): string 29 | { 30 | return self::ENUM; 31 | } 32 | 33 | public function getMappedDatabaseTypes(AbstractPlatform $platform): array 34 | { 35 | return [self::ENUM]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidEnumKeyException.php: -------------------------------------------------------------------------------- 1 | > $enumClass */ 8 | public function __construct(mixed $invalidKey, string $enumClass) 9 | { 10 | $invalidValueType = gettype($invalidKey); 11 | $enumKeys = implode(', ', $enumClass::getKeys()); 12 | $enumClassName = class_basename($enumClass); 13 | 14 | parent::__construct("Cannot construct an instance of {$enumClassName} using the key ({$invalidValueType}) `{$invalidKey}`. Possible keys are [{$enumKeys}]."); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidEnumMemberException.php: -------------------------------------------------------------------------------- 1 | > $enum */ 8 | public function __construct(mixed $invalidValue, string $enum) 9 | { 10 | $invalidValueType = gettype($invalidValue); 11 | $enumValues = implode(', ', $enum::getValues()); 12 | $enumClassName = class_basename($enum); 13 | 14 | parent::__construct("Cannot construct an instance of {$enumClassName} using the value ({$invalidValueType}) `{$invalidValue}`. Possible values are [{$enumValues}]."); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/FlaggedEnum.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class FlaggedEnum extends Enum 13 | { 14 | /** 15 | * The value of one of the enum members. 16 | * 17 | * @var int 18 | */ 19 | public $value; 20 | 21 | /** Unset, do not access. */ 22 | public string $key; 23 | 24 | /** Unset, do not access. */ 25 | public string $description; 26 | 27 | public const None = 0; 28 | 29 | /** 30 | * Construct a FlaggedEnum instance. 31 | * 32 | * @param int|self|array $flags 33 | */ 34 | public function __construct(mixed $flags = []) 35 | { 36 | unset($this->key, $this->description); // @phpstan-ignore unset.possiblyHookedProperty,unset.possiblyHookedProperty (latest PHPStan on PHP 8.4) 37 | 38 | if (is_array($flags)) { 39 | $this->setFlags($flags); 40 | } else { 41 | try { 42 | parent::__construct($flags); 43 | } catch (InvalidEnumMemberException) { 44 | $this->value = $flags; 45 | } 46 | } 47 | } 48 | 49 | /** @param int|static|array $enumValue */ 50 | public static function fromValue(mixed $enumValue): static 51 | { 52 | return parent::fromValue($enumValue); 53 | } 54 | 55 | /** Attempt to instantiate a new Enum using the given key or value. */ 56 | public static function coerce(mixed $enumKeyOrValue): ?static 57 | { 58 | if (is_integer($enumKeyOrValue)) { 59 | return static::fromValue($enumKeyOrValue); 60 | } 61 | 62 | return parent::coerce($enumKeyOrValue); 63 | } 64 | 65 | /** 66 | * Return a FlaggedEnum instance with defined flags. 67 | * 68 | * @param array $flags 69 | */ 70 | public static function flags(array $flags): static 71 | { 72 | return static::fromValue($flags); 73 | } 74 | 75 | /** 76 | * Set the flags for the enum to the given array of flags. 77 | * 78 | * @param array $flags 79 | */ 80 | public function setFlags(array $flags): static 81 | { 82 | $this->value = array_reduce( 83 | $flags, 84 | static fn (int $carry, int|self $flag): int => $carry 85 | | ($flag instanceof self 86 | ? $flag->value 87 | : $flag), 88 | 0 89 | ); 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Add the given flag to the enum. 96 | * 97 | * @param int|static $flag 98 | */ 99 | public function addFlag(int|self $flag): static 100 | { 101 | $this->value |= ($flag instanceof self 102 | ? $flag->value 103 | : $flag); 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Add the given flags to the enum. 110 | * 111 | * @param array $flags 112 | */ 113 | public function addFlags(array $flags): static 114 | { 115 | foreach ($flags as $flag) { 116 | $this->addFlag($flag); 117 | } 118 | 119 | return $this; 120 | } 121 | 122 | /** Add all flags to the enum. */ 123 | public function addAllFlags(): static 124 | { 125 | return (new static())->addFlags(self::getValues()); 126 | } 127 | 128 | /** 129 | * Remove the given flag from the enum. 130 | * 131 | * @param int|static $flag 132 | */ 133 | public function removeFlag(int|self $flag): static 134 | { 135 | $this->value &= ~($flag instanceof self 136 | ? $flag->value 137 | : $flag); 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Remove the given flags from the enum. 144 | * 145 | * @param array $flags 146 | */ 147 | public function removeFlags(array $flags): static 148 | { 149 | foreach ($flags as $flag) { 150 | $this->removeFlag($flag); 151 | } 152 | 153 | return $this; 154 | } 155 | 156 | /** Remove all flags from the enum. */ 157 | public function removeAllFlags(): static 158 | { 159 | return static::None(); 160 | } 161 | 162 | /** 163 | * Check if the enum has the specified flag. 164 | * 165 | * @param int|static $flag 166 | */ 167 | public function hasFlag(int|self $flag): bool 168 | { 169 | $flagValue = ($flag instanceof self 170 | ? $flag->value 171 | : $flag); 172 | 173 | if ($flagValue === 0) { 174 | return false; 175 | } 176 | 177 | return ($flagValue & $this->value) === $flagValue; 178 | } 179 | 180 | /** 181 | * Check if the enum has all specified flags. 182 | * 183 | * @param array $flags 184 | */ 185 | public function hasFlags(array $flags): bool 186 | { 187 | foreach ($flags as $flag) { 188 | if (! static::hasFlag($flag)) { 189 | return false; 190 | } 191 | } 192 | 193 | return true; 194 | } 195 | 196 | /** 197 | * Check if the enum does not have the specified flag. 198 | * 199 | * @param int|static $flag 200 | */ 201 | public function notHasFlag(int|Enum $flag): bool 202 | { 203 | return ! $this->hasFlag($flag); 204 | } 205 | 206 | /** 207 | * Check if the enum doesn't have any of the specified flags. 208 | * 209 | * @param array $flags 210 | */ 211 | public function notHasFlags(array $flags): bool 212 | { 213 | foreach ($flags as $flag) { 214 | if (! static::notHasFlag($flag)) { 215 | return false; 216 | } 217 | } 218 | 219 | return true; 220 | } 221 | 222 | /** 223 | * Return the flags as an array of instances. 224 | * 225 | * @return array 226 | */ 227 | public function getFlags(): array 228 | { 229 | $members = static::getInstances(); 230 | $flags = []; 231 | 232 | foreach ($members as $member) { 233 | if ($this->hasFlag($member)) { 234 | $flags[] = $member; 235 | } 236 | } 237 | 238 | return $flags; 239 | } 240 | 241 | /** Check if there are multiple flags set on the enum. */ 242 | public function hasMultipleFlags(): bool 243 | { 244 | return ($this->value & ($this->value - 1)) !== 0; 245 | } 246 | 247 | /** Get the bitmask for the enum. */ 248 | public function getBitmask(): int 249 | { 250 | return (int) decbin($this->value); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/PHPStan/EnumMethodReflection.php: -------------------------------------------------------------------------------- 1 | classReflection; 25 | } 26 | 27 | public function getDeprecatedDescription(): ?string 28 | { 29 | return $this->constantReflection()->getDeprecatedDescription(); 30 | } 31 | 32 | public function getDocComment(): ?string 33 | { 34 | return null; 35 | } 36 | 37 | public function getName(): string 38 | { 39 | return $this->name; 40 | } 41 | 42 | public function getPrototype(): ClassMemberReflection 43 | { 44 | return $this; 45 | } 46 | 47 | public function getThrowType(): ?Type 48 | { 49 | return null; 50 | } 51 | 52 | public function getVariants(): array 53 | { 54 | return [ 55 | new FunctionVariant( 56 | TemplateTypeMap::createEmpty(), 57 | null, 58 | [], 59 | false, 60 | new StaticType($this->classReflection) 61 | ), 62 | ]; 63 | } 64 | 65 | public function hasSideEffects(): TrinaryLogic 66 | { 67 | return TrinaryLogic::createNo(); 68 | } 69 | 70 | public function isDeprecated(): TrinaryLogic 71 | { 72 | return $this->constantReflection()->isDeprecated(); 73 | } 74 | 75 | public function isFinal(): TrinaryLogic 76 | { 77 | return TrinaryLogic::createNo(); 78 | } 79 | 80 | public function isInternal(): TrinaryLogic 81 | { 82 | return $this->constantReflection()->isInternal(); 83 | } 84 | 85 | public function isPrivate(): bool 86 | { 87 | return false; 88 | } 89 | 90 | public function isPublic(): bool 91 | { 92 | return true; 93 | } 94 | 95 | public function isStatic(): bool 96 | { 97 | return true; 98 | } 99 | 100 | protected function constantReflection(): ConstantReflection 101 | { 102 | return $this->classReflection->getConstant($this->name); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/PHPStan/EnumMethodsClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Enum::class) 15 | && $classReflection->hasConstant($methodName); 16 | } 17 | 18 | public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection 19 | { 20 | return new EnumMethodReflection($classReflection, $methodName); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/PHPStan/UniqueValuesRule.php: -------------------------------------------------------------------------------- 1 | */ 13 | final class UniqueValuesRule implements Rule 14 | { 15 | public function getNodeType(): string 16 | { 17 | return InClassNode::class; 18 | } 19 | 20 | public function processNode(Node $node, Scope $scope): array 21 | { 22 | assert($node instanceof InClassNode); 23 | 24 | $reflection = $node->getClassReflection(); 25 | if (! $reflection->isSubclassOf(Enum::class)) { 26 | return []; 27 | } 28 | 29 | $constants = []; 30 | foreach ($reflection->getNativeReflection()->getReflectionConstants() as $constant) { 31 | $constants[$constant->name] = $constant->getValue(); 32 | } 33 | 34 | $duplicateConstants = []; 35 | foreach ($constants as $name => $value) { 36 | $constantsWithValue = array_filter($constants, fn (mixed $v): bool => $v === $value); 37 | if (count($constantsWithValue) > 1) { 38 | $duplicateConstants[] = json_encode(array_keys($constantsWithValue)); 39 | } 40 | } 41 | $duplicateConstants = array_unique($duplicateConstants); 42 | 43 | if (count($duplicateConstants) > 0) { 44 | $fqcn = $reflection->getName(); 45 | $constantsString = implode(',', $duplicateConstants); 46 | 47 | return [ 48 | RuleErrorBuilder::message("Enum class {$fqcn} contains constants with duplicate values: {$constantsString}.") 49 | ->identifier('enum.duplicateValues') 50 | ->build(), 51 | ]; 52 | } 53 | 54 | return []; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Rector/ToNativeImplementationRector.php: -------------------------------------------------------------------------------- 1 | 45 | */ 46 | class UserType extends Enum 47 | { 48 | const ADMIN = 1; 49 | const MEMBER = 2; 50 | } 51 | CODE_SAMPLE 52 | 53 | , 54 | <<<'CODE_SAMPLE' 55 | enum UserType : int 56 | { 57 | case ADMIN = 1; 58 | case MEMBER = 2; 59 | } 60 | CODE_SAMPLE, 61 | [ 62 | UserType::class, 63 | ], 64 | ), 65 | ]); 66 | } 67 | 68 | public function getNodeTypes(): array 69 | { 70 | return [Class_::class]; 71 | } 72 | 73 | /** 74 | * @see \Rector\Php81\NodeFactory\EnumFactory 75 | * 76 | * @param Class_ $class 77 | */ 78 | public function refactor(Node $class): ?Node 79 | { 80 | $this->classes ??= [new ObjectType(Enum::class)]; 81 | 82 | if (! $this->inConfiguredClasses($class)) { 83 | return null; 84 | } 85 | 86 | $enum = new Enum_( 87 | $this->nodeNameResolver->getShortName($class), 88 | [], 89 | ['startLine' => $class->getStartLine(), 'endLine' => $class->getEndLine()] 90 | ); 91 | $enum->namespacedName = $class->namespacedName; 92 | 93 | $phpDocInfo = $this->phpDocInfoFactory->createFromNode($class); 94 | if ($phpDocInfo) { 95 | $phpDocInfo->removeByType(MethodTagValueNode::class); 96 | $phpDocInfo->removeByType(ExtendsTagValueNode::class); 97 | 98 | $phpdoc = $this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo); 99 | // By removing unnecessary tags, we are usually left with a couple of redundant newlines. 100 | // There might be valuable ones to keep in long descriptions which will unfortunately 101 | // also be removed, but this should be less common. 102 | $withoutEmptyNewlines = preg_replace('/ \*\n/', '', $phpdoc); 103 | if ($withoutEmptyNewlines) { 104 | $enum->setDocComment(new Doc($withoutEmptyNewlines)); 105 | } 106 | } 107 | 108 | $enum->stmts = $class->getTraitUses(); 109 | 110 | $constants = $class->getConstants(); 111 | 112 | $constantValues = array_map( 113 | fn (ClassConst $classConst): mixed => $this->valueResolver->getValue( 114 | $classConst->consts[0]->value 115 | ), 116 | $constants 117 | ); 118 | $enumScalarType = $this->enumScalarType($constantValues); 119 | if ($enumScalarType) { 120 | $enum->scalarType = new Identifier($enumScalarType); 121 | } 122 | 123 | foreach ($constants as $constant) { 124 | $constConst = $constant->consts[0]; 125 | $enumCase = new EnumCase( 126 | $constConst->name, 127 | $constConst->value, 128 | [], 129 | ['startLine' => $constConst->getStartLine(), 'endLine' => $constConst->getEndLine()], 130 | ); 131 | 132 | // mirror comments 133 | $enumCase->setAttribute(AttributeKey::PHP_DOC_INFO, $constant->getAttribute(AttributeKey::PHP_DOC_INFO)); 134 | $enumCase->setAttribute(AttributeKey::COMMENTS, $constant->getAttribute(AttributeKey::COMMENTS)); 135 | 136 | $enum->stmts[] = $enumCase; 137 | } 138 | 139 | $enum->stmts = [...$enum->stmts, ...$class->getMethods()]; 140 | 141 | return $enum; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Rector/ToNativeRector.php: -------------------------------------------------------------------------------- 1 | */ 23 | protected array $classes; 24 | 25 | public function __construct( 26 | protected ValueResolver $valueResolver 27 | ) {} 28 | 29 | /** @param array $configuration */ 30 | public function configure(array $configuration): void 31 | { 32 | $this->classes = array_map( 33 | static fn (string $class): ObjectType => new ObjectType($class), 34 | $configuration, 35 | ); 36 | } 37 | 38 | protected function inConfiguredClasses(Node $node): bool 39 | { 40 | // When `get_class()` is used as a string, e.g. `get_class(0) . ''`, 41 | // isObjectType produces true - thus triggering a refactor: `get_class(0)->value . ''`. 42 | // To avoid this, we check if the node is constant a boolean type (true or false). 43 | $nodeType = $this->getType($node); 44 | if ($nodeType->isTrue()->yes() || $nodeType->isFalse()->yes()) { 45 | return false; 46 | } 47 | 48 | foreach ($this->classes as $class) { 49 | if ($this->isObjectType($node, $class)) { 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | 57 | /** @param array $constantValues */ 58 | protected function enumScalarType(array $constantValues): ?string 59 | { 60 | if ($constantValues === []) { 61 | return null; 62 | } 63 | 64 | // Assume the first constant value has the correct type 65 | $value = Arr::first($constantValues); 66 | if (is_string($value)) { 67 | return 'string'; 68 | } 69 | 70 | if (is_int($value)) { 71 | return 'int'; 72 | } 73 | 74 | return null; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Rector/ToNativeUsagesRector.php: -------------------------------------------------------------------------------- 1 | is(UserType::ADMIN); 71 | CODE_SAMPLE 72 | 73 | , 74 | <<<'CODE_SAMPLE' 75 | $user = UserType::ADMIN; 76 | $user === UserType::ADMIN; 77 | CODE_SAMPLE, 78 | [ 79 | UserType::class, 80 | ], 81 | ), 82 | ]); 83 | } 84 | 85 | public function getNodeTypes(): array 86 | { 87 | return [ 88 | New_::class, 89 | ArrayItem::class, 90 | ArrayDimFetch::class, 91 | BinaryOp::class, 92 | Ternary::class, 93 | Cast::class, 94 | Encapsed::class, 95 | Assign::class, 96 | AssignOp::class, 97 | AssignRef::class, 98 | ArrowFunction::class, 99 | Return_::class, 100 | Param::class, 101 | Match_::class, 102 | Switch_::class, 103 | CallLike::class, 104 | PropertyFetch::class, 105 | ]; 106 | } 107 | 108 | public function refactor(Node $node): ?Node 109 | { 110 | $this->classes ??= [new ObjectType(Enum::class)]; 111 | 112 | if ($node instanceof ArrayItem) { 113 | return $this->refactorArrayItem($node); 114 | } 115 | 116 | if ($node instanceof ArrayDimFetch) { 117 | return $this->refactorArrayDimFetch($node); 118 | } 119 | 120 | if ($node instanceof BinaryOp) { 121 | return $this->refactorBinaryOp($node); 122 | } 123 | 124 | if ($node instanceof Ternary) { 125 | return $this->refactorTernary($node); 126 | } 127 | 128 | if ($node instanceof Cast) { 129 | return $this->refactorCast($node); 130 | } 131 | 132 | if ($node instanceof Encapsed) { 133 | return $this->refactorEncapsed($node); 134 | } 135 | 136 | if ($node instanceof Assign || $node instanceof AssignOp || $node instanceof AssignRef) { 137 | return $this->refactorAssign($node); 138 | } 139 | 140 | if ($node instanceof ArrowFunction) { 141 | return $this->refactorArrowFunction($node); 142 | } 143 | 144 | if ($node instanceof Return_) { 145 | return $this->refactorReturn($node); 146 | } 147 | 148 | if ($node instanceof Param) { 149 | return $this->refactorParam($node); 150 | } 151 | 152 | if ($node instanceof Match_) { 153 | return $this->refactorMatch($node); 154 | } 155 | 156 | if ($node instanceof Switch_) { 157 | return $this->refactorSwitch($node); 158 | } 159 | 160 | if ($node instanceof CallLike) { 161 | if ($node instanceof New_ && $this->inConfiguredClasses($node->class)) { 162 | return $this->refactorNewOrFromValue($node); 163 | } 164 | 165 | if ($node instanceof StaticCall && $this->inConfiguredClasses($node->class)) { 166 | if ($this->isName($node->name, 'fromValue')) { 167 | return $this->refactorNewOrFromValue($node); 168 | } 169 | 170 | if ($this->isName($node->name, 'fromKey')) { 171 | return $this->refactorFromKey($node); 172 | } 173 | 174 | if ($this->isName($node->name, 'getInstances')) { 175 | return $this->refactorGetInstances($node); 176 | } 177 | 178 | if ($this->isName($node->name, 'getKeys')) { 179 | return $this->refactorGetKeys($node); 180 | } 181 | 182 | if ($this->isName($node->name, 'getValues')) { 183 | return $this->refactorGetValues($node); 184 | } 185 | 186 | if ($this->isName($node->name, 'getRandomInstance')) { 187 | return $this->refactorGetRandomInstance($node); 188 | } 189 | 190 | if ($this->isName($node->name, 'hasValue')) { 191 | return $this->refactorHasValue($node); 192 | } 193 | 194 | return $this->refactorMaybeMagicStaticCall($node); 195 | } 196 | 197 | if ( 198 | ($node instanceof MethodCall || $node instanceof NullsafeMethodCall) 199 | && $this->inConfiguredClasses($node->var) 200 | ) { 201 | if ($this->isName($node->name, 'is')) { 202 | return $this->refactorIsOrIsNot($node, true); 203 | } 204 | 205 | if ($this->isName($node->name, 'isNot')) { 206 | return $this->refactorIsOrIsNot($node, false); 207 | } 208 | 209 | if ($this->isName($node->name, 'in')) { 210 | return $this->refactorInOrNotIn($node, true); 211 | } 212 | 213 | if ($this->isName($node->name, 'notIn')) { 214 | return $this->refactorInOrNotIn($node, false); 215 | } 216 | 217 | if ($this->isName($node->name, '__toString')) { 218 | return $this->refactorMagicToString($node); 219 | } 220 | } 221 | 222 | return $this->refactorCall($node); 223 | } 224 | 225 | if ($node instanceof PropertyFetch) { 226 | if (! $this->inConfiguredClasses($node->var)) { 227 | return null; 228 | } 229 | 230 | if ($this->isName($node->name, 'key')) { 231 | return $this->refactorKey($node); 232 | } 233 | } 234 | 235 | return null; 236 | } 237 | 238 | /** 239 | * @see Enum::__construct() 240 | * @see Enum::fromValue() 241 | */ 242 | protected function refactorNewOrFromValue(New_|StaticCall $node): ?Node 243 | { 244 | $class = $node->class; 245 | if ($class instanceof Name) { 246 | $classString = $class->toString(); 247 | 248 | if ($node->isFirstClassCallable()) { 249 | return new StaticCall($class, 'from', [new VariadicPlaceholder()]); 250 | } 251 | 252 | $args = $node->args; 253 | if (isset($args[0])) { 254 | $argValue = $args[0]->value; 255 | if ($argValue instanceof ClassConstFetch) { 256 | $argValueClass = $argValue->class; 257 | $argValueName = $argValue->name; 258 | if ( 259 | $argValueClass instanceof Name 260 | && $argValueClass->toString() === $classString 261 | && $argValueName instanceof Identifier 262 | ) { 263 | return $this->createEnumCaseAccess($class, $argValueName->name); 264 | } 265 | } 266 | 267 | return new StaticCall($class, 'from', [new Arg($argValue)]); 268 | } 269 | } 270 | 271 | return null; 272 | } 273 | 274 | /** @see Enum::fromKey() */ 275 | protected function refactorFromKey(StaticCall $call): ?Node 276 | { 277 | $class = $call->class; 278 | if ($class instanceof Name) { 279 | $makeFromKey = function (Expr $key) use ($class): Expr { 280 | $paramName = lcfirst($class->getLast()); 281 | $paramVariable = new Variable($paramName); 282 | 283 | $enumInstanceMatchesKey = new ArrowFunction([ 284 | 'params' => [new Param($paramVariable, null, $class)], 285 | 'returnType' => new Identifier('bool'), 286 | 'expr' => new Identical( 287 | new PropertyFetch($paramVariable, 'name'), 288 | $key, 289 | ), 290 | ]); 291 | 292 | $arrayMatching = new FuncCall( 293 | new Name('array_filter'), 294 | [ 295 | new Arg(new StaticCall($class, 'cases')), 296 | new Arg($enumInstanceMatchesKey), 297 | ], 298 | ); 299 | 300 | return new StaticCall( 301 | new Name(Arr::class), 302 | 'first', 303 | [ 304 | new Arg($arrayMatching), 305 | ], 306 | ); 307 | }; 308 | 309 | if ($call->isFirstClassCallable()) { 310 | $keyVariable = new Variable('key'); 311 | 312 | return new ArrowFunction([ 313 | 'static' => true, 314 | 'params' => [new Param($keyVariable, null, new Identifier('string'))], 315 | 'returnType' => $class, 316 | 'expr' => $makeFromKey($keyVariable), 317 | ]); 318 | } 319 | 320 | $args = $call->args; 321 | if (isset($args[0])) { 322 | $argValue = $args[0]->value; 323 | 324 | return $makeFromKey($argValue); 325 | } 326 | } 327 | 328 | return null; 329 | } 330 | 331 | /** @see Enum::getInstances() */ 332 | protected function refactorGetInstances(StaticCall $call): ?StaticCall 333 | { 334 | $class = $call->class; 335 | if ($class instanceof Name) { 336 | return new StaticCall($class, 'cases'); 337 | } 338 | 339 | return null; 340 | } 341 | 342 | /** @see Enum::getKeys() */ 343 | protected function refactorGetKeys(StaticCall $call): ?Node 344 | { 345 | $class = $call->class; 346 | if ($class instanceof Name) { 347 | $args = $call->args; 348 | if ($args === []) { 349 | $paramName = lcfirst($class->getLast()); 350 | $paramVariable = new Variable($paramName); 351 | 352 | return new FuncCall( 353 | new Name('array_map'), 354 | [ 355 | new Arg( 356 | new ArrowFunction([ 357 | 'static' => true, 358 | 'params' => [new Param($paramVariable, null, $class)], 359 | 'returnType' => new Identifier('string'), 360 | 'expr' => new PropertyFetch($paramVariable, 'name'), 361 | ]) 362 | ), 363 | new Arg( 364 | new StaticCall($class, 'cases') 365 | ), 366 | ] 367 | ); 368 | } 369 | } 370 | 371 | return null; 372 | } 373 | 374 | /** @see Enum::getValues() */ 375 | protected function refactorGetValues(StaticCall $call): ?Node 376 | { 377 | $class = $call->class; 378 | if ($class instanceof Name) { 379 | $args = $call->args; 380 | if ($args === []) { 381 | $paramName = lcfirst($class->getLast()); 382 | $paramVariable = new Variable($paramName); 383 | 384 | return new FuncCall( 385 | new Name('array_map'), 386 | [ 387 | new Arg( 388 | new ArrowFunction([ 389 | 'static' => true, 390 | 'params' => [new Param($paramVariable, null, $class)], 391 | 'expr' => new PropertyFetch($paramVariable, 'value'), 392 | ]) 393 | ), 394 | new Arg( 395 | new StaticCall($class, 'cases') 396 | ), 397 | ], 398 | ); 399 | } 400 | } 401 | 402 | return null; 403 | } 404 | 405 | /** @see Enum::getRandomInstance() */ 406 | protected function refactorGetRandomInstance(StaticCall $call): ?Node 407 | { 408 | return new MethodCall( 409 | new FuncCall(new Name('fake')), 410 | 'randomElement', 411 | [new Arg(new StaticCall($call->class, 'cases'))] 412 | ); 413 | } 414 | 415 | /** @see Enum::hasValue() */ 416 | protected function refactorHasValue(StaticCall $call): ?Node 417 | { 418 | $class = $call->class; 419 | if ($class instanceof Name) { 420 | $makeTryFromNotNull = function (Arg $arg) use ($class): NotIdentical { 421 | $tryFrom = new StaticCall( 422 | $class, 423 | 'tryFrom', 424 | [$arg] 425 | ); 426 | $null = new ConstFetch(new Name('null')); 427 | 428 | return new NotIdentical($tryFrom, $null); 429 | }; 430 | 431 | if ($call->isFirstClassCallable()) { 432 | $valueVariable = new Variable('value'); 433 | $valueVariableArg = new Arg($valueVariable); 434 | 435 | $tryFromNotNull = $makeTryFromNotNull($valueVariableArg); 436 | 437 | $enumScalarType = $this->enumScalarTypeFromClassName($class); 438 | if ($enumScalarType === 'int') { 439 | $expr = new BooleanAnd( 440 | new FuncCall(new Name('is_int'), [$valueVariableArg]), 441 | $tryFromNotNull 442 | ); 443 | } elseif ($enumScalarType === 'string') { 444 | $expr = new BooleanAnd( 445 | new FuncCall(new Name('is_string'), [$valueVariableArg]), 446 | $tryFromNotNull 447 | ); 448 | } else { 449 | $expr = $tryFromNotNull; 450 | } 451 | 452 | return new ArrowFunction([ 453 | 'static' => true, 454 | 'params' => [new Param($valueVariable, null, new Identifier('mixed'))], 455 | 'returnType' => new Identifier('bool'), 456 | 'expr' => $expr, 457 | ]); 458 | } 459 | 460 | $args = $call->args; 461 | $firstArg = $args[0] ?? null; 462 | if ($firstArg instanceof Arg) { 463 | $firstArgValue = $firstArg->value; 464 | 465 | if ( 466 | $firstArgValue instanceof ClassConstFetch 467 | && $firstArgValue->class->toString() === $class->toString() 468 | ) { 469 | return new ConstFetch(new Name('true')); 470 | } 471 | 472 | $firstArgType = $this->getType($firstArgValue); 473 | 474 | $enumScalarType = $this->enumScalarTypeFromClassName($class); 475 | if ($enumScalarType === 'int') { 476 | $firstArgTypeIsInt = $firstArgType->isInteger(); 477 | if ($firstArgTypeIsInt->yes()) { 478 | return $makeTryFromNotNull($firstArg); 479 | } 480 | 481 | if ($firstArgTypeIsInt->no()) { 482 | return new ConstFetch(new Name('false')); 483 | } 484 | 485 | return new BooleanAnd( 486 | new FuncCall(new Name('is_int'), [$firstArg]), 487 | $makeTryFromNotNull($firstArg) 488 | ); 489 | } 490 | if ($enumScalarType === 'string') { 491 | $firstArgTypeIsString = $firstArgType->isString(); 492 | if ($firstArgTypeIsString->yes()) { 493 | return $makeTryFromNotNull($firstArg); 494 | } 495 | 496 | if ($firstArgTypeIsString->no()) { 497 | return new ConstFetch(new Name('false')); 498 | } 499 | 500 | return new BooleanAnd( 501 | new FuncCall(new Name('is_string'), [$firstArg]), 502 | $makeTryFromNotNull($firstArg) 503 | ); 504 | } 505 | 506 | return $makeTryFromNotNull($firstArg); 507 | } 508 | } 509 | 510 | return null; 511 | } 512 | 513 | protected function enumScalarTypeFromClassName(Name $class): ?string 514 | { 515 | $type = $this->getType($class); 516 | if (! $type instanceof FullyQualifiedObjectType) { 517 | return null; 518 | } 519 | 520 | $classReflection = $type->getClassReflection(); 521 | if (! $classReflection) { 522 | return null; 523 | } 524 | 525 | $nativeReflection = $classReflection->getNativeReflection(); 526 | if (! $nativeReflection instanceof \ReflectionClass) { 527 | return null; 528 | } 529 | 530 | return $this->enumScalarType($nativeReflection->getConstants()); 531 | } 532 | 533 | /** 534 | * @see Enum::__callStatic() 535 | * @see Enum::__call() 536 | */ 537 | protected function refactorMaybeMagicStaticCall(StaticCall $call): ?Node 538 | { 539 | $name = $call->name; 540 | if ($name instanceof Expr) { 541 | return null; 542 | } 543 | 544 | $class = $call->class; 545 | if ($class instanceof Name) { 546 | if ($class->isSpecialClassName()) { 547 | $type = $this->getType($class); 548 | if (! $type instanceof FullyQualifiedObjectType) { 549 | return null; 550 | } 551 | $fullyQualifiedClassName = $type->getClassName(); 552 | } else { 553 | $fullyQualifiedClassName = $class->toString(); 554 | } 555 | $constName = $name->toString(); 556 | if (defined("{$fullyQualifiedClassName}::{$constName}")) { 557 | return $this->createEnumCaseAccess($class, $constName); 558 | } 559 | } 560 | 561 | return null; 562 | } 563 | 564 | /** 565 | * @see Enum::is() 566 | * @see Enum::isNot() 567 | */ 568 | protected function refactorIsOrIsNot(MethodCall|NullsafeMethodCall $call, bool $is): ?Node 569 | { 570 | $comparison = $is 571 | ? Identical::class 572 | : NotIdentical::class; 573 | 574 | if ($call->isFirstClassCallable()) { 575 | $param = new Variable('value'); 576 | 577 | return new ArrowFunction([ 578 | 'params' => [new Param($param, null, new Identifier('mixed'))], 579 | 'returnType' => new Identifier('bool'), 580 | 'expr' => new $comparison($call->var, $param, [self::COMPARED_AGAINST_ENUM_INSTANCE => true]), 581 | ]); 582 | } 583 | 584 | $args = $call->getArgs(); 585 | if (isset($args[0])) { 586 | $arg = $args[0]; 587 | $right = $arg->value; 588 | 589 | $var = $call->var; 590 | $left = $this->willBeEnumInstance($right) 591 | ? $var 592 | : new PropertyFetch($var, 'value'); 593 | 594 | return new $comparison($left, $right, [self::COMPARED_AGAINST_ENUM_INSTANCE => true]); 595 | } 596 | 597 | return null; 598 | } 599 | 600 | /** 601 | * @see Enum::in() 602 | * @see Enum::notIn() 603 | */ 604 | protected function refactorInOrNotIn(MethodCall|NullsafeMethodCall $call, bool $in): ?Node 605 | { 606 | $args = $call->args; 607 | if (isset($args[0]) && $args[0] instanceof Arg) { 608 | $enumArg = new Arg($call->var); 609 | $valuesArg = $args[0]; 610 | 611 | $valuesValue = $valuesArg->value; 612 | if ($valuesValue instanceof Array_) { 613 | foreach ($valuesValue->items as $item) { 614 | $item->setAttribute(self::COMPARED_AGAINST_ENUM_INSTANCE, true); 615 | } 616 | } 617 | 618 | if ($this->isObjectType($valuesValue, new ObjectType(Enumerable::class))) { 619 | return new MethodCall( 620 | $valuesValue, 621 | new Identifier($in 622 | ? 'contains' 623 | : 'doesntContain'), 624 | [$enumArg], 625 | ); 626 | } 627 | 628 | $haystackArg = $this->getType($valuesValue)->isArray()->yes() 629 | ? $valuesArg 630 | : new Arg( 631 | new FuncCall( 632 | new Name('iterator_to_array'), 633 | [$valuesArg], 634 | ), 635 | ); 636 | 637 | $inArray = new FuncCall( 638 | new Name('in_array'), 639 | [$enumArg, $haystackArg], 640 | [self::COMPARED_AGAINST_ENUM_INSTANCE => true], 641 | ); 642 | 643 | return $in 644 | ? $inArray 645 | : new BooleanNot($inArray); 646 | } 647 | 648 | return null; 649 | } 650 | 651 | /** @see Enum::__toString() */ 652 | protected function refactorMagicToString(MethodCall|NullsafeMethodCall $call): Cast 653 | { 654 | return new String_( 655 | $this->createValueFetch($call->var, $call instanceof NullsafeMethodCall) 656 | ); 657 | } 658 | 659 | /** @see Enum::$key */ 660 | protected function refactorKey(PropertyFetch $fetch): ?Node 661 | { 662 | return new PropertyFetch($fetch->var, 'name'); 663 | } 664 | 665 | protected function refactorMatch(Match_ $match): ?Node 666 | { 667 | $cond = $match->cond; 668 | while ($cond instanceof AlwaysRememberedExpr) { // @phpstan-ignore phpstanApi.class (backwards compatibility not guaranteed) 669 | $cond = $cond->getExpr(); // @phpstan-ignore phpstanApi.method (backwards compatibility not guaranteed) 670 | } 671 | if (($cond instanceof PropertyFetch || $cond instanceof NullsafePropertyFetch) 672 | && $this->inConfiguredClasses($cond->var) 673 | ) { 674 | $var = $cond->var; 675 | $varType = $this->getType($var); 676 | 677 | $armsAreExclusivelyEnumsOrNull = true; 678 | foreach ($match->arms as $arm) { 679 | if ($arm->conds === null) { 680 | continue; 681 | } 682 | 683 | foreach ($arm->conds as $armCond) { 684 | $isEnum = $varType->equals($this->getType($armCond)) 685 | || ($armCond instanceof ClassConstFetch && $this->inConfiguredClasses($armCond->class)); 686 | $isNull = $this->getType($armCond)->isNull()->yes(); 687 | 688 | if (! $isEnum && ! $isNull) { 689 | $armsAreExclusivelyEnumsOrNull = false; 690 | } 691 | } 692 | } 693 | 694 | if ($armsAreExclusivelyEnumsOrNull) { 695 | return new Match_($var, $match->arms, $match->getAttributes()); 696 | } 697 | } 698 | 699 | if ($this->inConfiguredClasses($cond)) { 700 | return null; 701 | } 702 | 703 | $arms = []; 704 | foreach ($match->arms as $arm) { 705 | $arms[] = $arm->conds === null 706 | ? $arm 707 | : new MatchArm( 708 | array_map(fn (Expr $expr) => $this->convertToValueFetch($expr) ?? $expr, $arm->conds), 709 | $arm->body, 710 | $arm->getAttributes(), 711 | ); 712 | } 713 | 714 | return new Match_($cond, $arms, $match->getAttributes()); 715 | } 716 | 717 | protected function refactorSwitch(Switch_ $switch): ?Node 718 | { 719 | $cond = $switch->cond; 720 | if (($cond instanceof PropertyFetch || $cond instanceof NullsafePropertyFetch) 721 | && $this->inConfiguredClasses($cond->var) 722 | ) { 723 | $var = $cond->var; 724 | $varType = $this->getType($var); 725 | 726 | $casesAreExclusivelyEnumsOrNull = true; 727 | foreach ($switch->cases as $case) { 728 | $caseCond = $case->cond; 729 | if ($caseCond === null) { 730 | continue; 731 | } 732 | 733 | $isEnum = $varType->equals($this->getType($caseCond)) 734 | || ($caseCond instanceof ClassConstFetch && $this->inConfiguredClasses($caseCond->class)); 735 | $isNull = $this->getType($caseCond)->isNull()->yes(); 736 | 737 | if (! $isEnum && ! $isNull) { 738 | $casesAreExclusivelyEnumsOrNull = false; 739 | } 740 | } 741 | 742 | if ($casesAreExclusivelyEnumsOrNull) { 743 | return new Switch_($var, $switch->cases, $switch->getAttributes()); 744 | } 745 | } 746 | 747 | if ($this->inConfiguredClasses($cond)) { 748 | return null; 749 | } 750 | 751 | $cases = []; 752 | foreach ($switch->cases as $case) { 753 | $caseCond = $case->cond; 754 | $cases[] = new Case_( 755 | $this->convertToValueFetch($caseCond) ?? $caseCond, 756 | $case->stmts, 757 | $case->getAttributes(), 758 | ); 759 | } 760 | 761 | return new Switch_($cond, $cases, $switch->getAttributes()); 762 | } 763 | 764 | protected function refactorArrayItem(ArrayItem $arrayItem): ?Node 765 | { 766 | $key = $arrayItem->key; 767 | $convertedKey = $this->convertConstToValueFetch($key); 768 | 769 | $value = $arrayItem->value; 770 | $hasAttribute = $arrayItem->hasAttribute(self::COMPARED_AGAINST_ENUM_INSTANCE); 771 | $convertedValue = $hasAttribute 772 | ? null 773 | : $this->convertConstToValueFetch($value); 774 | 775 | if ($convertedKey || $convertedValue) { 776 | return new ArrayItem( 777 | $convertedValue ?? $value, 778 | $convertedKey ?? $key, 779 | $arrayItem->byRef, 780 | $arrayItem->getAttributes(), 781 | $arrayItem->unpack, 782 | ); 783 | } 784 | 785 | return null; 786 | } 787 | 788 | protected function willBeEnumInstance(Expr $expr): bool 789 | { 790 | if ($expr instanceof ClassConstFetch && $this->inConfiguredClasses($expr->class)) { 791 | return true; 792 | } 793 | 794 | return $this->inConfiguredClasses($expr); 795 | } 796 | 797 | protected function convertToValueFetch(?Expr $expr): ?Expr 798 | { 799 | if (! $expr) { 800 | return null; 801 | } 802 | 803 | $constValueFetch = $this->convertConstToValueFetch($expr); 804 | if ($constValueFetch) { 805 | return $constValueFetch; 806 | } 807 | 808 | if ($this->inConfiguredClasses($expr)) { 809 | return $this->createValueFetch($expr, $this->nodeTypeResolver->isNullableType($expr)); 810 | } 811 | 812 | return null; 813 | } 814 | 815 | protected function convertConstToValueFetch(?Expr $expr): ?Expr 816 | { 817 | if (! $expr 818 | || $expr->hasAttribute(self::CONVERTED_INSTANTIATION) 819 | || $expr->hasAttribute(self::COMPARED_AGAINST_ENUM_INSTANCE) 820 | ) { 821 | return null; 822 | } 823 | 824 | if ( 825 | $expr instanceof ClassConstFetch 826 | && $this->inConfiguredClasses($expr->class) 827 | && $expr->name->name !== 'class' 828 | ) { 829 | return $this->createValueFetch($expr, false); 830 | } 831 | 832 | return null; 833 | } 834 | 835 | protected function refactorBinaryOp(BinaryOp $binaryOp): ?Node 836 | { 837 | if ($binaryOp->hasAttribute(self::COMPARED_AGAINST_ENUM_INSTANCE)) { 838 | return null; 839 | } 840 | 841 | $left = $binaryOp->left; 842 | $right = $binaryOp->right; 843 | 844 | if ($binaryOp instanceof Coalesce) { 845 | // ->isString()->yes() could be string or string|null, but since it is one the left side of ?? we assume the latter 846 | if ($this->getType($left)->isString()->yes() && ! $this->willBeEnumInstance($left)) { 847 | $convertedRight = $this->convertToValueFetch($right); 848 | if ($convertedRight) { 849 | return new Coalesce($left, $convertedRight, $binaryOp->getAttributes()); 850 | } 851 | } 852 | if ($this->getType($right)->isString()->yes() && ! $this->willBeEnumInstance($right)) { 853 | $convertedLeft = $this->convertToValueFetch($left); 854 | if ($convertedLeft) { 855 | return new Coalesce($convertedLeft, $right, $binaryOp->getAttributes()); 856 | } 857 | } 858 | 859 | $convertedLeft = $this->convertConstToValueFetch($left); 860 | $convertedRight = $this->convertConstToValueFetch($right); 861 | 862 | if ($convertedLeft || $convertedRight) { 863 | return new Coalesce( 864 | $convertedLeft ?? $left, 865 | $convertedRight ?? $right, 866 | $binaryOp->getAttributes(), 867 | ); 868 | } 869 | 870 | return null; 871 | } 872 | 873 | if ($binaryOp instanceof Equal 874 | || $binaryOp instanceof Identical 875 | || $binaryOp instanceof NotEqual 876 | || $binaryOp instanceof NotIdentical 877 | ) { 878 | // Comparison of two class constants of the same class will become enum comparison 879 | if ( 880 | ($left instanceof ClassConstFetch && $right instanceof ClassConstFetch) 881 | && ($left->class instanceof Name && $right->class instanceof Name) 882 | && ($left->class->toString() === $right->class->toString()) 883 | ) { 884 | return null; 885 | } 886 | 887 | if ( 888 | ($left instanceof PropertyFetch || $left instanceof NullsafePropertyFetch) 889 | && $this->inConfiguredClasses($left->var) 890 | && $this->willBeEnumInstance($right) 891 | ) { 892 | return new $binaryOp($left->var, $right, $binaryOp->getAttributes()); 893 | } 894 | 895 | if ( 896 | ($right instanceof PropertyFetch || $right instanceof NullsafePropertyFetch) 897 | && $this->inConfiguredClasses($right->var) 898 | && $this->willBeEnumInstance($left) 899 | ) { 900 | return new $binaryOp($left, $right->var, $binaryOp->getAttributes()); 901 | } 902 | 903 | // If either side of the comparison is an enum, do not convert 904 | if ($this->inConfiguredClasses($left) || $this->inConfiguredClasses($right)) { 905 | return null; 906 | } 907 | 908 | $convertedLeft = $this->convertConstToValueFetch($left); 909 | $convertedRight = $this->convertConstToValueFetch($right); 910 | 911 | if ($convertedLeft || $convertedRight) { 912 | return new $binaryOp( 913 | $convertedLeft ?? $left, 914 | $convertedRight ?? $right, 915 | $binaryOp->getAttributes(), 916 | ); 917 | } 918 | 919 | return null; 920 | } 921 | 922 | // The remaining operators are: arithmetic, bitwise, comparison, logical, string. 923 | // They do not support enums and only work with the underlying values. 924 | $convertedLeft = $this->convertToValueFetch($left); 925 | $convertedRight = $this->convertToValueFetch($right); 926 | if ($convertedLeft || $convertedRight) { 927 | return new $binaryOp( 928 | $convertedLeft ?? $left, 929 | $convertedRight ?? $right, 930 | $binaryOp->getAttributes(), 931 | ); 932 | } 933 | 934 | return null; 935 | } 936 | 937 | protected function refactorAssign(Assign|AssignOp|AssignRef $assign): ?Node 938 | { 939 | $convertedExpr = $this->convertConstToValueFetch($assign->expr); 940 | $var = $assign->var; 941 | if ($convertedExpr && ! $this->inConfiguredClasses($var)) { 942 | return new $assign($var, $convertedExpr, $assign->getAttributes()); 943 | } 944 | 945 | return null; 946 | } 947 | 948 | protected function refactorCall(CallLike $call): ?CallLike 949 | { 950 | // At this point, we know the call is neither new'ing up a Bensampo\Enum\Enum, 951 | // nor is it statically or dynamically calling any of its methods which require 952 | // special conversion rules. Thus, we are safe to transform any const fetches to values. 953 | 954 | if ($call->isFirstClassCallable()) { 955 | return null; 956 | } 957 | 958 | $args = []; 959 | foreach ($call->getArgs() as $arg) { 960 | $args[] = new Arg( 961 | $this->convertConstToValueFetch($arg->value) ?? $arg->value, 962 | $arg->byRef, 963 | $arg->unpack, 964 | $arg->getAttributes(), 965 | $arg->name, 966 | ); 967 | } 968 | 969 | if ($call instanceof FuncCall && ! $call->hasAttribute(self::COMPARED_AGAINST_ENUM_INSTANCE)) { 970 | return new FuncCall($call->name, $args, $call->getAttributes()); 971 | } 972 | 973 | if ($call instanceof New_) { 974 | return new New_($call->class, $args, $call->getAttributes()); 975 | } 976 | 977 | if ($call instanceof MethodCall || $call instanceof NullsafeMethodCall) { 978 | return new $call($call->var, $call->name, $args, $call->getAttributes()); 979 | } 980 | 981 | if ($call instanceof StaticCall) { 982 | return new StaticCall($call->class, $call->name, $args, $call->getAttributes()); 983 | } 984 | 985 | return null; 986 | } 987 | 988 | protected function refactorReturn(Return_ $return): ?Node 989 | { 990 | $expr = $return->expr; 991 | if (! $expr || $expr->hasAttribute(self::CONVERTED_INSTANTIATION)) { 992 | return null; 993 | } 994 | 995 | $convertedExpr = $this->convertConstToValueFetch($expr); 996 | if ($convertedExpr) { 997 | return new Return_($convertedExpr, $return->getAttributes()); 998 | } 999 | 1000 | return null; 1001 | } 1002 | 1003 | protected function refactorArrayDimFetch(ArrayDimFetch $arrayDimFetch): ?Node 1004 | { 1005 | $convertedDim = $this->convertToValueFetch($arrayDimFetch->dim); 1006 | if ($convertedDim) { 1007 | return new ArrayDimFetch($arrayDimFetch->var, $convertedDim); 1008 | } 1009 | 1010 | return null; 1011 | } 1012 | 1013 | protected function refactorEncapsed(Encapsed $encapsed): Encapsed 1014 | { 1015 | $parts = []; 1016 | foreach ($encapsed->parts as $part) { 1017 | if ($part instanceof EncapsedStringPart) { 1018 | $parts[] = $part; 1019 | } else { 1020 | $parts[] = $this->convertToValueFetch($part) ?? $part; 1021 | } 1022 | } 1023 | 1024 | return new Encapsed($parts, $encapsed->getAttributes()); 1025 | } 1026 | 1027 | protected function refactorCast(Cast $cast): ?Cast 1028 | { 1029 | $convertedExpr = $this->convertToValueFetch($cast->expr); 1030 | if ($convertedExpr) { 1031 | return new $cast($convertedExpr, $cast->getAttributes()); 1032 | } 1033 | 1034 | return null; 1035 | } 1036 | 1037 | protected function createValueFetch(Expr $expr, bool $isNullable): NullsafePropertyFetch|PropertyFetch 1038 | { 1039 | return $isNullable 1040 | ? new NullsafePropertyFetch($expr, 'value') 1041 | : new PropertyFetch($expr, 'value'); 1042 | } 1043 | 1044 | protected function refactorArrowFunction(ArrowFunction $arrowFunction): ?ArrowFunction 1045 | { 1046 | $convertedExpr = $this->convertConstToValueFetch($arrowFunction->expr); 1047 | if ($convertedExpr) { 1048 | return new ArrowFunction( 1049 | [ 1050 | 'static' => $arrowFunction->static, 1051 | 'byRef' => $arrowFunction->byRef, 1052 | 'params' => $arrowFunction->params, 1053 | 'returnType' => $arrowFunction->returnType, 1054 | 'expr' => $convertedExpr, 1055 | 'attrGroups' => $arrowFunction->attrGroups, 1056 | ], 1057 | $arrowFunction->getAttributes(), 1058 | ); 1059 | } 1060 | 1061 | return null; 1062 | } 1063 | 1064 | protected function createEnumCaseAccess(Name $class, string $constName): ClassConstFetch 1065 | { 1066 | return new ClassConstFetch( 1067 | $class, 1068 | $constName, 1069 | [self::CONVERTED_INSTANTIATION => true], 1070 | ); 1071 | } 1072 | 1073 | protected function refactorParam(Param $param): ?Param 1074 | { 1075 | $convertedDefault = $this->convertConstToValueFetch($param->default); 1076 | if ($convertedDefault) { 1077 | return new Param( 1078 | $param->var, 1079 | $convertedDefault, 1080 | $param->type, 1081 | $param->byRef, 1082 | $param->variadic, 1083 | $param->getAttributes(), 1084 | $param->flags, 1085 | $param->attrGroups, 1086 | ); 1087 | } 1088 | 1089 | return null; 1090 | } 1091 | 1092 | protected function refactorTernary(Ternary $ternary): ?Node 1093 | { 1094 | $if = $ternary->if; 1095 | $convertedIf = $this->convertConstToValueFetch($if); 1096 | 1097 | $else = $ternary->else; 1098 | $convertedElse = $this->convertConstToValueFetch($else); 1099 | 1100 | if ($convertedIf || $convertedElse) { 1101 | return new Ternary( 1102 | $ternary->cond, 1103 | $convertedIf ?? $if, 1104 | $convertedElse ?? $else, 1105 | $ternary->getAttributes(), 1106 | ); 1107 | } 1108 | 1109 | return null; 1110 | } 1111 | } 1112 | -------------------------------------------------------------------------------- /src/Rector/implementation.php: -------------------------------------------------------------------------------- 1 | import(env(EnumToNativeCommand::BASE_RECTOR_CONFIG_PATH_ENV)); 9 | $rectorConfig->ruleWithConfiguration(ToNativeImplementationRector::class, [ 10 | env(EnumToNativeCommand::TO_NATIVE_CLASS_ENV), 11 | ]); 12 | }; 13 | -------------------------------------------------------------------------------- /src/Rector/usages-and-implementation.php: -------------------------------------------------------------------------------- 1 | import(env(EnumToNativeCommand::BASE_RECTOR_CONFIG_PATH_ENV)); 10 | $classes = [ 11 | env(EnumToNativeCommand::TO_NATIVE_CLASS_ENV), 12 | ]; 13 | $rectorConfig->ruleWithConfiguration(ToNativeUsagesRector::class, $classes); 14 | $rectorConfig->ruleWithConfiguration(ToNativeImplementationRector::class, $classes); 15 | }; 16 | -------------------------------------------------------------------------------- /src/Rector/usages.php: -------------------------------------------------------------------------------- 1 | import(env(EnumToNativeCommand::BASE_RECTOR_CONFIG_PATH_ENV)); 9 | $rectorConfig->ruleWithConfiguration(ToNativeUsagesRector::class, [ 10 | env(EnumToNativeCommand::TO_NATIVE_CLASS_ENV), 11 | ]); 12 | }; 13 | -------------------------------------------------------------------------------- /src/Rules/Enum.php: -------------------------------------------------------------------------------- 1 | > */ 14 | protected string $enumClass 15 | ) { 16 | if (! class_exists($this->enumClass)) { 17 | throw new \InvalidArgumentException("Cannot validate against the enum, the class {$this->enumClass} doesn't exist."); 18 | } 19 | } 20 | 21 | /** 22 | * Determine if the validation rule passes. 23 | * 24 | * @param string $attribute 25 | */ 26 | public function passes($attribute, $value): bool 27 | { 28 | return $value instanceof $this->enumClass; 29 | } 30 | 31 | /** 32 | * Get the validation error message. 33 | * 34 | * @return string|array 35 | */ 36 | public function message(): string|array 37 | { 38 | return trans()->has('validation.enum') 39 | ? __('validation.enum') 40 | : __('laravelEnum::messages.enum'); 41 | } 42 | 43 | /** 44 | * Convert the rule to a validation string. 45 | * 46 | * @see \Illuminate\Validation\ValidationRuleParser::parseParameters 47 | */ 48 | public function __toString(): string 49 | { 50 | return "{$this->rule}:{$this->enumClass}"; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Rules/EnumKey.php: -------------------------------------------------------------------------------- 1 | > */ 14 | protected string $enumClass 15 | ) { 16 | if (! class_exists($this->enumClass)) { 17 | throw new \InvalidArgumentException("Cannot validate against the enum, the class {$this->enumClass} doesn't exist."); 18 | } 19 | } 20 | 21 | /** 22 | * Determine if the validation rule passes. 23 | * 24 | * @param string $attribute 25 | */ 26 | public function passes($attribute, $value): bool 27 | { 28 | return is_string($value) && $this->enumClass::hasKey($value); 29 | } 30 | 31 | /** 32 | * Get the validation error message. 33 | * 34 | * @return string|array 35 | */ 36 | public function message(): string|array 37 | { 38 | return trans()->has('validation.enum_key') 39 | ? __('validation.enum_key') 40 | : __('laravelEnum::messages.enum_key'); 41 | } 42 | 43 | /** 44 | * Convert the rule to a validation string. 45 | * 46 | * @see \Illuminate\Validation\ValidationRuleParser::parseParameters 47 | */ 48 | public function __toString(): string 49 | { 50 | return "{$this->rule}:{$this->enumClass}"; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Rules/EnumValue.php: -------------------------------------------------------------------------------- 1 | enumClass)) { 19 | throw new \InvalidArgumentException("Cannot validate against the enum, the class {$this->enumClass} doesn't exist."); 20 | } 21 | } 22 | 23 | /** 24 | * Determine if the validation rule passes. 25 | * 26 | * @param string $attribute 27 | */ 28 | public function passes($attribute, $value): bool 29 | { 30 | if (is_subclass_of($this->enumClass, FlaggedEnum::class) && (is_integer($value) || ctype_digit($value))) { 31 | // Unset all possible flag values 32 | foreach ($this->enumClass::getValues() as $enumValue) { 33 | assert(is_int($enumValue), 'Flagged enum values must be int'); 34 | $value &= ~$enumValue; 35 | } 36 | 37 | // All bits should be unset 38 | return $value === 0; 39 | } 40 | 41 | return $this->enumClass::hasValue($value, $this->strict); 42 | } 43 | 44 | /** 45 | * Get the validation error message. 46 | * 47 | * @return string|array 48 | */ 49 | public function message(): string|array 50 | { 51 | return trans()->has('validation.enum_value') 52 | ? __('validation.enum_value') 53 | : __('laravelEnum::messages.enum_value'); 54 | } 55 | 56 | /** 57 | * Convert the rule to a validation string. 58 | * 59 | * @see \Illuminate\Validation\ValidationRuleParser::parseParameters 60 | */ 61 | public function __toString(): string 62 | { 63 | $strict = $this->strict ? 'true' : 'false'; 64 | 65 | return "{$this->rule}:{$this->enumClass},{$strict}"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Traits/QueriesFlaggedEnums.php: -------------------------------------------------------------------------------- 1 | $query 12 | * 13 | * @return \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> 14 | */ 15 | public function scopeHasFlag(Builder $query, string $column, int|FlaggedEnum $flag): Builder 16 | { 17 | return $query->whereRaw("{$column} & ? > 0", [$flag]); 18 | } 19 | 20 | /** 21 | * @param \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> $query 22 | * 23 | * @return \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> 24 | */ 25 | public function scopeNotHasFlag(Builder $query, string $column, int|FlaggedEnum $flag): Builder 26 | { 27 | return $query->whereRaw("not {$column} & ? > 0", [$flag]); 28 | } 29 | 30 | /** 31 | * @param \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> $query 32 | * @param array $flags 33 | * 34 | * @return \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> 35 | */ 36 | public function scopeHasAllFlags(Builder $query, string $column, array $flags): Builder 37 | { 38 | $mask = $this->flagsSum($flags); 39 | 40 | return $query->whereRaw("{$column} & ? = ?", [$mask, $mask]); 41 | } 42 | 43 | /** 44 | * @param \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> $query 45 | * @param array $flags 46 | * 47 | * @return \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> 48 | */ 49 | public function scopeHasAnyFlags(Builder $query, string $column, array $flags): Builder 50 | { 51 | $mask = $this->flagsSum($flags); 52 | 53 | return $query->whereRaw("{$column} & ? > 0", [$mask]); 54 | } 55 | 56 | /** @param array $flags */ 57 | protected function flagsSum(array $flags): int 58 | { 59 | return array_reduce( 60 | $flags, 61 | static fn (int $carry, int|FlaggedEnum $flag): int => $carry 62 | + ($flag instanceof FlaggedEnum 63 | ? $flag->value 64 | : $flag), 65 | 0 66 | ); 67 | } 68 | } 69 | --------------------------------------------------------------------------------