0

First-class callable syntax — strlen(...) as a callable (PHP 8.1+)

Intermediate5 min read·php-06-005
compare

Concept

PHP 8.1 introduced first-class callable syntax (FCS), allowing any callable to be referenced as a proper Closure object using the callable(...) notation with literal ... (the splat with no arguments). Previously, creating a closure from a named function required wrapping it: fn($x) => strlen($x) or Closure::fromCallable('strlen'). With FCS you write strlen(...) and PHP returns an equivalent Closure object — fully typed, immediately usable anywhere a callable is expected.

The resulting closure preserves the original function's signature and type information. Static analysis tools and IDEs can introspect it correctly. This is a significant improvement over string callables ('strlen') and array callables ([$object, 'method']), both of which are opaque to static analysis. The string form 'strlen' is also not refactoring-safe; FCS strlen(...) will produce an error immediately if the function does not exist, whereas the string form fails only at runtime when called.

FCS works uniformly across all callable forms:

FormFCS syntax
Named functionstrlen(...)
Static methodStr::of(...)
Instance method$obj->method(...)
Built-inarray_map(...)
ConstructorNot supported — use fn(...$args) => new Foo(...$args)

The returned closure binds the object scope at creation time for instance methods. If $obj->method(...) is stored and called later, it calls method on the captured $obj instance.

In combination with higher-order array functions, FCS dramatically reduces boilerplate. array_map(strtolower(...), $strings) is cleaner than the arrow function alternative and identical in performance. Laravel's Collection::map(strtolower(...)) reads like declarative English.

Code Example

php
<?php
declare(strict_types=1);

// Named function → Closure
$lengthFn = strlen(...);
var_dump($lengthFn);           // object(Closure)
echo $lengthFn('hello') . PHP_EOL; // 5

// Use with array_map — no wrapping closure needed
$words  = ['Hello', 'World', 'PHP'];
$lower  = array_map(strtolower(...), $words);
var_dump($lower); // ['hello', 'world', 'php']

// Static method reference
class MathHelper
{
    public static function square(int $n): int
    {
        return $n ** 2;
    }
}

$squareFn = MathHelper::square(...);
$squares  = array_map($squareFn, [1, 2, 3, 4, 5]);
var_dump($squares); // [1, 4, 9, 16, 25]

// Instance method reference — captures $this implicitly
class Formatter
{
    public function __construct(private readonly string $prefix) {}

    public function format(string $value): string
    {
        return $this->prefix . $value;
    }
}

$formatter    = new Formatter('[INFO] ');
$formatFn     = $formatter->format(...); // captures $formatter
$formatted    = array_map($formatFn, ['Request started', 'DB queried']);
var_dump($formatted);
// ['[INFO] Request started', '[INFO] DB queried']

// Pipe-style usage with usort
$names = ['Charlie', 'Alice', 'Bob'];
usort($names, strcmp(...)); // compare(...) as comparator
var_dump($names); // ['Alice', 'Bob', 'Charlie']

Interview Q&A

Q: What advantage does first-class callable syntax have over string callables like 'strlen'?

Three concrete advantages. First, safety: strlen(...) causes a compile-time parse error if strlen does not exist; 'strlen' silently carries the wrong string until the runtime call_user_func invocation. Second, static analysis: tools like PHPStan and Psalm understand strlen(...) as a Closure<(string $string): int> with full type information; string callables are opaque. Third, refactoring safety: renaming a method in an IDE will update $obj->newName(...) references but will not find the string 'oldName' in a call_user_func call. The only scenario where you might still use strings is when the callable is determined dynamically at runtime from configuration — and even then, you should wrap it immediately with Closure::fromCallable() for type safety downstream.


Q: How does FCS differ from Closure::fromCallable()?

They produce the same result — a Closure wrapping the target callable — but FCS is syntactic sugar baked into the language grammar, while Closure::fromCallable() is a static factory method that accepts any callable (including dynamically constructed ones like [$object, 'method'] built at runtime). FCS requires a literal, known callable at the point of writing. Closure::fromCallable() is the right tool when the callable is assembled programmatically: $method = 'process'; $closure = Closure::fromCallable([$service, $method]);.


Q: Can you use FCS with constructors, and if not, what is the canonical workaround?

Constructors are not supported by first-class callable syntax. new Foo(...) would be ambiguous with the named argument spread and PHP chose not to allow it. The canonical workaround is an arrow function: $factory = fn(...$args) => new Foo(...$args). In Laravel's container, $concrete bindings that are class name strings are resolved by a similar closure created internally: the container wraps the class name in a closure that calls new $concrete(...$resolved) after building the dependency list via Reflection. If you need a named factory, wrap it: $makeOrder = fn(int $userId, Cart $cart) => new Order($userId, $cart).