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.
Your post is so informative. Thanks for sharing it! carports, Port Charlotte, Florida