★ Discovering PHP's first-class callable syntax

发布于 2023-03-13 23:00

When looking at recent changes in the Laravel framework, I saw some PHP syntax that I didn't see before. Because I've been working with PHP for over 20 years and have a firm grasp of the language, I was surprised to see new syntax for the first time.

Look at these lines in the bin/facades.php in laravel/framework.

 $resolvedMethods = $proxies->map(fn ($fqcn) => new ReflectionClass($fqcn))
        ->flatMap(fn ($class) => [$class, ...resolveDocMixins($class)])
        ->flatMap(resolveMethods(...))
        ->reject(isMagic(...))
        ->reject(isInternal(...))
        ->reject(isDeprecated(...))
        ->reject(fulfillsBuiltinInterface(...))
        ->reject(fn ($method) => conflictsWithFacade($facade, $method))
        ->unique(resolveName(...))
        ->map(normaliseDetails(...));

The syntax I had never seen before was those three dots inside a function call.

->flatMap(resolveMethods(...))

You might have seen the ... operator (aka the spread or splat operator) in various other contexts.

You can use it to unpack arrays...

$parts = ['apple', 'pear'];
$fruits = ['banana', 'orange', ...$parts, 'watermelon'];
// ['banana', 'orange', 'apple', 'pear', 'watermelon'];

... or to grab arguments for a function:

function myFunction(...$arguments) {
	var_dump($arguments); // shows an array ['a', 'b', 'c']
}
	
myFunction('a', 'b', 'c');

A neat trick you can also do is to pass all parameters of a function to another function.

function myFunction(...$arguments) {
	// $arguments now holds an array of all passed arguments
	
	// all elements in the array will be passed as 
	//separate arguments to `anotherFunction`.
	anotherFunction(...$arguments);
}

First-class callable syntax

In this case...

->flatMap(resolveMethods(...))

the ... operator does something completely else. In this context ...` is called "the first class callable syntax". It's been available since PHP 8.1. It will wrap the function it is used in in a closure.

So this code...

$myFunction = strtoupper(...);

... is equivalent to:

$myFunction = function(...$arguments) {
	return strtoupper(...$argument);
}

So any arguments you pass to it, will be passed to the function you're wrapping in a closure.

Let's use it.

$myFunction('a') // returns 'A';

Let's unpack it using a simple example. Imagine you have this collection you want to uppercase.

collect(['a', 'b', 'c'])
   ->map(function($letter) {
      return strtoupper($letter);
   });

Using the first-class callable syntax, you can rewrite that code like this.

collect(['a', 'b', 'c'])
   ->map(strtoupper(...));

Cool, right?

Of course, you can also use it for non-global functions, such as class functions as well.

class MyClass()
{
    public function execute(): Collection
    {
        return collect(['a', 'b', 'c'])
            ->map($this->doubleString(...));
    }
    
    public function doubleString(string $string): string
    {
        return $string . $string;
    }
}

 // returns a collection with 'aa', 'bb, and 'cc'.
(new MyClass)->execute();

Very neat!

If you want to know more about this syntax, check out this excellent post at PHP Watch, which also lists the limitations and edge cases.