datomatic/enum-helper

Simple opinionated framework agnostic PHP 8.1 enum helper

v2.0.0 2024-10-31 10:56 UTC

This package is auto-updated.

Last update: 2024-12-02 10:10:24 UTC


README

Enum Helper-DarkEnum Helper-Light

Enum Helper

Latest Version on Packagist Pest Tests number GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

A simple and opinionated collections of PHP 8.1 enum helpers inspired by archtechx/enums and BenSampo/laravel-enum.
This package is framework agnostic, but if you use Laravel consider to use this linked package datomatic/laravel-enum-helper and datomatic/laravel-enum-collections.

Upgrade from v1.x

Please see the upgrade.md file.

Functionalities summary

  • Invokable cases: get the value of enum "invoking" it statically
  • Construct enum by name or value: wrap(), from(), tryFrom(), fromName(), tryFromName(), fromValue(), tryFromValue() methods
  • Enums Inspection: isPure(), isBacked(), has(), hasName(), hasValue() methods
  • Enums Equality: is(), isNot(), in(), notIn() methods
  • Names: methods to have a list of case names (names(), namesByValue())
  • Values: methods to have a list of case values (values(), valuesByName())
  • Serialization: get an unique identifier from instance or instance from identifier (serialize(), unserialize())
  • Descriptions & Translations: add description to enum with optional translation (description(),descriptions(),descriptionsByName(),descriptionsByValue(),nullableDescriptionsByValue())
  • Labels: add label to enum (label(),labels(),labelsByName(),labelsByValue(),nullableLabelsByValue())
  • Properties: methods to have a list of properties (dynamicList(),dynamicByKey())

Installation

PHP 8.1+ is required.

composer require datomatic/enum-helper

Usage

You can use the traits you need, but for convenience, you can use only the EnumHelper trait that includes (EnumInvokable, EnumFroms, EnumNames, EnumValues, EnumInspection, EnumEquality).
EnumDescription and EnumSerialization are separated from EnumHelper because they cover edge cases.

The helper support both pure enum (e.g. PureEnum, PascalCasePureEnum) and BackedEnum (e.g. IntBackedEnum, StringBackedEnum).

In all examples we'll use the classes described below:

use Datomatic\EnumHelper\EnumHelper;

// Pure enum
enum PureEnum
{
    use EnumHelper;
    
    case PENDING;
    case ACCEPTED;
    case DISCARDED;
    case NO_RESPONSE;
}
enum PascalCasePureEnum
{
    use EnumHelper;
    
    case Pending;
    case Accepted;
    case Discarded;
    case NoResponse;
}

// BackedEnum
enum StringBackedEnum: string
{
    use EnumHelper;
    
    case PENDING = 'P';
    case ACCEPTED = 'A';
    case DISCARDED = 'D';
    case NO_RESPONSE = 'N';
}
enum IntBackedEnum: int
{
    use EnumHelper;
    
    case PENDING = 0;
    case ACCEPTED = 1;
    case DISCARDED = 2;
    case NO_RESPONSE = 3;
}

The package works with cases written in UPPER_CASE, snake_case and PascalCase.

Jump To

Invokable Cases

This helper lets you get the value of a BackedEnum, or the name of a pure enum, by "invoking" it both statically (PureEnum::pending()), and as an instance ($status()).
A good approach is to call methods in camelCase mode, but you can invoke the enum in all cases ::STATICALLY(), ::statically() or ::Statically().

IntBackedEnum::PENDING // PureEnum enum instance
IntBackedEnum::pending(); // 0

That way permits you to use enum invoke into an array keys definition:

'statuses' => [
    PureEnum::pending() => 'some configuration',
...

or in database interactions $db_field_definition->default(PureEnum::pending()) or invoke instances to get the primitive value

public function updateStatus(int $status): void;

$task->updateStatus(IntBackedEnum::pending());

Examples use static calls to get the primitive value

// Pure Enum
PureEnum::noResponse(); // 'NO_RESPONSE'
PureEnum::NO_RESPONSE(); // 'NO_RESPONSE'
PureEnum::NoResponse(); // 'NO_RESPONSE'

// Pure Enum with PascalCase
PascalCasePureEnum::noResponse(); // 'NoResponse'
PascalCasePureEnum::NO_RESPONSE(); // 'NoResponse'
PascalCasePureEnum::NoResponse(); // 'NoResponse'

// IntBackedEnum
IntBackedEnum::pending(); // 0

// StringBackedEnum
StringBackedEnum::pending(); // 'P'

IDE code completion

To have a code completion you can get autosuggestions while typing the enum case and then add () or you can add phpDoc @method tags to the enum class to define all invokable cases like this:

/**
 * @method static string pending()
 * @method static string accepted()
 * @method static string discarded()
 * @method static string noResponse()
 */
enum PureEnum
...

From FromName

This helper adds from() and tryFrom() to pure enums, fromValue() and tryFromValue() (alias of from() and tryFrom()), fromName() and tryFromName() to all enums

Important Notes:

  • BackedEnum instances already implement their own from() and tryFrom() methods, which will not be overridden by this trait.

from()

// Pure Enum
PureEnum::from('PENDING'); // PureEnum::PENDING
PascalCasePureEnum::from('Pending'); // PascalCasePureEnum::Pending
PureEnum::from('MISSING'); // ValueError Exception
// BackedEnum
StringBackedEnum::from('P'); // StringBackedEnum::PENDING
StringBackedEnum::from('M'); // ValueError Exception

tryFrom()

// Pure Enum
PureEnum::tryFrom('PENDING'); // PureEnum::PENDING
PureEnum::tryFrom('MISSING'); // null
// BackedEnum
StringBackedEnum::tryFrom('P'); // StringBackedEnum::PENDING
StringBackedEnum::tryFrom('M'); // null

fromName()

// Pure Enum
PureEnum::fromName('PENDING'); // PureEnum::PENDING
PureEnum::fromName('MISSING'); // ValueError Exception
// BackedEnum
StringBackedEnum::fromName('PENDING'); // StringBackedEnum::PENDING
StringBackedEnum::fromName('MISSING'); // ValueError Exception

tryFromName()

// Pure Enum
PureEnum::tryFromName('PENDING'); // PureEnum::PENDING
PureEnum::tryFromName('MISSING'); // null
// BackedEnum
StringBackedEnum::tryFromName('PENDING'); // StringBackedEnum::PENDING
StringBackedEnum::tryFromName('MISSING'); // null

Inspection

This helper permits check the type of enum (isPure(),isBacked()) and if enum contains a case name or value (has(), doesntHave(), hasName(), doesntHaveName(), hasValue(), doesntHaveValue()).

isPure(), isBacked(), isIntBacked()and isStringBacked()

With these methods you can check the type of the enum instance.

PureEnum::PENDING->isPure() // true
PureEnum::PENDING->isBacked() // false
IntBackedEnum::PENDING->isPure() // false
IntBackedEnum::PENDING->isIntBacked() // true
StringBackedEnum::PENDING->isBacked() // true
StringBackedEnum::PENDING->isStringBacked() // true

has() and doesntHave()

has() method permit checking if an enum has a case (name or value) by passing int, string or enum instance. For convenience, there is also an doesntHave() method which is the exact reverse of the has() method.

PureEnum::has('PENDING') // true
IntBackedEnum::has(10) // false
IntBackedEnum::has(1) // true
IntBackedEnum::has('1') // true
StringBackedEnum::has('ACCEPTED') // true
StringBackedEnum::has('A') // true
StringBackedEnum::doesntHave('A') // false

hasName() and doesntHaveName()

hasName() method permit checking if an enum has a case name. For convenience, there is also an doesntHaveName() method which is the exact reverse of the hasName() method.

PureEnum::hasName('PENDING') // true
PureEnum::hasName('P') // false
IntBackedEnum::hasName('ACCEPTED') // true
IntBackedEnum::hasName(1) // false
StringBackedEnum::doesntHaveName('ACDSIED') // true
StringBackedEnum::hasName('A') // false

hasValue() and doesntHaveValue()

hasValue() method permit checking if an enum has a case by passing int, string or enum instance. For convenience, there is also an doesntHaveValue() method which is the exact reverse of the hasValue() method.

PureEnum::hasValue('PENDING') // true
PureEnum::hasValue('P') // false
IntBackedEnum::hasValue('ACCEPTED') // false
IntBackedEnum::hasValue(1) // true
StringBackedEnum::doesntHaveValue('Z') // true
StringBackedEnum::hasValue('A') // true

Equality

This helper permits to compare an enum instance (is(),isNot()) and search if it is present inside an array (in(),notIn()).

is() and isNot()

is() method permit checking the equality of an instance against an enum instance, a case name, or a case value.
For convenience, there is also an isNot() method which is the exact reverse of the is() method.

$enum = PureEnum::PENDING;
$enum->is(PureEnum::PENDING); // true
PureEnum::PENDING->is(PureEnum::ACCEPTED); // false
PureEnum::PENDING->is('PENDING'); // true
PureEnum::PENDING->is('ACCEPTED'); // false
PureEnum::PENDING->isNot('ACCEPTED'); // true


$backedEnum = IntBackedEnum::PENDING;
$backedEnum->is(IntBackedEnum::PENDING); // true
IntBackedEnum::PENDING->is(IntBackedEnum::ACCEPTED); // false
IntBackedEnum::PENDING->is(0); // true
IntBackedEnum::PENDING->is('PENDING'); // true
StringBackedEnum::PENDING->is('P'); // true
StringBackedEnum::PENDING->isNot('P'); // false

in() and notIn()

in() method permit to see if an instance matches on an array of instances, names or values. For convenience, there is also a notIn() method which is the exact reverse of the i() method.

$enum = PureEnum::PENDING;
$enum->in([PureEnum::PENDING,PureEnum::ACCEPTED]); // true
PureEnum::PENDING->in([PureEnum::DISCARDED, PureEnum::ACCEPTED]); // false
PureEnum::PENDING->in(['PENDING', 'ACCEPTED']); // true
PureEnum::PENDING->in(['ACCEPTED', 'DISCARDED']); // false
PureEnum::PENDING->notIn(['ACCEPTED']); // true

$backedEnum = IntBackedEnum::PENDING;
$backedEnum->in([IntBackedEnum::PENDING, IntBackedEnum::ACCEPTED]); // true
IntBackedEnum::PENDING->in([IntBackedEnum::ACCEPTED])// false
IntBackedEnum::PENDING->in([0, 1, 2]); // true
IntBackedEnum::PENDING->in([2, 3]); // false
IntBackedEnum::PENDING->in(['PENDING', 'ACCEPTED']); // true
IntBackedEnum::PENDING->in(['DISCARDED', 'ACCEPTED']); // false
StringBackedEnum::PENDING->in(['P', 'D']); // true
StringBackedEnum::PENDING->notIn(['A','D']); // true

Names

This helper offer names() and namesByValue() methods.

names()

This method returns a list of case names in the enum.

PureEnum::names(); // ['PENDING', 'ACCEPTED', 'DISCARDED', 'NO_RESPONSE']
PascalCasePureEnum::names(); // ['Pending', 'Accepted', 'Discarded', 'NoResponse']
StringBackedEnum::names(); // ['PENDING', 'ACCEPTED', 'DISCARDED', 'NO_RESPONSE']
// Subset
PureEnum::names([PureEnum::NO_RESPONSE, PureEnum::DISCARDED]); // ['NO_RESPONSE', 'DISCARDED']
PascalCasePureEnum::names([PascalCasePureEnum::Accepted, PascalCasePureEnum::Discarded]); // ['Accepted', 'Discarded']

namesByValue()

This method returns an associative array of [value => name] on BackedEnum, [name => name] on pure enum.

PureEnum::namesByValue(); // [ 'PENDING' => 'PENDING', 'ACCEPTED' => 'ACCEPTED', 'DISCARDED' => 'DISCARDED'...
StringBackedEnum::namesByValue(); // [ 'P' => 'PENDING', 'A' => 'ACCEPTED', 'D' => 'DISCARDED'...
IntBackedEnum::namesByValue(); // [ 0=>'PENDING', 1=>'ACCEPTED', 2=>'DISCARDED'...
// Subset
IntBackedEnum::namesByValue([IntBackedEnum::NO_RESPONSE, IntBackedEnum::DISCARDED]); // [ 3=>'NO_RESPONSE', 2=>'DISCARDED']

Values

This helper offer values() and valuesByName() methods.

values()

This method returns a list of case values for BackedEnum or a list of case names for pure enums.

PureEnum::values(); // ['PENDING', 'ACCEPTED', 'DISCARDED', 'NO_RESPONSE']
StringBackedEnum::values(); // ['P', 'A', 'D', 'N']
IntBackedEnum::values(); // [0, 1, 2, 3]
// Subset
PureEnum::values([PureEnum::NO_RESPONSE, PureEnum::DISCARDED]); // ['NO_RESPONSE', 'DISCARDED']
StringBackedEnum::values([StringBackedEnum::NO_RESPONSE, StringBackedEnum::DISCARDED]); // ['N', 'D']
IntBackedEnum::values([IntBackedEnum::NO_RESPONSE, IntBackedEnum::DISCARDED]); // [3, 2]

valuesByName()

This method returns a associative array of [name => value] on BackedEnum, [name => name] on pure enum.

PureEnum::valuesByName(); // ['PENDING' => 'PENDING','ACCEPTED' => 'ACCEPTED','DISCARDED' => 'DISCARDED',...]
StringBackedEnum::valuesByName(); // ['PENDING' => 'P','ACCEPTED' => 'A','DISCARDED' => 'D','NO_RESPONSE' => 'N']
IntBackedEnum::valuesByName(); // ['PENDING' => 0,'ACCEPTED' => 1,'DISCARDED' => 2,'NO_RESPONSE' => 3]
// Subset
PureEnum::valuesByName([PureEnum::NO_RESPONSE, PureEnum::DISCARDED]); // ['NO_RESPONSE' => 'NO_RESPONSE', 'DISCARDED' => 'DISCARDED']
StringBackedEnum::valuesByName([StringBackedEnum::NO_RESPONSE, StringBackedEnum::DISCARDED]); // ['NO_RESPONSE' => 'N', 'DISCARDED' => 'D']
IntBackedEnum::valuesByName([IntBackedEnum::NO_RESPONSE, IntBackedEnum::DISCARDED]); // ['NO_RESPONSE' => 3, 'DISCARDED' => 2]

Serialization

This helper permits to get an unique identifier from enum or an enum instance from identifier.

The helper is not included on the base EnumHelper trait and does not depend on it, so if you need it you must use EnumSerialization.

use Datomatic\EnumHelper\Traits\EnumSerialization;

enum PureEnum
{
    use EnumSerialization;
    
    ...

serialize()

This method returns the enum unique identifier based on Namespace\ClassName::CASE_NAME. You can use this identifier to save multiple types of enums in a database on a polymorphic column.

PureEnum::PENDING->serialize(); // Namespace\PureEnum.PENDING
$enum = StringBackedEnum::NO_RESPONSE;
$enum->serialize(); // Namespace\StringBackedEnum.NO_RESPONSE

unserialize()

This method returns an enum instance from unique identifier.

PureEnum::unserialize('Namespace\PureEnum::PENDING'); // PureEnum::PENDING
IntBackedEnum::unserialize('Namespace\IntBackedEnum::PENDING'); // IntBackedEnum::PENDING
IntBackedEnum::unserialize('NOT::valid::uniqueId'); // throw InvalidSerializedValue Exception
IntBackedEnum::unserialize('Wrong\Namespace\IntBackedEnum::PENDING'); // throw InvalidSerializedValue Exception
IntBackedEnum::unserialize('Namespace\IntBackedEnum::MISSING'); // throw InvalidSerializedValue Exception

Global unserializeEnum() helper

The method unserialize() has little possibility of use because it's related to only an enum class. A better approach is to create a global helper to instantiate any enum from serialization like this:

use Datomatic\EnumHelper\Exceptions\InvalidSerializedValue;

public function unserializeEnum(string $value): object
{
    if (
        !strpos($value, '.')
        || substr_count($value, '.') !== 1
    ) {
        throw InvalidSerializedValue::serializedFormatIsInvalid($value);
    }

    list($enumClass, $enumName) = explode('.', $value);

    foreach ($enumClass::cases() as $case){
        if( $case->name === $enumName){
                return $case;
            }
        }
    }
    
    throw InvalidSerializedValue::caseNotPresent($case);
}

json_encode on PureEnum

By default, you can't use json_encode() on PureEnum because it hasn't a value. Using this trait and implementing JsonSerializable interface you can use json_encode().

enum Status implements \JsonSerializable
{
    use EnumSerialization;
    
    case PENDING;
    case ACCEPTED;
}

json_encode(Status::PENDING); // '"PENDING"'

Descriptions and Translations

This helper permits to have a description of each case of an enum. Work with both singular language and multilingual application. This is useful when you need descriptions to characterize the cases better or in a multilingual context.

The helper is not included on the base EnumHelper trait and does not depend on it, so if you need it you must use EnumDescription and implement the abstract description() method to define the descriptions. You can use it on both pure enums and BackedEnum.

use Datomatic\EnumHelper\EnumHelper;
use Datomatic\EnumHelper\Traits\EnumDescription;

enum StringBackedEnum: string
{
    use EnumHelper;
    use EnumDescription;
    
    case PENDING = 'P';
    case ACCEPTED = 'A';
    case DISCARDED = 'D';
    case NO_RESPONSE = 'N';

    public function description(?string $lang = null): string
    {
        return match ($this) {
            self::PENDING => 'Await decision',
            self::ACCEPTED => 'Recognized valid',
            self::DISCARDED => 'No longer useful',
            self::NO_RESPONSE => 'No response',
        };
    }

After the implementation of description() method you can use it

PureEnum::PENDING->description(); // 'Await decision'

Localization

You can change the description() method with your translation method/helper to translate the descriptions.

public function description(?string $lang = null): string
    {
        // this is only an example of implementation... translate method not exist
        // if $lang is null you have to use the current locale
        return return translate('status.'$this->name, $lang);
        
        // or translate each case
        return match ($this) {
            self::PENDING => translate('Await decision'),
            self::ACCEPTED => translate('Recognized valid'),
            self::DISCARDED => translate('No longer useful'),
            self::NO_RESPONSE => translate('No response'),
        };
        
        //or use EnumSerialization trait
        return translate($this->serialize(), $lang);
    }

After the implementation of description method you can use it

$enum = PureEnum::PENDING;
$enum->description(); // 'Await decision'
$enum->description('it'); // 🇮🇹 'In attesa'

descriptions()

This method returns a list of case descriptions of enum.

StringBackedEnum::descriptions(); // ['Await decision','Recognized valid','No longer useful','No response']
// Subset
StringBackedEnum::descriptions([StringBackedEnum::ACCEPTED, StringBackedEnum::NO_RESPONSE]); // ['Recognized valid','No response']

descriptionsByValue()

This method returns an associative array of [value => description] on BackedEnum, [name => description] on pure enum.

StringBackedEnum::descriptionsByValue(); // ['P' => 'Await decision', 'A' => 'Recognized valid',...
PureEnum::descriptionsByValue(); // ['PENDING' => 'Await decision', 'ACCEPTED' => 'Recognized valid',...
PureEnum::descriptionsByValue(lang: 'it'); // ['PENDING' => 'In attesa', 'ACCEPTED' => 'Valido',...
// Subset
StringBackedEnum::descriptionsByValue([StringBackedEnum::DISCARDED, StringBackedEnum::ACCEPTED]); // ['D' => 'No longer useful', 'A' => 'Recognized valid']
PureEnum::descriptionsByValue([[PureEnum::PENDING, PureEnum::DISCARDED]); // ['PENDING' => 'Await decision', 'DISCARDED' => 'No longer useful']
PureEnum::descriptionsByValue([[PureEnum::PENDING, PureEnum::DISCARDED],'it'); // ['PENDING' => 'In attesa', 'DISCARDED' => 'Scartato']

nullableDescriptionsByValue()

This method prepend to descriptionsByValue() returns a default value usefull when do you need nullable select on a form.

StringBackedEnum::nullableDescriptionsByValue('Select value'); // [null => 'Select value', 'P' => 'Await decision', 'A' => 'Recognized valid',...

descriptionsByName()

This method returns an associative array of [name => description].

StringBackedEnum::descriptionsByName(); // ['PENDING' => 'Await decision', 'ACCEPTED' => 'Recognized valid',...
PureEnum::descriptionsByName(lang: 'it'); // ['PENDING' => 'In attesa', 'ACCEPTED' => 'Valido',...
// Subset
StringBackedEnum::descriptionsByName([StringBackedEnum::DISCARDED, StringBackedEnum::ACCEPTED]); // ['DISCARDED' => 'No longer useful', 'ACCEPTED' => 'Recognized valid']
PureEnum::descriptionsByName([[PureEnum::PENDING, PureEnum::DISCARDED],'it'); // ['PENDING' => 'In attesa', 'DISCARDED' => 'Scartato']

Labels

The EnumLabel trait it's the same of EnumDescription but you can use if prefer call label method instead description.

Properties

The EnumProperties trait it's used to get properties list dynamically. If your enum has a method to define a property like color() you can use this trait in this mode:

StringBackedEnum::dynamicList(method: 'color');
StringBackedEnum::dynamicByKey(key: 'value', method: 'color');

//Subset and Locale
StringBackedEnum::dynamicList(method: 'color',[StringBackedEnum::DISCARDED, StringBackedEnum::ACCEPTED], 'it');
StringBackedEnum::dynamicByKey(key: 'value', method: 'color', [StringBackedEnum::DISCARDED, StringBackedEnum::ACCEPTED], 'it');