DEV Community

david duymelinck
david duymelinck

Posted on

PHP fun: enum key collection

For a project I started working on yesterday I needed a way to use a enum as the keys for a collection. But if you do something like $array = [ MyEnum::A => 'b']; it results in following error: Fatal error: Uncaught TypeError: Cannot access offset of type MyEnum on array.

A workaround is to use the name property of the enum, but this can create problems like accidentally using the value property if it is a backed enum.
And the processing code also needs to use the name property. This takes away from the enum ease of use for comparisons.

I looked around of a solution and I found the DS\Map class, but it is part of an extension. While the extension provides more usable data structures than SPL, I thought I could come up with a solution that is enough for my use case.

What it needs to do

  • Provide an easy way to link an enum to a string
  • Check if the enum is present in the collection
  • Return the string that matches the enum

To provide an easy way I was thinking to use the spread operator in the constructor to add both the enums and their strings.
Then filter that array in keys, the enums, and values, the strings, arrays.
Those two arrays will used to check the existence of the enum in the collection and to get the value.

The code

public function __construct(ReplacementInterface|string ...$pairs)
{
   $keys = array_filter($pairs, fn($item) => $item instanceof ReplacementInterface);
   $values = array_filter($pairs, fn($item) => is_string($item));
   // Having more values than keys means storing too much information. 
   if (count($keys) < count($values)) {
      $values = array_slice($values, 0, count($keys));
   }

   $this->keys = array_values($keys);
   $this->values = array_values($values);
}
Enter fullscreen mode Exit fullscreen mode

The great thing in PHP now is that you can type hint the spread operator values. In my case this means stings and an interface that extends the UnitEnum interface.

Because the array_filter function keeps the $pairs array keys, I needed the array_values function to make the matching of the array keys work.

public function keyExists(ReplacementInterface $check) : bool
{
   return in_array($check, $this->keys);
}
Enter fullscreen mode Exit fullscreen mode

This is easy enough to understand.

public function getValue(ReplacementInterface $check) : string
{
   $valueKey = array_search($check, $this->keys);

   return isInt($valueKey) ? $this->values[$valueKey] : '';
}
Enter fullscreen mode Exit fullscreen mode

I used the is_int function to check the type, because I am only expecting an integer while the output of the array_search function can also be a string or false.
I assume there will be a case where the method is going to be used without checking the key first.

Result

I have a data structure that can accept an enum as a key.

I can build the collection in different ways.

// as pairs
new EnumKeyCollection(A::B, 'a', A::C, 'b');
// enums first
new EnumKeyCollection(A::B, A::C, 'a', 'b');
// strings first
new EnumKeyCollection('a', 'b', A::B, A::C);
Enter fullscreen mode Exit fullscreen mode

There are two ways I can check the existence of a key in the collection. With the dedicated keyExists method or checking the value of the getValue method.

I don't need to install a PHP extension to use it.

The main takeaway here is that with a little bit of thought you can find solutions that are developer friendly and the exact fit for the requirements you have.

Top comments (2)

Collapse
 
homeless-coder profile image
HomelessCoder

Hi David! Have you considered the SplObjectStorage? I think it might fit your requirements, as enums are objects, basically. It also provides the ArrayAccess interface:

enum MyEnum
{
    case A;
    case B;
}

var_dump(is_object(MyEnum::A)); // true
$enumCollection = new SplObjectStorage();
$enumCollection[MyEnum::A] = 'Value for A';
$enumCollection->offsetSet(MyEnum::B, 'Value for B');
var_dump($enumCollection[MyEnum::A]); // string(11) "Value for A"
var_dump($enumCollection[MyEnum::B]); // string(11) "Value for B"

foreach ($enumCollection as $key) {
    var_dump($key, $enumCollection->offsetGet($key));
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
xwero profile image
david duymelinck

The SPLObjectStorage class could replace the two arrays or I could even extend the class, but I wanted the instantiation of the object to be as easy as possible while having explicit types. Which means most of the code will be the same.
It is also one of the reasons I opted out using DS\Map.

The main reason to keep the array private is to insure data integrity. The fact that the SPLObjectStorage info is a mixed type could introduce problems for my use case.