xeriab/laravel-enumeration

Yet another simple, extensible and powerful enumeration implementation for Laravel.

dev-main 2022-07-05 23:59 UTC

This package is not auto-updated.

Last update: 2022-08-11 03:26:45 UTC


README

Laravel Enumeration

About Laravel Enumeration

Simple, extensible and powerful enumeration implementation for Laravel.

  • Enumeration key value pairs as class constants
  • Full-featured suite of methods
  • Enumeration instantiation
  • Flagged/Bitwise enums
  • Type hinting
  • Attribute casting
  • Enumeration artisan generator
  • Validation rules for passing enumeration key or values as input parameters
  • Localization support
  • Extendable via Macros

Created by Xeriab Nabil

Jump To

Installation

Requirements

  • Laravel 9 or higher
  • PHP 8.0 or higher

Via Composer

composer require xeriab/laravel-enumeration

Enumeration Library

Browse and download from a list of commonly used, community contributed enumerations.

Enumeration Library →

Basic Usage

Enumeration Definition

You can use the following Artisan command to generate a new enumeration class:

php artisan make:enumeration UserType

Now, you just need to add the possible values your enumeration can have as constants.

<?php

namespace App\Enums;

use Exen\Laravel\Enumeration\Enum;

final class UserType extends Enum
{
    const Administrator = 0;
    const Moderator = 1;
    const Subscriber = 2;
    const SuperAdministrator = 3;
}

That's it! Note that because the enumeration values are defined as plain constants, you can simply access them like any other class constant.

UserType::Administrator // Has a value of 0

Instantiation

It can be useful to instantiate enums in order to pass them between functions with the benefit of type hinting.

Additionally, it's impossible to instantiate an enumeration with an invalid value, therefore you can be certain that the passed value is always valid.

For convenience, enums can be instantiated in multiple ways:

// Standard new PHP class, passing the desired enumeration value as a parameter
$enumInstance = new UserType(UserType::Administrator);

// Same as the constructor, instantiate by value
$enumInstance = UserType::fromValue(UserType::Administrator);

// Use an enumeration key instead of its value
$enumInstance = UserType::fromKey('Administrator');

// Statically calling the key name as a method, utilizing __callStatic magic
$enumInstance = UserType::Administrator();

// Attempt to instantiate a new Enumeration using the given key or value. Returns null if the Enumeration cannot be instantiated.
$enumInstance = UserType::coerce($someValue);

If you want your IDE to autocomplete the static instantiation helpers, you can generate PHPDoc annotations through an artisan command.

By default, all Enumerations in app/Enums will be annotated (you can change the folder by passing a path to --folder)

php artisan enum:annotate

You can annotate a single class by specifying the class name

php artisan enum:annotate "App\Enums\UserType"

Instance Properties

Once you have an enumeration instance, you can access the key, value and description as properties.

$userType = UserType::fromValue(UserType::SuperAdministrator);

$userType->key; // SuperAdministrator
$userType->value; // 0
$userType->description; // Super Administrator

This is particularly useful if you're passing an enumeration instance to a blade view.

Instance Casting

Enumeration instances can be cast to string as they implement the __toString() magic method.
This also means they can be echoed in blade views, for example.

$userType = UserType::fromValue(UserType::SuperAdministrator);

(string) $userType // '0'

Instance Equality

You can check the equality of an instance against any value by passing it to the is method. For convenience, there is also an isNot method which is the exact reverse of the is method.

$admin = UserType::fromValue(UserType::Administrator);

$admin->is(UserType::Administrator);   // true
$admin->is($admin);                    // true
$admin->is(UserType::Administrator()); // true

$admin->is(UserType::Moderator);       // false
$admin->is(UserType::Moderator());     // false
$admin->is('random-value');            // false

You can also check to see if the instance's value matches against an array of possible values using the in method, and use notIn to check if instance value is not in an array of values. Iterables can also be checked against.

$admin = UserType::fromValue(UserType::Administrator);

$admin->in([UserType::Moderator, UserType::Administrator]);     // true
$admin->in([UserType::Moderator(), UserType::Administrator()]); // true

$admin->in([UserType::Moderator, UserType::Subscriber]);        // false
$admin->in(['random-value']);                                   // false

$admin->notIn([UserType::Moderator, UserType::Administrator]);     // false
$admin->notIn([UserType::Moderator(), UserType::Administrator()]); // false

$admin->notIn([UserType::Moderator, UserType::Subscriber]);        // true
$admin->notIn(['random-value']);                                   // true

Type Hinting

One of the benefits of enumeration instances is that it enables you to use type hinting, as shown below.

function canPerformAction(UserType $userType)
{
    if ($userType->is(UserType::SuperAdministrator)) {
        return true;
    }

    return false;
}

$userType1 = UserType::fromValue(UserType::SuperAdministrator);
$userType2 = UserType::fromValue(UserType::Moderator);

canPerformAction($userType1); // Returns true
canPerformAction($userType2); // Returns false

Flagged/Bitwise Enumeration

Standard enums represent a single value at a time, but flagged or bitwise enums are capable of representing multiple values simultaneously. This makes them perfect for when you want to express multiple selections of a limited set of options. A good example of this would be user permissions where there are a limited number of possible permissions but a user can have none, some or all of them.

You can create a flagged enumeration using the following artisan command:

php artisan make:enumeration UserPermissions --flagged

Defining values

When defining values you must use powers of 2, the easiest way to do this is by using the shift left << operator like so:

final class UserPermissions extends FlaggedEnum
{
    const ReadComments      = 1 << 0;
    const WriteComments     = 1 << 1;
    const EditComments      = 1 << 2;
    const DeleteComments    = 1 << 3;
    // The next one would be `1 << 4` and so on...
}

Defining shortcuts

You can use the bitwise or | to set a shortcut value which represents a given set of values.

final class UserPermissions extends FlaggedEnum
{
    const ReadComments      = 1 << 0;
    const WriteComments     = 1 << 1;
    const EditComments      = 1 << 2;
    const DeleteComments    = 1 << 3;

    // Shortcuts
    const Member = self::ReadComments | self::WriteComments; // Read and write.
    const Moderator = self::Member | self::EditComments; // All the permissions a Member has, plus Edit.
    const Admin = self::Moderator | self::DeleteComments; // All the permissions a Moderator has, plus Delete.
}

Instantiating a flagged enumeration

There are a couple of ways to instantiate a flagged enum:

// Standard new PHP class, passing the desired enumeration values as an array of values or array of enumeration instances
$permissions = new UserPermissions([UserPermissions::ReadComments, UserPermissions::EditComments]);
$permissions = new UserPermissions([UserPermissions::ReadComments(), UserPermissions::EditComments()]);

// Static flags method, again passing the desired enumeration values as an array of values or array of enumeration instances
$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::EditComments]);
$permissions = UserPermissions::flags([UserPermissions::ReadComments(), UserPermissions::EditComments()]);

Attribute casting works in the same way as single value enums.

Empty flagged enums

Flagged enums can contain no value at all. Every flagged enumeration has a pre-defined constant of None which is comparable to 0.

UserPermissions::flags([])->value === UserPermissions::None; // True

Flagged enumeration methods

In addition to the standard enumeration methods, there are a suite of helpful methods available on flagged enums.

Note: Anywhere where a static property is passed, you can also pass an enumeration instance.

setFlags(array $flags): Enumeration

Set the flags for the enumeration to the given array of flags.

$permissions = UserPermissions::flags([UserPermissions::ReadComments]);
$permissions->flags([UserPermissions::EditComments, UserPermissions::DeleteComments]); // Flags are now: EditComments, DeleteComments.

addFlag($flag): Enumeration

Add the given flag to the enumeration

$permissions = UserPermissions::flags([UserPermissions::ReadComments]);
$permissions->addFlag(UserPermissions::EditComments); // Flags are now: ReadComments, EditComments.

addFlags(array $flags): Enumeration

Add the given flags to the enumeration

$permissions = UserPermissions::flags([UserPermissions::ReadComments]);
$permissions->addFlags([UserPermissions::EditComments, UserPermissions::WriteComments]); // Flags are now: ReadComments, EditComments, WriteComments.

addAllFlags(): Enumeration

Add all flags to the enumeration

$permissions = UserPermissions::flags([UserPermissions::ReadComments]);
$permissions->addAllFlags(); // Enumeration now has all flags

removeFlag($flag): Enumeration

Remove the given flag from the enumeration

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]);
$permissions->removeFlag(UserPermissions::ReadComments); // Flags are now: WriteComments.

removeFlags(array $flags): Enumeration

Remove the given flags from the enumeration

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments, UserPermissions::EditComments]);
$permissions->removeFlags([UserPermissions::ReadComments, UserPermissions::WriteComments]); // Flags are now: EditComments.

removeAllFlags(): Enumeration

Remove all flags from the enumeration

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]);
$permissions->removeAllFlags();

hasFlag($flag): bool

Check if the enumeration has the specified flag.

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]);
$permissions->hasFlag(UserPermissions::ReadComments); // True
$permissions->hasFlag(UserPermissions::EditComments); // False

hasFlags(array $flags): bool

Check if the enumeration has all the specified flags.

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]);
$permissions->hasFlags([UserPermissions::ReadComments, UserPermissions::WriteComments]); // True
$permissions->hasFlags([UserPermissions::ReadComments, UserPermissions::EditComments]); // False

notHasFlag($flag): bool

Check if the enumeration does not have the specified flag.

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]);
$permissions->notHasFlag(UserPermissions::EditComments); // True
$permissions->notHasFlag(UserPermissions::ReadComments); // False

notHasFlags(array $flags): bool

Check if the enumeration doesn't have any of the specified flags.

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]);
$permissions->notHasFlags([UserPermissions::ReadComments, UserPermissions::EditComments]); // True
$permissions->notHasFlags([UserPermissions::ReadComments, UserPermissions::WriteComments]); // False

getFlags(): Enumeration[]

Return the flags as an array of instances.

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]);
$permissions->getFlags(); // [UserPermissions::ReadComments(), UserPermissions::WriteComments()];

hasMultipleFlags(): bool

Check if there are multiple flags set on the enumeration.

$permissions = UserPermissions::flags([UserPermissions::ReadComments, UserPermissions::WriteComments]);
$permissions->hasMultipleFlags(); // True;
$permissions->removeFlag(UserPermissions::ReadComments)->hasMultipleFlags(); // False

getBitmask(): int

Get the bitmask for the enumeration.

UserPermissions::Member()->getBitmask(); // 11;
UserPermissions::Moderator()->getBitmask(); // 111;
UserPermissions::Admin()->getBitmask(); // 1111;
UserPermissions::DeleteComments()->getBitmask(); // 1000;

Flagged enums in Eloquent queries

To use flagged enums directly in your Eloquent queries, you may use the QueriesFlaggedEnums trait on your model which provides you with the following methods:

hasFlag($column, $flag): Builder

User::hasFlag('permissions', UserPermissions::DeleteComments())->get();

notHasFlag($column, $flag): Builder

User::notHasFlag('permissions', UserPermissions::DeleteComments())->get();

hasAllFlags($column, $flags): Builder

User::hasAllFlags('permissions', [UserPermissions::EditComment(), UserPermissions::ReadComment()])->get();

hasAnyFlags($column, $flags): Builder

User::hasAnyFlags('permissions', [UserPermissions::DeleteComments(), UserPermissions::EditComments()])->get();

Attribute Casting

You may cast model attributes to enums using Laravel's built-in custom casting. This will cast the attribute to an enumeration instance when getting and back to the enumeration value when setting. Since Enumeration::class implements the Castable contract, you just need to specify the classname of the enum:

use xeriab\Enumeration\Tests\Enums\UserType;
use Illuminate\Database\Eloquent\Model;

class Example extends Model
{
    protected $casts = [
        'random_flag' => 'boolean',     // Example standard laravel cast
        'user_type' => UserType::class, // Example enumeration cast
    ];
}

Now, when you access the user_type attribute of your Example model, the underlying value will be returned as a UserType enumeration.

$example = Example::first();
$example->user_type // Instance of UserType

Review the methods and properties available on enumeration instances to get the most out of attribute casting.

You can set the value by either passing the enumeration value or another enumeration instance.

$example = Example::first();

// Set using enumeration value
$example->user_type = UserType::Moderator;

// Set using enumeration instance
$example->user_type = UserType::Moderator();

Customising $model->toArray() behaviour

When using toArray (or returning model/models from your controller as a response) Laravel will call the toArray method on the enumeration instance.

By default, this will return only the value in its native type. You may want to also have access to the other properties (key, description), for example to return to javascript app.

To customise this behaviour, you can override the toArray method on the enumeration instance.

// Example Enumeration
final class UserType extends Enum
{
    const ADMINISTRATOR = 0;
    const MODERATOR = 1;
}

$instance = UserType::Moderator();

// Default
public function toArray()
{
    return $this->value;
}
// Returns int(1)

// Return all properties
public function toArray()
{
    return $this;
}
// Returns an array of all the properties
// array(3) {
//  ["value"]=>
//  int(1)"
//  ["key"]=>
//  string(9) "MODERATOR"
//  ["description"]=>
//  string(9) "Moderator"
// }

Casting underlying native types

Many databases return everything as strings (for example, an integer may be returned as the string '1'). To reduce friction for users of the library, we use type coercion to figure out the intended value. If you'd prefer to control this, you can override the parseDatabase static method on your enumeration class:

final class UserType extends Enum
{
    const Administrator = 0;
    const Moderator = 1;

    public static function parseDatabase($value)
    {
        return (int) $value;
    }
}

Returning null from the parseDatabase method will cause the attribute on the model to also be null. This can be useful if your database stores inconsistent blank values such as empty strings instead of NULL.

Model Annotation

If you're casting attributes on your model to enums, the laravel-ide-helper package can be used to automatically generate property docblocks for you.

Migrations

Recommended

Because enums enforce consistency at the code level it's not necessary to do so again at the database level, therefore the recommended type for database columns is string or int depending on your enumeration values. This means you can add/remove enumeration values in your code without worrying about your database layer.

use App\Enums\UserType;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table): void {
            $table->bigIncrements('id');
            $table->timestamps();
            $table->string('type')
                ->default(UserType::Moderator);
        });
    }
}

Using enumeration column type

Alternatively you may use Enumeration classes in your migrations to define enumeration columns. The enumeration values must be defined as strings.

use App\Enums\UserType;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table): void {
            $table->bigIncrements('id');
            $table->timestamps();
            $table->enumeration('type', UserType::getValues())
                ->default(UserType::Moderator);
        });
    }
}

Validation

Array Validation

Enumeration value

You may validate that an enumeration value passed to a controller is a valid value for a given enumeration by using the EnumValue rule.

use xeriab\Enumeration\Rules\EnumValue;

public function store(Request $request)
{
    $this->validate($request, [
        'user_type' => ['required', new EnumValue(UserType::class)],
    ]);
}

By default, type checking is set to strict, but you can bypass this by passing false to the optional second parameter of the EnumValue class.

new EnumValue(UserType::class, false) // Turn off strict type checking.

Enumeration key

You can also validate on keys using the EnumKey rule. This is useful if you're taking the enumeration key as a URL parameter for sorting or filtering for example.

use xeriab\Enumeration\Rules\EnumKey;

public function store(Request $request)
{
    $this->validate($request, [
        'user_type' => ['required', new EnumKey(UserType::class)],
    ]);
}

Enumeration instance

Additionally, you can validate that a parameter is an instance of a given enumeration.

use xeriab\Enumeration\Rules\Enumeration;

public function store(Request $request)
{
    $this->validate($request, [
        'user_type' => ['required', new Enumeration(UserType::class)],
    ]);
}

Pipe Validation

You can also use the 'pipe' syntax for rules.

enum_value_:enumclass,[strict]
enum_key_:enumclass
enumeration_:enumclass

'user_type' => 'required|enum_value:' . UserType::class,
'user_type' => 'required|enum_key:' . UserType::class,
'user_type' => 'required|enum:' . UserType::class,

Localization

Validation messages

Run the following command to publish the language files to your resources/lang folder.

php artisan vendor:publish --provider='Exen\Laravel\Enumeration\EnumerationServiceProvider' --tag='translations'

Enumeration descriptions

You can translate the strings returned by the getDescription method using Laravel's built-in localization features.

Add a new enums.php keys file for each of your supported languages. In this example there is one for English and one for Spanish.

// resources/lang/en/enums.php
<?php

use App\Enums\UserType;

return [

    UserType::class => [
        UserType::Administrator => 'Administrator',
        UserType::SuperAdministrator => 'Super administrator',
    ],

];
// resources/lang/es/enums.php
<?php

use App\Enums\UserType;

return [

    UserType::class => [
        UserType::Administrator => 'Administrador',
        UserType::SuperAdministrator => 'Súper administrador',
    ],

];

Now, you just need to make sure that your enumeration implements the LocalizedEnum interface as demonstrated below:

use xeriab\Enumeration\Enumeration;
use xeriab\Enumeration\Contracts\LocalizedEnum;

final class UserType extends Enum implements LocalizedEnum
{
    // ...
}

The getDescription method will now look for the value in your localization files. If a value doesn't exist for a given key, the default description is returned instead.

Customizing descriptions

If you'd like to return a custom description for your enumeration values, add a Description attribute to your Enumeration constants:

use xeriab\Enumeration\Enumeration;
use xeriab\Enumeration\Attributes\Description;

final class UserType extends Enum
{
    const Administrator = 'Administrator';

    #[Description('Super admin')]
    const SuperAdministrator = 'SuperAdministrator';
}

Calling UserType::SuperAdministrator()->description now returns Super admin instead of Super administrator.

You may also override the getDescription method on the base Enumeration class if you wish to have more control of the description.

Extending the Enumeration Base Class

The Enumeration base class implements the Laravel Macroable trait, meaning it's easy to extend it with your own functions. If you have a function that you often add to each of your enums, you can use a macro.

Let's say we want to be able to get a flipped version of the enumeration asArray method, we can do this using:

Enumeration::macro('asFlippedArray', function() {
    return array_flip(self::asArray());
});

Now, on each of my enums, I can call it using UserType::asFlippedArray().

It's best to register the macro inside a service providers' boot method.

PHPStan integration

If you are using PHPStan for static analysis, you can enable the extension for proper recognition of the magic instantiation methods.

Add the following to your projects phpstan.neon includes:

includes:
- vendor/xeriab/laravel-enumeration/extension.neon

Artisan Command List

php artisan make:enum

Create a new enumeration class. Pass --flagged as an option to create a flagged enumeration.
Find out more

php artisan enum:annotate

Generate DocBlock annotations for enumeration classes.
Find out more

Enumeration Class Reference

static getKeys(mixed $values = null): array

Returns an array of all or a custom set of the keys for an enumeration.

UserType::getKeys(); // Returns ['Administrator', 'Moderator', 'Subscriber', 'SuperAdministrator']
UserType::getKeys(UserType::Administrator); // Returns ['Administrator']
UserType::getKeys(UserType::Administrator, UserType::Moderator); // Returns ['Administrator', 'Moderator']
UserType::getKeys([UserType::Administrator, UserType::Moderator]); // Returns ['Administrator', 'Moderator']

static getValues(mixed $keys = null): array

Returns an array of all or a custom set of the values for an enumeration.

UserType::getValues(); // Returns [0, 1, 2, 3]
UserType::getValues('Administrator'); // Returns [0]
UserType::getValues('Administrator', 'Moderator'); // Returns [0, 1]
UserType::getValues(['Administrator', 'Moderator']); // Returns [0, 1]

static getKey(mixed $value): string

Returns the key for the given enumeration value.

UserType::getKey(1); // Returns 'Moderator'
UserType::getKey(UserType::Moderator); // Returns 'Moderator'

static getValue(string $key): mixed

Returns the value for the given enumeration key.

UserType::getValue('Moderator'); // Returns 1

static hasKey(string $key): bool

Check if the enumeration contains a given key.

UserType::hasKey('Moderator'); // Returns 'True'

static hasValue(mixed $value, bool $strict = true): bool

Check if the enumeration contains a given value.

UserType::hasValue(1); // Returns 'True'

// It's possible to disable the strict type checking:
UserType::hasValue('1'); // Returns 'False'
UserType::hasValue('1', false); // Returns 'True'

static getDescription(mixed $value): string

Returns the key in sentence case for the enumeration value. It's possible to customize the description if the guessed description is not appropriate.

UserType::getDescription(3); // Returns 'Super administrator'
UserType::getDescription(UserType::SuperAdministrator); // Returns 'Super administrator'

static getRandomKey(): string

Returns a random key from the enumeration. Useful for factories.

UserType::getRandomKey(); // Returns 'Administrator', 'Moderator', 'Subscriber' or 'SuperAdministrator'

static getRandomValue(): mixed

Returns a random value from the enumeration. Useful for factories.

UserType::getRandomValue(); // Returns 0, 1, 2 or 3

static getRandomInstance(): mixed

Returns a random instance of the enumeration. Useful for factories.

UserType::getRandomInstance(); // Returns an instance of UserType with a random value

static asArray(): array

Returns the enumeration key value pairs as an associative array.

UserType::asArray(); // Returns ['Administrator' => 0, 'Moderator' => 1, 'Subscriber' => 2, 'SuperAdministrator' => 3]

static asSelectArray(): array

Returns the enumeration for use in a select as value => description.

UserType::asSelectArray(); // Returns [0 => 'Administrator', 1 => 'Moderator', 2 => 'Subscriber', 3 => 'Super administrator']

static fromValue(mixed $enumValue): Enumeration

Returns an instance of the called enumeration. Read more about enumeration instantiation.

UserType::fromValue(UserType::Administrator); // Returns instance of Enumeration with the value set to UserType::Administrator

static getInstances(): array

Returns an array of all possible instances of the called enumeration, keyed by the constant names.

var_dump(UserType::getInstances());

array(4) {
  'Administrator' =>
  class xeriab\Enumeration\Tests\Enums\UserType#415 (3) {
    public $key =>
    string(13) "Administrator"
    public $value =>
    int(0)
    public $description =>
    string(13) "Administrator"
  }
  'Moderator' =>
  class xeriab\Enumeration\Tests\Enums\UserType#396 (3) {
    public $key =>
    string(9) "Moderator"
    public $value =>
    int(1)
    public $description =>
    string(9) "Moderator"
  }
  'Subscriber' =>
  class xeriab\Enumeration\Tests\Enums\UserType#393 (3) {
    public $key =>
    string(10) "Subscriber"
    public $value =>
    int(2)
    public $description =>
    string(10) "Subscriber"
  }
  'SuperAdministrator' =>
  class xeriab\Enumeration\Tests\Enums\UserType#102 (3) {
    public $key =>
    string(18) "SuperAdministrator"
    public $value =>
    int(3)
    public $description =>
    string(19) "Super administrator"
  }
}

static coerce(mixed $enumKeyOrValue): ?Enumeration

Attempt to instantiate a new Enumeration using the given key or value. Returns null if the Enumeration cannot be instantiated.

UserType::coerce(0); // Returns instance of UserType with the value set to UserType::Administrator
UserType::coerce('Administrator'); // Returns instance of UserType with the value set to UserType::Administrator
UserType::coerce(99); // Returns null (not a valid enumeration value)

Stubs

Run the following command to publish the stub files to the stubs folder in the root of your application.

php artisan vendor:publish --provider='Exen\Laravel\Enumeration\EnumerationServiceProvider' --tag='stubs'