Enumerations on PHP

Enumerations on PHP

Depending upon use case, I would normally use something simple like the following:

abstract class DaysOfWeek
{
const Sunday = 0;
const Monday = 1;
// etc.
}

$today = DaysOfWeek::Sunday;

However, other use cases may require more validation of constants and values. Based on the comments below about reflection, and a few other notes, here's an expanded example which may better serve a much wider range of cases:

abstract class BasicEnum {
private static $constCacheArray = NULL;

private static function getConstants() {
if (self::$constCacheArray == NULL) {
self::$constCacheArray = [];
}
$calledClass = get_called_class();
if (!array_key_exists($calledClass, self::$constCacheArray)) {
$reflect = new ReflectionClass($calledClass);
self::$constCacheArray[$calledClass] = $reflect->getConstants();
}
return self::$constCacheArray[$calledClass];
}

public static function isValidName($name, $strict = false) {
$constants = self::getConstants();

if ($strict) {
return array_key_exists($name, $constants);
}

$keys = array_map('strtolower', array_keys($constants));
return in_array(strtolower($name), $keys);
}

public static function isValidValue($value, $strict = true) {
$values = array_values(self::getConstants());
return in_array($value, $values, $strict);
}
}

By creating a simple enum class that extends BasicEnum, you now have the ability to use methods thusly for simple input validation:

abstract class DaysOfWeek extends BasicEnum {
const Sunday = 0;
const Monday = 1;
const Tuesday = 2;
const Wednesday = 3;
const Thursday = 4;
const Friday = 5;
const Saturday = 6;
}

DaysOfWeek::isValidName('Humpday'); // false
DaysOfWeek::isValidName('Monday'); // true
DaysOfWeek::isValidName('monday'); // true
DaysOfWeek::isValidName('monday', $strict = true); // false
DaysOfWeek::isValidName(0); // false

DaysOfWeek::isValidValue(0); // true
DaysOfWeek::isValidValue(5); // true
DaysOfWeek::isValidValue(7); // false
DaysOfWeek::isValidValue('Friday'); // false

As a side note, any time I use reflection at least once on a static/const class where the data won't change (such as in an enum), I cache the results of those reflection calls, since using fresh reflection objects each time will eventually have a noticeable performance impact (Stored in an assocciative array for multiple enums).

Now that most people have finally upgraded to at least 5.3, and SplEnum is available, that is certainly a viable option as well--as long as you don't mind the traditionally unintuitive notion of having actual enum instantiations throughout your codebase. In the above example, BasicEnum and DaysOfWeek cannot be instantiated at all, nor should they be.

What is the recommended way of creating enum-like behaviour in PHP 7?

I would still use an array but based on the ReflectionClass to check if the given value is defined within the class constants

<?
class DayOfTheWeek
{
private $__value;
private $__day;

const Monday = 1;
const Tuesday = 2;
const Wednesday = 3;
const Thursday = 4;
const Friday = 5;
const Saturday = 6;
const Sunday = 7;

public function __construct(int $value)
{
$myClass = new ReflectionClass ( get_class($this) );
$constants = $myClass->getConstants();
foreach($constants as $dayName=>$dayValue) {
if($value === $dayValue) {
$this->__day = $dayName;
$this->__value = $dayValue;
break;
}
}
if(is_null($this->__day)) {
throw new InvalidArgumentException("Invalid day of the week");
}
}

public function GetValue(): int
{
return $this->__value;
}
}

Usage:

$return = new DayOfTheWeek(5);
var_dump($return);

Output:

object(DayOfTheWeek)#1 (2) {
["__value":"DayOfTheWeek":private]=>
int(5)
["__day":"DayOfTheWeek":private]=>
string(6) "Friday"
}

I was not sure about performance issues but found this on SO

Update: Upon reading the documentation it seems I was not the only one who came up with the idea...still pretty ugly though...

Enumerations on PHP

Depending upon use case, I would normally use something simple like the following:

abstract class DaysOfWeek
{
const Sunday = 0;
const Monday = 1;
// etc.
}

$today = DaysOfWeek::Sunday;

However, other use cases may require more validation of constants and values. Based on the comments below about reflection, and a few other notes, here's an expanded example which may better serve a much wider range of cases:

abstract class BasicEnum {
private static $constCacheArray = NULL;

private static function getConstants() {
if (self::$constCacheArray == NULL) {
self::$constCacheArray = [];
}
$calledClass = get_called_class();
if (!array_key_exists($calledClass, self::$constCacheArray)) {
$reflect = new ReflectionClass($calledClass);
self::$constCacheArray[$calledClass] = $reflect->getConstants();
}
return self::$constCacheArray[$calledClass];
}

public static function isValidName($name, $strict = false) {
$constants = self::getConstants();

if ($strict) {
return array_key_exists($name, $constants);
}

$keys = array_map('strtolower', array_keys($constants));
return in_array(strtolower($name), $keys);
}

public static function isValidValue($value, $strict = true) {
$values = array_values(self::getConstants());
return in_array($value, $values, $strict);
}
}

By creating a simple enum class that extends BasicEnum, you now have the ability to use methods thusly for simple input validation:

abstract class DaysOfWeek extends BasicEnum {
const Sunday = 0;
const Monday = 1;
const Tuesday = 2;
const Wednesday = 3;
const Thursday = 4;
const Friday = 5;
const Saturday = 6;
}

DaysOfWeek::isValidName('Humpday'); // false
DaysOfWeek::isValidName('Monday'); // true
DaysOfWeek::isValidName('monday'); // true
DaysOfWeek::isValidName('monday', $strict = true); // false
DaysOfWeek::isValidName(0); // false

DaysOfWeek::isValidValue(0); // true
DaysOfWeek::isValidValue(5); // true
DaysOfWeek::isValidValue(7); // false
DaysOfWeek::isValidValue('Friday'); // false

As a side note, any time I use reflection at least once on a static/const class where the data won't change (such as in an enum), I cache the results of those reflection calls, since using fresh reflection objects each time will eventually have a noticeable performance impact (Stored in an assocciative array for multiple enums).

Now that most people have finally upgraded to at least 5.3, and SplEnum is available, that is certainly a viable option as well--as long as you don't mind the traditionally unintuitive notion of having actual enum instantiations throughout your codebase. In the above example, BasicEnum and DaysOfWeek cannot be instantiated at all, nor should they be.

How to implement Enum like functionality in PHP?

Using const, perhaps.

class SomeClass {
const FIRSTVAL = 1;
const SECONDVAL = 2;
};

How to iterate over enum

You can generates a list of cases on an enum with cases() like this:

enum Shapes
{
case RECTANGLE;
case SQUARE;
case CIRCLE;
case OVAL;
}

foreach (Shapes::cases() as $shape) {
echo $shape->name . "\n";
}

The output of this is:

RECTANGLE
SQUARE
CIRCLE
OVAL

for PHP 8.1 and greater.

See: PHP Fiddle

How to cast to dynamically defined enums in PHP without using variables directly?

PHP enumerations are objects and can be determined by autoloading. So if your variable only contains the name of the enumeration, you can first check if the enumeration exists and then use BackedEnum::tryFrom() or BackedEnum::from() to get it.

<?php
declare(strict_types=1);
namespace Marcel\Enums;

enum Status: string
{
case Active = 'active';
case Retired = 'retired';
}


$fqcn = Status::class;
$enum = null;
$value = 'active';

if (enum_exists($fqcn)) {
$enum = $fqcn::from($value);
}

There is no additional functionality to get the object first and then execute some method of the object. You can get the fully qualified classname by get_class() and check in a next step, if the class (enum) exists and as a last step call the BackedEnum::from() method.

How to get all values of an enum in PHP?

After some research I found the answer. You can use the static method: cases().

enum Status
{
case PAID;
case Cancelled;
}

Status::cases();

The cases method will return an array with an enum (UnitEnum interface) for each value.

Getting values for an enum?

cases() returns the individual enum objects; getting their associated values is a case of looking at ->value on each one. In full:

$values = [];
foreach ( Type::cases() as $case ) {
$values[] = $case->value;
}

Fortunately, there is a built-in function array_column which basically performs this loop for you:

$values = array_column(Type::cases(), 'value');

You can also specify what you want to be the key of the resulting array, so lots of variations are possible depending what you need:

$enum_objects_as_list = Type::cases();
// [Type::OFFENSIVE, Type::SPAM, Type::IRRELEVANT]

$values_as_list = array_column(Type::cases(), 'value');
// [1, 2, 3]

$names_as_list = array_column(Type::cases(), 'name');
// ['OFFENSIVE', 'SPAM', 'IRRELEVANT']

$name_to_value_lookup = array_column(Type::cases(), 'value', 'name');
// ['OFFENSIVE' => 1, 'SPAM' => 2, 'IRRELEVANT' => 3]

$value_to_name_lookup = array_column(Type::cases(), 'name', 'value');
// [1 => 'OFFENSIVE', 2 => 'SPAM', 3 => 'IRRELEVANT']

$name_to_enum_object_lookup = array_column(Type::cases(), null, 'name');
// ['OFFENSIVE' => Type::OFFENSIVE, 'SPAM' => Type::SPAM, 'IRRELEVANT' => Type::IRRELEVANT]

$value_to_enum_object_lookup = array_column(Type::cases(), null, 'value');
// [1 => Type::OFFENSIVE, 2 => Type::SPAM, 3 => Type::IRRELEVANT]

Check if PHP enum contains case, like try() method on basic (not backed) enumerations

You can use the static method cases() for this. This returns an array of all values in the enum. The values have a "name" property that is a string representation you can check against (backed enums also have a "value" property that contains the string value you defined in the enum).

So an example implementation could be something like:

enum Fruit {
case APPLE;
case ORANGE;
case BANANA;
}

// String from user input
$fruit = $_POST['fruit'];

// Find matching fruit in all enum cases
$fruits = Fruit::cases();
$matchingFruitIndex = array_search($fruit, array_column($fruits, "name"));

// If found, eat it
if ($matchingFruitIndex !== false) {
$matchingFruit = $fruits[$matchingFruitIndex];
eatFruit($matchingFruit);
} else {
echo $fruit . " is not a valid Fruit";
}

function eatFruit(Fruit $fruit): void {
if ($fruit === Fruit::APPLE) {
echo "An apple a day keeps the doctor away";
} elseif ($fruit === Fruit::ORANGE) {
echo "When life gives you oranges, make orange juice";
} elseif ($fruit === Fruit::BANANA) {
echo "Banana for scale";
}
}

Working version with sample data: https://3v4l.org/ObD3s

If you want to do this more often with different enums, you could write a helper function for this:

function getEnumValue($value, $enumClass) {
$cases = $enumClass::cases();
$index = array_search($value, array_column($cases, "name"));
if ($index !== false) {
return $cases[$index];
}

return null;
}

$fruit = getEnumValue($_POST['fruit'], Fruit::class);
if ($fruit !== null) {
eatFruit($fruit);
} else {
echo $_POST['fruit'] . " is not a valid Fruit";
}

Example with the same sample data: https://3v4l.org/bL8Wa



Related Topics



Leave a reply



Submit