1

Somewhat surprisingly, PHP does not have native support for enumerations. With PHP 5.4, the native extension SPL_Types could be used to emulate this behavior with SplEnum, but the last update to this extension was in 2012. On PHP 7, attempting to install the extension will throw compilation errors due to changed interfaces.

As such, I wanted to know what the currently recommended way of getting enum-like behavior is. I want to be able to write code at least similar to this:

enum DaysOfTheWeek
{
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
    Sunday = 7
}

$day = DaysOfTheWeek::Monday;
echo $day; //Prints 1

My current code looks less concise and has lots of (unnecessary) overhead:

class DayOfTheWeek
{
    private __value;

    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)
    {
        if ($value !== DayOfTheWeek::Monday
            && $value !== DayOfTheWeek::Tuesday
            && $value !== DayOfTheWeek::Wednesday
            && $value !== DayOfTheWeek::Thursday
            && $value !== DayOfTheWeek::Friday
            && $value !== DayOfTheWeek::Saturday
            && $value !== DayOfTheWeek::Sunday
        )
        {
            throw new InvalidArgumentException("Invalid day of the week");
        }
    }

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

Some clarifications:

Why not use an abstract class with a few constant values?

Because this completely defeats the idea of type hinting in PHP 7. The idea is that I can tell a function to accept an instance of this enumeration and be sure that it's a legitimate value.

Why don't you optimize the days of the week? You could make the check simpler by seeing if it's between 0 and 6.

Because enumerations don't have to be in order, or show any relation to each other. An enumeration could have the values 56, 1999, 120, -12400, 8, -1239 and 44. Code for an enumeration should not depend on the values of the enumeration.

MechMK1
  • 3,278
  • 7
  • 37
  • 55
  • 1
    Think about using the range 0..6 (it counting begins at 0) instead of 1..7. This has some advantages, e.g: `$dayname = [ 'Mon'..,'Sun']` or if calculating `$day = ($day + 6 ) %7` to get the weekday of now+6. – Wiimm Apr 24 '19 at 09:31
  • @Wiimm I specifically did not use any "shortcuts", as I wanted to stay as generic as possible. An enumeration could contain any kinds of values, with might not have any relation to each other. Think of this as the "worst-case scenario", where no optimization at all can take place. – MechMK1 Apr 24 '19 at 09:32
  • Why not define an array starting from 1 to 7 (values being the days) and in the constructor you would just check if the key exists? Or define the range in a private ? – ka_lin Apr 24 '19 at 09:35
  • Final class with a private constructor? That way it can never be extended nor instantiated - that should give you a concrete set of constant values like `DayOfTheWeek::Monday` like an Enum... or am I missing something? – CD001 Apr 24 '19 at 09:35
  • @CD001 Yes, because instantiation is actually useful for an enumeration. – MechMK1 Apr 24 '19 at 09:38
  • @ka_lin You mean an associative array as the enum? – MechMK1 Apr 24 '19 at 09:38
  • @MechMK1 Yes an associative array or add a private array with accepted values...? – ka_lin Apr 24 '19 at 09:43
  • `$dayOfWeek = ["Monday" => 1, "Tuesday" => 2, ...];` – Barmar Apr 24 '19 at 09:47
  • Using *just* an associative array defeats the purpose of type-safety. Using an associative array inside a class defeats IDE support. – MechMK1 Apr 24 '19 at 09:47

2 Answers2

2

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...

ka_lin
  • 9,329
  • 6
  • 35
  • 56
  • 1
    I've upvoted this, but I'm waiting for a bit to accept it, as not to discourage more answers. As you stated yourself, it is needlessly ugly and complicated, but as it seems it's the best PHP can do. – MechMK1 Apr 24 '19 at 10:23
  • You can wait as long as you want, I am curious as well. Great question btw – ka_lin Apr 24 '19 at 10:26
1

I like to do this in my projects. If you are curious you will realize that you can build a hierarchy of enumerations, even give other uses to those classes.

https://gist.github.com/rafageist/aef9825b7c935cdeb0c6187a2d363909

I convert this solution in a real project. A completed solution is here: https://github.com/divengine/enum.

Install it:

composer require divengine\enum;

Define your enums:

<?php

namespace MyEnums;

use divengine\enum;

class Temperature extends enum {/* Father of all types of temperatures */}
class ExtremeTemperature extends Temperature {/* Father of all types of extreme temperatures */}
class FIRE extends ExtremeTemperature {}
class ICE extends ExtremeTemperature {}

class NormalTemperature extends Temperature {/* Father of all types of normal temperatures */}
class HOT extends NormalTemperature {}
class COOL extends NormalTemperature {}
class COLD extends NormalTemperature {}

Use your enums:

<?php

use MyEnums;


// Constants are good tricks, but optional
const COOL = COOL::class;

class AllTemperatures {
    const COOL = COOL::class; // maybe better
    const HOT = 'Enums\\HOT';  // ugly !!!

    //...
}

// Define some function with type hinting
function WhatShouldIdo(Temperature $temperature)
{
    switch (true) {
        case $temperature instanceof ExtremeTemperature:
            switch (true) {
                case $temperature instanceof FIRE:
                    return "Call the fire department";

                case $temperature instanceof ICE:
                    return "Warm up";
            }
            break;

        case $temperature instanceof NormalTemperature:
            switch ($temperature) {

                case HOT::class: // compare using classname
                    return "Drink a bear :D";

                case COOL or AllTemperatures::COOL: // compare using constants
                    return "Just go away !";

                case 'Enums\\COLD': // compare using string, ugly !!!
                    return "Wear a coat";
            }

            break;
    }

    return "I don't know";
}

// Call to function with a instance of any Temperature
echo WhatShouldIdo(new HOT()) . PHP_EOL;