PHP Tricks: Multi-value match()

in PHP8 months ago (edited)

Last time, I talked about fun uses of the match() expression in PHP, using unconventional types of expressions on the right-arm branch. This time, I want to expand that discussion to include fun tricks on the left-side of the =>, and how you can easily match by multiple values at the same time.

I'm going to assume you have read the previous installment about how match() works and what an "expression" means. Also, I don't claim to be the original source of these tricks. I'm just documenting them because they're cool. Let's dive in.

Array matching

A match() statement will do a strict equality === comparison on its parameter expression and each arm's comparison expression. While most commonly those expressions will be an enum or a string, they can be any PHP value.

Two PHP arrays are strictly equal if they have strictly equal values for each key, and the keys are in the same order. That means by carefully constructing arrays, we can compare multiple values at the same time much the same way as the spaceship operator (<=>).

Consider this example. In this case, we want to know if one user is allowed to "friend" another within a certain group. In particular, if they're both in the same group then they may do so. If only one of them is in the specified group, then they can but the user being friended has to give permission first. If neither of them are in the group, then they're not allowed to be friends. (Sounds like high school all over again...)

enum FriendPermission
{
    case Allow;
    case Deny;
    case WithApproval;
}

function mayFriend(User $u1, User $u2, Group $group): FriendPermission
{
    return match ([$u1->inGroup($group), $u2->inGroup($group)]) {
        [true, true] => FriendPermission::Allow,
        [true, false] => FriendPermission::WithApproval,
        [false, true] => FriendPermission::WithApproval,
        [false, false] => FriendPermission::Deny,
    };
}

In this case, the two inGroup() method calls will evaluate first, giving a match value that is an array of two booleans. Two booleans have four possible combinations, so we can match that 2-element array against the four possibilities. In this case the second and third have the same result, so they could be combined into a single arm if desired.

This logic could certainly be implemented without match(), using a series of probably nested if-else statements. But using a match() effectively gives us a visual lookup table of what the logic is we intend. We can tell at-a-glance what the code will do.

The same concept can be applied to arrays with more than two values, too, as well as array parts that are not boolean. However, this approach works best with low-cardinality options, or cases where most combinations fall through to a common default result. Booleans, enums, and get_class() results are good candidates to use with a multi-element match().

Evaluating to an array

Here's a different use of arrays with match() that is borrowed from my own AttributeUtils library:

function classElementInheritanceTree(\ReflectionProperty|\ReflectionMethod|\ReflectionClassConstant $subject): iterable
{
    $subjectName = $subject->getName();

    [$hasMethod, $getMethod] = match(get_class($subject)) {
        \ReflectionProperty::class => ['hasProperty', 'getProperty'],
        \ReflectionMethod::class => ['hasMethod', 'getMethod'],
        \ReflectionClassConstant::class => ['hasConstant', 'getReflectionConstant'],
    };

    foreach ($this->classAncestors($subject->getDeclaringClass()->name) as $class) {
        $rClass = new \ReflectionClass($class);
        if ($rClass->$hasMethod($subjectName)) {
            yield $rClass->$getMethod($subjectName);
        }
    }
}

There's some distracting context here, but the basic point is that PHP's reflection library is ridiculously inconsistent and badly typed, leading to weird type signatures like this. :-) This function takes a reflection object of some kind and applies the same logic to it regardless of which type it is. However, that logic varies only in the methods that need to be called on another reflection object. We could simply branch to three separate functions, but then those functions would be identical aside for just those method name strings. That seems sloppy.

Instead, match() evaluates to an array of method names, depending on which type of reflection object we're using. We then use PHP's array unpacking capability to split that array back out into individual variables, which can be used as method names in the second block.

More match() magic

There's likely other cool tricks that can be done with match() to make it even more powerful. Ideally, we can get pattern matching into future versions of PHP that will expand its capability even more.

That said, it isn't always the best solution. if-else is not going away. The goal should always be the most readable and maintainable code. Sometimes that is a traditional if-else approach. But often times match() can replace it with a cleaner, easier to read, and even easier to write alternative.

It's always good to have multiple options in your tool kit.

Sort:  

That's some cool tricks there, I am a php developer too, nice to meet you😊

Welcome to the party, pal. :-) Go ahead and start posting your own PHP content here. It's lonely with just me writing things.

Thanks pal,I will be sure to post php contents here regularly. Thanks for having me.

Love programing
Am still learning and improving day by day