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);
}
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);
}
This is easy enough to understand.
public function getValue(ReplacementInterface $check) : string
{
$valueKey = array_search($check, $this->keys);
return isInt($valueKey) ? $this->values[$valueKey] : '';
}
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);
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)
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:
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.