/PhpEnums

:nut_and_bolt: Provides enumerations for PHP & frameworks integrations

Primary LanguagePHPMIT LicenseMIT

Elao Enumerations

Latest Stable Version Total Downloads Monthly Downloads Latest Unstable Version License Build Status Coveralls Scrutinizer Code Quality php

This project is greatly inspired by the BiplaneEnumBundle and aims to provide the missing PHP enumerations support.

It will leverage integration of the main PHP frameworks and also provide bridges with other libraries when relevant.

Table of Contents

Why?

An enumeration is a data type, enclosing a single value from a predictable set of members (enumerators). Each enumerator name is a single identifier, materialized by a PHP constant.

Using an enum class provides many benefits:

  • Brings visibility in your code
  • Provides Type Hinting when using the enum class
  • Centralizes enumeration logic within a class
  • Defines utility methods or minor logic owned by your enumeration
  • Helps to describe how to read, serialize, export [, ...] an enumeration
  • Allows common libraries and frameworks integrations.

Enumerations are not options and are not meant to replace constants. Designing an enum type should be done by keeping your domain in mind, and should convey a strong meaning on your application logic.

Wrong use-cases:

  • A set of options used by a library or a method.
  • An unpredictable set of elements.
  • An non-reusable set of elements inside the application.
  • Long sets of elements (languages, locales, currencies, ...)
  • Holding variable data inside the enum type (use an intermediate value object holding the data and the enum instance instead).

Valid use-cases:

  • Gender, civility, predictable roles and permissions, ...
  • A set of supported nodes in an importer, or a set of predefined attributes.
  • In a game: predefined actions, movement directions, character classes, weapon types, ...
  • Any other set of restricted elements.

Why another library ?

  • myclabs/php-enum provides a base enum implementation as a class, inspired from \SplEnum. However, it doesn't provide as many features nor integrations as we wish to.
  • commerceguys/enum only acts as a utility class, but does not intend to instantiate enumerations. Hence, it doesn't allow as many features nor integrations with third-party libraries and frameworks. Manipulating enums as objects is also one of the first motivations of this project.
  • yethee/BiplaneEnumBundle is the first library we got inspiration from. But it was designed as a Symfony Bundle, whereas we opted for a component first approach. Integrations are then provided in a dedicated Bridge namespace and are not restricted to Symfony.

Finally, we used to create similar classes from scratch in some projects lately.
Providing our own package inspired from the best ones, on which we'll apply our own philosophy looks a better way to go.

Features

  • Base implementation for simple, readable and flagged (bitmask) enumerations based on the BiplaneEnumBundle ones.
  • Symfony Form component integration with form types.
  • Symfony Serializer component integration with a normalizer class.
  • Symfony Validator component integration with an enum constraint.
  • Symfony VarDumper component integration with a dedicated caster.
  • Doctrine DBAL integration with abstract classes in order to persist your enumeration in database.

Installation

$ composer require elao/enum

Even if you're using the Symfony full stack framework, there is nothing more to do.
We won't register anything in the Symfony DIC for now. Simply use provided classes.

Usage

Declare your own enumeration by creating a class extending Elao\Enum\Enum:

<?php

use Elao\Enum\Enum;

final class Gender extends Enum
{
    const UNKNOW = 'unknown';
    const MALE = 'male';
    const FEMALE = 'female';

    public static function values(): array
    {
        return [
            self::UNKNOW, 
            self::MALE, 
            self::FEMALE
        ];
    }
}

📝 It's recommended to make your enums classes final, because it won't make sense to extend them in most situations (unless you're creating a new base enum type), and you won't need to mock an enum type.

💡 You can also use the AutoDiscoveredValuesTrait to automagically guess values from the constants defined in your enum, so you don't have to implement EnumInterface::values() yourself.

Get an instance of your enum type:

<?php
$enum = Gender::get(Gender::Male);

You can easily retrieve the enumeration's value by using $enum->getValue();

📝 Enum values are supposed to be integers or strings.

Readable enums

Sometimes, enums may be displayed to the user, or exported in a human readable way.
Hence comes the ReadableEnum:

<?php

use Elao\Enum\ReadableEnum;

final class Gender extends ReadableEnum
{
    const UNKNOW = 'unknown';
    const MALE = 'male';
    const FEMALE = 'female';

    public static function values(): array
    {
        return [
            self::UNKNOW,
            self::MALE,
            self::FEMALE,
        ];
    }

    public static function readables(): array
    {
        return [
            self::UNKNOW => 'Unknown',
            self::MALE => 'Male',
            self::FEMALE => 'Female',
        ];
    }
}

The following snippet shows how to render the human readable value of an enum:

<?php
$enum = Gender::get(Gender::Male); 
$enum->getReadable(); // returns 'Male'
(string) $enum; // returns 'Male'

If you're using a translation library, you can also simply return translation keys from the ReadableEnumInterface::readables() method:

<?php

use Elao\Enum\ReadableEnum;

final class Gender extends ReadableEnum
{
    // ...
    
    public static function readables(): array
    {
        return [
            self::UNKNOW => 'enum.gender.unknown',
            self::MALE => 'enum.gender.male',
            self::FEMALE => 'enum.gender.female',
        ];
    }
}

Using Symfony's translation component:

 # app/Resources/translations/messages.en.yml
 enum.gender.unknown: 'Unknown'
 enum.gender.male: 'Male'
 enum.gender.female: 'Female'
<?php
$enum = Gender::get(Gender::MALE);
// get translator instance...
$translator->trans($enum); // returns 'Male'

Flagged enums

Flagged enumerations are used for bitwise operations. Each value of the enumeration is a single bit flag and can be combined together into a valid bitmask in a single enum instance.

<?php

use Elao\Enum\FlaggedEnum;

final class Permissions extends FlaggedEnum
{
    const EXECUTE = 1;
    const WRITE = 2;
    const READ = 4;

    // You can declare shortcuts for common bit flag combinations
    const ALL = self::EXECUTE | self::WRITE | self::READ;

    public static function values(): array
    {
        return [
            // Only declare valid bit flags:
            static::EXECUTE,
            static::WRITE,
            static::READ,
        ];
    }

    public static function readables(): array
    {
        return [
            static::EXECUTE => 'Execute',
            static::WRITE => 'Write',
            static::READ => 'Read',

            // You can define readable values for specific bit flag combinations:
            static::WRITE | static::READ => 'Read & write',
            static::EXECUTE | static::READ => 'Read & execute',
            static::ALL => 'All permissions',
        ];
    }
}

Get instances using bitwise operations and manipulate them:

<?php
$permissions = Permissions::get(Permissions::EXECUTE | Permissions::WRITE | Permissions::READ);
$permissions = $permissions->withoutFlags(Permissions::EXECUTE); // Returns an instance without "execute" flag
$permissions->getValue(); // Returns 6 (int)
$permissions->getFlags(); // Returns [2, 4] (=> [Permissions::EXECUTE, Permissions::WRITE])

$permissions = $permissions->withoutFlags(Permissions::READ | Permissions::WRITE); // Returns an instance without "read" and "write" flags
$permissions->getValue(); // Returns Permissions::NONE (0). Note: NONE is defined in parent class, FlaggedEnum.
$permissions->getFlags(); // Returns an empty array

$permissions = Permissions::get(Permissions::NONE); // Returns an empty bitmask instance
$permissions = $permissions->withFlags(Permissions::READ | Permissions::EXECUTE); // Returns an instance with "read" and "execute" permissions
$permissions->hasFlag(Permissions::READ); // True
$permissions->hasFlag(Permissions::READ | Permissions::EXECUTE); // True
$permissions->hasFlag(Permissions::WRITE); // False

Compare

Enumeration values are singletons (exact term in this case actually is multiton): it means you'll always get the exact same instance for a given value. Thus, in order to compare two instances, you can simply use the strict comparison operator in order to check references:

<?php
Gender::get(Gender::MALE) === Gender::get(Gender::FEMALE); // False
Gender::get(Gender::MALE) === Gender::get(Gender::MALE); // True
Permissions::get(Permissions::ALL) === Permissions::get(
    Permissions::READ | Permissions::WRITE | Permissions::EXECUTE
); // True

You can also override the EnumInterface::equals(EnumInterface $enumToCompare) in order to implement your own logic to determine if two instances should be considered the same.
The default implementation compares both enum type (the class) and value.

<?php
Gender::get(Gender::MALE)->equals(Gender::get(Gender::FEMALE)) // False
Gender::get(Gender::MALE)->equals(Gender::get(Gender::MALE)) // True

Lastly, you can simply compare an instance with a value by using the EnumInterface::is($value):

<?php
Gender::get(Gender::MALE)->is(Gender::FEMALE) // False
Gender::get(Gender::MALE)->is(Gender::MALE) // True

Shortcuts

Inspired from myclabs/php-enum, you can use shortcuts to instantiate your enumerations, thanks to PHP's __callStatic magic method:

<?php
Gender::MALE(); // Returns an instance of Gender with the MALE value

We recommend you to use this method, if and only if, you and your team use an IDE (e.g PhpStorm) able to interpret the @method tag in class definitions. Then, you can benefit from IDE completion by declaring the following:

<?php

/**
 * @method static Gender UNKNOW()
 * @method static Gender MALE()
 * @method static Gender FEMALE()
 */
final class Gender extends ReadableEnum
{
    const UNKNOW = 'unknown';
    const MALE = 'male';
    const FEMALE = 'female';
    
    // ...
}

Otherwise, simply implement the static methods yourself.

Integrations

Doctrine

You can store the raw value of an enumeration in the database, but still manipulate it as an object from your entities by creating a custom DBAL type, from scratch.

However, this library can help you by providing abstract classes for both string and integer based enumerations.

Create the DBAL type

First, create your DBAL type by extending either AbstractEnumType (string based enum) or AbstractIntegerEnumType (integer based enum, for flagged enums for instance):

<?php

use Elao\Enum\Bridge\Doctrine\DBAL\Types\AbstractEnumType;

final class GenderEnumType extends AbstractEnumType
{
    const NAME = 'gender';

    protected function getEnumClass(): string
    {
        return Gender::class;
    }

    public function getName()
    {
        return static::NAME;
    }
}

Register the DBAL type

Then, you'll simply need to register your DBAL type:

Manually

<?php
// in bootstrapping code
// ...
use Doctrine\DBAL\Types\Type;
Type::addType(GenderEnumType::NAME, GenderEnumType::class);

To convert the underlying database type of your new "gender" type directly into an instance of Gender when performing schema operations, the type has to be registered with the database platform as well:

<?php
$conn = $em->getConnection();
$conn->getDatabasePlatform()->registerDoctrineTypeMapping(GenderEnumType::NAME, GenderEnumType::class);

Using the Doctrine Bundle with Symfony

refs:

# app/config/config.yml
doctrine:
    dbal:
        types:
            gender:  AppBundle\Doctrine\DBAL\Types\GenderEnumType
        mapping_types:
            gender: string

Mapping

When registering the custom types in the configuration, you specify a unique name for the mapping type and map it to the corresponding fully qualified class name. Now the new type can be used when mapping columns:

<?php
class User
{
    /** @Column(type="gender") */
    private $gender;
}

Default value on null

Two methods allow to set a default value if null is retrieved from the database, or before persisting a value:

<?php

abstract class AbstractEnumType extends Type
{
    // ...

    /**
     * What should be returned on null value from the database.
     *
     * @return mixed
     */
    protected function onNullFromDatabase()
    {
        return null;
    }

    /**
     * What should be returned on null value from PHP.
     *
     * @return mixed
     */
    protected function onNullFromPhp()
    {
        return null;
    }
}

Override those methods in order to satisfy your needs.

Symfony Serializer component

Available for Symfony 2.8 and 3.1+

Simply register the following normalizer inside the DIC configuration:

# services.yml
services:
    app.enum_normalizer:
        class: 'Elao\Enum\Bridge\Symfony\Serializer\Normalizer\EnumNormalizer'
        public: false
        tags: [{ name: serializer.normalizer }]

Symfony Form component

Available for Symfony 2.8 and 3.1+

Simple enums

Simply use the EnumType:

<?php

use Elao\Enum\Bridge\Symfony\Form\Type\EnumType;
use MyApp\Enum\Gender;

// ...

$builder->add('gender', EnumType::class, [
    'enum_class' => Gender::class,
]);

// ...

$form->submit($data);
$form->get('gender')->getData(); // Will return a `Gender` instance (or null)

Only the enum_class option is required.

You can use any ChoiceType option as usual (for instance the multiple option).

The field data will be an instance of your enum. If you only want to map values, you can use the as_value option:

<?php

use Elao\Enum\Bridge\Symfony\Form\Type\EnumType;
use MyApp\Enum\Gender;

// ...

$builder->add('gender', EnumType::class, [
    'enum_class' => Gender::class,
    'as_value' => true,
]);

// ...

$form->submit($data);
$form->get('gender')->getData(); // Will return a string value defined in the `Gender` enum (or null)

You can restrict the list of proposed enumerations by overriding the choices option:

<?php

use Elao\Enum\Bridge\Symfony\Form\Type\EnumType;
use MyApp\Enum\Gender;

// ...

$builder->add('gender', EnumType::class, [
    'enum_class' => Gender::class,
    'choices' => [
        Gender::get(Gender::MALE), 
        Gender::get(Gender::FEMALE),
    ],
]);

// or:

$builder->add('gender', EnumType::class, [
    'enum_class' => Gender::class,
    'as_value' => true,
    'choices' => [
        Gender::readableFor(Gender::MALE) => Gender::MALE,
        Gender::readableFor(Gender::FEMALE) => Gender::FEMALE,
    ],
]);

Flagged enums

Simply use the FlaggedEnumType (which extends EnumType):

<?php

use Elao\Enum\Bridge\Symfony\Form\Type\FlaggedEnumType;
use MyApp\Enum\Permissions;

// ...

$builder->add('permissions', FlaggedEnumType::class, [
    'enum_class' => Permissions::class,
]);

// ...

$form->submit($data);
$form->get('permissions')->getData(); // Will return a single `Permissions` instance composed of selected bit flags

Same options are available, but on the contrary of the EnumType, the multiple option is always true and cannot be set to false (You'll always get a single enum instance though).

Symfony Validator component

Available for Symfony 2.8 and 3.1+

The library provides a Elao\Enum\Bridge\Symfony\Validator\Constraint\Enum constraint which makes use of Symfony's built-in Choice constraint and validator internally.

To use the constraint, simply provide the enum class:

# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\User:
    properties:
        gender:
            - Elao\Enum\Bridge\Symfony\Validator\Constraint\Enum: MyApp\Enum\Gender

If the property value is not an enum instance, set the asValue option to true in order to simply validate the enum value:

# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\User:
    properties:
        gender:
            - Elao\Enum\Bridge\Symfony\Validator\Constraint\Enum:
                class: MyApp\Enum\Gender
                asValue: true

You can restrict the available choices by setting the allowed values in the choices option:

# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\User:
    properties:
        gender:
            - Elao\Enum\Bridge\Symfony\Validator\Constraint\Enum:
                class: MyApp\Enum\Gender
                choices: 
                    - female
                    - !php/const:MyApp\Enum\Gender::MALE # You can use PHP constants with the YAML format since Symfony 3.2

The choice option only accepts enum values and normalize it internally to enum instances if asValue is false.

You can also use a callback:

# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\User:
    properties:
        gender:
            - Elao\Enum\Bridge\Symfony\Validator\Constraint\Enum:
                class: MyApp\Enum\Gender
                callback: ['allowedValues']

Where allowedValues is a static method of MyApp\Enum\Gender, returning allowed instances (⚠️ should return values if asValue is set to true).

Any other Choice option (as multiple, min, ...) is available with the Enum constraint.

Symfony VarDumper component

Available for Symfony 2.8 and 3.1+

By requiring this package and if symfony/var-dumper is installed, an EnumCaster is registered automatically to enhance enum instances dump output.

For instance, here's what it'll look like when dumping a flagged enum instance:

<?php

use Elao\Enum\Tests\Fixtures\Enum\Permissions;

dump(Permissions::get(Permissions::ALL));
HTML output CLI output
var-dumper-integration-cli var-dumper-integration-cli

Faker

The PhpEnums library provides an Elao\Enum\Bridge\Faker\Provider\EnumProvider to generate fixtures.

Its constructor receives a mapping between class aliases and your Enum classes' FQCN as first parameter:

<?php

use Elao\Enum\Bridge\Faker\Provider\EnumProvider;

$provider = new EnumProvider([
    'Civility' => Namespace\To\MyCivilityEnum::class,
    'Gender' => Namespace\To\MyGenderEnum::class,
]);

The provider exposes two public methods:

  • EnumProvider::enum(string $enumValueShortcut): EnumInterface in order to generate deterministic enums
  • EnumProvider::randomEnum(string $enumClassAlias): EnumInterface in order to generate random enums

Usage with Alice

If you're using the nelmio/alice package and the bundle it provides in order to generate fixtures, you can register the Faker provider by using the nelmio_alice.faker.generator:

# services.yml
services:
    app.faker.enum_provider:
        class: Elao\Enum\Bridge\Faker\Provider\EnumProvider
        arguments:
            - Civility: Namespace\To\MyCivilityEnum
              Gender: Namespace\To\MyGenderEnum
        tags: [{ name: nelmio_alice.faker.provider }]

The following example shows how to use the provider within a Yaml fixture file:

MyEntity:
    entity1:
        civility: <enum(Civility::MISTER)>
        gender: <enum(Gender::MALE>
        # You can use the pipe character in order to combine flagged enums:
        permissions: <enum(Permissions::READ|WRITE>
    entity2:
        civility: <randomEnum(Civility)>
        gender: <randomEnum(Gender)>
        permissions: <randomEnum(Permissions)>

📝 MISTER in <enum(Civility::MISTER)> refers to a constant defined in the Civility enum class, not to a constant's value ('mister' string for instance).

API

Simple enum

Method Static Returns Description
get($value) Yes static Returns the instance of the enum type for given value.
values() Yes int[]|string[] Should return any possible value for the enumeration.
accepts($value) Yes bool True if the value is acceptable for this enumeration.
instances() Yes static[] Instantiates and returns an array containing every enumeration instance for possible values.
getValue() No int|string Returns the enumeration instance value.
equals(EnumInterface $enum) No bool Determines whether two enumerations instances should be considered the same.
is($value) No bool Determines if the enumeration instance value is equal to the given value.

Readable enum

Method Static Returns Description
readables() Yes string[] Should return an array of the human representations indexed by possible values.
readableFor($value) Yes string Get the human representation for given enumeration value.
getReadable() No string Get the human representation for the current instance.
__toString() No string Allows to convert the instance to the human representation of the current value by casting it to a string.

Flagged enum

Method Static Returns Description
accepts($value) Yes bool Same as before, but accepts bit flags and bitmasks.
readableForNone() Yes string Override this method to replace the default human representation of the "no flag" value.
readableFor($value, string $separator = '; ') Yes string Same as before, but allows to specify a delimiter between single bit flags (if no human readable representation is found for the combination).
getReadable(string $separator = '; ') No string Same as before, but with a delimiter option (see above).
getFlags() No int[] Returns an array of bit flags set in the current enumeration instance.
hasFlag(int $bitFlag) No bool True if the current instance has the given bit flag(s).
withFlags(int $flags) No static Computes a new value with given flags, and returns the corresponding instance.
withoutFlags(int $flags) No static Computes a new value without given flags, and returns the corresponding instance.