├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── duster.json ├── helpers ├── cli.php └── core.php ├── phpstan-baseline.neon ├── phpstan.neon ├── pint.json ├── src ├── Actions │ └── OnCall.php ├── Capsules │ ├── CacheKey.php │ └── SessionKey.php ├── CasesCollection.php ├── CasesCollectionCast.php ├── Commands │ ├── EnumAnnotateCommand.php │ ├── EnumMakeCommand.php │ └── EnumTsCommand.php ├── Concerns │ ├── CollectsCases.php │ ├── Enumerates.php │ ├── EnumeratesCacheKeys.php │ ├── EnumeratesSessionKeys.php │ ├── IsMagic.php │ └── SelfAware.php ├── Contracts │ └── Bitwise.php ├── Data │ └── MethodAnnotation.php ├── Enums.php ├── Providers │ └── LaravelEnumServiceProvider.php └── Services │ ├── Annotator.php │ ├── Generator.php │ ├── Inspector.php │ ├── MethodAnnotations.php │ ├── TypeScript.php │ └── UseStatements.php └── stubs ├── bitwise.stub ├── enum.stub └── typescript.stub /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-enum` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://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.1.0 - 2025-02-19 30 | 31 | ### Added 32 | - Attempt to resolve translations when an item of a case cannot be resolved 33 | 34 | ### Changed 35 | - Require the latest version of the package Enum 36 | 37 | 38 | ## 2.0.0 - 2025-01-17 39 | 40 | ### Added 41 | - Laravel contracts and traits to cases collection 42 | - Magic translation 43 | - Eloquent casting for cases collection 44 | - Autowiring meta 45 | - Encapsulation of Laravel session and cache 46 | - Enums discoverability 47 | - Customizable stubs 48 | - Artisan command to create enums 49 | - Artisan command to annotate enums 50 | - Artisan command to turn PHP enums into TypeScript enums 51 | 52 | ### Changed 53 | - Package structure and initial files 54 | - Code compliant with PHPStan level 10 55 | 56 | ### Removed 57 | - All previous functionalities, the package now focuses on PHP native enums 58 | 59 | 60 | ## 1.3.1 - 2022-11-10 61 | 62 | ### Added 63 | - Support for PHP 8 and the latest Laravel releases. 64 | 65 | 66 | ## 1.3.0 - 2020-09-19 67 | 68 | ### Added 69 | - Support for Laravel 8. 70 | 71 | 72 | ## 1.2.1 - 2020-03-04 73 | 74 | ### Added 75 | - Support for Laravel 7. 76 | 77 | 78 | ## 1.2.0 - 2019-09-07 79 | 80 | ### Added 81 | - Support for Laravel 6. 82 | 83 | 84 | ## 1.1.0 - 2019-06-09 85 | 86 | ### Added 87 | - Option `--keys` to generate keys automatically. 88 | 89 | 90 | ## 1.0.0 - 2019-04-19 91 | 92 | ### Added 93 | - Artisan command to generate enum classes. 94 | -------------------------------------------------------------------------------- /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/laravel-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 | # 🎲 Laravel Enum 2 | 3 | [![Author][ico-author]][link-author] 4 | [![PHP Version][ico-php]][link-php] 5 | [![Laravel Version][ico-laravel]][link-laravel] 6 | [![Build Status][ico-actions]][link-actions] 7 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer] 8 | [![Quality Score][ico-code-quality]][link-code-quality] 9 | [![PHPStan Level][ico-phpstan]][link-phpstan] 10 | [![Total Downloads][ico-downloads]][link-downloads] 11 | [![Latest Version][ico-version]][link-packagist] 12 | [![Software License][ico-license]](LICENSE.md) 13 | [![PER][ico-per]][link-per] 14 | 15 | Laravel package to supercharge enum functionalities. 16 | 17 | > [!TIP] 18 | > Need a framework-agnostic solution? Consider using [🎲 Enum](https://github.com/cerbero90/enum) instead. 19 | 20 | 21 | ## 📦 Install 22 | 23 | Via Composer: 24 | 25 | ``` bash 26 | composer require cerbero/laravel-enum 27 | ``` 28 | 29 | ## 🔮 Usage 30 | 31 | * [🏷️ Meta](#%EF%B8%8F-meta) 32 | * [🧺 Cases collection](#-cases-collection) 33 | * [🪄 Magic translation](#-magic-translation) 34 | * [💊 Encapsulation](#-encapsulation) 35 | * [🗄️ Cache](#%EF%B8%8F-cache) 36 | * [🔓 Session](#-session) 37 | * [🦾 Artisan commands](#-artisan-commands) 38 | * [🗒️ enum:annotate](#%EF%B8%8F-enumannotate) 39 | * [🏗️ enum:make](#%EF%B8%8F-enummake) 40 | * [💙 enum:ts](#-enumts) 41 | 42 | This package provides all the functionalities of [🎲 Enum](https://github.com/cerbero90/enum) plus Laravel-specific features. 43 | 44 | To supercharge our enums, we just need to let them use the `Enumerates` trait: 45 | 46 | ```php 47 | use Cerbero\LaravelEnum\Concerns\Enumerates; 48 | 49 | enum Numbers: int 50 | { 51 | use Enumerates; 52 | 53 | case One = 1; 54 | case Two = 2; 55 | case Three = 3; 56 | } 57 | ``` 58 | 59 | 60 | ### 🏷️ Meta 61 | 62 | Laravel Enum extends [Enum's meta](https://github.com/cerbero90/enum?tab=readme-ov-file#%EF%B8%8F-meta) by allowing us to attach meta with a class name. This class is resolved by the Laravel container when invoking the corresponding meta method: 63 | 64 | ```php 65 | use Cerbero\Enum\Attributes\Meta; 66 | 67 | enum PayoutStatuses 68 | { 69 | use Enumerates; 70 | 71 | #[Meta(handler: SentPayoutHandler::class)] 72 | case Sent; 73 | 74 | #[Meta(handler: OnHoldPayoutHandler::class)] 75 | case OnHold; 76 | 77 | #[Meta(handler: DeclinedPayoutHandler::class)] 78 | case Declined; 79 | } 80 | ``` 81 | 82 | In the enum above, each case specifies a `handler` meta with a class name. When a case calls its `handler()` meta method, the corresponding class is resolved through the IoC container: 83 | 84 | ```php 85 | // 🐢 instead of this 86 | $handler = match ($payout->status) { 87 | PayoutStatuses::Sent => SentPayoutHandler::class, 88 | PayoutStatuses::OnHold => OnHoldPayoutHandler::class, 89 | PayoutStatuses::Declined => DeclinedPayoutHandler::class, 90 | }; 91 | 92 | return Container::getInstance()->make($handler); 93 | 94 | 95 | // 🐇 we can do this 96 | return $payout->status->handler(); 97 | ``` 98 | 99 | If we need to resolve a default class for most cases, we can attach the meta to the enum itself. Cases with their own meta override the default class: 100 | 101 | ```php 102 | use Cerbero\Enum\Attributes\Meta; 103 | 104 | #[Meta(handler: DefaultPayoutHandler::class)] 105 | enum PayoutStatuses 106 | { 107 | use Enumerates; 108 | 109 | #[Meta(handler: SentPayoutHandler::class)] 110 | case Sent; 111 | 112 | case OnHold; 113 | 114 | case Declined; 115 | } 116 | ``` 117 | 118 | In the example above, all cases calling the `handler()` method resolve the `DefaultPayoutHandler` class, except for the `Sent` case that resolves `SentPayoutHandler`. 119 | 120 | If the class to be resolved is callable (i.e. it implements the `__invoke()` method), that class will be both resolved and executed: 121 | 122 | ```php 123 | use Cerbero\Enum\Attributes\Meta; 124 | 125 | enum PayoutStatuses 126 | { 127 | use Enumerates; 128 | 129 | #[Meta(handlePayout: SentPayoutHandler::class)] 130 | case Sent; 131 | 132 | #[Meta(handlePayout: OnHoldPayoutHandler::class)] 133 | case OnHold; 134 | 135 | #[Meta(handlePayout: DeclinedPayoutHandler::class)] 136 | case Declined; 137 | } 138 | ``` 139 | 140 | In the enum above, each case specifies a `handlePayout` meta with a callable class. Calling `handlePayout()` resolves and invokes the class with any passed parameters. 141 | 142 | ```php 143 | // 🐢 instead of this 144 | $handler = match ($payout->status) { 145 | PayoutStatuses::Sent => SentPayoutHandler::class, 146 | PayoutStatuses::OnHold => OnHoldPayoutHandler::class, 147 | PayoutStatuses::Declined => DeclinedPayoutHandler::class, 148 | }; 149 | 150 | $handlePayout = Container::getInstance()->make($handler); 151 | 152 | return $handlePayout($payout); 153 | 154 | 155 | // 🐇 we can do this 156 | return $payout->status->handlePayout($payout); 157 | ``` 158 | 159 | If we need to run a default callable class for most cases, we can attach the meta to the enum itself. Cases with their own meta override the default callable class: 160 | 161 | ```php 162 | use Cerbero\Enum\Attributes\Meta; 163 | 164 | #[Meta(handlePayout: DefaultPayoutHandler::class)] 165 | enum PayoutStatuses 166 | { 167 | use Enumerates; 168 | 169 | #[Meta(handlePayout: SentPayoutHandler::class)] 170 | case Sent; 171 | 172 | case OnHold; 173 | 174 | case Declined; 175 | } 176 | ``` 177 | 178 | In the example above, all cases calling the `handlePayout()` method execute the `DefaultPayoutHandler` class, except for the `Sent` case, which runs the `SentPayoutHandler`. 179 | 180 | > [!TIP] 181 | > Our IDE can autocomplete meta methods thanks to the [`enum:annotate` command](#%EF%B8%8F-enumannotate). 182 | > 183 | > Class names in meta are annotated by identifying the common interface or parent class they share. 184 | 185 | 186 | ### 🧺 Cases collection 187 | 188 | The [original cases collection](https://github.com/cerbero90/enum?tab=readme-ov-file#-cases-collection) has been extended for better integration with the Laravel framework. 189 | 190 | The new cases collection implements the `Illuminate\Contracts\Support\Arrayable` and `Illuminate\Contracts\Support\Jsonable` contracts and it can be serialized into a JSON. 191 | 192 | It also leverages the following Laravel traits: 193 | - `Illuminate\Support\Traits\Conditionable` for conditional chaining of methods 194 | - `Illuminate\Support\Traits\Macroable` for adding methods to the collection at runtime 195 | - `Illuminate\Support\Traits\Tappable` for running custom logic while keeping method chaining 196 | 197 | Additionally, the new collection enables us to `dump()` and `dd()` its cases: 198 | 199 | ```php 200 | Numbers::collect()->dump(); 201 | 202 | Numbers::collect()->dd(); 203 | ``` 204 | 205 | Cases collection can be cast in an Eloquent model to store multiple cases in a single database column and then re-hydrate those cases back into a collection: 206 | 207 | ```php 208 | use Cerbero\LaravelEnum\CasesCollection; 209 | 210 | class User extends Model 211 | { 212 | // before Laravel 11 213 | protected $casts = [ 214 | 'numbers' => CasesCollection::class . ':' . Numbers::class, 215 | ]; 216 | 217 | // after Laravel 11 218 | protected function casts(): array 219 | { 220 | return [ 221 | 'numbers' => CasesCollection::of(Numbers::class), 222 | ]; 223 | } 224 | } 225 | ``` 226 | 227 | Once the cast is set, we can assign an array of names, values or cases to the cast property of the model and receive a cases collection when accessing the property: 228 | 229 | ```php 230 | $user->numbers = ['One', 'Two']; 231 | 232 | $user->numbers = [1, 2]; 233 | 234 | $user->numbers = [Numbers::One, Numbers::Two]; 235 | 236 | $user->numbers; // CasesCollection[Numbers::One, Numbers::Two] 237 | ``` 238 | 239 | The cases collection above is stored in the database as `["One","Two"]` for a pure enum, or as `[1,2]` for a backed enum. 240 | 241 | The cast also supports bitwise backed enums, so for instance, if we have a `Permissions` enum implementing the `Bitwise` contract: 242 | 243 | ```php 244 | use Cerbero\LaravelEnum\Contracts\Bitwise; 245 | 246 | enum Permissions: int implements Bitwise 247 | { 248 | use Enumerates; 249 | 250 | case CreatePost = 1; 251 | case UpdatePost = 2; 252 | case DeletePost = 4; 253 | } 254 | ``` 255 | 256 | and we cast the `permissions` property in our Eloquent model: 257 | 258 | ```php 259 | use Cerbero\LaravelEnum\CasesCollection; 260 | 261 | class User extends Model 262 | { 263 | // before Laravel 11 264 | protected $casts = [ 265 | 'permissions' => CasesCollection::class . ':' . Permissions::class, 266 | ]; 267 | 268 | // after Laravel 11 269 | protected function casts(): array 270 | { 271 | return [ 272 | 'permissions' => CasesCollection::of(Permissions::class), 273 | ]; 274 | } 275 | } 276 | ``` 277 | 278 | we can assign a bitwise value or an array of bitwise values/cases to the `permissions` property and receive a cases collection in return: 279 | 280 | ```php 281 | $user->permissions = 3; 282 | 283 | $user->permissions = 1 | 2; 284 | 285 | $user->permissions = Permissions::CreatePost->value | Permissions::UpdatePost->value; 286 | 287 | $user->permissions = [1, 2]; 288 | 289 | $user->permissions = [Permissions::CreatePost, Permissions::UpdatePost]; 290 | 291 | $user->permissions; // CasesCollection[Permissions::CreatePost, Permissions::UpdatePost] 292 | ``` 293 | 294 | The cases collection above is stored in the database as `3`, the result of the `OR` bitwise operator. 295 | 296 | ### 🪄 Magic translation 297 | 298 | On top of [Enum's magic](https://github.com/cerbero90/enum?tab=readme-ov-file#-magic), when a case calls an inaccessible method without a corresponding [meta](#%EF%B8%8F-meta) match, Laravel Enum assumes that we want to access a translation: 299 | 300 | ```php 301 | Numbers::One->description(); 302 | 303 | // lang/en/enums.php 304 | return [ 305 | Numbers::class => [ 306 | 'One' => [ 307 | 'description' => 'This is the case One.', 308 | ], 309 | ], 310 | ]; 311 | ``` 312 | 313 | By default the translation key is resolved as `enums.{enum namespace}.{case name}.{inaccessible method}`. If customization is needed, we can adjust the translation key: 314 | 315 | ```php 316 | use Cerbero\LaravelEnum\Enums; 317 | 318 | Enums::translateFrom(function(UnitEnum $case, string $method) { 319 | return sprintf('custom.%s.%s.%s', $case::class, $case->name, $method); 320 | }); 321 | ``` 322 | 323 | The above logic will resolve the translation key as `custom.{enum namespace}.{case name}.{inaccessible method}`. 324 | 325 | Additionally, we can pass named arguments to replace placeholders within our translations: 326 | 327 | ```php 328 | return [ 329 | Numbers::class => [ 330 | 'One' => [ 331 | 'description' => 'This is the case :value.', 332 | ], 333 | ], 334 | ]; 335 | 336 | // This is the case 1. 337 | Numbers::One->description(value: 1); 338 | ``` 339 | 340 | Translations are accessible through [enum operations](https://github.com/cerbero90/enum?tab=readme-ov-file#-enum-operations) as well: 341 | 342 | ```php 343 | $options = Numbers::pluck('description', 'value'); 344 | 345 | /* 346 | [ 347 | 1 => 'This is the case One.', 348 | 2 => 'This is the case Two.', 349 | 3 => 'This is the case Three.', 350 | ] 351 | */ 352 | ``` 353 | 354 | > [!TIP] 355 | > Our IDE can autocomplete translation methods thanks to the [`enum:annotate` command](#%EF%B8%8F-enumannotate). 356 | 357 | 358 | ### 💊 Encapsulation 359 | 360 | Laravel Enum offers extra traits to encapsulate Laravel features that deal with keys. By defining keys as enum cases, we can leverage Laravel features without having to remember or repeat such keys. 361 | 362 | The benefits of this approach are many: 363 | - avoiding scattered, error-prone strings throughout the codebase 364 | - preventing key misspellings 365 | - enabling IDE autocompletion 366 | - reviewing all application keys in a central location 367 | - updating keys in one place rather than replacing all instances 368 | 369 | 370 | #### 🗄️ Cache 371 | 372 | To encapsulate the Laravel cache, we can define a backed enum with all our cache keys and apply the `EnumeratesCacheKeys` trait: 373 | 374 | ```php 375 | use Cerbero\LaravelEnum\Concerns\EnumeratesCacheKeys; 376 | 377 | enum CacheKeys: string 378 | { 379 | use EnumeratesCacheKeys; 380 | 381 | case PostComments = 'posts.{int $postId}.comments'; 382 | case Tags = 'tags'; 383 | case TeamMemberPosts = 'teams.{string $teamId}.users.{string $userId}.posts'; 384 | } 385 | ``` 386 | 387 | The `EnumeratesCacheKeys` trait incorporates `Enumerates`, hence all the features of this package. We can now leverage all the Laravel cache methods by statically calling enum cases: 388 | 389 | ```php 390 | CacheKeys::Tags()->exists(); 391 | CacheKeys::Tags()->missing(); 392 | CacheKeys::Tags()->hasValue(); 393 | CacheKeys::Tags()->get($default); 394 | CacheKeys::Tags()->pull($default); 395 | CacheKeys::Tags()->put($value, $ttl); 396 | CacheKeys::Tags()->set($value, $ttl); 397 | CacheKeys::Tags()->add($value, $ttl); 398 | CacheKeys::Tags()->increment($value); 399 | CacheKeys::Tags()->decrement($value); 400 | CacheKeys::Tags()->forever($value); 401 | CacheKeys::Tags()->remember($ttl, $callback); 402 | CacheKeys::Tags()->rememberForever($callback); 403 | CacheKeys::Tags()->sear($callback); 404 | CacheKeys::Tags()->forget(); 405 | CacheKeys::Tags()->delete(); 406 | CacheKeys::Tags()->lock($seconds, $owner); 407 | CacheKeys::Tags()->restoreLock($owner); 408 | ``` 409 | 410 | When calling cases statically, we can pass parameters to resolve dynamic keys. Such parameters replace the `{...}` placeholders in the cache keys: 411 | 412 | ```php 413 | CacheKeys::TeamMemberPosts($teamId, $userId)->exists(); 414 | ``` 415 | 416 | 417 | #### 🔓 Session 418 | 419 | To encapsulate the Laravel session, we can define a backed enum with all our session keys and apply the `EnumeratesSessionKeys` trait: 420 | 421 | ```php 422 | use Cerbero\LaravelEnum\Concerns\EnumeratesSessionKeys; 423 | 424 | enum SessionKeys 425 | { 426 | use EnumeratesSessionKeys; 427 | 428 | case CartItems = 'cart-items'; 429 | case OnboardingStep = 'onboarding-step'; 430 | case FormsData = 'forms.{int $formId}.data'; 431 | } 432 | ``` 433 | 434 | The `EnumeratesSessionKeys` trait incorporates `Enumerates`, hence all the features of this package. We can now leverage all the Laravel session methods by statically calling enum cases: 435 | 436 | ```php 437 | SessionKeys::CartItems()->exists(); 438 | SessionKeys::CartItems()->missing(); 439 | SessionKeys::CartItems()->hasValue(); 440 | SessionKeys::CartItems()->get($default); 441 | SessionKeys::CartItems()->pull($default); 442 | SessionKeys::CartItems()->hasOldInput(); 443 | SessionKeys::CartItems()->getOldInput($default); 444 | SessionKeys::CartItems()->put($value); 445 | SessionKeys::CartItems()->remember($callback); 446 | SessionKeys::CartItems()->push($value); 447 | SessionKeys::CartItems()->increment($amount); 448 | SessionKeys::CartItems()->decrement($amount); 449 | SessionKeys::CartItems()->flash($value); 450 | SessionKeys::CartItems()->now($value); 451 | SessionKeys::CartItems()->remove(); 452 | SessionKeys::CartItems()->forget(); 453 | ``` 454 | 455 | When calling cases statically, we can pass parameters to resolve dynamic keys. Such parameters replace the `{...}` placeholders in the session keys: 456 | 457 | ```php 458 | SessionKeys::FormsData($formId)->exists(); 459 | ``` 460 | 461 | > [!TIP] 462 | > Our IDE can autocomplete case static methods thanks to the [`enum:annotate` command](#%EF%B8%8F-enumannotate). 463 | 464 | 465 | ### 🦾 Artisan commands 466 | 467 | A handy set of Artisan commands is provided out of the box to interact with enums seamlessly. 468 | 469 | Some commands generate enums or related files. If we want to customize such files, we can publish their stubs: 470 | 471 | ```bash 472 | php artisan vendor:publish --tag=laravel-enum-stubs 473 | ``` 474 | 475 | After publishing, the stubs can be modified within the `stubs/laravel-enum` directory, located at the root of our application. 476 | 477 | Certain commands supports the `--all` option to reference all enums in our application. By default, enums are searched in the `app/Enums` directory, but we can scan other folders as well: 478 | 479 | ```php 480 | use Cerbero\LaravelEnum\Enums; 481 | 482 | class AppServiceProvider extends ServiceProvider 483 | { 484 | public function boot(): void 485 | { 486 | Enums::setPaths('app/Enums', 'domain/*/Enums'); 487 | } 488 | } 489 | ``` 490 | 491 | In the example above, enums are searched in the `app/Enums` directory and all `Enums` sub-directories within `domain`, such as `domain/Posts/Enums`, `domain/Users/Enums`, etc. 492 | 493 | #### 🗒️ enum:annotate 494 | 495 | IDEs can autocomplete case static methods, meta methods, translations, etc. by running the `enum:annotate` command: 496 | 497 | ```bash 498 | php artisan enum:annotate 499 | ``` 500 | 501 | If we don't provide any argument, a prompt appears to choose which enums to annotate. Or, we can simply use the `--all` option to annotate all enums: 502 | 503 | ```bash 504 | php artisan enum:annotate --all 505 | 506 | php artisan enum:annotate -a 507 | ``` 508 | 509 | Alternatively, we can provide one or more enums directly to the `enum:annotate` command. Both slashes and quoted backslashes are acceptable for defining enum namespaces: 510 | 511 | ```bash 512 | php artisan enum:annotate App/Enums/Permissions "App\Enums\PayoutStatuses" 513 | ``` 514 | 515 | Lastly, if we wish to overwrite existing method annotations on enums, we can include the `--force` option: 516 | 517 | ```bash 518 | php artisan enum:annotate --force 519 | 520 | php artisan enum:annotate -f 521 | ``` 522 | 523 | #### 🏗️ enum:make 524 | 525 | The `enum:make` command allows us to create a new, automatically annotated enum with the cases we need: 526 | 527 | ```bash 528 | php artisan enum:make 529 | ``` 530 | 531 | If no arguments are given, prompts will guide us through defining the enum namespace, backing type and cases. Otherwise, all these details can be typed via command line: 532 | 533 | ```bash 534 | php artisan enum:make App/Enums/Enum CaseOne CaseTwo 535 | 536 | php artisan enum:make "App\Enums\Enum" CaseOne CaseTwo 537 | ``` 538 | 539 | For creating backed enums, we can manually set custom values for each case: 540 | 541 | ```bash 542 | php artisan enum:make App/Enums/Enum CaseOne=value1 CaseTwo=value2 543 | ``` 544 | 545 | Or, we can automatically assign values to cases by using the `--backed` option: 546 | 547 | ```bash 548 | php artisan enum:make App/Enums/Enum CaseOne CaseTwo --backed=int0 549 | ``` 550 | 551 | The `--backed` option accepts these values: 552 | 553 | - `int0`: assigns incremental integers starting from 0 (0, 1, 2...) 554 | - `int1`: assigns incremental integers starting from 1 (1, 2, 3...) 555 | - `bitwise`: assigns incremental bitwise values (1, 2, 4...) 556 | - `snake`: assigns the case name in snake case (case_one, case_two...) 557 | - `kebab`: assigns the case name in kebab case (case-one, case-two...) 558 | - `camel`: assigns the case name in camel case (caseOne, caseTwo...) 559 | - `lower`: assigns the case name in lower case (caseone, casetwo...) 560 | - `upper`: assigns the case name in upper case (CASEONE, CASETWO...) 561 | 562 | To overwrite an existing enum, we can include the `--force` option: 563 | 564 | ```bash 565 | php artisan enum:make App/Enums/Enum CaseOne CaseTwo --force 566 | 567 | php artisan enum:make App/Enums/Enum CaseOne CaseTwo -f 568 | ``` 569 | 570 | We can generate the TypeScript counterpart of the newly created enum by adding the `--typescript` option: 571 | 572 | ```bash 573 | php artisan enum:make App/Enums/Enum CaseOne CaseTwo --typescript 574 | 575 | php artisan enum:make App/Enums/Enum CaseOne CaseTwo -t 576 | ``` 577 | 578 | #### 💙 enum:ts 579 | 580 | The `ts` command converts enums to their TypeScript equivalents, ensuring backend and frontend synchronization: 581 | 582 | ```bash 583 | php artisan enum:ts 584 | ``` 585 | 586 | If we don't provide any argument, a prompt appears to choose which enums to synchronize. Or, we can simply use the `--all` option to synchronize all enums: 587 | 588 | ```bash 589 | php artisan enum:ts --all 590 | 591 | php artisan enum:ts -a 592 | ``` 593 | 594 | Alternatively, we can provide one or more enums directly to the `enum:ts` command. Both slashes and quoted backslashes are acceptable for defining enum namespaces: 595 | 596 | ```bash 597 | php artisan enum:ts App/Enums/Permissions "App\Enums\PayoutStatuses" 598 | ``` 599 | 600 | By default, enums are synchronized to `resources/js/enums/index.ts`, but this can be easily customized: 601 | 602 | ```php 603 | use Cerbero\LaravelEnum\Enums; 604 | 605 | class AppServiceProvider extends ServiceProvider 606 | { 607 | public function boot(): void 608 | { 609 | // custom static path 610 | Enums::setTypeScript('frontend/enums/index.ts'); 611 | 612 | // custom dynamic path 613 | Enums::setTypeScript(function (string $enum) { 614 | $domain = explode('\\', $enum)[1]; 615 | 616 | return "resources/js/modules/{$domain}/enums.ts"; 617 | }); 618 | } 619 | } 620 | ``` 621 | 622 | As shown above, we can either define a static path for TypeScript enums or dynamically set the TypeScript path for an enum based on its namespace. 623 | 624 | To update enums that have already been synchronized, we can use the `--force` option: 625 | 626 | ```bash 627 | php artisan enum:ts App/Enums/Enum --force 628 | 629 | php artisan enum:ts App/Enums/Enum -f 630 | ``` 631 | 632 | 633 | ## 📆 Change log 634 | 635 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 636 | 637 | ## 🧪 Testing 638 | 639 | ``` bash 640 | composer test 641 | ``` 642 | 643 | ## 💞 Contributing 644 | 645 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. 646 | 647 | ## 🧯 Security 648 | 649 | If you discover any security related issues, please email andrea.marco.sartori@gmail.com instead of using the issue tracker. 650 | 651 | ## 🏅 Credits 652 | 653 | - [Andrea Marco Sartori][link-author] 654 | - [All Contributors][link-contributors] 655 | 656 | ## ⚖️ License 657 | 658 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 659 | 660 | [ico-author]: https://img.shields.io/badge/author-cerbero90-blue?logo=x&style=flat-square&logoSize=auto 661 | [ico-php]: https://img.shields.io/packagist/php-v/cerbero/laravel-enum?color=%23767bb5&logo=php&style=flat-square&logoSize=auto 662 | [ico-laravel]: https://img.shields.io/static/v1?label=laravel&message=%E2%89%A59.0&color=ff2d20&logo=laravel&style=flat-square&logoSize=auto 663 | [ico-version]: https://img.shields.io/packagist/v/cerbero/laravel-enum.svg?label=version&style=flat-square 664 | [ico-actions]: https://img.shields.io/github/actions/workflow/status/cerbero90/laravel-enum/build.yml?branch=master&style=flat-square&logo=github&logoSize=auto 665 | [ico-license]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square 666 | [ico-per]: https://img.shields.io/static/v1?label=compliance&message=PER&color=blue&style=flat-square 667 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/cerbero90/laravel-enum.svg?style=flat-square&logo=scrutinizer&logoSize=auto 668 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/cerbero90/laravel-enum.svg?style=flat-square&logo=scrutinizer&logoSize=auto 669 | [ico-phpstan]: https://img.shields.io/badge/level-max-success?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGb0lEQVR42u1Xe1BUZRS/y4Kg8oiR3FCCBUySESZBRCiaBnmEsOzeSzsg+KxYYO9dEEftNRqZjx40FRZkTpqmOz5S2LsXlEZBciatkQnHDGYaGdFy1EpGMHl/p/PdFlt2rk5O+J9n5nA/vtf5ned3lnlISpRhafBlLRLHCtJGVrB/ZBDsaw2lUqzReGAC46DstTYfnSCGUjaaDvgxACo6j3vUenNdImeRXqdnWV5az5rrnzeZznj8J+E5Ftsclhf3s4J4CS/oRx5Bvon8ZU65FGYQxAwcf85a7CeRz+C41THejueydCZ7AAK34nwv3kHP/oUKdOL4K7258fF7Cud427O48RQeGkIGJ77N8fZqlrcfRP4d/x90WQfHXLeBt9dTrSlwl3V65ynWLM1SEA2qbNQckbe4Xmww10Hmy3shid0CMcmlEJtSDsl5VZBdfAgMvI3uuR+moJqN6LaxmpsOBeLCDmTifCB92RcQmbAUJvtqALc5sQr8p86gYBCcFdBq9wOin7NQax6ewlB6rqLZHf23FP10y3lj6uJtEBg2HxiVCtzd3SEwMBCio6Nh9uzZ4O/vLwOZ4OUNM2NyIGPFrvuzBG//lRPs+VQ2k1ki+ePkd84bskz7YFpYgizEz88P8vPzYffu3dDS0gJNTU1QXV0NqampRK1WIwgfiE4qhOyig0rC+pCvK8QUoML7uJVHA5kcQUp3DSpqWjc3d/Dy8oKioiLo6uqCoaEhuHb1KvT09AAhBFpbW4lOpyMyyIBQSCmoUQLQzgniNvz+obB2HS2RwBgE6dOxCyJogmNkP2u1Wrhw4QJ03+iGrR9XEd3CTNBn6eCbo40wPDwMdXV1BF1DVG5qiEtboxSUP6J71+D3NwUAhLOIRQzm7lnnhYUv7QFv/yDZ/Lm5ubK2DVI9iZ8bR8JDtEB57lNzENQN6OjoIGlpabIVZsYaMTO+hrikRRA1JxmSX9hE7/sJtVyF38tKsUCVZxBhz9jI3wGT/QJlADzPAyXrnj0kInzGHQCRMyOg/ed2uHjxIuE4TgYQHq2DLJqumashY+lnsMC4GVC5do6XVuK9l+4SkN8y+GfYeVJn2g++U7QygPT0dBgYGIDvT58mnF5PQcjC83PzSF9fH7S1tZGEhAQZQOT8JaA317oIkM6jS8uVLSDzOQqg23Uh+MlkOf00Gg0cP34c+vv74URzM9n41gby/rvvkc7OThlATU3NCGYJUXt4QaLuTYwBcTSOBmj1RD7D4Tsix4ByOjZRF/zgupDEbgZ3j4ly/qekpND0o5aQ44HS4OAgsVqtI1gTZO01IbG0aP1bknnxCDUvArHi+B0lJSlzglTFYO2udF3Ql9TCrHn5oEIreHp6QlRUFJSUlJCqqipSWVlJ8vLyCGYIFS7HS3zGa87mv4lcjLwLlStlLTKYYUUAlvrlDGcW45wKxXX6aqHZNutM+1oQBHFTewAKkoH4+vqCj48PYAGS5yb5amjNoO+CU2SL53NKpDD0vxHHmOJir7L5xUvZgm0us2R142ScOIyVqYvlpWU4XoHIP8DXL2b+wjdWeXh6U2FjmIIKmbWAYPFRMus62h/geIvjOQYlpuDysQrLL6Ger49HgW8jqvXUhI7UvDb9iaSTDqHtyItiF5Suw5ewF/Nd8VJ6zlhsn06bEhwX4NyfCvuGEeRpTmh4mkG68yDpyuzB9EUcjU5awbAgncPlAeSdAQER0zCndzqVbeXC4qDsMpvGEYBXRnsDx4N3Auf1FCTjTIaVtY/QTmd0I8bBVm1kejEubUfO01vqImn3c49X7qpeqI9inIgtbpxK3YrKfIJCt+OeV2nfUVFR4ca4EkVENyA7gkYcMfB1R5MMmxZ7ez/2KF5SSN1yV+158UPsJT0ZBcI2bRLtIXGoYu5FerOUiJe1OfsL3XEWH43l2KS+iJF9+S4FpcNgsc+j8cT8H4o1bfPg/qkLt50uJ1RzdMsGg0UqwfEN114Pwb1CtWTGg+Y9U5ClK9x7xUWI7BI5VQVp0AVcQ3bZkQhmnEgdHhKyNSZe16crtBIlc7sIb6cRLft2PCgoKGjijBDtjrAQ7a3EdMsxzIRflAFIhPb6mHYmYwX+WBlPQgskhgVryyJCQyNyBLsBQdQ6fgsQhyt6MSOOsWZ7gbH8wETmgRKAijatNL8Ngm0xx4tLcsps0Wzx4al0jXlI40B/A3pa144MDtSgAAAAAElFTkSuQmCC&logoSize=auto 670 | [ico-downloads]: https://img.shields.io/packagist/dt/cerbero/laravel-enum.svg?style=flat-square 671 | 672 | [link-author]: https://x.com/cerbero90 673 | [link-php]: https://www.php.net 674 | [link-laravel]: https://laravel.com 675 | [link-packagist]: https://packagist.org/packages/cerbero/laravel-enum 676 | [link-actions]: https://github.com/cerbero90/laravel-enum/actions?query=workflow%3Abuild 677 | [link-per]: https://www.php-fig.org/per/coding-style/ 678 | [link-scrutinizer]: https://scrutinizer-ci.com/g/cerbero90/laravel-enum/code-structure 679 | [link-code-quality]: https://scrutinizer-ci.com/g/cerbero90/laravel-enum 680 | [link-downloads]: https://packagist.org/packages/cerbero/laravel-enum 681 | [link-phpstan]: https://phpstan.org/ 682 | [link-contributors]: ../../contributors 683 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerbero/laravel-enum", 3 | "type": "library", 4 | "description": "Laravel package to supercharge enum functionalities.", 5 | "keywords": [ 6 | "laravel", 7 | "enum", 8 | "enumeration" 9 | ], 10 | "homepage": "https://github.com/cerbero90/laravel-enum", 11 | "license": "MIT", 12 | "authors": [{ 13 | "name": "Andrea Marco Sartori", 14 | "email": "andrea.marco.sartori@gmail.com", 15 | "homepage": "https://github.com/cerbero90", 16 | "role": "Developer" 17 | }], 18 | "require": { 19 | "php": "^8.1", 20 | "cerbero/enum": "^2.3.2", 21 | "illuminate/console": ">=9.0", 22 | "illuminate/contracts": ">=9.0", 23 | "illuminate/support": ">=9.0", 24 | "laravel/prompts": ">=0.1" 25 | }, 26 | "require-dev": { 27 | "orchestra/testbench": ">=7.0", 28 | "pestphp/pest": "^2.0", 29 | "phpstan/phpstan": "^2.0", 30 | "scrutinizer/ocular": "^1.8", 31 | "squizlabs/php_codesniffer": "^3.0", 32 | "tightenco/duster": "^2.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Cerbero\\LaravelEnum\\": "src" 37 | }, 38 | "files": [ 39 | "helpers/core.php", 40 | "helpers/cli.php" 41 | ] 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Cerbero\\LaravelEnum\\": "tests", 46 | "App\\": "tests/Skeleton/app", 47 | "Domain\\": "tests/Skeleton/domain", 48 | "Unloaded\\": "tests/Skeleton/unloaded" 49 | } 50 | }, 51 | "scripts": { 52 | "fix": "duster fix -u tlint,phpcodesniffer,pint", 53 | "lint": "duster lint -u tlint,phpcodesniffer,pint,phpstan", 54 | "test": "pest" 55 | }, 56 | "extra": { 57 | "branch-alias": { 58 | "dev-master": "1.0-dev" 59 | }, 60 | "laravel": { 61 | "providers": [ 62 | "Cerbero\\LaravelEnum\\Providers\\LaravelEnumServiceProvider" 63 | ] 64 | } 65 | }, 66 | "config": { 67 | "sort-packages": true, 68 | "allow-plugins": { 69 | "pestphp/pest-plugin": true 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /duster.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src" 4 | ], 5 | "exclude": [ 6 | "tests" 7 | ], 8 | "scripts": { 9 | "lint": { 10 | "phpstan": ["./vendor/bin/phpstan", "analyse", "--memory-limit=-1"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /helpers/cli.php: -------------------------------------------------------------------------------- 1 | $namespace 15 | * @param Closure(): bool $callback 16 | */ 17 | function output(OutputStyle $output, string $enum, Closure $callback): bool 18 | { 19 | $error = null; 20 | 21 | try { 22 | $succeeded = $callback(); 23 | } catch (Throwable $e) { 24 | $succeeded = false; 25 | $error = "\e[38;2;220;38;38m{$e?->getMessage()}\e[0m"; 26 | } 27 | 28 | if ($succeeded) { 29 | $output->writeln("\e[48;2;163;230;53m\e[38;2;63;98;18m\e[1m DONE \e[0m {$enum}" . PHP_EOL); 30 | } else { 31 | $output->writeln("\e[48;2;248;113;113m\e[38;2;153;27;27m\e[1m FAIL \e[0m {$enum} {$error}" . PHP_EOL); 32 | } 33 | 34 | return $succeeded; 35 | } 36 | 37 | /** 38 | * Annotate the given enum in a new process. 39 | * 40 | * @param class-string<\UnitEnum> $enum 41 | */ 42 | function runAnnotate(string $enum, bool $force = false): bool 43 | { 44 | return runEnum("annotate \"{$enum}\"" . ($force ? ' --force' : '')); 45 | } 46 | 47 | /** 48 | * Run the enum CLI in a new process. 49 | */ 50 | function runEnum(string $command): bool 51 | { 52 | // Once an enum is loaded, PHP accesses it from the memory and not from the disk. 53 | // Since our commands write on disk, the enum in memory might get out of sync. 54 | // To make sure that we are dealing with the current contents of such enum, 55 | // we spin a new process to load the latest state of the enum in memory. 56 | $cmd = vsprintf('"%s" "%s" %s 2>&1', [ 57 | PHP_BINARY, 58 | Enums::basePath('artisan'), 59 | "enum:{$command}", 60 | ]); 61 | 62 | ob_start(); 63 | 64 | $succeeded = passthru($cmd, $status) === null; 65 | 66 | ob_end_clean(); 67 | 68 | return $succeeded; 69 | } 70 | 71 | /** 72 | * Synchronize the given enum in TypeScript within a new process. 73 | * 74 | * @param class-string<\UnitEnum> $enum 75 | */ 76 | function runTs(string $enum, bool $force = false): bool 77 | { 78 | return runEnum("ts \"{$enum}\"" . ($force ? ' --force' : '')); 79 | } 80 | -------------------------------------------------------------------------------- /helpers/core.php: -------------------------------------------------------------------------------- 1 | $null . $common, 25 | count($types) == 1 => $null . reset($types), 26 | default => implode('|', $types) . ($null ? '|null' : ''), 27 | }; 28 | } 29 | 30 | /** 31 | * Retrieve the first interface or parent class in common with the given classes. 32 | * 33 | * @return ?class-string 34 | */ 35 | function commonInterfaceOrParent(string ...$classes): ?string 36 | { 37 | if ($classes !== array_filter($classes, namespaceExists(...))) { 38 | return null; 39 | } 40 | 41 | foreach (['class_implements', 'class_parents'] as $callback) { 42 | /** @var array $common */ 43 | if ($common = array_intersect(...array_map($callback, $classes))) { 44 | return reset($common); 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | 51 | /** 52 | * Determine whether the given namespace exists. 53 | */ 54 | function namespaceExists(string $namespace): bool 55 | { 56 | return class_exists($namespace) || interface_exists($namespace); 57 | } 58 | 59 | /** 60 | * Retrieve the return type of the given meta for the provided cases. 61 | * 62 | * @param list $cases 63 | */ 64 | function metaReturnType(string $meta, array $cases): string 65 | { 66 | $returnTypes = array_map(function (UnitEnum $case) use ($meta) { 67 | $value = $case->resolveMetaAttribute($meta); 68 | 69 | return is_string($value) && namespaceExists($value) ? $value : get_debug_type($value); 70 | }, $cases); 71 | 72 | return commonType(...$returnTypes); 73 | } 74 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | ignoreErrors: 6 | - '#Call to an undefined method UnitEnum::[a-zA-Z0-9\\_]+\(\)#' 7 | - identifier: trait.unused 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 | "phpdoc_summary": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Actions/OnCall.php: -------------------------------------------------------------------------------- 1 | $arguments 25 | * @throws InvalidArgumentException 26 | * @throws \ValueError 27 | */ 28 | public function __invoke(UnitEnum $case, string $name, array $arguments): mixed 29 | { 30 | try { 31 | $value = $case->resolveMetaAttribute($name); 32 | } catch (Throwable $e) { 33 | return $this->translate($case, $name, $arguments) ?? throw $e; 34 | } 35 | 36 | return match (true) { 37 | ! is_string($value) => $value, /** @phpstan-ignore-next-line argument.type */ 38 | method_exists($value, '__invoke') => call_user_func_array(App::make($value), $arguments), 39 | namespaceExists($value) => App::make($value, $arguments), 40 | default => $value, 41 | }; 42 | } 43 | 44 | /** 45 | * Retrieve the translation of the given key. 46 | * 47 | * @param array $arguments 48 | * @throws InvalidArgumentException 49 | */ 50 | protected function translate(UnitEnum $case, string $name, array $arguments): ?string 51 | { 52 | $key = Enums::resolveTranslationKey($case, $name); 53 | 54 | if (Lang::get($key) === $key) { 55 | return null; 56 | } 57 | 58 | if ($arguments && array_is_list($arguments)) { 59 | $method = sprintf('%s::%s->%s()', $case::class, $case->name, $name); 60 | 61 | throw new InvalidArgumentException("The method {$method} must be called with its named arguments"); 62 | } 63 | 64 | /** @var string */ 65 | return Lang::get($key, $arguments); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Capsules/CacheKey.php: -------------------------------------------------------------------------------- 1 | key); 29 | } 30 | 31 | /** 32 | * Determine whether the key doesn't exist in the cache. 33 | */ 34 | public function missing(): bool 35 | { 36 | return Cache::missing($this->key); 37 | } 38 | 39 | /** 40 | * Determine whether the key is present and not null. 41 | */ 42 | public function hasValue(): bool 43 | { 44 | return Cache::has($this->key); 45 | } 46 | 47 | /** 48 | * Retrieve the value of the key from the cache. 49 | */ 50 | public function get(mixed $default = null): mixed 51 | { 52 | return Cache::get($this->key, $default); 53 | } 54 | 55 | /** 56 | * Retrieve the value of the key from the cache and delete it. 57 | */ 58 | public function pull(mixed $default = null): mixed 59 | { 60 | return Cache::pull($this->key, $default); 61 | } 62 | 63 | /** 64 | * Store the value of the key in the cache. 65 | */ 66 | public function put(mixed $value, DateTimeInterface|DateInterval|int|null $ttl = null): bool 67 | { 68 | return Cache::put($this->key, $value, $ttl); 69 | } 70 | 71 | /** 72 | * Store the value of the key in the cache. 73 | */ 74 | public function set(mixed $value, DateTimeInterface|DateInterval|int|null $ttl = null): bool 75 | { 76 | /** @phpstan-ignore argument.type */ 77 | return Cache::set($this->key, $value, $ttl); 78 | } 79 | 80 | /** 81 | * Store the value of the key in the cache if the key does not exist. 82 | */ 83 | public function add(mixed $value, DateTimeInterface|DateInterval|int|null $ttl = null): bool 84 | { 85 | return Cache::add($this->key, $value, $ttl); 86 | } 87 | 88 | /** 89 | * Increment the value of the key in the cache. 90 | */ 91 | public function increment(float|int $value = 1): float|int|bool 92 | { 93 | /** @var float|int|bool */ 94 | return Cache::increment($this->key, $value); 95 | } 96 | 97 | /** 98 | * Decrement the value of the key in the cache. 99 | */ 100 | public function decrement(float|int $value = 1): float|int|bool 101 | { 102 | /** @var float|int|bool */ 103 | return Cache::decrement($this->key, $value); 104 | } 105 | 106 | /** 107 | * Store the value of the key in the cache indefinitely. 108 | */ 109 | public function forever(mixed $value): bool 110 | { 111 | return Cache::forever($this->key, $value); 112 | } 113 | 114 | /** 115 | * Retrieve or store the value of the key. 116 | */ 117 | public function remember(Closure|DateTimeInterface|DateInterval|int|null $ttl, Closure $callback): mixed 118 | { 119 | return Cache::remember($this->key, $ttl, $callback); 120 | } 121 | 122 | /** 123 | * Retrieve or store indefinitely the value of the key. 124 | */ 125 | public function rememberForever(Closure $callback): mixed 126 | { 127 | return Cache::rememberForever($this->key, $callback); 128 | } 129 | 130 | /** 131 | * Retrieve or store indefinitely the value of the key. 132 | */ 133 | public function sear(Closure $callback): mixed 134 | { 135 | return Cache::sear($this->key, $callback); 136 | } 137 | 138 | /** 139 | * Remove the key from the cache. 140 | */ 141 | public function forget(): bool 142 | { 143 | return Cache::forget($this->key); 144 | } 145 | 146 | /** 147 | * Delete the key from the cache. 148 | */ 149 | public function delete(): bool 150 | { 151 | return Cache::delete($this->key); 152 | } 153 | 154 | /** 155 | * Retrieve the lock instance. 156 | */ 157 | public function lock(int $seconds = 0, ?string $owner = null): Lock 158 | { 159 | return Cache::lock($this->key, $seconds, $owner); 160 | } 161 | 162 | /** 163 | * Restore the lock instance using the owner identifier. 164 | */ 165 | public function restoreLock(string $owner): Lock 166 | { 167 | return Cache::restoreLock($this->key, $owner); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Capsules/SessionKey.php: -------------------------------------------------------------------------------- 1 | key); 26 | } 27 | 28 | /** 29 | * Determine whether the key is missing from the session. 30 | */ 31 | public function missing(): bool 32 | { 33 | return Session::missing($this->key); 34 | } 35 | 36 | /** 37 | * Determine whether the key is present and not null. 38 | */ 39 | public function hasValue(): bool 40 | { 41 | return Session::has($this->key); 42 | } 43 | 44 | /** 45 | * Retrieve the value of the key from the session. 46 | */ 47 | public function get(mixed $default = null): mixed 48 | { 49 | return Session::get($this->key, $default); 50 | } 51 | 52 | /** 53 | * Retrieve the value of the key and then forget it. 54 | */ 55 | public function pull(mixed $default = null): mixed 56 | { 57 | return Session::pull($this->key, $default); 58 | } 59 | 60 | /** 61 | * Determine whether the session contains old input for the key. 62 | */ 63 | public function hasOldInput(): bool 64 | { 65 | return Session::hasOldInput($this->key); 66 | } 67 | 68 | /** 69 | * Retrieve the value from the flashed input. 70 | */ 71 | public function getOldInput(mixed $default = null): mixed 72 | { 73 | return Session::getOldInput($this->key, $default); 74 | } 75 | 76 | /** 77 | * Put the value of the key in the session. 78 | */ 79 | public function put(mixed $value = null): self 80 | { 81 | Session::put($this->key, $value); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Retrieve or store the value of the key. 88 | */ 89 | public function remember(Closure $callback): mixed 90 | { 91 | return Session::remember($this->key, $callback); 92 | } 93 | 94 | /** 95 | * Push a value onto the array of the key. 96 | */ 97 | public function push(mixed $value): self 98 | { 99 | Session::push($this->key, $value); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Increment the value of the key. 106 | */ 107 | public function increment(float|int $amount = 1): float|int 108 | { 109 | /** @var float|int @phpstan-ignore argument.type */ 110 | return Session::increment($this->key, $amount); 111 | } 112 | 113 | /** 114 | * Decrement the value of the key. 115 | */ 116 | public function decrement(float|int $amount = 1): float|int 117 | { 118 | /** @var float|int @phpstan-ignore argument.type */ 119 | return Session::decrement($this->key, $amount); 120 | } 121 | 122 | /** 123 | * Flash the value of the key to the session. 124 | */ 125 | public function flash(mixed $value = true): self 126 | { 127 | Session::flash($this->key, $value); 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Flash the value of the key to the session for immediate use. 134 | */ 135 | public function now(mixed $value): self 136 | { 137 | Session::now($this->key, $value); 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Remove the key from the session and retrieve its value. 144 | */ 145 | public function remove(): mixed 146 | { 147 | return Session::remove($this->key); 148 | } 149 | 150 | /** 151 | * Remove the key from the session. 152 | */ 153 | public function forget(): self 154 | { 155 | Session::forget($this->key); 156 | 157 | return $this; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/CasesCollection.php: -------------------------------------------------------------------------------- 1 | 22 | * @implements Arrayable 23 | */ 24 | class CasesCollection extends BaseCasesCollection implements Arrayable, Castable, Jsonable 25 | { 26 | use Conditionable; 27 | use Macroable; 28 | use Tappable; 29 | 30 | /** 31 | * Retrieve the caster to cast the collection. 32 | * 33 | * @param list $arguments 34 | * @return CasesCollectionCast 35 | */ 36 | public static function castUsing(array $arguments): CastsAttributes 37 | { 38 | /** @var CasesCollectionCast */ 39 | return new CasesCollectionCast($arguments[0] ?? ''); 40 | } 41 | 42 | /** 43 | * Retrieve the cast for the given enum. 44 | * 45 | * @param class-string $enum 46 | */ 47 | public static function of(string $enum): string 48 | { 49 | return static::class . ':' . $enum; 50 | } 51 | 52 | /** 53 | * Turn the collection into a JSON. 54 | * 55 | * @param int $options 56 | */ 57 | public function toJson($options = 0): string|false 58 | { 59 | return json_encode($this->jsonSerialize(), $options); 60 | } 61 | 62 | /** 63 | * Dump the cases and end the script. 64 | * 65 | * @codeCoverageIgnore 66 | */ 67 | public function dd(): never 68 | { 69 | $this->dump(); 70 | 71 | exit(1); 72 | } 73 | 74 | /** 75 | * Dump the cases. 76 | */ 77 | public function dump(): static 78 | { 79 | dump($this->cases); 80 | 81 | return $this; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/CasesCollectionCast.php: -------------------------------------------------------------------------------- 1 | , mixed> 20 | */ 21 | class CasesCollectionCast implements CastsAttributes 22 | { 23 | /** 24 | * Disable object caching. 25 | */ 26 | public bool $withoutObjectCaching = true; 27 | 28 | /** 29 | * Instantiate the class. 30 | */ 31 | public function __construct(private readonly string $enum) 32 | { 33 | if (! is_subclass_of($enum, UnitEnum::class)) { 34 | throw new InvalidArgumentException('The cast argument must be a valid enum'); 35 | } 36 | } 37 | 38 | /** 39 | * Transform the attribute from the underlying model values. 40 | * 41 | * @param string|int|null $value 42 | * @param array $attributes 43 | * @return ?CasesCollection 44 | * @throws \ValueError 45 | */ 46 | public function get(Model $model, string $key, mixed $value, array $attributes): ?CasesCollection 47 | { 48 | /** @var ?CasesCollection */ 49 | return match (true) { 50 | is_string($value) => $this->getByJson($value), /** @phpstan-ignore-next-line binaryOp.invalid */ 51 | is_int($value) => $this->enum::filter(fn(BackedEnum $case) => ($value & $case->value) == $case->value), 52 | default => null, 53 | }; 54 | } 55 | 56 | /** 57 | * Transform the given JSON into a cases collection. 58 | * 59 | * @return CasesCollection 60 | * @throws \ValueError 61 | */ 62 | protected function getByJson(string $json): CasesCollection 63 | { 64 | /** @var list $rawCases */ 65 | $rawCases = array_unique((array) json_decode($json, true)); 66 | /** @var TEnum[] $cases */ 67 | $cases = array_map(fn(string|int $value) => $this->enum::from($value), $rawCases); 68 | 69 | /** @var CasesCollection */ 70 | return new CasesCollection($cases); 71 | } 72 | 73 | /** 74 | * Transform the attribute to its underlying model values. 75 | * 76 | * @param array $attributes 77 | */ 78 | public function set(Model $model, string $key, mixed $value, array $attributes): string|int|null 79 | { 80 | $this->withoutObjectCaching = ! $value instanceof CasesCollection; 81 | 82 | return match (true) { 83 | $value instanceof CasesCollection => $value->toJson() ?: null, 84 | is_array($value) => $this->setByArray($value), 85 | is_int($value) => $value, 86 | default => null, 87 | }; 88 | } 89 | 90 | /** 91 | * Transform the given array into a serializable string. 92 | * 93 | * @param array $array 94 | */ 95 | protected function setByArray(array $array): string|int|null 96 | { 97 | if (is_subclass_of($this->enum, Bitwise::class)) { 98 | return array_reduce($array, function (?int $carry, mixed $item): int { 99 | /** @phpstan-ignore-next-line binaryOp.invalid */ 100 | return $carry |= $item instanceof BackedEnum ? $item->value : $item; 101 | }); 102 | } 103 | 104 | $values = reset($array) instanceof UnitEnum 105 | ? $this->enum::only(...array_column($array, 'name')) 106 | : array_values(array_unique($array)); 107 | 108 | return json_encode($values) ?: null; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Commands/EnumAnnotateCommand.php: -------------------------------------------------------------------------------- 1 | enums()) { 43 | $this->info('No enums to annotate.'); 44 | 45 | return self::SUCCESS; 46 | } 47 | 48 | $succeeded = true; 49 | $force = !! $this->option('force'); 50 | 51 | foreach($enums as $enum) { 52 | $succeeded = output($this->output, $enum, fn() => (new Annotator($enum))->annotate($force)) && $succeeded; 53 | } 54 | 55 | return $succeeded ? self::SUCCESS : self::FAILURE; 56 | } 57 | 58 | /** 59 | * Retrieve the enums to annotate. 60 | * 61 | * @return list> 62 | */ 63 | private function enums(): array 64 | { 65 | /** @var list> */ 66 | return match (true) { 67 | ! empty($enums = (array) $this->argument('enums')) => normalizeEnums($enums), 68 | empty($enums = [...Enums::namespaces()]) => [], 69 | $this->option('all') => $enums, 70 | /** @phpstan-ignore argument.type */ 71 | default => multiselect('Enums to annotate:', $enums, required: true, hint: 'Press space to select.'), 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Commands/EnumMakeCommand.php: -------------------------------------------------------------------------------- 1 | enum(); 49 | $force = !! $this->option('force'); 50 | 51 | if (enum_exists($enum) && ! $force) { 52 | $this->info("The enum {$enum} already exists."); 53 | 54 | return self::SUCCESS; 55 | } 56 | 57 | if (! $backed = $this->backed()) { 58 | $this->error('The option --backed supports only ' . implode(', ', Backed::names())); 59 | 60 | return self::FAILURE; 61 | } 62 | 63 | return $this->generate($enum, $backed); 64 | } 65 | 66 | /** 67 | * Retrieve the enum namespace. 68 | * 69 | * @return class-string<\UnitEnum> 70 | */ 71 | private function enum(): string 72 | { 73 | /** @var string $raw */ 74 | $raw = $this->argument('enum') ?: text('The namespace of the enum', 'App\Enums\Permissions', required: true); 75 | 76 | /** @var class-string<\UnitEnum> */ 77 | return strtr($raw, '/', '\\'); 78 | } 79 | 80 | /** 81 | * Retrieve the selected backed case, if any. 82 | * 83 | * @throws InvalidArgumentException 84 | */ 85 | private function backed(): ?Backed 86 | { 87 | if ($this->argument('enum') === null) { 88 | /** @phpstan-ignore argument.templateType */ 89 | $options = Backed::pluck('label', 'name'); 90 | /** @var array $options */ 91 | $name = select('How cases should be backed', $options); 92 | 93 | /** @var string $name */ 94 | return Backed::from($name); 95 | } 96 | 97 | if (is_null($name = $this->option('backed'))) { 98 | return Backed::pure; 99 | } 100 | 101 | /** @var string $name */ 102 | return Backed::tryFrom($name); 103 | } 104 | 105 | /** 106 | * Generate the enum. 107 | * 108 | * @param class-string<\UnitEnum> $enum 109 | */ 110 | private function generate(string $enum, Backed $backed): int 111 | { 112 | $generator = new Generator($enum, $this->cases($backed), $backed); 113 | $force = !! $this->option('force'); 114 | $typeScript = !! $this->option('typescript'); 115 | 116 | $succeeded = output($this->output, $enum, function () use ($generator, $enum, $force, $typeScript) { 117 | return $generator->generate($force) 118 | && runAnnotate($enum, $force) 119 | && ($typeScript ? runTs($enum, $force) : true); 120 | }); 121 | 122 | return $succeeded ? self::SUCCESS : self::FAILURE; 123 | } 124 | 125 | /** 126 | * Retrieve the cases, optionally backed. 127 | * 128 | * @return string[] 129 | */ 130 | private function cases(Backed $backed): array 131 | { 132 | $placeholder = $backed->is(Backed::custom) ? "Case1=value1\nCase2=value2" : "Case1\nCase2"; 133 | 134 | /** @var list */ 135 | return $this->argument('cases') 136 | ?: explode(PHP_EOL, trim(textarea('The cases (one per line)', $placeholder, required: true))); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Commands/EnumTsCommand.php: -------------------------------------------------------------------------------- 1 | enums()) { 43 | $this->info('No enums to annotate.'); 44 | 45 | return self::SUCCESS; 46 | } 47 | 48 | $succeeded = true; 49 | $force = !! $this->option('force'); 50 | 51 | foreach($enums as $enum) { 52 | $succeeded = output($this->output, $enum, fn() => (new TypeScript($enum))->sync($force)) && $succeeded; 53 | } 54 | 55 | return $succeeded ? self::SUCCESS : self::FAILURE; 56 | } 57 | 58 | /** 59 | * Retrieve the enums to annotate. 60 | * 61 | * @return list> 62 | */ 63 | private function enums(): array 64 | { 65 | /** @var list> */ 66 | return match (true) { 67 | ! empty($enums = (array) $this->argument('enums')) => normalizeEnums($enums), 68 | empty($enums = [...Enums::namespaces()]) => [], 69 | $this->option('all') => $enums, 70 | /** @phpstan-ignore argument.type */ 71 | default => multiselect('Enums to synchronize:', $enums, required: true, hint: 'Press space to select.'), 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Concerns/CollectsCases.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public static function collect(): CasesCollection 21 | { 22 | return new CasesCollection(self::cases()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Concerns/Enumerates.php: -------------------------------------------------------------------------------- 1 | value); 22 | 23 | return new CacheKey($key); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Concerns/EnumeratesSessionKeys.php: -------------------------------------------------------------------------------- 1 | value); 22 | 23 | return new SessionKey($key); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Concerns/IsMagic.php: -------------------------------------------------------------------------------- 1 | baseResolveItem($item); 32 | } catch (ValueError $e) { 33 | try { 34 | /** @var string $item */ 35 | return $this->resolveTranslation($item); 36 | } catch (ValueError) { 37 | throw $e; 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Retrieve the translation of the given key for this case. 44 | * 45 | * @throws ValueError 46 | */ 47 | public function resolveTranslation(string $key): string 48 | { 49 | $translationKey = Enums::resolveTranslationKey($this, $key); 50 | /** @var string */ 51 | $translation = Lang::get($translationKey); 52 | 53 | if ($translation !== $translationKey) { 54 | return $translation; 55 | } 56 | 57 | throw new ValueError(sprintf('The case %s::%s has no "%s" translation set', self::class, $this->name, $key)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Contracts/Bitwise.php: -------------------------------------------------------------------------------- 1 | value, $matches); 36 | 37 | $capsuleName = class_basename($capsule); 38 | $arguments = implode(', ', array_map('trim', $matches[1])); 39 | 40 | return new self($case->name, "static {$capsuleName} {$case->name}({$arguments})", [$capsule]); 41 | } 42 | 43 | /** 44 | * Retrieve the method annotation for the given invokable class. 45 | * 46 | * @param class-string $class 47 | */ 48 | public static function forInvokable(string $name, string $class): static 49 | { 50 | $parameters = $namespaces = []; 51 | $reflection = new ReflectionMethod($class, '__invoke'); 52 | $returnType = (string) $reflection->getReturnType() ?: 'mixed'; 53 | 54 | if (namespaceExists($returnType)) { 55 | /** @var class-string $returnType */ 56 | $namespaces[] = $returnType; 57 | 58 | $returnType = class_basename($returnType); 59 | } 60 | 61 | foreach ($reflection->getParameters() as $parameter) { 62 | $type = (string) $parameter->getType() ?: 'mixed'; 63 | 64 | if (namespaceExists($type)) { 65 | /** @var class-string $type */ 66 | $namespaces[] = $type; 67 | 68 | $type = class_basename($type); 69 | } 70 | 71 | $parameters[] = "{$type} \${$parameter->getName()}"; 72 | } 73 | 74 | return new self($name, sprintf('%s %s(%s)', $returnType, $name, implode(', ', $parameters)), $namespaces); 75 | } 76 | 77 | /** 78 | * Retrieve the method annotation for an instance method. 79 | */ 80 | public static function instance(string $name, string $returnType): static 81 | { 82 | $namespaces = []; 83 | $null = str_starts_with($returnType, '?') ? '?' : ''; 84 | $returnType = ltrim($returnType, '?'); 85 | 86 | if (namespaceExists($returnType)) { 87 | /** @var class-string $returnType */ 88 | $namespaces[] = $returnType; 89 | 90 | $returnType = class_basename($returnType); 91 | } 92 | 93 | return new self($name, "{$null}{$returnType} {$name}()", $namespaces); 94 | } 95 | 96 | /** 97 | * Retrieve the method annotation for the given translation. 98 | */ 99 | public static function forTranslation(string $name, string $translation): static 100 | { 101 | preg_match_all(self::RE_PLACEHOLDER, $translation, $matches); 102 | 103 | $parameters = array_map(fn(string $name) => "mixed \${$name}", $matches[1]); 104 | 105 | return new self($name, sprintf('string %s(%s)', $name, implode(', ', $parameters))); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Enums.php: -------------------------------------------------------------------------------- 1 | $arguments 35 | */ 36 | public static function handleCall(UnitEnum $case, string $name, array $arguments): mixed 37 | { 38 | return (static::$onCall ?: new OnCall())($case, $name, $arguments); 39 | } 40 | 41 | /** 42 | * Set the logic to resolve the translation key. 43 | * 44 | * @param callable(UnitEnum $case, string $method): string $callback 45 | */ 46 | public static function translateFrom(callable $callback): void 47 | { 48 | static::$translateFrom = $callback(...); 49 | } 50 | 51 | /** 52 | * Retrieve the translation key for the given case. 53 | */ 54 | public static function resolveTranslationKey(UnitEnum $case, ?string $method = null): string 55 | { 56 | return static::$translateFrom 57 | ? (static::$translateFrom)($case, (string) $method) 58 | : sprintf('enums.%s.%s%s', $case::class, $case->name, $method ? ".{$method}" : ''); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Providers/LaravelEnumServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 21 | */ 22 | private array $commands = [ 23 | Commands\EnumAnnotateCommand::class, 24 | Commands\EnumMakeCommand::class, 25 | Commands\EnumTsCommand::class, 26 | ]; 27 | 28 | /** 29 | * Bootstrap the package services. 30 | */ 31 | public function boot(): void 32 | { 33 | Enums::setBasePath($this->app->basePath()); 34 | 35 | if ($this->app->runningInConsole()) { 36 | $this->commands($this->commands); 37 | } 38 | 39 | $this->publishes([ 40 | __DIR__ . '/../../stubs' => $this->app->basePath('stubs/laravel-enum'), 41 | ], 'laravel-enum-stubs'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Services/Annotator.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class Annotator extends BaseAnnotator 17 | { 18 | /** 19 | * Instantiate the class. 20 | * 21 | * @param class-string $enum 22 | * @throws \InvalidArgumentException 23 | * @phpstan-ignore property.phpDocType 24 | */ 25 | public function __construct(protected string $enum) 26 | { 27 | $this->inspector = new Inspector($enum); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Services/Generator.php: -------------------------------------------------------------------------------- 1 | $namespace 20 | * @param string[] $cases 21 | */ 22 | public function __construct(string $namespace, array $cases, private readonly Backed $backed) 23 | { 24 | $this->enum = new GeneratingEnum($namespace, $backed->back($cases)); 25 | } 26 | 27 | /** 28 | * Retrieve the path of the stub. 29 | */ 30 | protected function stub(): string 31 | { 32 | return $this->backed->is(Backed::bitwise) 33 | ? __DIR__ . '/../../stubs/bitwise.stub' 34 | : __DIR__ . '/../../stubs/enum.stub'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Inspector.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class Inspector extends BaseInspector 21 | { 22 | /** 23 | * The main trait to supercharge enums. 24 | * 25 | * @var class-string 26 | */ 27 | protected string $mainTrait = Concerns\Enumerates::class; 28 | 29 | /** 30 | * The capsules keyed by the related trait. 31 | * 32 | * @var array 33 | */ 34 | private array $capsules = [ 35 | Concerns\EnumeratesCacheKeys::class => Capsules\CacheKey::class, 36 | Concerns\EnumeratesSessionKeys::class => Capsules\SessionKey::class, 37 | ]; 38 | 39 | /** 40 | * Determine whether the given namespace matches the enum namespace. 41 | */ 42 | public function hasSameNamespace(string $namespace): bool 43 | { 44 | return $this->reflection->getNamespaceName() . '\\' . class_basename($namespace) == $namespace; 45 | } 46 | 47 | /** 48 | * Retrieve the method annotation for the given case. 49 | */ 50 | public function caseAnnotation(UnitEnum $case): MethodAnnotation 51 | { 52 | foreach ($this->capsules as $trait => $capsule) { 53 | if ($this->uses($trait)) { 54 | /** @var \BackedEnum $case */ 55 | return MethodAnnotation::forCapsule($case, $capsule); 56 | } 57 | } 58 | 59 | return MethodAnnotation::forCase($case); 60 | } 61 | 62 | /** 63 | * Retrieve the use statements. 64 | * 65 | * @return array 66 | */ 67 | public function useStatements(bool $includeExisting = true): array 68 | { 69 | return $this->useStatements ??= [...new UseStatements($this, $includeExisting)]; 70 | } 71 | 72 | /** 73 | * Retrieve the method annotations. 74 | * 75 | * @return array 76 | */ 77 | public function methodAnnotations(bool $includeExisting = true): array 78 | { 79 | /** @var array */ 80 | return $this->methodAnnotations ??= [...new MethodAnnotations($this, $includeExisting)]; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Services/MethodAnnotations.php: -------------------------------------------------------------------------------- 1 | $inspector 18 | */ 19 | final class MethodAnnotations extends BaseMethodAnnotations 20 | { 21 | /** 22 | * Retrieve all the method annotations. 23 | * 24 | * @return array 25 | */ 26 | public function all(): array 27 | { 28 | /** @var array */ 29 | return [ 30 | ...$this->forCaseNames(), 31 | ...$this->forMetaAttributes(), 32 | ...$this->forTranslations(), 33 | ...$this->includeExisting ? $this->existing() : [], 34 | ]; 35 | } 36 | 37 | /** 38 | * Retrieve the method annotations for the case names. 39 | * 40 | * @return array 41 | */ 42 | public function forCaseNames(): array 43 | { 44 | $annotations = []; 45 | 46 | foreach ($this->inspector->cases() as $case) { 47 | $annotations[$case->name] = $this->inspector->caseAnnotation($case); 48 | } 49 | 50 | return $annotations; 51 | } 52 | 53 | /** 54 | * Retrieve the method annotations for the meta attributes. 55 | * 56 | * @return array 57 | */ 58 | public function forMetaAttributes(): array 59 | { 60 | $annotations = []; 61 | $cases = $this->inspector->cases(); 62 | 63 | foreach ($this->inspector->metaAttributeNames() as $meta) { 64 | $returnType = metaReturnType($meta, $cases); 65 | 66 | /** @var class-string $class */ 67 | $annotations[$meta] = method_exists($class = ltrim($returnType, '?'), '__invoke') 68 | ? MethodAnnotation::forInvokable($meta, $class) 69 | : MethodAnnotation::instance($meta, $returnType); 70 | } 71 | 72 | return $annotations; 73 | } 74 | 75 | /** 76 | * Retrieve the method annotations for the translations. 77 | * 78 | * @return array 79 | */ 80 | public function forTranslations(): array 81 | { 82 | $annotations = []; 83 | 84 | foreach ($this->inspector->cases() as $case) { 85 | $key = Enums::resolveTranslationKey($case); 86 | 87 | if ($key === $translations = Lang::get($key)) { 88 | continue; 89 | } 90 | 91 | /** @var array $translations */ 92 | foreach ($translations as $name => $translation) { 93 | $annotations[$name] ??= MethodAnnotation::forTranslation($name, $translation); 94 | } 95 | } 96 | 97 | return $annotations; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Services/TypeScript.php: -------------------------------------------------------------------------------- 1 | $inspector 13 | */ 14 | final class UseStatements extends BaseUseStatements 15 | { 16 | /** 17 | * Retrieve all the use statements. 18 | * 19 | * @return array 20 | */ 21 | public function all(): array 22 | { 23 | return [ 24 | ...$this->fromMethodAnnotations(), 25 | ...$this->existing(), 26 | ]; 27 | } 28 | 29 | /** 30 | * Retrieve the use statements from the method annotations. 31 | * 32 | * @return array 33 | */ 34 | public function fromMethodAnnotations(): array 35 | { 36 | $useStatements = []; 37 | 38 | foreach ($this->inspector->methodAnnotations($this->includeExisting) as $annotation) { 39 | foreach ($annotation->namespaces as $namespace) { 40 | if (! $this->inspector->hasSameNamespace($namespace)) { 41 | $useStatements[class_basename($namespace)] = $namespace; 42 | } 43 | } 44 | } 45 | 46 | return $useStatements; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /stubs/bitwise.stub: -------------------------------------------------------------------------------- 1 |