Sacha::corazzi();

PHP Enums: randomUnique

In my test suites I’ll often loop over all of an enum’s ::cases() (ideally using a data provider) when making assertions that rely on them in such a way that all possible combinations are covered.

Sometimes, however, the actual enum I’m using doesn’t matter for that I’m testing, and I just need a random value. Sometimes I need multiple random values in the same test case (maybe I’m building a collection), and I need them to be unique.

I’ve written this helper method that will return a random value from an enum, and will ensure that the value is unique each time it’s called.

public static function randomUnique(bool $reset = false): static
{
    static $used = [];

    if ($reset) {
        // Consecutive method calls within test cases quickly use up all the options
        $used = [];
    }
    
    $cases = self::cases();

    if (count($used) === count($cases)) {
        throw new RuntimeException('No more unique values available for '.self::class);
    }

    $used[] = $random = Arr::random(
        array_filter($cases, fn ($case) => ! in_array($case, $used))
    );

    return $random;
}

Example usage:

$objects = [];

for ($i = 0; $i < 3; $i++) {
    $randomEnum = MyEnum::randomUnique();
    
    $objects[] = new MyObject($randomEnum);
}

//...

How it works

The method starts by defining a static array that will be used to keep track of which values have already been used.

We get the enum’s cases and check if the number of used values is the same as the number of cases. If it is, we throw an exception since we’ve run out of unique values.

Then, we filter the cases array to remove any values that have already been used, and return a random value from the resulting array. I’m using Laravel’s Array::random() here, but you could use array_rand() or whatever else you prefer that’s available to you.

As we are fetching the random value, we also add it to the $used array. Finally, the random value is returned.

Caveats

The gotcha is that, once you run out of unique values, the method will throw an exception.

I’ve done this on purpose; if I’m using this method and the exception is thrown then it’s probably an indication that I need to rethink my testing logic.

Just in case, though, the optional $reset parameter can be used to reset the $used array between. For example, I could do something like this:

$groupOne = [];
$groupTwo = [];

for ($i = 0; $i < 3; $i++) { 
    $randomEnum = MyEnum::randomUnique();
    
    $groupOne[] = new MyObject($randomEnum);
}

for ($i = 0; $i < 5; $i++) { 
    // Reset the used array during the first iteration 
    $randomEnum = MyEnum::randomUnique($i === 0);
    
    $groupTwo[] = new MyObject($randomEnum);
}

//...

It’s a very contrived example, but you get the point.