★ Discovering PHP's first-class callable syntax
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.