├── phpstan-baseline.neon ├── stubs ├── typescript.stub └── enum.stub ├── duster.json ├── phpstan.neon ├── src ├── Concerns │ ├── Enumerates.php │ ├── IsMagic.php │ ├── Compares.php │ ├── Hydrates.php │ ├── SelfAware.php │ └── CollectsCases.php ├── Data │ ├── GeneratingEnum.php │ └── MethodAnnotation.php ├── Attributes │ └── Meta.php ├── Services │ ├── UseStatements.php │ ├── Generator.php │ ├── MethodAnnotations.php │ ├── TypeScript.php │ ├── Inspector.php │ └── Annotator.php ├── Enums │ └── Backed.php ├── Enums.php └── CasesCollection.php ├── pint.json ├── cli ├── ts.php ├── annotate.php ├── make.php └── help ├── bin └── enum ├── LICENSE.md ├── CONTRIBUTING.md ├── composer.json ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── helpers ├── cli.php └── core.php └── README.md /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /stubs/typescript.stub: -------------------------------------------------------------------------------- 1 | export enum {{ name }} { 2 | {{ cases }} 3 | } 4 | -------------------------------------------------------------------------------- /stubs/enum.stub: -------------------------------------------------------------------------------- 1 | (new TypeScript($enum))->sync($force)) && $succeeded; 23 | } 24 | 25 | return $succeeded; 26 | -------------------------------------------------------------------------------- /cli/annotate.php: -------------------------------------------------------------------------------- 1 | (new Annotator($enum))->annotate($force)) && $succeeded; 23 | } 24 | 25 | return $succeeded; 26 | -------------------------------------------------------------------------------- /bin/enum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | getMessage()); 22 | } 23 | 24 | exit($outcome ? 0 : 1); 25 | } 26 | 27 | require path(__DIR__ . '/../cli/help'); 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cli/make.php: -------------------------------------------------------------------------------- 1 | generate($force) 39 | && runAnnotate($enum, $force) 40 | && ($typeScript ? runTs($enum, $force) : true); 41 | }); 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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|^3.0|^4.0", 22 | "phpstan/phpstan": "^2.0", 23 | "squizlabs/php_codesniffer": "^3.0", 24 | "tightenco/duster": "^2.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Cerbero\\Enum\\": "src" 29 | }, 30 | "files": [ 31 | "helpers/core.php", 32 | "helpers/cli.php" 33 | ] 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Cerbero\\Enum\\": "tests", 38 | "App\\": "tests/Skeleton/app", 39 | "Domain\\": "tests/Skeleton/domain" 40 | } 41 | }, 42 | "bin": ["bin/enum"], 43 | "scripts": { 44 | "fix": "duster fix -u tlint,phpcodesniffer,pint", 45 | "lint": "duster lint -u tlint,phpcodesniffer,pint,phpstan", 46 | "test": "pest" 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-master": "1.0-dev" 51 | } 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "allow-plugins": { 56 | "pestphp/pest-plugin": true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Concerns/Hydrates.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(int|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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.5 - 2025-11-25 30 | 31 | ### Fixed 32 | - Build pipeline 33 | 34 | 35 | ## 2.3.4 - 2025-11-25 36 | 37 | ### Added 38 | - Support for PHP 8.5 39 | 40 | 41 | ## 2.3.3 - 2025-06-25 42 | 43 | ### Fixed 44 | - Type-hinting to avoid IDEs confusion with backed enums native methods 45 | 46 | 47 | ## 2.3.2 - 2025-01-30 48 | 49 | ### Fixed 50 | - Give priority to properties when resolving a case item 51 | 52 | 53 | ## 2.3.1 - 2025-01-15 54 | 55 | ### Changed 56 | - Removed unneeded readonly modifiers 57 | 58 | 59 | ## 2.3.0 - 2025-01-12 60 | 61 | ### Added 62 | - Enums discoverability 63 | - Console command to create an enum 64 | - Console command to annotate enums 65 | - Console command to turn PHP enums into TypeScript enums 66 | 67 | ### Changed 68 | - Improved static analysis 69 | - CasesCollection::groupBy() does not wrap the result into a collection 70 | 71 | 72 | ## 2.2.1 - 2024-11-22 73 | 74 | ### Added 75 | - Full support for PHP 8.4 76 | 77 | ### Changed 78 | - Improved error message for invalid meta 79 | 80 | 81 | ## 2.2.0 - 2024-11-19 82 | 83 | ### Added 84 | - Method `SelfAware::metaAttributeNames()` to list the names of all meta attributes 85 | 86 | ### Changed 87 | - Upgraded PHPStan to v2 88 | 89 | 90 | ## 2.1.0 - 2024-10-30 91 | 92 | ### Added 93 | - Method has() to the cases collection 94 | - JsonSerializable and Stringable interfaces to the cases collection 95 | - Methods isBackedByInteger() and isBackedByString() to the SelfAware trait 96 | 97 | ### Changed 98 | - Allow any callable when setting the logic for magic methods 99 | - Allow meta inheritance when getting meta names 100 | - Improve generics in cases collection 101 | - Simplify logic by negating methods in the Compares trait 102 | 103 | ### Deprecated 104 | - Nothing 105 | 106 | ### Fixed 107 | - Nothing 108 | 109 | ### Removed 110 | - Nothing 111 | 112 | ### Security 113 | - Nothing 114 | 115 | 116 | ## 2.0.0 - 2024-10-05 117 | 118 | ### Added 119 | - Custom and default implementation of magic methods 120 | - The `Meta` attribute and related methods 121 | - Method `value()` to get the value of a backed case or the name of a pure case 122 | - Methods `toArray()`, `map()` to the `CasesCollection` 123 | - Generics in docblocks 124 | - Static analysis 125 | 126 | ### Changed 127 | - Renamed keys to meta 128 | - `CasesCollection` methods return an instance of the collection whenever possible 129 | - `CasesCollection::groupBy()` groups into instances of the collection 130 | - Filtering methods keep the collection keys 131 | - Renamed methods `CollectsCases::casesBy*()` to `CollectsCases::keyBy*()` 132 | - Renamed `cases()` to `all()` in `CasesCollection` 133 | - Renamed `get()` to `resolveMeta()` in `SelfAware` 134 | - When hydrating from meta, the value is no longer mandatory and it defaults to `true` 135 | - The value for `pluck()` is now mandatory 136 | - Renamed sorting methods 137 | - Introduced PER code style 138 | 139 | ### Removed 140 | - Parameter `$default` from the `CasesCollection::first()` method 141 | 142 | 143 | ## 1.0.0 - 2022-07-12 144 | 145 | ### Added 146 | - First implementation of the package 147 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | /** @phpstan-ignore offsetAccess.invalidOffset */ 174 | $result[$case->resolveItem($key)] = $case->resolveItem($value); 175 | } 176 | } 177 | 178 | return $result; 179 | } 180 | 181 | /** 182 | * Retrieve the result of mapping over the cases. 183 | * 184 | * @template TMapValue 185 | * 186 | * @param callable(TEnum, array-key): TMapValue $callback 187 | * @return array 188 | */ 189 | public function map(callable $callback): array 190 | { 191 | $keys = array_keys($this->cases); 192 | $values = array_map($callback, $this->cases, $keys); 193 | 194 | return array_combine($keys, $values); 195 | } 196 | 197 | /** 198 | * Retrieve the cases keyed by their own name. 199 | */ 200 | public function keyByName(): static 201 | { 202 | return $this->keyBy('name'); 203 | } 204 | 205 | /** 206 | * Retrieve the cases keyed by the given key. 207 | * 208 | * @param (callable(TEnum): array-key)|string $key 209 | */ 210 | public function keyBy(callable|string $key): static 211 | { 212 | $keyed = []; 213 | 214 | foreach ($this->cases as $case) { 215 | /** @phpstan-ignore offsetAccess.invalidOffset */ 216 | $keyed[$case->resolveItem($key)] = $case; 217 | } 218 | 219 | return new static($keyed); 220 | } 221 | 222 | /** 223 | * Retrieve the cases keyed by their own value. 224 | */ 225 | public function keyByValue(): static 226 | { 227 | return $this->enumIsBacked ? $this->keyBy('value') : new static([]); 228 | } 229 | 230 | /** 231 | * Retrieve the cases grouped by the given key. 232 | * 233 | * @param (callable(TEnum): array-key)|string $key 234 | * @return array> 235 | */ 236 | public function groupBy(callable|string $key): array 237 | { 238 | $grouped = []; 239 | 240 | foreach ($this->cases as $case) { 241 | /** @phpstan-ignore offsetAccess.invalidOffset */ 242 | $grouped[$case->resolveItem($key)][] = $case; 243 | } 244 | 245 | foreach ($grouped as $key => $cases) { 246 | $grouped[$key] = new static($cases); 247 | } 248 | 249 | /** @var array> */ 250 | return $grouped; 251 | } 252 | 253 | /** 254 | * Retrieve a new collection with the filtered cases. 255 | * 256 | * @param (callable(TEnum): bool)|string $filter 257 | */ 258 | public function filter(callable|string $filter): static 259 | { 260 | $callback = is_callable($filter) ? $filter : fn(UnitEnum $case) => $case->resolveItem($filter) === true; 261 | 262 | return new static(array_filter($this->cases, $callback)); 263 | } 264 | 265 | /** 266 | * Retrieve a new collection of cases having only the given names. 267 | */ 268 | public function only(string ...$name): static 269 | { 270 | return $this->filter(fn(UnitEnum $case) => in_array($case->name, $name)); 271 | } 272 | 273 | /** 274 | * Retrieve a collection of cases not having the given names. 275 | */ 276 | public function except(string ...$name): static 277 | { 278 | return $this->filter(fn(UnitEnum $case) => !in_array($case->name, $name)); 279 | } 280 | 281 | /** 282 | * Retrieve a new collection of backed cases having only the given values. 283 | */ 284 | public function onlyValues(string|int ...$value): static 285 | { 286 | return $this->filter(fn(UnitEnum $case) => $this->enumIsBacked && in_array($case->value, $value, true)); 287 | } 288 | 289 | /** 290 | * Retrieve a new collection of backed cases not having the given values. 291 | */ 292 | public function exceptValues(string|int ...$value): static 293 | { 294 | return $this->filter(fn(UnitEnum $case) => $this->enumIsBacked && !in_array($case->value, $value, true)); 295 | } 296 | 297 | /** 298 | * Retrieve a new collection of cases sorted by their own name ascending. 299 | */ 300 | public function sort(): static 301 | { 302 | return $this->sortBy('name'); 303 | } 304 | 305 | /** 306 | * Retrieve a new collection of cases sorted by the given key ascending. 307 | * 308 | * @param (callable(TEnum): mixed)|string $key 309 | */ 310 | public function sortBy(callable|string $key): static 311 | { 312 | $cases = $this->cases; 313 | 314 | uasort($cases, fn(UnitEnum $a, UnitEnum $b) => $a->resolveItem($key) <=> $b->resolveItem($key)); 315 | 316 | return new static($cases); 317 | } 318 | 319 | /** 320 | * Retrieve a new collection of cases sorted by their own value ascending. 321 | */ 322 | public function sortByValue(): static 323 | { 324 | return $this->enumIsBacked ? $this->sortBy('value') : new static([]); 325 | } 326 | 327 | /** 328 | * Retrieve a new collection of cases sorted by their own name descending. 329 | */ 330 | public function sortDesc(): static 331 | { 332 | return $this->sortByDesc('name'); 333 | } 334 | 335 | /** 336 | * Retrieve a new collection of cases sorted by the given key descending. 337 | * 338 | * @param (callable(TEnum): mixed)|string $key 339 | */ 340 | public function sortByDesc(callable|string $key): static 341 | { 342 | $cases = $this->cases; 343 | 344 | uasort($cases, fn(UnitEnum $a, UnitEnum $b) => $b->resolveItem($key) <=> $a->resolveItem($key)); 345 | 346 | return new static($cases); 347 | } 348 | 349 | /** 350 | * Retrieve a new collection of cases sorted by their own value descending. 351 | */ 352 | public function sortByDescValue(): static 353 | { 354 | return $this->enumIsBacked ? $this->sortByDesc('value') : new static([]); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎲 Enum 2 | 3 | [![Author][ico-author]][link-author] 4 | [![PHP Version][ico-php]][link-php] 5 | [![Latest Version][ico-version]][link-packagist] 6 | [![Software License][ico-license]](LICENSE.md) 7 | [![Build Status][ico-actions]][link-actions] 8 | [![Code Quality][ico-code-quality]][link-code-quality] 9 | [![Coverage][ico-coverage]][link-coverage] 10 | [![PHPStan Level][ico-phpstan]][link-phpstan] 11 | [![Total Downloads][ico-downloads]][link-downloads] 12 | 13 | Zero-dependencies package to supercharge enum functionalities. 14 | 15 | > [!TIP] 16 | > Need to supercharge enums in a Laravel application? 17 | > 18 | > Consider using [🎲 Laravel Enum](https://github.com/cerbero90/laravel-enum) instead. 19 | 20 | 21 | ## 📦 Install 22 | 23 | Via Composer: 24 | 25 | ``` bash 26 | composer require cerbero/enum 27 | ``` 28 | 29 | ## 🔮 Usage 30 | 31 | * [⚖️ Comparison](#%EF%B8%8F-comparison) 32 | * [🏷️ Meta](#%EF%B8%8F-meta) 33 | * [🚰 Hydration](#-hydration) 34 | * [🎲 Enum operations](#-enum-operations) 35 | * [🧺 Cases collection](#-cases-collection) 36 | * [🪄 Magic](#-magic) 37 | * [🤳 Self-awareness](#-self-awareness) 38 | * [🦾 Console commands](#-console-commands) 39 | * [🗒️ annotate](#%EF%B8%8F-annotate) 40 | * [🏗️ make](#%EF%B8%8F-make) 41 | * [💙 ts](#-ts) 42 | 43 | To supercharge our enums with all the features provided by this package, we can let our enums use the `Enumerates` trait: 44 | 45 | ```php 46 | use Cerbero\Enum\Concerns\Enumerates; 47 | 48 | enum PureEnum 49 | { 50 | use Enumerates; 51 | 52 | case One; 53 | case Two; 54 | case Three; 55 | } 56 | 57 | enum BackedEnum: int 58 | { 59 | use Enumerates; 60 | 61 | case One = 1; 62 | case Two = 2; 63 | case Three = 3; 64 | } 65 | ``` 66 | 67 | 68 | ### ⚖️ Comparison 69 | 70 | We can check whether an enum includes some names or values. Pure enums check for names and backed enums check for values: 71 | 72 | ```php 73 | PureEnum::has('One'); // true 74 | PureEnum::has('four'); // false 75 | PureEnum::doesntHave('One'); // false 76 | PureEnum::doesntHave('four'); // true 77 | 78 | BackedEnum::has(1); // true 79 | BackedEnum::has(4); // false 80 | BackedEnum::doesntHave(1); // false 81 | BackedEnum::doesntHave(4); // true 82 | ``` 83 | 84 | Otherwise we can check whether cases match a given name or value: 85 | 86 | ```php 87 | PureEnum::One->is('One'); // true 88 | PureEnum::One->is(1); // false 89 | PureEnum::One->is('four'); // false 90 | PureEnum::One->isNot('One'); // false 91 | PureEnum::One->isNot(1); // true 92 | PureEnum::One->isNot('four'); // true 93 | 94 | BackedEnum::One->is(1); // true 95 | BackedEnum::One->is('1'); // false 96 | BackedEnum::One->is(4); // false 97 | BackedEnum::One->isNot(1); // false 98 | BackedEnum::One->isNot('1'); // true 99 | BackedEnum::One->isNot(4); // true 100 | ``` 101 | 102 | Comparisons can also be performed against arrays: 103 | 104 | ```php 105 | PureEnum::One->in(['One', 'four']); // true 106 | PureEnum::One->in([1, 4]); // false 107 | PureEnum::One->notIn(['One', 'four']); // false 108 | PureEnum::One->notIn([1, 4]); // true 109 | 110 | BackedEnum::One->in([1, 4]); // true 111 | BackedEnum::One->in(['One', 'four']); // false 112 | BackedEnum::One->notIn([1, 4]); // false 113 | BackedEnum::One->notIn(['One', 'four']); // true 114 | ``` 115 | 116 | 117 | ### 🏷️ Meta 118 | 119 | 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: 120 | 121 | ```php 122 | enum BackedEnum: int 123 | { 124 | use Enumerates; 125 | 126 | #[Meta(color: 'red', shape: 'triangle')] 127 | case One = 1; 128 | 129 | #[Meta(color: 'green', shape: 'square')] 130 | case Two = 2; 131 | 132 | #[Meta(color: 'blue', shape: 'circle')] 133 | case Three = 3; 134 | 135 | public function isOdd(): bool 136 | { 137 | return $this->value % 2 != 0; 138 | } 139 | } 140 | ``` 141 | 142 | 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. 143 | 144 | To access a case meta, we can simply call the method having the same name of the wanted meta: 145 | 146 | ```php 147 | BackedEnum::Two->color(); // green 148 | ``` 149 | 150 | > [!TIP] 151 | > Our IDE can autocomplete meta methods thanks to the [`annotate` command](#%EF%B8%8F-annotate). 152 | 153 | `#[Meta]` attributes can also be attached to the enum itself to provide default values when a case does not declare its own meta values: 154 | 155 | ```php 156 | #[Meta(color: 'red', shape: 'triangle')] 157 | enum BackedEnum: int 158 | { 159 | use Enumerates; 160 | 161 | case One = 1; 162 | 163 | #[Meta(color: 'green', shape: 'square')] 164 | case Two = 2; 165 | 166 | case Three = 3; 167 | } 168 | ``` 169 | 170 | In the above example all cases have a `red` color and a `triangle` shape, except the case `Two` that overrides the default meta values. 171 | 172 | Meta can also be leveraged for the [hydration](#-hydration), [elaboration](#-enum-operations) and [collection](#-cases-collection) of cases. 173 | 174 | 175 | ### 🚰 Hydration 176 | 177 | An enum case can be instantiated from its own name, value (if backed) or [meta](#%EF%B8%8F-meta): 178 | 179 | ```php 180 | PureEnum::from('One'); // PureEnum::One 181 | PureEnum::from('four'); // throws ValueError 182 | PureEnum::tryFrom('One'); // PureEnum::One 183 | PureEnum::tryFrom('four'); // null 184 | PureEnum::fromName('One'); // PureEnum::One 185 | PureEnum::fromName('four'); // throws ValueError 186 | PureEnum::tryFromName('One'); // PureEnum::One 187 | PureEnum::tryFromName('four'); // null 188 | PureEnum::fromMeta('color', 'red'); // CasesCollection[PureEnum::One] 189 | PureEnum::fromMeta('color', 'purple'); // throws ValueError 190 | PureEnum::fromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 191 | PureEnum::fromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[PureEnum::One, PureEnum::Three] 192 | PureEnum::tryFromMeta('color', 'red'); // CasesCollection[PureEnum::One] 193 | PureEnum::tryFromMeta('color', 'purple'); // null 194 | PureEnum::tryFromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 195 | PureEnum::tryFromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[PureEnum::One, PureEnum::Three] 196 | 197 | BackedEnum::from(1); // BackedEnum::One 198 | BackedEnum::from('1'); // throws ValueError 199 | BackedEnum::tryFrom(1); // BackedEnum::One 200 | BackedEnum::tryFrom('1'); // null 201 | BackedEnum::fromName('One'); // BackedEnum::One 202 | BackedEnum::fromName('four'); // throws ValueError 203 | BackedEnum::tryFromName('One'); // BackedEnum::One 204 | BackedEnum::tryFromName('four'); // null 205 | BackedEnum::fromMeta('color', 'red'); // CasesCollection[BackedEnum::One] 206 | BackedEnum::fromMeta('color', 'purple'); // throws ValueError 207 | BackedEnum::fromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 208 | BackedEnum::fromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[BackedEnum::One, BackedEnum::Three] 209 | BackedEnum::tryFromMeta('color', 'red'); // CasesCollection[BackedEnum::One] 210 | BackedEnum::tryFromMeta('color', 'purple'); // null 211 | BackedEnum::tryFromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 212 | BackedEnum::tryFromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[BackedEnum::One, BackedEnum::Three] 213 | ``` 214 | 215 | Hydrating from meta can return multiple cases. To facilitate further processing, such cases are [collected into a `CasesCollection`](#-cases-collection). 216 | 217 | 218 | ### 🎲 Enum operations 219 | 220 | A number of operations can be performed against an enum to affect all its cases: 221 | 222 | ```php 223 | PureEnum::collect(); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] 224 | PureEnum::count(); // 3 225 | PureEnum::first(); // PureEnum::One 226 | PureEnum::first(fn(PureEnum $case, int $key) => ! $case->isOdd()); // PureEnum::Two 227 | PureEnum::names(); // ['One', 'Two', 'Three'] 228 | PureEnum::values(); // [] 229 | PureEnum::pluck('name'); // ['One', 'Two', 'Three'] 230 | PureEnum::pluck('color'); // ['red', 'green', 'blue'] 231 | PureEnum::pluck(fn(PureEnum $case) => $case->isOdd()); // [true, false, true] 232 | PureEnum::pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] 233 | PureEnum::pluck(fn(PureEnum $case) => $case->isOdd(), fn(PureEnum $case) => $case->name); // ['One' => true, 'Two' => false, 'Three' => true] 234 | PureEnum::map(fn(PureEnum $case, int $key) => $case->name . $key); // ['One0', 'Two1', 'Three2'] 235 | PureEnum::keyByName(); // CasesCollection['One' => PureEnum::One, 'Two' => PureEnum::Two, 'Three' => PureEnum::Three] 236 | PureEnum::keyBy('color'); // CasesCollection['red' => PureEnum::One, 'green' => PureEnum::Two, 'blue' => PureEnum::Three] 237 | PureEnum::keyByValue(); // CasesCollection[] 238 | PureEnum::groupBy('color'); // ['red' => CasesCollection[PureEnum::One], 'green' => CasesCollection[PureEnum::Two], 'blue' => CasesCollection[PureEnum::Three]] 239 | PureEnum::filter('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] 240 | PureEnum::filter(fn(PureEnum $case) => $case->isOdd()); // CasesCollection[PureEnum::One, PureEnum::Three] 241 | PureEnum::only('Two', 'Three'); // CasesCollection[PureEnum::Two, PureEnum::Three] 242 | PureEnum::except('Two', 'Three'); // CasesCollection[PureEnum::One] 243 | PureEnum::onlyValues(2, 3); // CasesCollection[] 244 | PureEnum::exceptValues(2, 3); // CasesCollection[] 245 | PureEnum::sort(); // CasesCollection[PureEnum::One, PureEnum::Three, PureEnum::Two] 246 | PureEnum::sortBy('color'); // CasesCollection[PureEnum::Three, PureEnum::Two, PureEnum::One] 247 | PureEnum::sortByValue(); // CasesCollection[] 248 | PureEnum::sortDesc(); // CasesCollection[PureEnum::Two, PureEnum::Three, PureEnum::One] 249 | PureEnum::sortByDesc(fn(PureEnum $case) => $case->color()); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] 250 | PureEnum::sortByDescValue(); // CasesCollection[] 251 | 252 | BackedEnum::collect(); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] 253 | BackedEnum::count(); // 3 254 | BackedEnum::first(); // BackedEnum::One 255 | BackedEnum::first(fn(BackedEnum $case, int $key) => ! $case->isOdd()); // BackedEnum::Two 256 | BackedEnum::names(); // ['One', 'Two', 'Three'] 257 | BackedEnum::values(); // [1, 2, 3] 258 | BackedEnum::pluck('value'); // [1, 2, 3] 259 | BackedEnum::pluck('color'); // ['red', 'green', 'blue'] 260 | BackedEnum::pluck(fn(BackedEnum $case) => $case->isOdd()); // [true, false, true] 261 | BackedEnum::pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] 262 | BackedEnum::pluck(fn(BackedEnum $case) => $case->isOdd(), fn(BackedEnum $case) => $case->name); // ['One' => true, 'Two' => false, 'Three' => true] 263 | BackedEnum::map(fn(BackedEnum $case, int $key) => $case->name . $key); // ['One0', 'Two1', 'Three2'] 264 | BackedEnum::keyByName(); // CasesCollection['One' => BackedEnum::One, 'Two' => BackedEnum::Two, 'Three' => BackedEnum::Three] 265 | BackedEnum::keyBy('color'); // CasesCollection['red' => BackedEnum::One, 'green' => BackedEnum::Two, 'blue' => BackedEnum::Three] 266 | BackedEnum::keyByValue(); // CasesCollection[1 => BackedEnum::One, 2 => BackedEnum::Two, 3 => BackedEnum::Three] 267 | BackedEnum::groupBy('color'); // ['red' => CasesCollection[BackedEnum::One], 'green' => CasesCollection[BackedEnum::Two], 'blue' => CasesCollection[BackedEnum::Three]] 268 | BackedEnum::filter('isOdd'); // CasesCollection[BackedEnum::One, BackedEnum::Three] 269 | BackedEnum::filter(fn(BackedEnum $case) => $case->isOdd()); // CasesCollection[BackedEnum::One, BackedEnum::Three] 270 | BackedEnum::only('Two', 'Three'); // CasesCollection[BackedEnum::Two, BackedEnum::Three] 271 | BackedEnum::except('Two', 'Three'); // CasesCollection[BackedEnum::One] 272 | BackedEnum::onlyValues(2, 3); // CasesCollection[] 273 | BackedEnum::exceptValues(2, 3); // CasesCollection['Two' => false, 'Three' => true] 274 | BackedEnum::sort(); // CasesCollection[BackedEnum::One, BackedEnum::Three, BackedEnum::Two] 275 | BackedEnum::sortBy('color'); // CasesCollection[BackedEnum::Three, BackedEnum::Two, BackedEnum::One] 276 | BackedEnum::sortByValue(); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] 277 | BackedEnum::sortDesc(); // CasesCollection[BackedEnum::Two, BackedEnum::Three, BackedEnum::One] 278 | BackedEnum::sortByDescValue(); // CasesCollection[BackedEnum::Three, BackedEnum::Two, BackedEnum::One] 279 | BackedEnum::sortByDesc(fn(BackedEnum $case) => $case->color()); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] 280 | ``` 281 | 282 | 283 | ### 🧺 Cases collection 284 | 285 | 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: 286 | 287 | ```php 288 | PureEnum::filter('isOdd')->sortBy('color')->pluck('color', 'name'); // ['Three' => 'blue', 'One' => 'red'] 289 | ``` 290 | 291 | Cases can be collected by calling `collect()` or any other [enum operation](#-enum-operations) returning a `CasesCollection`: 292 | 293 | ```php 294 | PureEnum::collect(); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] 295 | 296 | BackedEnum::only('One', 'Two'); // CasesCollection[BackedEnum::One, BackedEnum::Two] 297 | ``` 298 | 299 | We can iterate a cases collection within any loop: 300 | 301 | ```php 302 | foreach (PureEnum::collect() as $case) { 303 | echo $case->name; 304 | } 305 | ``` 306 | 307 | All the [enum operations listed above](#-enum-operations) are also available when dealing with a collection of cases. 308 | 309 | 310 | ### 🪄 Magic 311 | 312 | 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: 313 | 314 | ```php 315 | PureEnum::One(); // 'One' 316 | 317 | BackedEnum::One(); // 1 318 | ``` 319 | 320 | > [!TIP] 321 | > Our IDE can autocomplete cases static methods thanks to the [`annotate` command](#%EF%B8%8F-annotate). 322 | 323 | We can also obtain the name or value of a case by simply invoking it: 324 | 325 | ```php 326 | $case = PureEnum::One; 327 | $case(); // 'One' 328 | 329 | $case = BackedEnum::One; 330 | $case(); // 1 331 | ``` 332 | 333 | When calling an inaccessible method of a case, by default the value of the meta matching the missing method is returned: 334 | 335 | ```php 336 | PureEnum::One->color(); // 'red' 337 | 338 | BackedEnum::One->shape(); // 'triangle' 339 | ``` 340 | 341 | > [!TIP] 342 | > Our IDE can autocomplete meta methods thanks to the [`annotate` command](#%EF%B8%8F-annotate). 343 | 344 | Depending on our needs, we can customize the default behavior of all enums in our application when invoking a case or calling inaccessible methods: 345 | 346 | ```php 347 | use Cerbero\Enum\Enums; 348 | use UnitEnum; 349 | 350 | // define the logic to run when calling an inaccessible method of an enum 351 | Enums::onStaticCall(function(string $enum, string $name, array $arguments) { 352 | // $enum is the fully qualified name of the enum that called the inaccessible method 353 | // $name is the inaccessible method name 354 | // $arguments are the parameters passed to the inaccessible method 355 | }); 356 | 357 | // define the logic to run when calling an inaccessible method of a case 358 | Enums::onCall(function(UnitEnum $case, string $name, array $arguments) { 359 | // $case is the instance of the case that called the inaccessible method 360 | // $name is the inaccessible method name 361 | // $arguments are the parameters passed to the inaccessible method 362 | }); 363 | 364 | // define the logic to run when invoking a case 365 | Enums::onInvoke(function(UnitEnum $case, mixed ...$arguments) { 366 | // $case is the instance of the case that is being invoked 367 | // $arguments are the parameters passed when invoking the case 368 | }); 369 | ``` 370 | 371 | 372 | ### 🤳 Self-awareness 373 | 374 | Some internal methods are also available and can be useful for inspecting enums or auto-generating code: 375 | 376 | ```php 377 | PureEnum::isPure(); // true 378 | PureEnum::isBacked(); // false 379 | PureEnum::isBackedByInteger(); // false 380 | PureEnum::isBackedByString(); // false 381 | PureEnum::metaNames(); // ['color', 'shape', 'isOdd'] 382 | PureEnum::metaAttributeNames(); // ['color', 'shape'] 383 | PureEnum::One->resolveItem('name'); // 'One' 384 | PureEnum::One->resolveMeta('isOdd'); // true 385 | PureEnum::One->resolveMetaAttribute('color'); // 'red' 386 | PureEnum::One->value(); // 'One' 387 | 388 | BackedEnum::isPure(); // false 389 | BackedEnum::isBacked(); // true 390 | BackedEnum::isBackedByInteger(); // true 391 | BackedEnum::isBackedByString(); // false 392 | BackedEnum::metaNames(); // ['color', 'shape', 'isOdd'] 393 | BackedEnum::metaAttributeNames(); // ['color', 'shape'] 394 | BackedEnum::One->resolveItem('value'); // 1 395 | BackedEnum::One->resolveMeta('isOdd'); // true 396 | BackedEnum::One->resolveMetaAttribute('color'); // 'red' 397 | BackedEnum::One->value(); // 1 398 | ``` 399 | 400 | 401 | ### 🦾 Console commands 402 | 403 | This package provides a handy binary, built to automate different tasks. To learn how to use it, we can simply run it: 404 | 405 | ```bash 406 | ./vendor/bin/enum 407 | ``` 408 | 409 | 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: 410 | 411 | ```php 412 |