├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bin └── enum ├── cli ├── annotate.php ├── help ├── make.php └── ts.php ├── composer.json ├── duster.json ├── helpers ├── cli.php └── core.php ├── phpstan-baseline.neon ├── phpstan.neon ├── pint.json ├── src ├── Attributes │ └── Meta.php ├── CasesCollection.php ├── Concerns │ ├── CollectsCases.php │ ├── Compares.php │ ├── Enumerates.php │ ├── Hydrates.php │ ├── IsMagic.php │ └── SelfAware.php ├── Data │ ├── GeneratingEnum.php │ └── MethodAnnotation.php ├── Enums.php ├── Enums │ └── Backed.php └── Services │ ├── Annotator.php │ ├── Generator.php │ ├── Inspector.php │ ├── MethodAnnotations.php │ ├── TypeScript.php │ └── UseStatements.php └── stubs ├── enum.stub └── typescript.stub /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `enum` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles. 6 | 7 | 8 | ## NEXT - YYYY-MM-DD 9 | 10 | ### Added 11 | - Nothing 12 | 13 | ### Changed 14 | - Nothing 15 | 16 | ### Deprecated 17 | - Nothing 18 | 19 | ### Fixed 20 | - Nothing 21 | 22 | ### Removed 23 | - Nothing 24 | 25 | ### Security 26 | - Nothing 27 | 28 | 29 | ## 2.3.2 - 2025-01-30 30 | 31 | ### Fixed 32 | - Give priority to properties when resolving a case item 33 | 34 | 35 | ## 2.3.1 - 2025-01-15 36 | 37 | ### Changed 38 | - Removed unneeded readonly modifiers 39 | 40 | 41 | ## 2.3.0 - 2025-01-12 42 | 43 | ### Added 44 | - Enums discoverability 45 | - Console command to create an enum 46 | - Console command to annotate enums 47 | - Console command to turn PHP enums into TypeScript enums 48 | 49 | ### Changed 50 | - Improved static analysis 51 | - CasesCollection::groupBy() does not wrap the result into a collection 52 | 53 | 54 | ## 2.2.1 - 2024-11-22 55 | 56 | ### Added 57 | - Full support for PHP 8.4 58 | 59 | ### Changed 60 | - Improved error message for invalid meta 61 | 62 | 63 | ## 2.2.0 - 2024-11-19 64 | 65 | ### Added 66 | - Method `SelfAware::metaAttributeNames()` to list the names of all meta attributes 67 | 68 | ### Changed 69 | - Upgraded PHPStan to v2 70 | 71 | 72 | ## 2.1.0 - 2024-10-30 73 | 74 | ### Added 75 | - Method has() to the cases collection 76 | - JsonSerializable and Stringable interfaces to the cases collection 77 | - Methods isBackedByInteger() and isBackedByString() to the SelfAware trait 78 | 79 | ### Changed 80 | - Allow any callable when setting the logic for magic methods 81 | - Allow meta inheritance when getting meta names 82 | - Improve generics in cases collection 83 | - Simplify logic by negating methods in the Compares trait 84 | 85 | ### Deprecated 86 | - Nothing 87 | 88 | ### Fixed 89 | - Nothing 90 | 91 | ### Removed 92 | - Nothing 93 | 94 | ### Security 95 | - Nothing 96 | 97 | 98 | ## 2.0.0 - 2024-10-05 99 | 100 | ### Added 101 | - Custom and default implementation of magic methods 102 | - The `Meta` attribute and related methods 103 | - Method `value()` to get the value of a backed case or the name of a pure case 104 | - Methods `toArray()`, `map()` to the `CasesCollection` 105 | - Generics in docblocks 106 | - Static analysis 107 | 108 | ### Changed 109 | - Renamed keys to meta 110 | - `CasesCollection` methods return an instance of the collection whenever possible 111 | - `CasesCollection::groupBy()` groups into instances of the collection 112 | - Filtering methods keep the collection keys 113 | - Renamed methods `CollectsCases::casesBy*()` to `CollectsCases::keyBy*()` 114 | - Renamed `cases()` to `all()` in `CasesCollection` 115 | - Renamed `get()` to `resolveMeta()` in `SelfAware` 116 | - When hydrating from meta, the value is no longer mandatory and it defaults to `true` 117 | - The value for `pluck()` is now mandatory 118 | - Renamed sorting methods 119 | - Introduced PER code style 120 | 121 | ### Removed 122 | - Parameter `$default` from the `CasesCollection::first()` method 123 | 124 | 125 | ## 1.0.0 - 2022-07-12 126 | 127 | ### Added 128 | - First implementation of the package 129 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `andrea.marco.sartori@gmail.com`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/cerbero90/enum). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Andrea Marco Sartori 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎲 Enum 2 | 3 | [![Author][ico-author]][link-author] 4 | [![PHP Version][ico-php]][link-php] 5 | [![Build Status][ico-actions]][link-actions] 6 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer] 7 | [![Quality Score][ico-code-quality]][link-code-quality] 8 | [![PHPStan Level][ico-phpstan]][link-phpstan] 9 | [![Total Downloads][ico-downloads]][link-downloads] 10 | [![Latest Version][ico-version]][link-packagist] 11 | [![Software License][ico-license]](LICENSE.md) 12 | [![PER][ico-per]][link-per] 13 | 14 | Zero-dependencies package to supercharge enum functionalities. 15 | 16 | > [!TIP] 17 | > Need to supercharge enums in a Laravel application? 18 | > 19 | > Consider using [🎲 Laravel Enum](https://github.com/cerbero90/laravel-enum) instead. 20 | 21 | 22 | ## 📦 Install 23 | 24 | Via Composer: 25 | 26 | ``` bash 27 | composer require cerbero/enum 28 | ``` 29 | 30 | ## 🔮 Usage 31 | 32 | * [⚖️ Comparison](#%EF%B8%8F-comparison) 33 | * [🏷️ Meta](#%EF%B8%8F-meta) 34 | * [🚰 Hydration](#-hydration) 35 | * [🎲 Enum operations](#-enum-operations) 36 | * [🧺 Cases collection](#-cases-collection) 37 | * [🪄 Magic](#-magic) 38 | * [🤳 Self-awareness](#-self-awareness) 39 | * [🦾 Console commands](#-console-commands) 40 | * [🗒️ annotate](#%EF%B8%8F-annotate) 41 | * [🏗️ make](#%EF%B8%8F-make) 42 | * [💙 ts](#-ts) 43 | 44 | To supercharge our enums with all the features provided by this package, we can let our enums use the `Enumerates` trait: 45 | 46 | ```php 47 | use Cerbero\Enum\Concerns\Enumerates; 48 | 49 | enum PureEnum 50 | { 51 | use Enumerates; 52 | 53 | case One; 54 | case Two; 55 | case Three; 56 | } 57 | 58 | enum BackedEnum: int 59 | { 60 | use Enumerates; 61 | 62 | case One = 1; 63 | case Two = 2; 64 | case Three = 3; 65 | } 66 | ``` 67 | 68 | 69 | ### ⚖️ Comparison 70 | 71 | We can check whether an enum includes some names or values. Pure enums check for names and backed enums check for values: 72 | 73 | ```php 74 | PureEnum::has('One'); // true 75 | PureEnum::has('four'); // false 76 | PureEnum::doesntHave('One'); // false 77 | PureEnum::doesntHave('four'); // true 78 | 79 | BackedEnum::has(1); // true 80 | BackedEnum::has(4); // false 81 | BackedEnum::doesntHave(1); // false 82 | BackedEnum::doesntHave(4); // true 83 | ``` 84 | 85 | Otherwise we can check whether cases match a given name or value: 86 | 87 | ```php 88 | PureEnum::One->is('One'); // true 89 | PureEnum::One->is(1); // false 90 | PureEnum::One->is('four'); // false 91 | PureEnum::One->isNot('One'); // false 92 | PureEnum::One->isNot(1); // true 93 | PureEnum::One->isNot('four'); // true 94 | 95 | BackedEnum::One->is(1); // true 96 | BackedEnum::One->is('1'); // false 97 | BackedEnum::One->is(4); // false 98 | BackedEnum::One->isNot(1); // false 99 | BackedEnum::One->isNot('1'); // true 100 | BackedEnum::One->isNot(4); // true 101 | ``` 102 | 103 | Comparisons can also be performed against arrays: 104 | 105 | ```php 106 | PureEnum::One->in(['One', 'four']); // true 107 | PureEnum::One->in([1, 4]); // false 108 | PureEnum::One->notIn(['One', 'four']); // false 109 | PureEnum::One->notIn([1, 4]); // true 110 | 111 | BackedEnum::One->in([1, 4]); // true 112 | BackedEnum::One->in(['One', 'four']); // false 113 | BackedEnum::One->notIn([1, 4]); // false 114 | BackedEnum::One->notIn(['One', 'four']); // true 115 | ``` 116 | 117 | 118 | ### 🏷️ Meta 119 | 120 | Meta add extra information to a case. Meta can be added by implementing a public non-static method and/or by attaching `#[Meta]` attributes to cases: 121 | 122 | ```php 123 | enum BackedEnum: int 124 | { 125 | use Enumerates; 126 | 127 | #[Meta(color: 'red', shape: 'triangle')] 128 | case One = 1; 129 | 130 | #[Meta(color: 'green', shape: 'square')] 131 | case Two = 2; 132 | 133 | #[Meta(color: 'blue', shape: 'circle')] 134 | case Three = 3; 135 | 136 | public function isOdd(): bool 137 | { 138 | return $this->value % 2 != 0; 139 | } 140 | } 141 | ``` 142 | 143 | The above enum defines 3 meta for each case: `color`, `shape` and `isOdd`. The `#[Meta]` attributes are ideal to declare static information, whilst public non-static methods are ideal to declare dynamic information. 144 | 145 | To access a case meta, we can simply call the method having the same name of the wanted meta: 146 | 147 | ```php 148 | BackedEnum::Two->color(); // green 149 | ``` 150 | 151 | > [!TIP] 152 | > Our IDE can autocomplete meta methods thanks to the [`annotate` command](#%EF%B8%8F-annotate). 153 | 154 | `#[Meta]` attributes can also be attached to the enum itself to provide default values when a case does not declare its own meta values: 155 | 156 | ```php 157 | #[Meta(color: 'red', shape: 'triangle')] 158 | enum BackedEnum: int 159 | { 160 | use Enumerates; 161 | 162 | case One = 1; 163 | 164 | #[Meta(color: 'green', shape: 'square')] 165 | case Two = 2; 166 | 167 | case Three = 3; 168 | } 169 | ``` 170 | 171 | In the above example all cases have a `red` color and a `triangle` shape, except the case `Two` that overrides the default meta values. 172 | 173 | Meta can also be leveraged for the [hydration](#-hydration), [elaboration](#-enum-operations) and [collection](#-cases-collection) of cases. 174 | 175 | 176 | ### 🚰 Hydration 177 | 178 | An enum case can be instantiated from its own name, value (if backed) or [meta](#%EF%B8%8F-meta): 179 | 180 | ```php 181 | PureEnum::from('One'); // PureEnum::One 182 | PureEnum::from('four'); // throws ValueError 183 | PureEnum::tryFrom('One'); // PureEnum::One 184 | PureEnum::tryFrom('four'); // null 185 | PureEnum::fromName('One'); // PureEnum::One 186 | PureEnum::fromName('four'); // throws ValueError 187 | PureEnum::tryFromName('One'); // PureEnum::One 188 | PureEnum::tryFromName('four'); // null 189 | PureEnum::fromMeta('color', 'red'); // CasesCollection[PureEnum::One] 190 | PureEnum::fromMeta('color', 'purple'); // throws ValueError 191 | PureEnum::fromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 192 | PureEnum::fromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[PureEnum::One, PureEnum::Three] 193 | PureEnum::tryFromMeta('color', 'red'); // CasesCollection[PureEnum::One] 194 | PureEnum::tryFromMeta('color', 'purple'); // null 195 | PureEnum::tryFromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 196 | PureEnum::tryFromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[PureEnum::One, PureEnum::Three] 197 | 198 | BackedEnum::from(1); // BackedEnum::One 199 | BackedEnum::from('1'); // throws ValueError 200 | BackedEnum::tryFrom(1); // BackedEnum::One 201 | BackedEnum::tryFrom('1'); // null 202 | BackedEnum::fromName('One'); // BackedEnum::One 203 | BackedEnum::fromName('four'); // throws ValueError 204 | BackedEnum::tryFromName('One'); // BackedEnum::One 205 | BackedEnum::tryFromName('four'); // null 206 | BackedEnum::fromMeta('color', 'red'); // CasesCollection[BackedEnum::One] 207 | BackedEnum::fromMeta('color', 'purple'); // throws ValueError 208 | BackedEnum::fromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 209 | BackedEnum::fromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[BackedEnum::One, BackedEnum::Three] 210 | BackedEnum::tryFromMeta('color', 'red'); // CasesCollection[BackedEnum::One] 211 | BackedEnum::tryFromMeta('color', 'purple'); // null 212 | BackedEnum::tryFromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 213 | BackedEnum::tryFromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[BackedEnum::One, BackedEnum::Three] 214 | ``` 215 | 216 | Hydrating from meta can return multiple cases. To facilitate further processing, such cases are [collected into a `CasesCollection`](#-cases-collection). 217 | 218 | 219 | ### 🎲 Enum operations 220 | 221 | A number of operations can be performed against an enum to affect all its cases: 222 | 223 | ```php 224 | PureEnum::collect(); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] 225 | PureEnum::count(); // 3 226 | PureEnum::first(); // PureEnum::One 227 | PureEnum::first(fn(PureEnum $case, int $key) => ! $case->isOdd()); // PureEnum::Two 228 | PureEnum::names(); // ['One', 'Two', 'Three'] 229 | PureEnum::values(); // [] 230 | PureEnum::pluck('name'); // ['One', 'Two', 'Three'] 231 | PureEnum::pluck('color'); // ['red', 'green', 'blue'] 232 | PureEnum::pluck(fn(PureEnum $case) => $case->isOdd()); // [true, false, true] 233 | PureEnum::pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] 234 | PureEnum::pluck(fn(PureEnum $case) => $case->isOdd(), fn(PureEnum $case) => $case->name); // ['One' => true, 'Two' => false, 'Three' => true] 235 | PureEnum::map(fn(PureEnum $case, int $key) => $case->name . $key); // ['One0', 'Two1', 'Three2'] 236 | PureEnum::keyByName(); // CasesCollection['One' => PureEnum::One, 'Two' => PureEnum::Two, 'Three' => PureEnum::Three] 237 | PureEnum::keyBy('color'); // CasesCollection['red' => PureEnum::One, 'green' => PureEnum::Two, 'blue' => PureEnum::Three] 238 | PureEnum::keyByValue(); // CasesCollection[] 239 | PureEnum::groupBy('color'); // ['red' => CasesCollection[PureEnum::One], 'green' => CasesCollection[PureEnum::Two], 'blue' => CasesCollection[PureEnum::Three]] 240 | PureEnum::filter('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 241 | PureEnum::filter(fn(PureEnum $case) => $case->isOdd()); // CasesCollection[PureEnum::One, PureEnum::Three] 242 | PureEnum::only('Two', 'Three'); // CasesCollection[PureEnum::Two, PureEnum::Three] 243 | PureEnum::except('Two', 'Three'); // CasesCollection[PureEnum::One] 244 | PureEnum::onlyValues(2, 3); // CasesCollection[] 245 | PureEnum::exceptValues(2, 3); // CasesCollection[] 246 | PureEnum::sort(); // CasesCollection[PureEnum::One, PureEnum::Three, PureEnum::Two] 247 | PureEnum::sortBy('color'); // CasesCollection[PureEnum::Three, PureEnum::Two, PureEnum::One] 248 | PureEnum::sortByValue(); // CasesCollection[] 249 | PureEnum::sortDesc(); // CasesCollection[PureEnum::Two, PureEnum::Three, PureEnum::One] 250 | PureEnum::sortByDesc(fn(PureEnum $case) => $case->color()); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] 251 | PureEnum::sortByDescValue(); // CasesCollection[] 252 | 253 | BackedEnum::collect(); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] 254 | BackedEnum::count(); // 3 255 | BackedEnum::first(); // BackedEnum::One 256 | BackedEnum::first(fn(BackedEnum $case, int $key) => ! $case->isOdd()); // BackedEnum::Two 257 | BackedEnum::names(); // ['One', 'Two', 'Three'] 258 | BackedEnum::values(); // [1, 2, 3] 259 | BackedEnum::pluck('value'); // [1, 2, 3] 260 | BackedEnum::pluck('color'); // ['red', 'green', 'blue'] 261 | BackedEnum::pluck(fn(BackedEnum $case) => $case->isOdd()); // [true, false, true] 262 | BackedEnum::pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] 263 | BackedEnum::pluck(fn(BackedEnum $case) => $case->isOdd(), fn(BackedEnum $case) => $case->name); // ['One' => true, 'Two' => false, 'Three' => true] 264 | BackedEnum::map(fn(BackedEnum $case, int $key) => $case->name . $key); // ['One0', 'Two1', 'Three2'] 265 | BackedEnum::keyByName(); // CasesCollection['One' => BackedEnum::One, 'Two' => BackedEnum::Two, 'Three' => BackedEnum::Three] 266 | BackedEnum::keyBy('color'); // CasesCollection['red' => BackedEnum::One, 'green' => BackedEnum::Two, 'blue' => BackedEnum::Three] 267 | BackedEnum::keyByValue(); // CasesCollection[1 => BackedEnum::One, 2 => BackedEnum::Two, 3 => BackedEnum::Three] 268 | BackedEnum::groupBy('color'); // ['red' => CasesCollection[BackedEnum::One], 'green' => CasesCollection[BackedEnum::Two], 'blue' => CasesCollection[BackedEnum::Three]] 269 | BackedEnum::filter('isOdd'); // CasesCollection[BackedEnum::One, BackedEnum::Three] 270 | BackedEnum::filter(fn(BackedEnum $case) => $case->isOdd()); // CasesCollection[BackedEnum::One, BackedEnum::Three] 271 | BackedEnum::only('Two', 'Three'); // CasesCollection[BackedEnum::Two, BackedEnum::Three] 272 | BackedEnum::except('Two', 'Three'); // CasesCollection[BackedEnum::One] 273 | BackedEnum::onlyValues(2, 3); // CasesCollection[] 274 | BackedEnum::exceptValues(2, 3); // CasesCollection['Two' => false, 'Three' => true] 275 | BackedEnum::sort(); // CasesCollection[BackedEnum::One, BackedEnum::Three, BackedEnum::Two] 276 | BackedEnum::sortBy('color'); // CasesCollection[BackedEnum::Three, BackedEnum::Two, BackedEnum::One] 277 | BackedEnum::sortByValue(); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] 278 | BackedEnum::sortDesc(); // CasesCollection[BackedEnum::Two, BackedEnum::Three, BackedEnum::One] 279 | BackedEnum::sortByDescValue(); // CasesCollection[BackedEnum::Three, BackedEnum::Two, BackedEnum::One] 280 | BackedEnum::sortByDesc(fn(BackedEnum $case) => $case->color()); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] 281 | ``` 282 | 283 | 284 | ### 🧺 Cases collection 285 | 286 | When an [enum operation](#-enum-operations) can return multiple cases, they are collected into a `CasesCollection` which provides a fluent API to perform further operations on the set of cases: 287 | 288 | ```php 289 | PureEnum::filter('isOdd')->sortBy('color')->pluck('color', 'name'); // ['Three' => 'blue', 'One' => 'red'] 290 | ``` 291 | 292 | Cases can be collected by calling `collect()` or any other [enum operation](#-enum-operations) returning a `CasesCollection`: 293 | 294 | ```php 295 | PureEnum::collect(); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] 296 | 297 | BackedEnum::only('One', 'Two'); // CasesCollection[BackedEnum::One, BackedEnum::Two] 298 | ``` 299 | 300 | We can iterate a cases collection within any loop: 301 | 302 | ```php 303 | foreach (PureEnum::collect() as $case) { 304 | echo $case->name; 305 | } 306 | ``` 307 | 308 | All the [enum operations listed above](#-enum-operations) are also available when dealing with a collection of cases. 309 | 310 | 311 | ### 🪄 Magic 312 | 313 | Enums can implement magic methods to be invoked or to handle calls to inaccessible methods. By default when calling an inaccessible static method, the name or value of the case matching the missing method is returned: 314 | 315 | ```php 316 | PureEnum::One(); // 'One' 317 | 318 | BackedEnum::One(); // 1 319 | ``` 320 | 321 | > [!TIP] 322 | > Our IDE can autocomplete cases static methods thanks to the [`annotate` command](#%EF%B8%8F-annotate). 323 | 324 | We can also obtain the name or value of a case by simply invoking it: 325 | 326 | ```php 327 | $case = PureEnum::One; 328 | $case(); // 'One' 329 | 330 | $case = BackedEnum::One; 331 | $case(); // 1 332 | ``` 333 | 334 | When calling an inaccessible method of a case, by default the value of the meta matching the missing method is returned: 335 | 336 | ```php 337 | PureEnum::One->color(); // 'red' 338 | 339 | BackedEnum::One->shape(); // 'triangle' 340 | ``` 341 | 342 | > [!TIP] 343 | > Our IDE can autocomplete meta methods thanks to the [`annotate` command](#%EF%B8%8F-annotate). 344 | 345 | Depending on our needs, we can customize the default behavior of all enums in our application when invoking a case or calling inaccessible methods: 346 | 347 | ```php 348 | use Cerbero\Enum\Enums; 349 | use UnitEnum; 350 | 351 | // define the logic to run when calling an inaccessible method of an enum 352 | Enums::onStaticCall(function(string $enum, string $name, array $arguments) { 353 | // $enum is the fully qualified name of the enum that called the inaccessible method 354 | // $name is the inaccessible method name 355 | // $arguments are the parameters passed to the inaccessible method 356 | }); 357 | 358 | // define the logic to run when calling an inaccessible method of a case 359 | Enums::onCall(function(UnitEnum $case, string $name, array $arguments) { 360 | // $case is the instance of the case that called the inaccessible method 361 | // $name is the inaccessible method name 362 | // $arguments are the parameters passed to the inaccessible method 363 | }); 364 | 365 | // define the logic to run when invoking a case 366 | Enums::onInvoke(function(UnitEnum $case, mixed ...$arguments) { 367 | // $case is the instance of the case that is being invoked 368 | // $arguments are the parameters passed when invoking the case 369 | }); 370 | ``` 371 | 372 | 373 | ### 🤳 Self-awareness 374 | 375 | Some internal methods are also available and can be useful for inspecting enums or auto-generating code: 376 | 377 | ```php 378 | PureEnum::isPure(); // true 379 | PureEnum::isBacked(); // false 380 | PureEnum::isBackedByInteger(); // false 381 | PureEnum::isBackedByString(); // false 382 | PureEnum::metaNames(); // ['color', 'shape', 'isOdd'] 383 | PureEnum::metaAttributeNames(); // ['color', 'shape'] 384 | PureEnum::One->resolveItem('name'); // 'One' 385 | PureEnum::One->resolveMeta('isOdd'); // true 386 | PureEnum::One->resolveMetaAttribute('color'); // 'red' 387 | PureEnum::One->value(); // 'One' 388 | 389 | BackedEnum::isPure(); // false 390 | BackedEnum::isBacked(); // true 391 | BackedEnum::isBackedByInteger(); // true 392 | BackedEnum::isBackedByString(); // false 393 | BackedEnum::metaNames(); // ['color', 'shape', 'isOdd'] 394 | BackedEnum::metaAttributeNames(); // ['color', 'shape'] 395 | BackedEnum::One->resolveItem('value'); // 1 396 | BackedEnum::One->resolveMeta('isOdd'); // true 397 | BackedEnum::One->resolveMetaAttribute('color'); // 'red' 398 | BackedEnum::One->value(); // 1 399 | ``` 400 | 401 | 402 | ### 🦾 Console commands 403 | 404 | This package provides a handy binary, built to automate different tasks. To learn how to use it, we can simply run it: 405 | 406 | ```bash 407 | ./vendor/bin/enum 408 | ``` 409 | 410 | For the console commands to work properly, the application base path is automatically guessed. However, in case of issues, we can manually set it by creating an `enums.php` file in the root of our app: 411 | 412 | ```php 413 | getMessage()); 22 | } 23 | 24 | exit($outcome ? 0 : 1); 25 | } 26 | 27 | require path(__DIR__ . '/../cli/help'); 28 | -------------------------------------------------------------------------------- /cli/annotate.php: -------------------------------------------------------------------------------- 1 | (new Annotator($enum))->annotate($force)) && $succeeded; 23 | } 24 | 25 | return $succeeded; 26 | -------------------------------------------------------------------------------- /cli/help: -------------------------------------------------------------------------------- 1 | Annotate enums to ease IDE autocompletion. 2 | 3 | Usage: enum annotate enum1 [enum2 ...] 4 | 5 | Available options: 6 | 7 | -a, --all Whether all enums should be annotated 8 | -f, --force Whether existing annotations should be overwritten 9 | 10 | Examples: 11 | enum annotate App/Enums/MyEnum 12 | enum annotate "App\Enums\MyEnum" 13 | enum annotate App/Enums/MyEnum1 App/Enums/MyEnum2 14 | enum annotate App/Enums/MyEnum --force 15 | enum annotate --all 16 | enum annotate --all --force 17 | 18 | ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― 19 | 20 | Create a new enum. 21 | 22 | Usage: enum make enum case1 case2 23 | 24 | Available options: 25 | 26 | --backed=VALUE How cases should be backed. VALUE is either: 27 | snake|camel|kebab|upper|lower|int0|int1|bitwise 28 | -f, --force Whether the existing enum should be overwritten 29 | -t, --typescript Whether the enum should be synced in TypeScript 30 | 31 | Examples: 32 | enum make App/Enums/MyEnum Case1 Case2 33 | enum make "App\Enums\MyEnum" Case1 Case2 34 | enum make App/Enums/MyEnum Case1=value1 Case2=value2 35 | enum make App/Enums/MyEnum Case1 Case2 --backed=int1 36 | enum make App/Enums/MyEnum Case1 Case2 --force 37 | enum make App/Enums/MyEnum Case1 Case2 --backed=bitwise --force 38 | enum make App/Enums/MyEnum Case1 Case2 --typescript 39 | 40 | ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― 41 | 42 | Synchronize enums in TypeScript. 43 | 44 | Usage: enum ts enum1 [enum2 ...] 45 | 46 | Available options: 47 | 48 | -a, --all Whether all enums should be synchronized 49 | -f, --force Whether existing enums should be overwritten 50 | 51 | Examples: 52 | enum ts App/Enums/MyEnum 53 | enum ts "App\Enums\MyEnum" 54 | enum ts App/Enums/MyEnum1 App/Enums/MyEnum2 55 | enum ts App/Enums/MyEnum --force 56 | enum ts --all 57 | enum ts --all --force 58 | -------------------------------------------------------------------------------- /cli/make.php: -------------------------------------------------------------------------------- 1 | generate($force) 39 | && runAnnotate($enum, $force) 40 | && ($typeScript ? runTs($enum, $force) : true); 41 | }); 42 | -------------------------------------------------------------------------------- /cli/ts.php: -------------------------------------------------------------------------------- 1 | (new TypeScript($enum))->sync($force)) && $succeeded; 23 | } 24 | 25 | return $succeeded; 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerbero/enum", 3 | "type": "library", 4 | "description": "Zero-dependencies package to supercharge enum functionalities.", 5 | "keywords": [ 6 | "enum", 7 | "enumeration" 8 | ], 9 | "homepage": "https://github.com/cerbero90/enum", 10 | "license": "MIT", 11 | "authors": [{ 12 | "name": "Andrea Marco Sartori", 13 | "email": "andrea.marco.sartori@gmail.com", 14 | "homepage": "https://github.com/cerbero90", 15 | "role": "Developer" 16 | }], 17 | "require": { 18 | "php": "^8.1" 19 | }, 20 | "require-dev": { 21 | "pestphp/pest": "^2.0", 22 | "phpstan/phpstan": "^2.0", 23 | "scrutinizer/ocular": "^1.9", 24 | "squizlabs/php_codesniffer": "^3.0", 25 | "tightenco/duster": "^2.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Cerbero\\Enum\\": "src" 30 | }, 31 | "files": [ 32 | "helpers/core.php", 33 | "helpers/cli.php" 34 | ] 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Cerbero\\Enum\\": "tests", 39 | "App\\": "tests/Skeleton/app", 40 | "Domain\\": "tests/Skeleton/domain" 41 | } 42 | }, 43 | "bin": ["bin/enum"], 44 | "scripts": { 45 | "fix": "duster fix -u tlint,phpcodesniffer,pint", 46 | "lint": "duster lint -u tlint,phpcodesniffer,pint,phpstan", 47 | "test": "pest" 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-master": "1.0-dev" 52 | } 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "pestphp/pest-plugin": true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /duster.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src" 4 | ], 5 | "exclude": [ 6 | "tests" 7 | ], 8 | "scripts": { 9 | "lint": { 10 | "phpstan": ["./vendor/bin/phpstan", "analyse"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /helpers/cli.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | function splitArgv(array $argv): array 37 | { 38 | $arguments = $options = []; 39 | 40 | foreach (array_slice($argv, 2) as $item) { 41 | if (str_starts_with($item, '-')) { 42 | $options[] = $item; 43 | } else { 44 | $arguments[] = $item; 45 | } 46 | } 47 | 48 | return [$arguments, $options]; 49 | } 50 | 51 | /** 52 | * Set enum paths from the given options. 53 | * 54 | * @param string[] $options 55 | */ 56 | function setPathsByOptions(array $options): void 57 | { 58 | if ($basePath = option('base-path', $options)) { 59 | Enums::setBasePath($basePath); 60 | } 61 | 62 | if ($paths = option('paths', $options)) { 63 | Enums::setPaths(...explode(',', $paths)); 64 | } 65 | } 66 | 67 | /** 68 | * Retrieve the value of the given option. 69 | * 70 | * @param string[] $options 71 | */ 72 | function option(string $name, array $options): ?string 73 | { 74 | $prefix = "--{$name}="; 75 | 76 | foreach ($options as $option) { 77 | if (str_starts_with($option, $prefix)) { 78 | $segments = explode('=', $option, limit: 2); 79 | 80 | return $segments[1] === '' ? null : $segments[1]; 81 | } 82 | } 83 | 84 | return null; 85 | } 86 | 87 | /** 88 | * Retrieve the normalized namespaces of the given enums. 89 | * 90 | * @param list $enums 91 | * @return list> 92 | */ 93 | function normalizeEnums(array $enums): array 94 | { 95 | $namespaces = array_map(fn(string $enum) => strtr($enum, '/', '\\'), $enums); 96 | 97 | return array_unique(array_filter($namespaces, 'enum_exists')); 98 | } 99 | 100 | /** 101 | * Print out the outcome of the given enum operation. 102 | * 103 | * @param class-string<\UnitEnum> $namespace 104 | * @param Closure(): bool $callback 105 | */ 106 | function enumOutcome(string $enum, Closure $callback): bool 107 | { 108 | $error = null; 109 | 110 | try { 111 | $succeeded = $callback(); 112 | } catch (Throwable $e) { 113 | $succeeded = false; 114 | $error = "\e[38;2;220;38;38m{$e?->getMessage()}\e[0m"; 115 | } 116 | 117 | if ($succeeded) { 118 | fwrite(STDOUT, "\e[48;2;163;230;53m\e[38;2;63;98;18m\e[1m DONE \e[0m {$enum}" . PHP_EOL . PHP_EOL); 119 | } else { 120 | fwrite(STDERR, "\e[48;2;248;113;113m\e[38;2;153;27;27m\e[1m FAIL \e[0m {$enum} {$error}" . PHP_EOL . PHP_EOL); 121 | } 122 | 123 | return $succeeded; 124 | } 125 | 126 | /** 127 | * Annotate the given enum within a new process. 128 | * 129 | * @param class-string<\UnitEnum> $enum 130 | */ 131 | function runAnnotate(string $enum, bool $force = false): bool 132 | { 133 | // Once an enum is loaded, PHP accesses it from the memory and not from the disk. 134 | // Since we are writing on the disk, the enum in memory might get out of sync. 135 | // To ensure that the annotations reflect the current content of such enum, 136 | // we spin a new process to load in memory the latest state of the enum. 137 | ob_start(); 138 | 139 | $succeeded = cli("annotate \"{$enum}\"" . ($force ? ' --force' : '')); 140 | 141 | ob_end_clean(); 142 | 143 | return $succeeded; 144 | } 145 | 146 | /** 147 | * Run the enum CLI in a new process. 148 | */ 149 | function cli(string $command, ?int &$status = null): bool 150 | { 151 | $cmd = vsprintf('"%s" "%s" %s 2>&1', [ 152 | PHP_BINARY, 153 | path(__DIR__ . '/../bin/enum'), 154 | $command, 155 | ]); 156 | 157 | return passthru($cmd, $status) === null; 158 | } 159 | 160 | /** 161 | * Synchronize the given enum in TypeScript within a new process. 162 | * 163 | * @param class-string<\UnitEnum> $enum 164 | */ 165 | function runTs(string $enum, bool $force = false): bool 166 | { 167 | // Once an enum is loaded, PHP accesses it from the memory and not from the disk. 168 | // Since we are writing on the disk, the enum in memory might get out of sync. 169 | // To make sure that we are synchronizing the current content of such enum, 170 | // we spin a new process to load in memory the latest state of the enum. 171 | ob_start(); 172 | 173 | $succeeded = cli("ts \"{$enum}\"" . ($force ? ' --force' : '')); 174 | 175 | ob_end_clean(); 176 | 177 | return $succeeded; 178 | } 179 | -------------------------------------------------------------------------------- /helpers/core.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | function yieldLines(string $path): Generator 15 | { 16 | $stream = fopen($path, 'rb'); 17 | 18 | try { 19 | while (($line = fgets($stream, 1024)) !== false) { 20 | yield $line; 21 | } 22 | } finally { 23 | is_resource($stream) && fclose($stream); 24 | } 25 | } 26 | 27 | /** 28 | * Retrieve the PSR-4 map of the composer file. 29 | * 30 | * @return array 31 | */ 32 | function psr4(): array 33 | { 34 | if (! is_file($path = Enums::basePath('composer.json'))) { 35 | return []; 36 | } 37 | 38 | $composer = (array) json_decode((string) file_get_contents($path), true); 39 | 40 | /** @var array */ 41 | return $composer['autoload']['psr-4'] ?? []; 42 | } 43 | 44 | /** 45 | * Retrieve the traits used by the given target recursively. 46 | * 47 | * @return array 48 | */ 49 | function traitsUsedBy(string $target): array 50 | { 51 | $traits = class_uses($target) ?: []; 52 | 53 | foreach ($traits as $trait) { 54 | $traits += traitsUsedBy($trait); 55 | } 56 | 57 | return $traits; 58 | } 59 | 60 | /** 61 | * Retrieve the given value in snake case. 62 | */ 63 | function snake(string $value, string $delimiter = '_'): string 64 | { 65 | $value = preg_replace('/\s+/u', '', ucwords($value)); 66 | 67 | return strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value)); 68 | } 69 | 70 | /** 71 | * Retrieve the given value in camel case. 72 | */ 73 | function camel(string $value): string 74 | { 75 | $words = explode(' ', str_replace(['-', '_'], ' ', $value)); 76 | $studly = array_map('ucfirst', $words); 77 | 78 | return lcfirst(implode($studly)); 79 | } 80 | 81 | /** 82 | * Parse the given raw string containing the name and value of a case. 83 | * 84 | * @return array 85 | */ 86 | function parseCaseValue(string $raw): array 87 | { 88 | [$rawName, $rawValue] = explode('=', $raw, limit: 2); 89 | $trimmed = trim($rawValue); 90 | $value = is_numeric($trimmed) ? (int) $trimmed : $trimmed; 91 | 92 | return [trim($rawName) => $value]; 93 | } 94 | 95 | /** 96 | * Retrieve the backing type depending on the given value. 97 | */ 98 | function backingType(mixed $value): ?string 99 | { 100 | return match (true) { 101 | is_int($value) => 'int', 102 | is_string($value) => str_contains($value, '<<') ? 'int' : 'string', 103 | default => null, 104 | }; 105 | } 106 | 107 | /** 108 | * Retrieve the common type among the given types. 109 | */ 110 | function commonType(string ...$types): string 111 | { 112 | $null = ''; 113 | $types = array_unique($types); 114 | 115 | if (($index = array_search('null', $types)) !== false) { 116 | $null = '?'; 117 | 118 | unset($types[$index]); 119 | } 120 | 121 | if (count($types) == 1) { 122 | return $null . reset($types); 123 | } 124 | 125 | return implode('|', $types) . ($null ? '|null' : ''); 126 | } 127 | 128 | /** 129 | * Retrieve only the name of the given namespace. 130 | */ 131 | function className(string $namespace): string 132 | { 133 | return basename(strtr($namespace, '\\', '/')); 134 | } 135 | 136 | /** 137 | * Split the given FQCN into namespace and name. 138 | * 139 | * @param class-string $namespace 140 | * @return list 141 | */ 142 | function splitNamespace(string $namespace): array 143 | { 144 | $segments = explode('\\', $namespace); 145 | $name = (string) array_pop($segments); 146 | 147 | return [implode('\\', $segments), $name]; 148 | } 149 | 150 | /** 151 | * Retrieve the absolute path of the given namespace. 152 | * 153 | * @param class-string $namespace 154 | */ 155 | function namespaceToPath(string $namespace): string 156 | { 157 | $path = Enums::basePath($namespace) . '.php'; 158 | 159 | foreach (psr4() as $root => $relative) { 160 | if (str_starts_with($namespace, $root)) { 161 | $relative = path($relative) . DIRECTORY_SEPARATOR; 162 | 163 | return strtr($path, [$root => $relative]); 164 | } 165 | } 166 | 167 | return $path; 168 | } 169 | 170 | /** 171 | * Retrieve the normalized path. 172 | */ 173 | function path(string $path): string 174 | { 175 | $segments = []; 176 | $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); 177 | $path = rtrim($path, DIRECTORY_SEPARATOR); 178 | $head = str_starts_with($path, DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : ''; 179 | 180 | foreach (explode(DIRECTORY_SEPARATOR, $path) as $segment) { 181 | if ($segment === '..') { 182 | array_pop($segments); 183 | } elseif ($segment !== '' && $segment !== '.') { 184 | $segments[] = $segment; 185 | } 186 | } 187 | 188 | return $head . implode(DIRECTORY_SEPARATOR, $segments); 189 | } 190 | 191 | /** 192 | * Create the directory for the given path if missing. 193 | */ 194 | function ensureParentDirectory(string $path): bool 195 | { 196 | if (file_exists($directory = dirname($path))) { 197 | return true; 198 | } 199 | 200 | return mkdir($directory, 0755, recursive: true); 201 | } 202 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | ignoreErrors: 6 | - '#Access to an undefined property (?:TEnum of )?[a-zA-Z0-9\\_]+::\$value.#' 7 | - '#Call to an undefined (?:static )?method (?:TEnum of )?UnitEnum::[a-zA-Z0-9\\_]+\(\).#' 8 | includes: 9 | - phpstan-baseline.neon 10 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "per", 3 | "rules": { 4 | "align_multiline_comment": true, 5 | "combine_consecutive_issets": true, 6 | "combine_consecutive_unsets": true, 7 | "concat_space": {"spacing": "one"}, 8 | "explicit_string_variable": true, 9 | "ordered_imports": { 10 | "sort_algorithm": "alpha", 11 | "imports_order": [ 12 | "class", 13 | "function", 14 | "const" 15 | ] 16 | }, 17 | "simple_to_complex_string_variable": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Attributes/Meta.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $all; 22 | 23 | /** 24 | * Instantiate the class. 25 | */ 26 | public function __construct(mixed ...$meta) 27 | { 28 | foreach ($meta as $key => $value) { 29 | if (! is_string($key)) { 30 | throw new InvalidArgumentException('The name of meta must be a string'); 31 | } 32 | 33 | $this->all[$key] = $value; 34 | } 35 | } 36 | 37 | /** 38 | * Retrieve the meta names. 39 | * 40 | * @return string[] 41 | */ 42 | public function names(): array 43 | { 44 | return array_keys($this->all); 45 | } 46 | 47 | /** 48 | * Determine whether the given meta exists. 49 | */ 50 | public function has(string $meta): bool 51 | { 52 | return array_key_exists($meta, $this->all); 53 | } 54 | 55 | /** 56 | * Retrieve the value for the given meta. 57 | */ 58 | public function get(string $meta): mixed 59 | { 60 | return $this->all[$meta] ?? null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/CasesCollection.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class CasesCollection implements Countable, IteratorAggregate, JsonSerializable, Stringable 23 | { 24 | /** 25 | * Whether the cases belong to a backed enum. 26 | */ 27 | protected readonly bool $enumIsBacked; 28 | 29 | /** 30 | * Instantiate the class. 31 | * 32 | * @param array $cases 33 | */ 34 | final public function __construct(protected readonly array $cases) 35 | { 36 | $this->enumIsBacked = reset($cases) instanceof BackedEnum; 37 | } 38 | 39 | /** 40 | * Turn the collection into a string. 41 | */ 42 | public function __toString(): string 43 | { 44 | return (string) json_encode($this->jsonSerialize()); 45 | } 46 | 47 | /** 48 | * Turn the collection into a JSON serializable array. 49 | * 50 | * @return list 51 | */ 52 | public function jsonSerialize(): array 53 | { 54 | return $this->enumIsBacked ? $this->values() : $this->names(); 55 | } 56 | 57 | /** 58 | * Retrieve the count of cases. 59 | */ 60 | public function count(): int 61 | { 62 | return count($this->cases); 63 | } 64 | 65 | /** 66 | * Retrieve the iterable cases. 67 | * 68 | * @return Traversable 69 | */ 70 | public function getIterator(): Traversable 71 | { 72 | yield from $this->cases; 73 | } 74 | 75 | /** 76 | * Retrieve all the cases as a plain array. 77 | * 78 | * @return array 79 | */ 80 | public function all(): array 81 | { 82 | return $this->cases; 83 | } 84 | 85 | /** 86 | * Determine whether the collection contains the given case. 87 | */ 88 | public function has(mixed $case): bool 89 | { 90 | foreach ($this->cases as $instance) { 91 | if ($instance->is($case)) { 92 | return true; 93 | } 94 | } 95 | 96 | return false; 97 | } 98 | 99 | /** 100 | * Retrieve all the cases as a plain array recursively. 101 | * 102 | * @return array 103 | */ 104 | public function toArray(): array 105 | { 106 | $array = []; 107 | 108 | foreach ($this->cases as $key => $value) { 109 | $array[$key] = $value instanceof static ? $value->toArray() : $value; 110 | } 111 | 112 | return $array; 113 | } 114 | 115 | /** 116 | * Retrieve the first case. 117 | * 118 | * @param ?callable(TEnum, array-key): bool $callback 119 | * @return ?TEnum 120 | */ 121 | public function first(?callable $callback = null): mixed 122 | { 123 | $callback ??= fn() => true; 124 | 125 | foreach ($this->cases as $key => $case) { 126 | if ($callback($case, $key)) { 127 | return $case; 128 | } 129 | } 130 | 131 | return null; 132 | } 133 | 134 | /** 135 | * Retrieve all the names of the cases. 136 | * 137 | * @return list 138 | */ 139 | public function names(): array 140 | { 141 | /** @var list */ 142 | return array_column($this->cases, 'name'); 143 | } 144 | 145 | /** 146 | * Retrieve all the values of the backed cases. 147 | * 148 | * @return list 149 | */ 150 | public function values(): array 151 | { 152 | /** @var list */ 153 | return array_column($this->cases, 'value'); 154 | } 155 | 156 | /** 157 | * Retrieve an array of values optionally keyed by the given key. 158 | * 159 | * @template TPluckValue 160 | * 161 | * @param (callable(TEnum): TPluckValue)|string $value 162 | * @param (callable(TEnum): array-key)|string|null $key 163 | * @return array 164 | */ 165 | public function pluck(callable|string $value, callable|string|null $key = null): array 166 | { 167 | $result = []; 168 | 169 | foreach ($this->cases as $case) { 170 | if ($key === null) { 171 | $result[] = $case->resolveItem($value); 172 | } else { 173 | $result[$case->resolveItem($key)] = $case->resolveItem($value); 174 | } 175 | } 176 | 177 | return $result; 178 | } 179 | 180 | /** 181 | * Retrieve the result of mapping over the cases. 182 | * 183 | * @template TMapValue 184 | * 185 | * @param callable(TEnum, array-key): TMapValue $callback 186 | * @return array 187 | */ 188 | public function map(callable $callback): array 189 | { 190 | $keys = array_keys($this->cases); 191 | $values = array_map($callback, $this->cases, $keys); 192 | 193 | return array_combine($keys, $values); 194 | } 195 | 196 | /** 197 | * Retrieve the cases keyed by their own name. 198 | */ 199 | public function keyByName(): static 200 | { 201 | return $this->keyBy('name'); 202 | } 203 | 204 | /** 205 | * Retrieve the cases keyed by the given key. 206 | * 207 | * @param (callable(TEnum): array-key)|string $key 208 | */ 209 | public function keyBy(callable|string $key): static 210 | { 211 | $keyed = []; 212 | 213 | foreach ($this->cases as $case) { 214 | $keyed[$case->resolveItem($key)] = $case; 215 | } 216 | 217 | return new static($keyed); 218 | } 219 | 220 | /** 221 | * Retrieve the cases keyed by their own value. 222 | */ 223 | public function keyByValue(): static 224 | { 225 | return $this->enumIsBacked ? $this->keyBy('value') : new static([]); 226 | } 227 | 228 | /** 229 | * Retrieve the cases grouped by the given key. 230 | * 231 | * @param (callable(TEnum): array-key)|string $key 232 | * @return array> 233 | */ 234 | public function groupBy(callable|string $key): array 235 | { 236 | $grouped = []; 237 | 238 | foreach ($this->cases as $case) { 239 | $grouped[$case->resolveItem($key)][] = $case; 240 | } 241 | 242 | foreach ($grouped as $key => $cases) { 243 | $grouped[$key] = new static($cases); 244 | } 245 | 246 | /** @var array> */ 247 | return $grouped; 248 | } 249 | 250 | /** 251 | * Retrieve a new collection with the filtered cases. 252 | * 253 | * @param (callable(TEnum): bool)|string $filter 254 | */ 255 | public function filter(callable|string $filter): static 256 | { 257 | $callback = is_callable($filter) ? $filter : fn(UnitEnum $case) => $case->resolveItem($filter) === true; 258 | 259 | return new static(array_filter($this->cases, $callback)); 260 | } 261 | 262 | /** 263 | * Retrieve a new collection of cases having only the given names. 264 | */ 265 | public function only(string ...$name): static 266 | { 267 | return $this->filter(fn(UnitEnum $case) => in_array($case->name, $name)); 268 | } 269 | 270 | /** 271 | * Retrieve a collection of cases not having the given names. 272 | */ 273 | public function except(string ...$name): static 274 | { 275 | return $this->filter(fn(UnitEnum $case) => !in_array($case->name, $name)); 276 | } 277 | 278 | /** 279 | * Retrieve a new collection of backed cases having only the given values. 280 | */ 281 | public function onlyValues(string|int ...$value): static 282 | { 283 | return $this->filter(fn(UnitEnum $case) => $this->enumIsBacked && in_array($case->value, $value, true)); 284 | } 285 | 286 | /** 287 | * Retrieve a new collection of backed cases not having the given values. 288 | */ 289 | public function exceptValues(string|int ...$value): static 290 | { 291 | return $this->filter(fn(UnitEnum $case) => $this->enumIsBacked && !in_array($case->value, $value, true)); 292 | } 293 | 294 | /** 295 | * Retrieve a new collection of cases sorted by their own name ascending. 296 | */ 297 | public function sort(): static 298 | { 299 | return $this->sortBy('name'); 300 | } 301 | 302 | /** 303 | * Retrieve a new collection of cases sorted by the given key ascending. 304 | * 305 | * @param (callable(TEnum): mixed)|string $key 306 | */ 307 | public function sortBy(callable|string $key): static 308 | { 309 | $cases = $this->cases; 310 | 311 | uasort($cases, fn(UnitEnum $a, UnitEnum $b) => $a->resolveItem($key) <=> $b->resolveItem($key)); 312 | 313 | return new static($cases); 314 | } 315 | 316 | /** 317 | * Retrieve a new collection of cases sorted by their own value ascending. 318 | */ 319 | public function sortByValue(): static 320 | { 321 | return $this->enumIsBacked ? $this->sortBy('value') : new static([]); 322 | } 323 | 324 | /** 325 | * Retrieve a new collection of cases sorted by their own name descending. 326 | */ 327 | public function sortDesc(): static 328 | { 329 | return $this->sortByDesc('name'); 330 | } 331 | 332 | /** 333 | * Retrieve a new collection of cases sorted by the given key descending. 334 | * 335 | * @param (callable(TEnum): mixed)|string $key 336 | */ 337 | public function sortByDesc(callable|string $key): static 338 | { 339 | $cases = $this->cases; 340 | 341 | uasort($cases, fn(UnitEnum $a, UnitEnum $b) => $b->resolveItem($key) <=> $a->resolveItem($key)); 342 | 343 | return new static($cases); 344 | } 345 | 346 | /** 347 | * Retrieve a new collection of cases sorted by their own value descending. 348 | */ 349 | public function sortByDescValue(): static 350 | { 351 | return $this->enumIsBacked ? $this->sortByDesc('value') : new static([]); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/Concerns/CollectsCases.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function collect(): CasesCollection 20 | { 21 | return new CasesCollection(self::cases()); 22 | } 23 | 24 | /** 25 | * Retrieve the count of cases. 26 | */ 27 | public static function count(): int 28 | { 29 | return self::collect()->count(); 30 | } 31 | 32 | /** 33 | * Retrieve the first case. 34 | * 35 | * @param ?callable(self, array-key): bool $callback 36 | */ 37 | public static function first(?callable $callback = null): ?self 38 | { 39 | return self::collect()->first($callback); 40 | } 41 | 42 | /** 43 | * Retrieve the name of all the cases. 44 | * 45 | * @return string[] 46 | */ 47 | public static function names(): array 48 | { 49 | return self::collect()->names(); 50 | } 51 | 52 | /** 53 | * Retrieve the value of all the backed cases. 54 | * 55 | * @return list 56 | */ 57 | public static function values(): array 58 | { 59 | return self::collect()->values(); 60 | } 61 | 62 | /** 63 | * Retrieve an array of values optionally keyed by the given key. 64 | * 65 | * @template TPluckValue 66 | * 67 | * @param (callable(self): TPluckValue)|string $value 68 | * @param (callable(self): array-key)|string|null $key 69 | * @return array 70 | */ 71 | public static function pluck(callable|string $value, callable|string|null $key = null): array 72 | { 73 | return self::collect()->pluck($value, $key); 74 | } 75 | 76 | /** 77 | * Retrieve the result of mapping over all the cases. 78 | * 79 | * @template TMapValue 80 | * 81 | * @param callable(self, array-key): TMapValue $callback 82 | * @return array 83 | */ 84 | public static function map(callable $callback): array 85 | { 86 | return self::collect()->map($callback); 87 | } 88 | 89 | /** 90 | * Retrieve all the cases keyed by their own name. 91 | * 92 | * @return CasesCollection 93 | */ 94 | public static function keyByName(): CasesCollection 95 | { 96 | return self::collect()->keyByName(); 97 | } 98 | 99 | /** 100 | * Retrieve all the cases keyed by the given key. 101 | * 102 | * @param (callable(self): array-key)|string $key 103 | * @return CasesCollection 104 | */ 105 | public static function keyBy(callable|string $key): CasesCollection 106 | { 107 | return self::collect()->keyBy($key); 108 | } 109 | 110 | /** 111 | * Retrieve all the cases keyed by their own value. 112 | * 113 | * @return CasesCollection 114 | */ 115 | public static function keyByValue(): CasesCollection 116 | { 117 | return self::collect()->keyByValue(); 118 | } 119 | 120 | /** 121 | * Retrieve all the cases grouped by the given key. 122 | * 123 | * @param (callable(self): array-key)|string $key 124 | * @return array> 125 | */ 126 | public static function groupBy(callable|string $key): array 127 | { 128 | return self::collect()->groupBy($key); 129 | } 130 | 131 | /** 132 | * Retrieve only the filtered cases. 133 | * 134 | * @param (callable(self): bool)|string $filter 135 | * @return CasesCollection 136 | */ 137 | public static function filter(callable|string $filter): CasesCollection 138 | { 139 | return self::collect()->filter($filter); 140 | } 141 | 142 | /** 143 | * Retrieve only the cases having the given names. 144 | * 145 | * @return CasesCollection 146 | */ 147 | public static function only(string ...$names): CasesCollection 148 | { 149 | return self::collect()->only(...$names); 150 | } 151 | 152 | /** 153 | * Retrieve only the cases not having the given names. 154 | * 155 | * @return CasesCollection 156 | */ 157 | public static function except(string ...$names): CasesCollection 158 | { 159 | return self::collect()->except(...$names); 160 | } 161 | 162 | /** 163 | * Retrieve only the cases having the given values. 164 | * 165 | * @return CasesCollection 166 | */ 167 | public static function onlyValues(string|int ...$values): CasesCollection 168 | { 169 | return self::collect()->onlyValues(...$values); 170 | } 171 | 172 | /** 173 | * Retrieve only the cases not having the given values. 174 | * 175 | * @return CasesCollection 176 | */ 177 | public static function exceptValues(string|int ...$values): CasesCollection 178 | { 179 | return self::collect()->exceptValues(...$values); 180 | } 181 | 182 | /** 183 | * Retrieve all the cases sorted by their own name ascending. 184 | * 185 | * @return CasesCollection 186 | */ 187 | public static function sort(): CasesCollection 188 | { 189 | return self::collect()->sort(); 190 | } 191 | 192 | /** 193 | * Retrieve all the cases sorted by the given key ascending. 194 | * 195 | * @param (callable(self): mixed)|string $key 196 | * @return CasesCollection 197 | */ 198 | public static function sortBy(callable|string $key): CasesCollection 199 | { 200 | return self::collect()->sortBy($key); 201 | } 202 | 203 | /** 204 | * Retrieve all the cases sorted by their own value ascending. 205 | * 206 | * @return CasesCollection 207 | */ 208 | public static function sortByValue(): CasesCollection 209 | { 210 | return self::collect()->sortByValue(); 211 | } 212 | 213 | /** 214 | * Retrieve all the cases sorted by their own name descending. 215 | * 216 | * @return CasesCollection 217 | */ 218 | public static function sortDesc(): CasesCollection 219 | { 220 | return self::collect()->sortDesc(); 221 | } 222 | 223 | /** 224 | * Retrieve all the cases sorted by the given key descending. 225 | * 226 | * @param (callable(self): mixed)|string $key 227 | * @return CasesCollection 228 | */ 229 | public static function sortByDesc(callable|string $key): CasesCollection 230 | { 231 | return self::collect()->sortByDesc($key); 232 | } 233 | 234 | /** 235 | * Retrieve all the cases sorted by their own value descending. 236 | * 237 | * @return CasesCollection 238 | */ 239 | public static function sortByDescValue(): CasesCollection 240 | { 241 | return self::collect()->sortByDescValue(); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Concerns/Compares.php: -------------------------------------------------------------------------------- 1 | is($target)) { 19 | return true; 20 | } 21 | } 22 | 23 | return false; 24 | } 25 | 26 | /** 27 | * Determine whether the enum does not include the given target. 28 | */ 29 | public static function doesntHave(mixed $target): bool 30 | { 31 | return !self::has($target); 32 | } 33 | 34 | /** 35 | * Determine whether this case matches the given target. 36 | */ 37 | public function is(mixed $target): bool 38 | { 39 | return in_array($target, [$this, self::isPure() ? $this->name : $this->value], true); 40 | } 41 | 42 | /** 43 | * Determine whether this case does not match the given target. 44 | */ 45 | public function isNot(mixed $target): bool 46 | { 47 | return !$this->is($target); 48 | } 49 | 50 | /** 51 | * Determine whether this case matches at least one of the given targets. 52 | * 53 | * @param iterable $targets 54 | */ 55 | public function in(iterable $targets): bool 56 | { 57 | foreach ($targets as $target) { 58 | if ($this->is($target)) { 59 | return true; 60 | } 61 | } 62 | 63 | return false; 64 | } 65 | 66 | /** 67 | * Determine whether this case does not match any of the given targets. 68 | * 69 | * @param iterable $targets 70 | */ 71 | public function notIn(iterable $targets): bool 72 | { 73 | return !$this->in($targets); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Concerns/Enumerates.php: -------------------------------------------------------------------------------- 1 | name === $name) { 47 | return $case; 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | /** 55 | * Retrieve the case hydrated from the given name or NULL. 56 | * This method can be called by pure enums only. 57 | */ 58 | public static function tryFrom(string $name): ?static 59 | { 60 | return self::tryFromName($name); 61 | } 62 | 63 | /** 64 | * Retrieve all the cases hydrated from the given meta or fail. 65 | * 66 | * @return CasesCollection 67 | * @throws ValueError 68 | */ 69 | public static function fromMeta(string $meta, mixed $value = true): CasesCollection 70 | { 71 | if ($cases = self::tryFromMeta($meta, $value)) { 72 | return $cases; 73 | } 74 | 75 | throw new ValueError(sprintf('Invalid value for the meta "%s" for enum "%s"', $meta, self::class)); 76 | } 77 | 78 | /** 79 | * Retrieve all the cases hydrated from the given meta or NULL. 80 | * 81 | * @return ?CasesCollection 82 | */ 83 | public static function tryFromMeta(string $meta, mixed $value = true): ?CasesCollection 84 | { 85 | $cases = []; 86 | 87 | foreach (self::cases() as $case) { 88 | $metaValue = $case->resolveMeta($meta); 89 | 90 | if ((is_callable($value) && $value($metaValue) === true) || $metaValue === $value) { 91 | $cases[] = $case; 92 | } 93 | } 94 | 95 | return $cases ? new CasesCollection($cases) : null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Concerns/IsMagic.php: -------------------------------------------------------------------------------- 1 | $arguments 18 | */ 19 | public static function __callStatic(string $name, array $arguments): mixed 20 | { 21 | return Enums::handleStaticCall(self::class, $name, $arguments); 22 | } 23 | 24 | /** 25 | * Handle the call to an inaccessible case method. 26 | * 27 | * @param array $arguments 28 | */ 29 | public function __call(string $name, array $arguments): mixed 30 | { 31 | return Enums::handleCall($this, $name, $arguments); 32 | } 33 | 34 | /** 35 | * Handle the invocation of a case. 36 | */ 37 | public function __invoke(mixed ...$arguments): mixed 38 | { 39 | return Enums::handleInvoke($this, ...$arguments); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Concerns/SelfAware.php: -------------------------------------------------------------------------------- 1 | getBackingType() === 'int'; 43 | } 44 | 45 | /** 46 | * Determine whether the enum is backed by string. 47 | */ 48 | public static function isBackedByString(): bool 49 | { 50 | return (string) (new ReflectionEnum(self::class))->getBackingType() === 'string'; 51 | } 52 | 53 | /** 54 | * Retrieve all the meta names of the enum. 55 | * 56 | * @return list 57 | */ 58 | public static function metaNames(): array 59 | { 60 | $meta = self::metaAttributeNames(); 61 | $enum = new ReflectionEnum(self::class); 62 | 63 | foreach ($enum->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 64 | if (! $method->isStatic() && $method->getFileName() == $enum->getFileName()) { 65 | $meta[] = $method->getShortName(); 66 | } 67 | } 68 | 69 | return array_values(array_unique($meta)); 70 | } 71 | 72 | /** 73 | * Retrieve all the meta attribute names of the enum. 74 | * 75 | * @return list 76 | */ 77 | public static function metaAttributeNames(): array 78 | { 79 | $meta = []; 80 | $enum = new ReflectionEnum(self::class); 81 | 82 | foreach ($enum->getAttributes(Meta::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 83 | array_push($meta, ...$attribute->newInstance()->names()); 84 | } 85 | 86 | foreach ($enum->getCases() as $case) { 87 | foreach ($case->getAttributes(Meta::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 88 | array_push($meta, ...$attribute->newInstance()->names()); 89 | } 90 | } 91 | 92 | return array_values(array_unique($meta)); 93 | } 94 | 95 | /** 96 | * Retrieve the given item of this case. 97 | * 98 | * @template TItemValue 99 | * 100 | * @param (callable(self): TItemValue)|string $item 101 | * @return TItemValue 102 | * @throws ValueError 103 | */ 104 | public function resolveItem(callable|string $item): mixed 105 | { 106 | return match (true) { 107 | is_string($item) && property_exists($this, $item) => $this->$item, 108 | is_callable($item) => $item($this), 109 | default => $this->resolveMeta($item), 110 | }; 111 | } 112 | 113 | /** 114 | * Retrieve the given meta of this case. 115 | * 116 | * @throws ValueError 117 | */ 118 | public function resolveMeta(string $meta): mixed 119 | { 120 | $enum = new ReflectionEnum($this); 121 | $enumFileName = $enum->getFileName(); 122 | 123 | foreach ($enum->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 124 | if (! $method->isStatic() && $method->getFileName() == $enumFileName && $method->getShortName() == $meta) { 125 | return $this->$meta(); 126 | } 127 | } 128 | 129 | return $this->resolveMetaAttribute($meta); 130 | } 131 | 132 | /** 133 | * Retrieve the given meta from the attributes. 134 | * 135 | * @throws ValueError 136 | */ 137 | public function resolveMetaAttribute(string $meta): mixed 138 | { 139 | $case = new ReflectionEnumUnitCase($this, $this->name); 140 | 141 | foreach ($case->getAttributes(Meta::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 142 | if (($metadata = $attribute->newInstance())->has($meta)) { 143 | return $metadata->get($meta); 144 | } 145 | } 146 | 147 | foreach ($case->getEnum()->getAttributes(Meta::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 148 | if (($metadata = $attribute->newInstance())->has($meta)) { 149 | return $metadata->get($meta); 150 | } 151 | } 152 | 153 | throw new ValueError(sprintf('The case %s::%s has no "%s" meta set', self::class, $this->name, $meta)); 154 | } 155 | 156 | /** 157 | * Retrieve the value of a backed case or the name of a pure case. 158 | */ 159 | public function value(): string|int 160 | { 161 | /** @var string|int @phpstan-ignore property.notFound */ 162 | return $this->value ?? $this->name; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Data/GeneratingEnum.php: -------------------------------------------------------------------------------- 1 | $fullNamespace 45 | * @param array $cases 46 | */ 47 | public function __construct(public readonly string $fullNamespace, public readonly array $cases) 48 | { 49 | [$this->namespace, $this->name] = splitNamespace($fullNamespace); 50 | 51 | $this->path = namespaceToPath($fullNamespace); 52 | 53 | $this->exists = enum_exists($fullNamespace); 54 | 55 | $this->backingType = backingType(reset($cases)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Data/MethodAnnotation.php: -------------------------------------------------------------------------------- 1 | value ?? null) ? 'int' : 'string'; 26 | 27 | return new static($case->name, "static {$returnType} {$case->name}()"); 28 | } 29 | 30 | /** 31 | * Retrieve the method annotation for an instance method. 32 | */ 33 | public static function instance(string $name, string $returnType): static 34 | { 35 | return new static($name, "{$returnType} {$name}()"); 36 | } 37 | 38 | /** 39 | * Instantiate the class. 40 | * 41 | * @param list $namespaces 42 | */ 43 | final public function __construct( 44 | public readonly string $name, 45 | public readonly string $annotation, 46 | public readonly array $namespaces = [], 47 | ) { 48 | $this->isStatic = str_starts_with($annotation, 'static'); 49 | } 50 | 51 | /** 52 | * Retrieve the method annotation string. 53 | */ 54 | public function __toString(): string 55 | { 56 | return "@method {$this->annotation}"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Enums.php: -------------------------------------------------------------------------------- 1 | |string $enum): string|string 35 | */ 36 | protected static Closure|string $typeScript = 'resources/js/enums/index.ts'; 37 | 38 | /** 39 | * The logic to run when an inaccessible enum method is called. 40 | * 41 | * @var ?Closure(class-string $enum, string $name, array $arguments): mixed 42 | */ 43 | protected static ?Closure $onStaticCall = null; 44 | 45 | /** 46 | * The logic to run when an inaccessible case method is called. 47 | * 48 | * @var ?Closure(UnitEnum $case, string $name, array $arguments): mixed 49 | */ 50 | protected static ?Closure $onCall = null; 51 | 52 | /** 53 | * The logic to run when a case is invoked. 54 | * 55 | * @var ?Closure(UnitEnum $case, mixed ...$arguments): mixed 56 | */ 57 | protected static ?Closure $onInvoke = null; 58 | 59 | /** 60 | * Set the application base path. 61 | */ 62 | public static function setBasePath(string $path): void 63 | { 64 | static::$basePath = path($path); 65 | } 66 | 67 | /** 68 | * Retrieve the application base path, optionally appending the given path. 69 | */ 70 | public static function basePath(?string $path = null): string 71 | { 72 | $basePath = static::$basePath ?: dirname(__DIR__, 4); 73 | 74 | return $path === null ? $basePath : $basePath . DIRECTORY_SEPARATOR . ltrim(path($path), '\/'); 75 | } 76 | 77 | /** 78 | * Set the glob paths to find all the application enums. 79 | */ 80 | public static function setPaths(string ...$paths): void 81 | { 82 | static::$paths = array_map(path(...), $paths); 83 | } 84 | 85 | /** 86 | * Retrieve the paths to find all the application enums. 87 | * 88 | * @return string[] 89 | */ 90 | public static function paths(): array 91 | { 92 | return static::$paths; 93 | } 94 | 95 | /** 96 | * Set the TypeScript path to sync enums in. 97 | * 98 | * @param callable(class-string|string $enum): string|string $path 99 | */ 100 | public static function setTypeScript(callable|string $path): void 101 | { 102 | /** @phpstan-ignore assign.propertyType */ 103 | static::$typeScript = is_callable($path) ? $path(...) : $path; 104 | } 105 | 106 | /** 107 | * Retrieve the TypeScript path, optionally for the given enum. 108 | * 109 | * @param class-string|string $enum 110 | * @return string 111 | */ 112 | public static function typeScript(string $enum = ''): string 113 | { 114 | return static::$typeScript instanceof Closure ? (static::$typeScript)($enum) : static::$typeScript; 115 | } 116 | 117 | /** 118 | * Yield the namespaces of all the application enums. 119 | * 120 | * @return Generator> 121 | */ 122 | public static function namespaces(): Generator 123 | { 124 | $psr4 = psr4(); 125 | 126 | foreach (static::paths() as $path) { 127 | $pattern = static::basePath($path) . DIRECTORY_SEPARATOR . '*.php'; 128 | 129 | foreach (new GlobIterator($pattern) as $fileInfo) { 130 | /** @var \SplFileInfo $fileInfo */ 131 | $enumPath = (string) $fileInfo->getRealPath(); 132 | 133 | foreach ($psr4 as $root => $relative) { 134 | $absolute = static::basePath($relative) . DIRECTORY_SEPARATOR; 135 | 136 | if (str_starts_with($enumPath, $absolute)) { 137 | $enum = strtr($enumPath, [$absolute => $root, '/' => '\\', '.php' => '']); 138 | 139 | if (enum_exists($enum)) { 140 | yield $enum; 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Set the logic to run when an inaccessible enum method is called. 150 | * 151 | * @param callable(class-string $enum, string $name, array $arguments): mixed $callback 152 | */ 153 | public static function onStaticCall(callable $callback): void 154 | { 155 | static::$onStaticCall = $callback(...); 156 | } 157 | 158 | /** 159 | * Handle the call to an inaccessible enum method. 160 | * 161 | * @param class-string $enum 162 | * @param array $arguments 163 | */ 164 | public static function handleStaticCall(string $enum, string $name, array $arguments): mixed 165 | { 166 | return static::$onStaticCall 167 | ? (static::$onStaticCall)($enum, $name, $arguments) 168 | : $enum::fromName($name)->value(); /** @phpstan-ignore method.nonObject */ 169 | } 170 | 171 | /** 172 | * Set the logic to run when an inaccessible case method is called. 173 | * 174 | * @param callable(UnitEnum $case, string $name, array $arguments): mixed $callback 175 | */ 176 | public static function onCall(callable $callback): void 177 | { 178 | static::$onCall = $callback(...); 179 | } 180 | 181 | /** 182 | * Handle the call to an inaccessible case method. 183 | * 184 | * @param array $arguments 185 | */ 186 | public static function handleCall(UnitEnum $case, string $name, array $arguments): mixed 187 | { 188 | return static::$onCall ? (static::$onCall)($case, $name, $arguments) : $case->resolveMetaAttribute($name); 189 | } 190 | 191 | /** 192 | * Set the logic to run when a case is invoked. 193 | * 194 | * @param callable(UnitEnum $case, mixed ...$arguments): mixed $callback 195 | */ 196 | public static function onInvoke(callable $callback): void 197 | { 198 | static::$onInvoke = $callback(...); 199 | } 200 | 201 | /** 202 | * Handle the invocation of a case. 203 | */ 204 | public static function handleInvoke(UnitEnum $case, mixed ...$arguments): mixed 205 | { 206 | return static::$onInvoke ? (static::$onInvoke)($case, ...$arguments) : $case->value(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Enums/Backed.php: -------------------------------------------------------------------------------- 1 | 57 | * @throws \ValueError 58 | */ 59 | public static function backCases(array $cases, ?string $strategy = null): array 60 | { 61 | $backed = match (true) { 62 | is_string($strategy) => self::fromName($strategy), 63 | default => str_contains($cases[0] ?? '', '=') ? self::custom : self::pure, 64 | }; 65 | 66 | return $backed->back($cases); 67 | } 68 | 69 | /** 70 | * Retrieve the given cases after backing them. 71 | * 72 | * @param string[] $cases 73 | * @return array 74 | */ 75 | public function back(array $cases): array 76 | { 77 | $backedCases = []; 78 | $pairs = $this->yieldPairs(); 79 | 80 | foreach ($cases as $case) { 81 | $backedCases += $pairs->send($case); 82 | 83 | $pairs->next(); 84 | } 85 | 86 | return $backedCases; 87 | } 88 | 89 | /** 90 | * Yield the case-value pairs. 91 | * 92 | * @return Generator> 93 | */ 94 | public function yieldPairs(): Generator 95 | { 96 | $i = 0; 97 | 98 | $callback = match ($this) { 99 | self::pure => fn(string $name) => [$name => null], 100 | self::custom => parseCaseValue(...), 101 | self::snake => fn(string $name) => [$name => snake($name)], 102 | self::camel => fn(string $name) => [$name => camel($name)], 103 | self::kebab => fn(string $name) => [$name => snake($name, '-')], 104 | self::upper => fn(string $name) => [$name => strtoupper($name)], 105 | self::lower => fn(string $name) => [$name => strtolower($name)], 106 | self::int0 => fn(string $name, int $i) => [$name => $i], 107 | self::int1 => fn(string $name, int $i) => [$name => $i + 1], 108 | self::bitwise => fn(string $name, int $i) => [$name => "1 << {$i}"], 109 | }; 110 | 111 | /** @phpstan-ignore while.alwaysTrue */ 112 | while (true) { 113 | /** @phpstan-ignore-next-line */ 114 | yield $callback(yield, $i++); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Services/Annotator.php: -------------------------------------------------------------------------------- 1 | 44 | */ 45 | protected Inspector $inspector; 46 | 47 | /** 48 | * Instantiate the class. 49 | * 50 | * @param class-string $enum 51 | * @throws InvalidArgumentException 52 | */ 53 | public function __construct(protected string $enum) 54 | { 55 | $this->inspector = new Inspector($enum); 56 | } 57 | 58 | /** 59 | * Annotate the given enum. 60 | */ 61 | public function annotate(bool $overwrite = false): bool 62 | { 63 | if (empty($annotations = $this->inspector->methodAnnotations(! $overwrite))) { 64 | return true; 65 | } 66 | 67 | $docBlock = $this->inspector->docBlock(); 68 | $filename = $this->inspector->filename(); 69 | $oldContent = (string) file_get_contents($filename); 70 | $methodAnnotations = $this->formatMethodAnnotations($annotations); 71 | $useStatements = $this->formatUseStatements($this->inspector->useStatements(! $overwrite)); 72 | $newContent = (string) preg_replace(static::RE_USE_STATEMENTS, $useStatements, $oldContent, 1); 73 | 74 | $newContent = match (true) { 75 | empty($docBlock) => $this->addDocBlock($methodAnnotations, $newContent), 76 | str_contains($docBlock, '@method') => $this->replaceAnnotations($methodAnnotations, $newContent), 77 | default => $this->addAnnotations($methodAnnotations, $newContent, $docBlock), 78 | }; 79 | 80 | return file_put_contents($filename, $newContent) !== false; 81 | } 82 | 83 | /** 84 | * Retrieve the formatted method annotations. 85 | * 86 | * @param array $annotations 87 | */ 88 | protected function formatMethodAnnotations(array $annotations): string 89 | { 90 | $mapped = array_map(fn(MethodAnnotation $annotation) => " * {$annotation}", $annotations); 91 | 92 | return implode(PHP_EOL, $mapped); 93 | } 94 | 95 | /** 96 | * Retrieve the formatted use statements. 97 | * 98 | * @param array $statements 99 | */ 100 | protected function formatUseStatements(array $statements): string 101 | { 102 | array_walk($statements, function (string &$namespace, string $alias) { 103 | $namespace = "use {$namespace}" . (className($namespace) == $alias ? ';' : " as {$alias};"); 104 | }); 105 | 106 | return implode(PHP_EOL, $statements); 107 | } 108 | 109 | /** 110 | * Add a docBlock with the given method annotations. 111 | */ 112 | protected function addDocBlock(string $methodAnnotations, string $content): string 113 | { 114 | $replacement = implode(PHP_EOL, ['/**', $methodAnnotations, ' */', '$1']); 115 | 116 | return (string) preg_replace(static::RE_ENUM, $replacement, $content, 1); 117 | } 118 | 119 | /** 120 | * Replace existing method annotations with the given method annotations. 121 | */ 122 | protected function replaceAnnotations(string $methodAnnotations, string $content): string 123 | { 124 | return (string) preg_replace(static::RE_METHOD_ANNOTATIONS, $methodAnnotations, $content, 1); 125 | } 126 | 127 | /** 128 | * Add the given method annotations to the provided docBlock. 129 | */ 130 | protected function addAnnotations(string $methodAnnotations, string $content, string $docBlock): string 131 | { 132 | $newDocBlock = str_replace(' */', implode(PHP_EOL, [' *', $methodAnnotations, ' */']), $docBlock); 133 | 134 | return str_replace($docBlock, $newDocBlock, $content); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Services/Generator.php: -------------------------------------------------------------------------------- 1 | $namespace 26 | * @param string[] $cases 27 | * @throws \ValueError 28 | */ 29 | public function __construct(string $namespace, array $cases, ?string $backed = null) 30 | { 31 | $this->enum = new GeneratingEnum($namespace, Backed::backCases($cases, $backed)); 32 | } 33 | 34 | /** 35 | * Generate the given enum. 36 | */ 37 | public function generate(bool $overwrite = false): bool 38 | { 39 | if ($this->enum->exists && ! $overwrite) { 40 | return true; 41 | } 42 | 43 | ensureParentDirectory($this->enum->path); 44 | 45 | $stub = (string) file_get_contents($this->stub()); 46 | $content = strtr($stub, $this->replacements()); 47 | 48 | return file_put_contents($this->enum->path, $content) !== false; 49 | } 50 | 51 | /** 52 | * Retrieve the path of the stub. 53 | */ 54 | protected function stub(): string 55 | { 56 | return __DIR__ . '/../../stubs/enum.stub'; 57 | } 58 | 59 | /** 60 | * Retrieve the replacements for the placeholders. 61 | * 62 | * @return array 63 | */ 64 | protected function replacements(): array 65 | { 66 | return [ 67 | '{{ name }}' => $this->enum->name, 68 | '{{ namespace }}' => $this->enum->namespace, 69 | '{{ backingType }}' => $this->enum->backingType ? ": {$this->enum->backingType}" : '', 70 | '{{ cases }}' => $this->formatCases($this->enum->cases), 71 | ]; 72 | } 73 | 74 | /** 75 | * Retrieve the given cases formatted as a string 76 | * 77 | * @param array $cases 78 | */ 79 | protected function formatCases(array $cases): string 80 | { 81 | $formatted = []; 82 | 83 | foreach ($cases as $name => $value) { 84 | $formattedValue = match (true) { 85 | is_int($value), str_contains((string) $value, '<<') => " = {$value}", 86 | is_string($value) => ' = ' . (str_contains($value, "'") ? "\"{$value}\"" : "'{$value}'"), 87 | default => '', 88 | }; 89 | 90 | $formatted[] = " case {$name}{$formattedValue};"; 91 | } 92 | 93 | return implode(PHP_EOL . PHP_EOL, $formatted); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Services/Inspector.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | protected ReflectionEnum $reflection; 32 | 33 | /** 34 | * The method annotations. 35 | * 36 | * @var array 37 | */ 38 | protected array $methodAnnotations; 39 | 40 | /** 41 | * The use statements. 42 | * 43 | * @var array 44 | */ 45 | protected array $useStatements; 46 | 47 | /** 48 | * Instantiate the class. 49 | * 50 | * @param class-string $enum 51 | */ 52 | public function __construct(protected string $enum) 53 | { 54 | $this->reflection = new ReflectionEnum($enum); 55 | 56 | $this->assertEnumUsesMainTrait(); 57 | } 58 | 59 | /** 60 | * Assert that the enum uses the main trait. 61 | */ 62 | protected function assertEnumUsesMainTrait(): void 63 | { 64 | if (! $this->uses($this->mainTrait)) { 65 | throw new InvalidArgumentException("The enum {$this->enum} must use the trait {$this->mainTrait}"); 66 | } 67 | } 68 | 69 | /** 70 | * Retrieve the enum filename. 71 | */ 72 | public function filename(): string 73 | { 74 | return (string) $this->reflection->getFileName(); 75 | } 76 | 77 | /** 78 | * Retrieve the DocBlock of the enum. 79 | */ 80 | public function docBlock(): string 81 | { 82 | return $this->reflection->getDocComment() ?: ''; 83 | } 84 | 85 | /** 86 | * Retrieve the enum cases. 87 | * 88 | * @return list 89 | */ 90 | public function cases(): array 91 | { 92 | /** @var list */ 93 | return $this->enum::cases(); 94 | } 95 | 96 | /** 97 | * Retrieve the meta attribute names of the enum. 98 | * 99 | * @return list 100 | */ 101 | public function metaAttributeNames(): array 102 | { 103 | /** @var list */ 104 | return $this->enum::metaAttributeNames(); 105 | } 106 | 107 | /** 108 | * Determine whether the enum uses the given trait. 109 | */ 110 | public function uses(string $trait): bool 111 | { 112 | return isset($this->traits()[$trait]); 113 | } 114 | 115 | /** 116 | * Retrieve all the enum traits. 117 | * 118 | * @return array 119 | */ 120 | public function traits(): array 121 | { 122 | $traits = []; 123 | 124 | foreach ($this->reflection->getTraitNames() as $trait) { 125 | $traits += [$trait => $trait, ...traitsUsedBy($trait)]; 126 | } 127 | 128 | /** @var array */ 129 | return $traits; 130 | } 131 | 132 | /** 133 | * Retrieve the use statements. 134 | * 135 | * @return array 136 | */ 137 | public function useStatements(bool $includeExisting = true): array 138 | { 139 | return $this->useStatements ??= [...new UseStatements($this, $includeExisting)]; 140 | } 141 | 142 | /** 143 | * Retrieve the method annotations. 144 | * 145 | * @return array 146 | */ 147 | public function methodAnnotations(bool $includeExisting = true): array 148 | { 149 | return $this->methodAnnotations ??= [...new MethodAnnotations($this, $includeExisting)]; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Services/MethodAnnotations.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class MethodAnnotations implements IteratorAggregate 21 | { 22 | /** 23 | * The regular expression to extract method annotations already annotated on the enum. 24 | * 25 | * @var string 26 | */ 27 | public const RE_METHOD = '~@method\s+((?:static)?\s*[^\s]+\s+([^\(]+).*)~'; 28 | 29 | /** 30 | * Instantiate the class. 31 | * 32 | * @param Inspector $inspector 33 | */ 34 | public function __construct( 35 | protected Inspector $inspector, 36 | protected bool $includeExisting, 37 | ) {} 38 | 39 | /** 40 | * Retrieve the sorted, iterable method annotations. 41 | * 42 | * @return ArrayIterator 43 | */ 44 | public function getIterator(): Traversable 45 | { 46 | $annotations = $this->all(); 47 | 48 | uasort($annotations, function (MethodAnnotation $a, MethodAnnotation $b) { 49 | return [$b->isStatic, $a->name] <=> [$a->isStatic, $b->name]; 50 | }); 51 | 52 | return new ArrayIterator($annotations); 53 | } 54 | 55 | /** 56 | * Retrieve all the method annotations. 57 | * 58 | * @return array 59 | */ 60 | public function all(): array 61 | { 62 | return [ 63 | ...$this->forCaseNames(), 64 | ...$this->forMetaAttributes(), 65 | ...$this->includeExisting ? $this->existing() : [], 66 | ]; 67 | } 68 | 69 | /** 70 | * Retrieve the method annotations for the case names. 71 | * 72 | * @return array 73 | */ 74 | public function forCaseNames(): array 75 | { 76 | $annotations = []; 77 | 78 | foreach ($this->inspector->cases() as $case) { 79 | $annotations[$case->name] = MethodAnnotation::forCase($case); 80 | } 81 | 82 | return $annotations; 83 | } 84 | 85 | /** 86 | * Retrieve the method annotations for the meta attributes. 87 | * 88 | * @return array 89 | */ 90 | public function forMetaAttributes(): array 91 | { 92 | $annotations = []; 93 | $cases = $this->inspector->cases(); 94 | 95 | foreach ($this->inspector->metaAttributeNames() as $meta) { 96 | $types = array_map(fn(UnitEnum $case) => get_debug_type($case->resolveMetaAttribute($meta)), $cases); 97 | 98 | $annotations[$meta] = MethodAnnotation::instance($meta, commonType(...$types)); 99 | } 100 | 101 | return $annotations; 102 | } 103 | 104 | /** 105 | * Retrieve the method annotations already annotated on the enum. 106 | * 107 | * @return array 108 | */ 109 | public function existing(): array 110 | { 111 | $annotations = []; 112 | 113 | preg_match_all(static::RE_METHOD, $this->inspector->docBlock(), $matches, PREG_SET_ORDER); 114 | 115 | foreach ($matches as $match) { 116 | $annotations[$match[2]] = new MethodAnnotation($match[2], $match[1]); 117 | } 118 | 119 | return $annotations; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Services/TypeScript.php: -------------------------------------------------------------------------------- 1 | $enum 27 | */ 28 | public function __construct(protected string $enum) 29 | { 30 | $this->path = Enums::basePath(Enums::typeScript($enum)); 31 | } 32 | 33 | /** 34 | * Synchronize the enum in TypeScript. 35 | */ 36 | public function sync(bool $overwrite = false): bool 37 | { 38 | return match (true) { 39 | ! file_exists($this->path) => $this->createEnum(), 40 | $this->enumIsMissing() => $this->appendEnum(), 41 | $overwrite => $this->replaceEnum(), 42 | default => true, 43 | }; 44 | } 45 | 46 | /** 47 | * Create the TypeScript file for the enum. 48 | */ 49 | protected function createEnum(): bool 50 | { 51 | ensureParentDirectory($this->path); 52 | 53 | return file_put_contents($this->path, $this->transform()) !== false; 54 | } 55 | 56 | /** 57 | * Append the enum to the TypeScript file. 58 | */ 59 | protected function appendEnum(): bool 60 | { 61 | return file_put_contents($this->path, PHP_EOL . $this->transform(), flags: FILE_APPEND) !== false; 62 | } 63 | 64 | /** 65 | * Retrieved the enum transformed for TypeScript. 66 | */ 67 | public function transform(): string 68 | { 69 | $stub = (string) file_get_contents($this->stub()); 70 | 71 | return strtr($stub, $this->replacements()); 72 | } 73 | 74 | /** 75 | * Retrieve the path of the stub. 76 | */ 77 | protected function stub(): string 78 | { 79 | return __DIR__ . '/../../stubs/typescript.stub'; 80 | } 81 | 82 | /** 83 | * Retrieve the stub replacements. 84 | * 85 | * @return array 86 | */ 87 | protected function replacements(): array 88 | { 89 | return [ 90 | '{{ name }}' => className($this->enum), 91 | '{{ cases }}' => $this->formatCases(), 92 | ]; 93 | } 94 | 95 | /** 96 | * Retrieve the enum cases formatted as a string 97 | */ 98 | protected function formatCases(): string 99 | { 100 | $cases = array_map(function (UnitEnum $case) { 101 | /** @var string|int|null $value */ 102 | $value = is_string($value = $case->value ?? null) ? "'{$value}'" : $value; 103 | 104 | return " {$case->name}" . ($value === null ? ',' : " = {$value},"); 105 | }, $this->enum::cases()); 106 | 107 | return implode(PHP_EOL, $cases); 108 | } 109 | 110 | /** 111 | * Determine whether the enum is missing. 112 | */ 113 | protected function enumIsMissing(): bool 114 | { 115 | $name = className($this->enum); 116 | 117 | return preg_match("~^export enum {$name}~im", (string) file_get_contents($this->path)) === 0; 118 | } 119 | 120 | /** 121 | * Replace the enum in the TypeScript file. 122 | */ 123 | protected function replaceEnum(): bool 124 | { 125 | $name = className($this->enum); 126 | $oldContent = (string) file_get_contents($this->path); 127 | $newContent = preg_replace("~^(export enum {$name}[^}]+})~im", trim($this->transform()), $oldContent); 128 | 129 | return file_put_contents($this->path, $newContent) !== false; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Services/UseStatements.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class UseStatements implements IteratorAggregate 20 | { 21 | /** 22 | * The regular expression to extract the use statements already present on the enum. 23 | * 24 | * @var string 25 | */ 26 | public const RE_STATEMENT = '~^use\s+([^\s;]+)(?:\s+as\s+([^;]+))?~i'; 27 | 28 | /** 29 | * Instantiate the class. 30 | * 31 | * @param Inspector<\UnitEnum> $inspector 32 | */ 33 | public function __construct( 34 | protected Inspector $inspector, 35 | protected bool $includeExisting, 36 | ) {} 37 | 38 | /** 39 | * Retrieve the sorted, iterable use statements. 40 | * 41 | * @return ArrayIterator 42 | */ 43 | public function getIterator(): Traversable 44 | { 45 | $useStatements = $this->all(); 46 | 47 | asort($useStatements); 48 | 49 | return new ArrayIterator($useStatements); 50 | } 51 | 52 | /** 53 | * Retrieve all the use statements. 54 | * 55 | * @return array 56 | */ 57 | public function all(): array 58 | { 59 | return $this->existing(); 60 | } 61 | 62 | /** 63 | * Retrieve the use statements already present on the enum. 64 | * 65 | * @return array 66 | */ 67 | public function existing(): array 68 | { 69 | $useStatements = []; 70 | 71 | foreach (yieldLines($this->inspector->filename()) as $line) { 72 | if (strpos($line, 'enum') === 0) { 73 | break; 74 | } 75 | 76 | if (preg_match(static::RE_STATEMENT, $line, $matches)) { 77 | $useStatements[$matches[2] ?? className($matches[1])] = $matches[1]; 78 | } 79 | } 80 | 81 | /** @var array */ 82 | return $useStatements; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /stubs/enum.stub: -------------------------------------------------------------------------------- 1 |