cerbero / enum
Zero-dependencies package to supercharge enum functionalities.
Fund package maintenance!
cerbero90
Installs: 91 499
Dependents: 2
Suggesters: 0
Security: 0
Stars: 329
Watchers: 8
Forks: 2
Open Issues: 0
Requires
- php: ^8.1
Requires (Dev)
- pestphp/pest: ^2.0
- phpstan/phpstan: ^2.0
- scrutinizer/ocular: ^1.9
- squizlabs/php_codesniffer: ^3.0
- tightenco/duster: ^2.0
This package is auto-updated.
Last update: 2025-01-14 23:24:23 UTC
README
Zero-dependencies package to supercharge enum functionalities.
Tip
Need to supercharge enums in a Laravel application?
Consider using ๐ฒ Laravel Enum instead.
๐ฆ Install
Via Composer:
composer require cerbero/enum
๐ฎ Usage
- โ๏ธ Comparison
- ๐ท๏ธ Meta
- ๐ฐ Hydration
- ๐ฒ Enum operations
- ๐งบ Cases collection
- ๐ช Magic
- ๐คณ Self-awareness
- ๐ฆพ Console commands
To supercharge our enums with all the features provided by this package, we can let our enums use the Enumerates
trait:
use Cerbero\Enum\Concerns\Enumerates; enum PureEnum { use Enumerates; case One; case Two; case Three; } enum BackedEnum: int { use Enumerates; case One = 1; case Two = 2; case Three = 3; }
โ๏ธ Comparison
We can check whether an enum includes some names or values. Pure enums check for names and backed enums check for values:
PureEnum::has('One'); // true PureEnum::has('four'); // false PureEnum::doesntHave('One'); // false PureEnum::doesntHave('four'); // true BackedEnum::has(1); // true BackedEnum::has(4); // false BackedEnum::doesntHave(1); // false BackedEnum::doesntHave(4); // true
Otherwise we can check whether cases match a given name or value:
PureEnum::One->is('One'); // true PureEnum::One->is(1); // false PureEnum::One->is('four'); // false PureEnum::One->isNot('One'); // false PureEnum::One->isNot(1); // true PureEnum::One->isNot('four'); // true BackedEnum::One->is(1); // true BackedEnum::One->is('1'); // false BackedEnum::One->is(4); // false BackedEnum::One->isNot(1); // false BackedEnum::One->isNot('1'); // true BackedEnum::One->isNot(4); // true
Comparisons can also be performed against arrays:
PureEnum::One->in(['One', 'four']); // true PureEnum::One->in([1, 4]); // false PureEnum::One->notIn(['One', 'four']); // false PureEnum::One->notIn([1, 4]); // true BackedEnum::One->in([1, 4]); // true BackedEnum::One->in(['One', 'four']); // false BackedEnum::One->notIn([1, 4]); // false BackedEnum::One->notIn(['One', 'four']); // true
๐ท๏ธ Meta
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:
enum BackedEnum: int { use Enumerates; #[Meta(color: 'red', shape: 'triangle')] case One = 1; #[Meta(color: 'green', shape: 'square')] case Two = 2; #[Meta(color: 'blue', shape: 'circle')] case Three = 3; public function isOdd(): bool { return $this->value % 2 != 0; } }
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.
To access a case meta, we can simply call the method having the same name of the wanted meta:
BackedEnum::Two->color(); // green
Tip
Our IDE can autocomplete meta methods thanks to the annotate
command.
#[Meta]
attributes can also be attached to the enum itself to provide default values when a case does not declare its own meta values:
#[Meta(color: 'red', shape: 'triangle')] enum BackedEnum: int { use Enumerates; case One = 1; #[Meta(color: 'green', shape: 'square')] case Two = 2; case Three = 3; }
In the above example all cases have a red
color and a triangle
shape, except the case Two
that overrides the default meta values.
Meta can also be leveraged for the hydration, elaboration and collection of cases.
๐ฐ Hydration
An enum case can be instantiated from its own name, value (if backed) or meta:
PureEnum::from('One'); // PureEnum::One PureEnum::from('four'); // throws ValueError PureEnum::tryFrom('One'); // PureEnum::One PureEnum::tryFrom('four'); // null PureEnum::fromName('One'); // PureEnum::One PureEnum::fromName('four'); // throws ValueError PureEnum::tryFromName('One'); // PureEnum::One PureEnum::tryFromName('four'); // null PureEnum::fromMeta('color', 'red'); // CasesCollection[PureEnum::One] PureEnum::fromMeta('color', 'purple'); // throws ValueError PureEnum::fromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] PureEnum::fromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[PureEnum::One, PureEnum::Three] PureEnum::tryFromMeta('color', 'red'); // CasesCollection[PureEnum::One] PureEnum::tryFromMeta('color', 'purple'); // null PureEnum::tryFromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] PureEnum::tryFromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[PureEnum::One, PureEnum::Three] BackedEnum::from(1); // BackedEnum::One BackedEnum::from('1'); // throws ValueError BackedEnum::tryFrom(1); // BackedEnum::One BackedEnum::tryFrom('1'); // null BackedEnum::fromName('One'); // BackedEnum::One BackedEnum::fromName('four'); // throws ValueError BackedEnum::tryFromName('One'); // BackedEnum::One BackedEnum::tryFromName('four'); // null BackedEnum::fromMeta('color', 'red'); // CasesCollection[BackedEnum::One] BackedEnum::fromMeta('color', 'purple'); // throws ValueError BackedEnum::fromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] BackedEnum::fromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[BackedEnum::One, BackedEnum::Three] BackedEnum::tryFromMeta('color', 'red'); // CasesCollection[BackedEnum::One] BackedEnum::tryFromMeta('color', 'purple'); // null BackedEnum::tryFromMeta('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] BackedEnum::tryFromMeta('shape', fn(string $shape) => in_array($shape, ['square', 'circle'])); // CasesCollection[BackedEnum::One, BackedEnum::Three]
Hydrating from meta can return multiple cases. To facilitate further processing, such cases are collected into a CasesCollection
.
๐ฒ Enum operations
A number of operations can be performed against an enum to affect all its cases:
PureEnum::collect(); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] PureEnum::count(); // 3 PureEnum::first(); // PureEnum::One PureEnum::first(fn(PureEnum $case, int $key) => ! $case->isOdd()); // PureEnum::Two PureEnum::names(); // ['One', 'Two', 'Three'] PureEnum::values(); // [] PureEnum::pluck('name'); // ['One', 'Two', 'Three'] PureEnum::pluck('color'); // ['red', 'green', 'blue'] PureEnum::pluck(fn(PureEnum $case) => $case->isOdd()); // [true, false, true] PureEnum::pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] PureEnum::pluck(fn(PureEnum $case) => $case->isOdd(), fn(PureEnum $case) => $case->name); // ['One' => true, 'Two' => false, 'Three' => true] PureEnum::map(fn(PureEnum $case, int $key) => $case->name . $key); // ['One0', 'Two1', 'Three2'] PureEnum::keyByName(); // CasesCollection['One' => PureEnum::One, 'Two' => PureEnum::Two, 'Three' => PureEnum::Three] PureEnum::keyBy('color'); // CasesCollection['red' => PureEnum::One, 'green' => PureEnum::Two, 'blue' => PureEnum::Three] PureEnum::keyByValue(); // CasesCollection[] PureEnum::groupBy('color'); // ['red' => CasesCollection[PureEnum::One], 'green' => CasesCollection[PureEnum::Two], 'blue' => CasesCollection[PureEnum::Three]] PureEnum::filter('isOdd'); // CasesCollection[PureEnum::One, PureEnum::Three] PureEnum::filter(fn(PureEnum $case) => $case->isOdd()); // CasesCollection[PureEnum::One, PureEnum::Three] PureEnum::only('Two', 'Three'); // CasesCollection[PureEnum::Two, PureEnum::Three] PureEnum::except('Two', 'Three'); // CasesCollection[PureEnum::One] PureEnum::onlyValues(2, 3); // CasesCollection[] PureEnum::exceptValues(2, 3); // CasesCollection[] PureEnum::sort(); // CasesCollection[PureEnum::One, PureEnum::Three, PureEnum::Two] PureEnum::sortBy('color'); // CasesCollection[PureEnum::Three, PureEnum::Two, PureEnum::One] PureEnum::sortByValue(); // CasesCollection[] PureEnum::sortDesc(); // CasesCollection[PureEnum::Two, PureEnum::Three, PureEnum::One] PureEnum::sortByDesc(fn(PureEnum $case) => $case->color()); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] PureEnum::sortByDescValue(); // CasesCollection[] BackedEnum::collect(); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] BackedEnum::count(); // 3 BackedEnum::first(); // BackedEnum::One BackedEnum::first(fn(BackedEnum $case, int $key) => ! $case->isOdd()); // BackedEnum::Two BackedEnum::names(); // ['One', 'Two', 'Three'] BackedEnum::values(); // [1, 2, 3] BackedEnum::pluck('value'); // [1, 2, 3] BackedEnum::pluck('color'); // ['red', 'green', 'blue'] BackedEnum::pluck(fn(BackedEnum $case) => $case->isOdd()); // [true, false, true] BackedEnum::pluck('color', 'shape'); // ['triangle' => 'red', 'square' => 'green', 'circle' => 'blue'] BackedEnum::pluck(fn(BackedEnum $case) => $case->isOdd(), fn(BackedEnum $case) => $case->name); // ['One' => true, 'Two' => false, 'Three' => true] BackedEnum::map(fn(BackedEnum $case, int $key) => $case->name . $key); // ['One0', 'Two1', 'Three2'] BackedEnum::keyByName(); // CasesCollection['One' => BackedEnum::One, 'Two' => BackedEnum::Two, 'Three' => BackedEnum::Three] BackedEnum::keyBy('color'); // CasesCollection['red' => BackedEnum::One, 'green' => BackedEnum::Two, 'blue' => BackedEnum::Three] BackedEnum::keyByValue(); // CasesCollection[1 => BackedEnum::One, 2 => BackedEnum::Two, 3 => BackedEnum::Three] BackedEnum::groupBy('color'); // ['red' => CasesCollection[BackedEnum::One], 'green' => CasesCollection[BackedEnum::Two], 'blue' => CasesCollection[BackedEnum::Three]] BackedEnum::filter('isOdd'); // CasesCollection[BackedEnum::One, BackedEnum::Three] BackedEnum::filter(fn(BackedEnum $case) => $case->isOdd()); // CasesCollection[BackedEnum::One, BackedEnum::Three] BackedEnum::only('Two', 'Three'); // CasesCollection[BackedEnum::Two, BackedEnum::Three] BackedEnum::except('Two', 'Three'); // CasesCollection[BackedEnum::One] BackedEnum::onlyValues(2, 3); // CasesCollection[] BackedEnum::exceptValues(2, 3); // CasesCollection['Two' => false, 'Three' => true] BackedEnum::sort(); // CasesCollection[BackedEnum::One, BackedEnum::Three, BackedEnum::Two] BackedEnum::sortBy('color'); // CasesCollection[BackedEnum::Three, BackedEnum::Two, BackedEnum::One] BackedEnum::sortByValue(); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three] BackedEnum::sortDesc(); // CasesCollection[BackedEnum::Two, BackedEnum::Three, BackedEnum::One] BackedEnum::sortByDescValue(); // CasesCollection[BackedEnum::Three, BackedEnum::Two, BackedEnum::One] BackedEnum::sortByDesc(fn(BackedEnum $case) => $case->color()); // CasesCollection[BackedEnum::One, BackedEnum::Two, BackedEnum::Three]
๐งบ Cases collection
When an enum operation can return multiple cases, they are collected into a CasesCollection
which provides a fluent API to perform further operations on the set of cases:
PureEnum::filter('isOdd')->sortBy('color')->pluck('color', 'name'); // ['Three' => 'blue', 'One' => 'red']
Cases can be collected by calling collect()
or any other enum operation returning a CasesCollection
:
PureEnum::collect(); // CasesCollection[PureEnum::One, PureEnum::Two, PureEnum::Three] BackedEnum::only('One', 'Two'); // CasesCollection[BackedEnum::One, BackedEnum::Two]
We can iterate a cases collection within any loop:
foreach (PureEnum::collect() as $case) { echo $case->name; }
All the enum operations listed above are also available when dealing with a collection of cases.
๐ช Magic
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:
PureEnum::One(); // 'One' BackedEnum::One(); // 1
Tip
Our IDE can autocomplete cases static methods thanks to the annotate
command.
We can also obtain the name or value of a case by simply invoking it:
$case = PureEnum::One; $case(); // 'One' $case = BackedEnum::One; $case(); // 1
When calling an inaccessible method of a case, by default the value of the meta matching the missing method is returned:
PureEnum::One->color(); // 'red' BackedEnum::One->shape(); // 'triangle'
Tip
Our IDE can autocomplete meta methods thanks to the annotate
command.
Depending on our needs, we can customize the default behavior of all enums in our application when invoking a case or calling inaccessible methods:
use Cerbero\Enum\Enums; use UnitEnum; // define the logic to run when calling an inaccessible method of an enum Enums::onStaticCall(function(string $enum, string $name, array $arguments) { // $enum is the fully qualified name of the enum that called the inaccessible method // $name is the inaccessible method name // $arguments are the parameters passed to the inaccessible method }); // define the logic to run when calling an inaccessible method of a case Enums::onCall(function(UnitEnum $case, string $name, array $arguments) { // $case is the instance of the case that called the inaccessible method // $name is the inaccessible method name // $arguments are the parameters passed to the inaccessible method }); // define the logic to run when invoking a case Enums::onInvoke(function(UnitEnum $case, mixed ...$arguments) { // $case is the instance of the case that is being invoked // $arguments are the parameters passed when invoking the case });
๐คณ Self-awareness
Some internal methods are also available and can be useful for inspecting enums or auto-generating code:
PureEnum::isPure(); // true PureEnum::isBacked(); // false PureEnum::isBackedByInteger(); // false PureEnum::isBackedByString(); // false PureEnum::metaNames(); // ['color', 'shape', 'isOdd'] PureEnum::metaAttributeNames(); // ['color', 'shape'] PureEnum::One->resolveItem('name'); // 'One' PureEnum::One->resolveMeta('isOdd'); // true PureEnum::One->resolveMetaAttribute('color'); // 'red' PureEnum::One->value(); // 'One' BackedEnum::isPure(); // false BackedEnum::isBacked(); // true BackedEnum::isBackedByInteger(); // true BackedEnum::isBackedByString(); // false BackedEnum::metaNames(); // ['color', 'shape', 'isOdd'] BackedEnum::metaAttributeNames(); // ['color', 'shape'] BackedEnum::One->resolveItem('value'); // 1 BackedEnum::One->resolveMeta('isOdd'); // true BackedEnum::One->resolveMetaAttribute('color'); // 'red' BackedEnum::One->value(); // 1
๐ฆพ Console commands
This package provides a handy binary, built to automate different tasks. To learn how to use it, we can simply run it:
./vendor/bin/enum
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:
<?php use Cerbero\Enum\Enums; Enums::setBasePath(__DIR__);
Some commands support the option --all
to reference all the enums of our application. We can set the paths where enums live in our app in the enums.php
configuration file as well:
Enums::setPaths('app/Enums', 'domain/*/Enums');
In the above example, enums are discovered in the app/Enums
directory and in all Enums
sub-folders belonging to domain
, e.g. domain/Posts/Enums
, domain/Users/Enums
, etc.
๐๏ธ annotate
The annotate
command automatically adds method annotations to enums, making IDEs autocompletion possible:
./vendor/bin/enum annotate App/Enums/Enum
./vendor/bin/enum annotate "App\Enums\Enum"
We can provide more than one enum to annotate, if needed:
./vendor/bin/enum annotate App/Enums/Enum1 App/Enums/Enum2 ./vendor/bin/enum annotate "App\Enums\Enum1" "App\Enums\Enum2"
Otherwise we can annotate all our enums at once by enabling the option --all
:
./vendor/bin/enum annotate --all ./vendor/bin/enum annotate -a
If we want to overwrite method annotations already annotated on enums, we can add the option --force
:
./vendor/bin/enum annotate App/Enums/Enum --force ./vendor/bin/enum annotate App/Enums/Enum -f
๐๏ธ make
The make
command creates a new - automatically annotated - enum with the cases that we provide:
./vendor/bin/enum make App/Enums/Enum CaseOne CaseTwo
./vendor/bin/enum make "App\Enums\Enum" CaseOne CaseTwo
If we need to create backed enums, we can specify a custom value for each case:
./vendor/bin/enum make App/Enums/Enum CaseOne=value1 CaseTwo=value2
Otherwise we can automatically assign values to cases by setting the --backed
option:
./vendor/bin/enum make App/Enums/Enum CaseOne CaseTwo --backed=int0
The option --backed
supports the following values:
int0
: assign an incremental integer starting from 0 (0, 1, 2...)int1
: assign an incremental integer starting from 1 (1, 2, 3...)bitwise
: assign an incremental bitwise value (1, 2, 4...)snake
: assign the case name in snake case (case_one, case_two...)kebab
: assign the case name in kebab case (case-one, case-two...)camel
: assign the case name in camel case (caseOne, caseTwo...)lower
: assign the case name in lower case (caseone, casetwo...)upper
: assign the case name in upper case (CASEONE, CASETWO...)
If we want to overwrite an existing enum, we can add the option --force
:
php artisan enum:make App/Enums/Enum CaseOne CaseTwo --force php artisan enum:make App/Enums/Enum CaseOne CaseTwo -f
We can generate the TypeScript counterpart of the newly created enum by adding the --typescript
option:
php artisan enum:make App/Enums/Enum CaseOne CaseTwo --typescript php artisan enum:make App/Enums/Enum CaseOne CaseTwo -t
๐ ts
The ts
command turns enums into their TypeScript counterpart, synchronizing backend with frontend:
./vendor/bin/enum ts App/Enums/Enum
./vendor/bin/enum ts "App\Enums\Enum"
We can provide more than one enum to synchronize in TypeScript, if needed:
./vendor/bin/enum ts App/Enums/Enum1 App/Enums/Enum2 ./vendor/bin/enum ts "App\Enums\Enum1" "App\Enums\Enum2"
Otherwise we can synchronize all our enums at once by enabling the option --all
:
./vendor/bin/enum ts --all ./vendor/bin/enum ts -a
By default enums are synchronized in resources/js/enums/index.ts
, however we can customize it in our enums.php
configuration file:
<?php use Cerbero\Enum\Enums; // custom static path Enums::setTypeScript('frontend/enums/index.ts'); // custom dynamic path Enums::setTypeScript(function (string $enum) { $domain = explode('\\', $enum)[1]; return "resources/js/modules/{$domain}/enums.ts"; });
As seen above, we can either set a static path for our TypeScript enums or dynamically set the TypeScript path of an enum depending on its namespace.
If we want to update previously synchronized enums, we can add the option --force
:
./vendor/bin/enum ts App/Enums/Enum --force ./vendor/bin/enum ts App/Enums/Enum -f
๐ Change log
Please see CHANGELOG for more information on what has changed recently.
๐งช Testing
composer test
๐ Contributing
Please see CONTRIBUTING and CODE_OF_CONDUCT for details.
๐งฏ Security
If you discover any security related issues, please email andrea.marco.sartori@gmail.com instead of using the issue tracker.
๐ Credits
โ๏ธ License
The MIT License (MIT). Please see License File for more information.