Laravel package to supercharge enum functionalities.
Tip
Need a framework-agnostic solution? Consider using 🎲 Enum instead.
Via Composer:
composer require cerbero/laravel-enum
This package provides all the functionalities of 🎲 Enum plus Laravel-specific features.
To supercharge our enums, we just need to let them use the Enumerates
trait:
use Cerbero\LaravelEnum\Concerns\Enumerates;
enum Numbers: int
{
use Enumerates;
case One = 1;
case Two = 2;
case Three = 3;
}
Laravel Enum extends Enum's meta by allowing us to attach meta with a class name. This class is resolved by the Laravel container when invoking the corresponding meta method:
use Cerbero\Enum\Attributes\Meta;
enum PayoutStatuses
{
use Enumerates;
#[Meta(handler: SentPayoutHandler::class)]
case Sent;
#[Meta(handler: OnHoldPayoutHandler::class)]
case OnHold;
#[Meta(handler: DeclinedPayoutHandler::class)]
case Declined;
}
In the enum above, each case specifies a handler
meta with a class name. When a case calls its handler()
meta method, the corresponding class is resolved through the IoC container:
// 🐢 instead of this
$handler = match ($payout->status) {
PayoutStatuses::Sent => SentPayoutHandler::class,
PayoutStatuses::OnHold => OnHoldPayoutHandler::class,
PayoutStatuses::Declined => DeclinedPayoutHandler::class,
};
return Container::getInstance()->make($handler);
// 🐇 we can do this
return $payout->status->handler();
If we need to resolve a default class for most cases, we can attach the meta to the enum itself. Cases with their own meta override the default class:
use Cerbero\Enum\Attributes\Meta;
#[Meta(handler: DefaultPayoutHandler::class)]
enum PayoutStatuses
{
use Enumerates;
#[Meta(handler: SentPayoutHandler::class)]
case Sent;
case OnHold;
case Declined;
}
In the example above, all cases calling the handler()
method resolve the DefaultPayoutHandler
class, except for the Sent
case that resolves SentPayoutHandler
.
If the class to be resolved is callable (i.e. it implements the __invoke()
method), that class will be both resolved and executed:
use Cerbero\Enum\Attributes\Meta;
enum PayoutStatuses
{
use Enumerates;
#[Meta(handlePayout: SentPayoutHandler::class)]
case Sent;
#[Meta(handlePayout: OnHoldPayoutHandler::class)]
case OnHold;
#[Meta(handlePayout: DeclinedPayoutHandler::class)]
case Declined;
}
In the enum above, each case specifies a handlePayout
meta with a callable class. Calling handlePayout()
resolves and invokes the class with any passed parameters.
// 🐢 instead of this
$handler = match ($payout->status) {
PayoutStatuses::Sent => SentPayoutHandler::class,
PayoutStatuses::OnHold => OnHoldPayoutHandler::class,
PayoutStatuses::Declined => DeclinedPayoutHandler::class,
};
$handlePayout = Container::getInstance()->make($handler);
return $handlePayout($payout);
// 🐇 we can do this
return $payout->status->handlePayout($payout);
If we need to run a default callable class for most cases, we can attach the meta to the enum itself. Cases with their own meta override the default callable class:
use Cerbero\Enum\Attributes\Meta;
#[Meta(handlePayout: DefaultPayoutHandler::class)]
enum PayoutStatuses
{
use Enumerates;
#[Meta(handlePayout: SentPayoutHandler::class)]
case Sent;
case OnHold;
case Declined;
}
In the example above, all cases calling the handlePayout()
method execute the DefaultPayoutHandler
class, except for the Sent
case, which runs the SentPayoutHandler
.
Tip
Our IDE can autocomplete meta methods thanks to the enum:annotate
command.
Class names in meta are annotated by identifying the common interface or parent class they share.
The original cases collection has been extended for better integration with the Laravel framework.
The new cases collection implements the Illuminate\Contracts\Support\Arrayable
and Illuminate\Contracts\Support\Jsonable
contracts and it can be serialized into a JSON.
It also leverages the following Laravel traits:
Illuminate\Support\Traits\Conditionable
for conditional chaining of methodsIlluminate\Support\Traits\Macroable
for adding methods to the collection at runtimeIlluminate\Support\Traits\Tappable
for running custom logic while keeping method chaining
Additionally, the new collection enables us to dump()
and dd()
its cases:
Numbers::collect()->dump();
Numbers::collect()->dd();
Cases collection can be cast in an Eloquent model to store multiple cases in a single database column and then re-hydrate those cases back into a collection:
use Cerbero\LaravelEnum\CasesCollection;
class User extends Model
{
// before Laravel 11
protected $casts = [
'numbers' => CasesCollection::class . ':' . Numbers::class,
];
// after Laravel 11
protected function casts(): array
{
return [
'numbers' => CasesCollection::of(Numbers::class),
];
}
}
Once the cast is set, we can assign an array of names, values or cases to the cast property of the model and receive a cases collection when accessing the property:
$user->numbers = ['One', 'Two'];
$user->numbers = [1, 2];
$user->numbers = [Numbers::One, Numbers::Two];
$user->numbers; // CasesCollection[Numbers::One, Numbers::Two]
The cases collection above is stored in the database as ["One","Two"]
for a pure enum, or as [1,2]
for a backed enum.
The cast also supports bitwise backed enums, so for instance, if we have a Permissions
enum implementing the Bitwise
contract:
use Cerbero\LaravelEnum\Contracts\Bitwise;
enum Permissions: int implements Bitwise
{
use Enumerates;
case CreatePost = 1;
case UpdatePost = 2;
case DeletePost = 4;
}
and we cast the permissions
property in our Eloquent model:
use Cerbero\LaravelEnum\CasesCollection;
class User extends Model
{
// before Laravel 11
protected $casts = [
'permissions' => CasesCollection::class . ':' . Permissions::class,
];
// after Laravel 11
protected function casts(): array
{
return [
'permissions' => CasesCollection::of(Permissions::class),
];
}
}
we can assign a bitwise value or an array of bitwise values/cases to the permissions
property and receive a cases collection in return:
$user->permissions = 3;
$user->permissions = 1 | 2;
$user->permissions = Permissions::CreatePost->value | Permissions::UpdatePost->value;
$user->permissions = [1, 2];
$user->permissions = [Permissions::CreatePost, Permissions::UpdatePost];
$user->permissions; // CasesCollection[Permissions::CreatePost, Permissions::UpdatePost]
The cases collection above is stored in the database as 3
, the result of the OR
bitwise operator.
On top of Enum's magic, when a case calls an inaccessible method without a corresponding meta match, Laravel Enum assumes that we want to access a translation:
Numbers::One->description();
// lang/en/enums.php
return [
Numbers::class => [
'One' => [
'description' => 'This is the case One.',
],
],
];
By default the translation key is resolved as enums.{enum namespace}.{case name}.{inaccessible method}
. If customization is needed, we can adjust the translation key:
use Cerbero\LaravelEnum\Enums;
Enums::translateFrom(function(UnitEnum $case, string $method) {
return sprintf('custom.%s.%s.%s', $case::class, $case->name, $method);
});
The above logic will resolve the translation key as custom.{enum namespace}.{case name}.{inaccessible method}
.
Additionally, we can pass named arguments to replace placeholders within our translations:
return [
Numbers::class => [
'One' => [
'description' => 'This is the case :value.',
],
],
];
// This is the case 1.
Numbers::One->description(value: 1);
Tip
Our IDE can autocomplete translation methods thanks to the enum:annotate
command.
Laravel Enum offers extra traits to encapsulate Laravel features that deal with keys. By defining keys as enum cases, we can leverage Laravel features without having to remember or repeat such keys.
The benefits of this approach are many:
- avoiding scattered, error-prone strings throughout the codebase
- preventing key misspellings
- enabling IDE autocompletion
- reviewing all application keys in a central location
- updating keys in one place rather than replacing all instances
To encapsulate the Laravel cache, we can define a backed enum with all our cache keys and apply the EnumeratesCacheKeys
trait:
use Cerbero\LaravelEnum\Concerns\EnumeratesCacheKeys;
enum CacheKeys: string
{
use EnumeratesCacheKeys;
case PostComments = 'posts.{int $postId}.comments';
case Tags = 'tags';
case TeamMemberPosts = 'teams.{string $teamId}.users.{string $userId}.posts';
}
The EnumeratesCacheKeys
trait incorporates Enumerates
, hence all the features of this package. We can now leverage all the Laravel cache methods by statically calling enum cases:
CacheKeys::Tags()->exists();
CacheKeys::Tags()->missing();
CacheKeys::Tags()->hasValue();
CacheKeys::Tags()->get($default);
CacheKeys::Tags()->pull($default);
CacheKeys::Tags()->put($value, $ttl);
CacheKeys::Tags()->set($value, $ttl);
CacheKeys::Tags()->add($value, $ttl);
CacheKeys::Tags()->increment($value);
CacheKeys::Tags()->decrement($value);
CacheKeys::Tags()->forever($value);
CacheKeys::Tags()->remember($ttl, $callback);
CacheKeys::Tags()->rememberForever($callback);
CacheKeys::Tags()->sear($callback);
CacheKeys::Tags()->forget();
CacheKeys::Tags()->delete();
CacheKeys::Tags()->lock($seconds, $owner);
CacheKeys::Tags()->restoreLock($owner);
When calling cases statically, we can pass parameters to resolve dynamic keys. Such parameters replace the {...}
placeholders in the cache keys:
CacheKeys::TeamMemberPosts($teamId, $userId)->exists();
To encapsulate the Laravel session, we can define a backed enum with all our session keys and apply the EnumeratesSessionKeys
trait:
use Cerbero\LaravelEnum\Concerns\EnumeratesSessionKeys;
enum SessionKeys
{
use EnumeratesSessionKeys;
case CartItems = 'cart-items';
case OnboardingStep = 'onboarding-step';
case FormsData = 'forms.{int $formId}.data';
}
The EnumeratesSessionKeys
trait incorporates Enumerates
, hence all the features of this package. We can now leverage all the Laravel session methods by statically calling enum cases:
SessionKeys::CartItems()->exists();
SessionKeys::CartItems()->missing();
SessionKeys::CartItems()->hasValue();
SessionKeys::CartItems()->get($default);
SessionKeys::CartItems()->pull($default);
SessionKeys::CartItems()->hasOldInput();
SessionKeys::CartItems()->getOldInput($default);
SessionKeys::CartItems()->put($value);
SessionKeys::CartItems()->remember($callback);
SessionKeys::CartItems()->push($value);
SessionKeys::CartItems()->increment($amount);
SessionKeys::CartItems()->decrement($amount);
SessionKeys::CartItems()->flash($value);
SessionKeys::CartItems()->now($value);
SessionKeys::CartItems()->remove();
SessionKeys::CartItems()->forget();
When calling cases statically, we can pass parameters to resolve dynamic keys. Such parameters replace the {...}
placeholders in the session keys:
SessionKeys::FormsData($formId)->exists();
Tip
Our IDE can autocomplete case static methods thanks to the enum:annotate
command.
A handy set of Artisan commands is provided out of the box to interact with enums seamlessly.
Some commands generate enums or related files. If we want to customize such files, we can publish their stubs:
php artisan vendor:publish --tag=laravel-enum-stubs
After publishing, the stubs can be modified within the stubs/laravel-enum
directory, located at the root of our application.
Certain commands supports the --all
option to reference all enums in our application. By default, enums are searched in the app/Enums
directory, but we can scan other folders as well:
use Cerbero\LaravelEnum\Enums;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Enums::setPaths('app/Enums', 'domain/*/Enums');
}
}
In the example above, enums are searched in the app/Enums
directory and all Enums
sub-directories within domain
, such as domain/Posts/Enums
, domain/Users/Enums
, etc.
IDEs can autocomplete case static methods, meta methods, translations, etc. by running the enum:annotate
command:
php artisan enum:annotate
If we don't provide any argument, a prompt appears to choose which enums to annotate. Or, we can simply use the --all
option to annotate all enums:
php artisan enum:annotate --all
php artisan enum:annotate -a
Alternatively, we can provide one or more enums directly to the enum:annotate
command. Both slashes and quoted backslashes are acceptable for defining enum namespaces:
php artisan enum:annotate App/Enums/Permissions "App\Enums\PayoutStatuses"
Lastly, if we wish to overwrite existing method annotations on enums, we can include the --force
option:
php artisan enum:annotate --force
php artisan enum:annotate -f
The enum:make
command allows us to create a new, automatically annotated enum with the cases we need:
php artisan enum:make
If no arguments are given, prompts will guide us through defining the enum namespace, backing type and cases. Otherwise, all these details can be typed via command line:
php artisan enum:make App/Enums/Enum CaseOne CaseTwo
php artisan enum:make "App\Enums\Enum" CaseOne CaseTwo
For creating backed enums, we can manually set custom values for each case:
php artisan enum:make App/Enums/Enum CaseOne=value1 CaseTwo=value2
Or, we can automatically assign values to cases by using the --backed
option:
php artisan enum:make App/Enums/Enum CaseOne CaseTwo --backed=int0
The --backed
option accepts these values:
int0
: assigns incremental integers starting from 0 (0, 1, 2...)int1
: assigns incremental integers starting from 1 (1, 2, 3...)bitwise
: assigns incremental bitwise values (1, 2, 4...)snake
: assigns the case name in snake case (case_one, case_two...)kebab
: assigns the case name in kebab case (case-one, case-two...)camel
: assigns the case name in camel case (caseOne, caseTwo...)lower
: assigns the case name in lower case (caseone, casetwo...)upper
: assigns the case name in upper case (CASEONE, CASETWO...)
To overwrite an existing enum, we can include the --force
option:
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
The ts
command converts enums to their TypeScript equivalents, ensuring backend and frontend synchronization:
php artisan enum:ts
If we don't provide any argument, a prompt appears to choose which enums to synchronize. Or, we can simply use the --all
option to synchronize all enums:
php artisan enum:ts --all
php artisan enum:ts -a
Alternatively, we can provide one or more enums directly to the enum:ts
command. Both slashes and quoted backslashes are acceptable for defining enum namespaces:
php artisan enum:ts App/Enums/Permissions "App\Enums\PayoutStatuses"
By default, enums are synchronized to resources/js/enums/index.ts
, but this can be easily customized:
use Cerbero\LaravelEnum\Enums;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// 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 shown above, we can either define a static path for TypeScript enums or dynamically set the TypeScript path for an enum based on its namespace.
To update enums that have already been synchronized, we can use the --force
option:
php artisan enum:ts App/Enums/Enum --force
php artisan enum:ts App/Enums/Enum -f
Please see CHANGELOG for more information on what has changed recently.
composer test
Please see CONTRIBUTING and CODE_OF_CONDUCT for details.
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.