├── .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 |

2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------