First-Class Callable Syntax in PHP: Method References Without Strings and Arrays
AI generated
<?php
8.4
PHP · PHP 8.1+ · Callables · Core Language
First-Class Callable Syntax in PHP 8.4
Method References Without Strings and Arrays

Anyone who still references methods and functions through strings like 'strlen' or arrays like [$object, 'method'] gives up type checking, IDE refactoring, and reliable visibility checks. First-class callable syntax turns those references into real, parser-resolved expressions and closes one of the last string-based gaps in the language.

18 min read funcName(...) · $obj->method(...) · Class::method(...) PHP 8.1 · 8.2 · 8.3 · 8.4

1. Context: the problem with string and array callables before PHP 8.1

PHP has used the umbrella term callable since its earlier versions for anything that can be invoked. Up to PHP 8.0, there were essentially two notations for this: a string like 'strlen', which references a global function, and an array like [$object, 'method'] or ['Class', 'method'], which references an instance method or a static method. Both forms work reliably, but they treat the method name as a plain string that PHP only evaluates at runtime.

That is exactly the structural problem: the parser sees ['Mailer', 'send'] as nothing more than two string literals. It cannot check at compile time whether the class Mailer even exists, whether it has a method send, or whether that method's signature matches the call context. A typo like ['Mailer', 'sedn'] only surfaces when the call actually happens, often in production, with a "Call to undefined method" error. IDEs cannot reliably reference such strings, which turns a rename refactor of the method into a silent trap: the string callable stays unchanged and only breaks at runtime.

PHP 8.1 solves exactly this problem with first-class callable syntax. Instead of encoding method references as strings or arrays, they become real expressions that the parser resolves immediately. The following sections cover the syntax, the differences from existing mechanisms, visibility, performance, and the practical migration of existing code in detail.

2. The syntax in detail: funcName(...), $obj->method(...), Class::method(...)

First-class callable syntax consists of a function or method call in which, instead of concrete arguments, exactly three dots appear in the parentheses: funcName(...). These three dots are not a spread operator and not a variadic call, but a distinct token that the parser interprets as an instruction to create a closure from the reference. The parentheses may contain only the three dots; any additional argument causes a parse error.

The syntax works in four basic forms: as a global function (strlen(...)), as an instance method ($object->method(...)), as a static method (Class::method(...)), and, inside classes, additionally via self::method(...), parent::method(...), and static::method(...). In every case, PHP internally creates a Closure object that adopts the full signature of the target function, including default values, variadic parameters, and named arguments.

One detail that is easy to overlook: the nullsafe operator cannot be combined with first-class callable syntax. An expression like $object?->method(...) is not syntactically allowed and causes a fatal error. Anyone who wants to make a potentially null-valued reference callable must perform the null check explicitly beforehand, before applying first-class callable syntax.


<?php

declare(strict_types=1);

// Global function reference: parser resolves "strlen" immediately
$length = strlen(...);
echo $length('First-Class-Callable-Syntax'); // 27

// Instance method reference, $this is bound automatically
final class Mailer
{
    public function send(string $to, string $subject): bool
    {
        // ... sending logic omitted
        return true;
    }
}

$mailer = new Mailer();
$sendFn = $mailer->send(...);
$sendFn('dev@mironsoft.de', 'Deploy fertig');

// Static method reference, no $this bound
final class Formatter
{
    public static function currency(float $amount): string
    {
        return number_format($amount, 2) . ' EUR';
    }
}

$formatFn = Formatter::currency(...);
echo $formatFn(1299.9);

// Nullsafe operator cannot be combined with first-class callable syntax:
// $mailer?->send(...); // Fatal error: Cannot use nullsafe operator here

3. Difference from Closure::fromCallable() and manual closure creation

Before PHP 8.1, Closure::fromCallable() was the closest alternative for turning a method reference into a reusable closure. The crucial difference: Closure::fromCallable([$object, 'method']) still accepts a classic callable value, that is, an array of object and string. Resolving that string still happens internally at runtime, merely wrapped in a reusable closure object. For the parser and for every static analysis tool, the method name remains an opaque string.

First-class callable syntax, $object->method(...), on the other hand, is recognized by the parser as its own expression type and resolved directly, without the detour through a callable value. Both variants produce a functionally equivalent Closure object at runtime, but only the second is visible to IDEs and analyzers as an actual method call.

Before PHP 8.1, closures were often rebuilt by hand, for example with function (float $net) use ($calc) { return $calc->withTax($net); }. This approach forces you to reproduce the entire target signature by hand, including default values, variadic parameters, and named arguments, which becomes another source of errors every time the original method's signature changes. First-class callable syntax adopts the signature automatically and therefore stays in sync with the target method, without manual upkeep.


<?php

declare(strict_types=1);

final class PriceCalculator
{
    public function withTax(float $net): float
    {
        return $net * 1.19;
    }
}

$calc = new PriceCalculator();

// Old style: string/array callable resolved through Closure::fromCallable()
$oldClosure = Closure::fromCallable([$calc, 'withTax']);

// First-class callable syntax: parser resolves the reference directly
$newClosure = $calc->withTax(...);

// Both behave identically at call time, but only the second is
// visible to static analysis and IDE refactoring tools
var_dump($oldClosure(100.0) === $newClosure(100.0)); // true

// Manual closure before PHP 8.1 had to re-declare the full signature,
// including variadics and default values
$manualClosure = function (float $net) use ($calc): float {
    return $calc->withTax($net);
};

4. Benefits for static analysis and IDE refactoring

Static analysis tools like PHPStan and Psalm can check the referenced function or method for first-class callable syntax just like a normal call. If the function does not exist, if the number of parameters is wrong, or if the method is not visible from the current context, the analysis tool reports the error already at build time, long before the code runs in production. With a string callable like 'strln', by contrast, the typo remains undetected until the function is actually invoked.

IDEs like PhpStorm also treat $object->method(...) like a regular method call: "Go to Declaration", "Find Usages", and "Rename" work reliably because the reference is syntactically and unambiguously bound to the method declaration. With string and array callables, the IDE has to rely on heuristics that regularly fail with dynamically assembled strings or with namespaces containing several identically named classes.

Another effect concerns type inference: with array_map(strval(...), $values), the analyzer knows the signature of strval and can infer the return type of the whole expression as list<string>. With array_map('strval', $values), the analyzer first has to resolve the string name against the internal function table, which in more complex cases, for example callables from variables, is no longer even possible.

5. Use with array_map, array_filter, and usort

The most practical application of first-class callable syntax lies in higher-order functions like array_map, array_filter, and usort, which appear in almost every codebase. Instead of array_map('strtoupper', $array), you write array_map(strtoupper(...), $array), with no functional change but full visibility for analyzers and the IDE.

The benefit is even clearer with instance methods: array_filter($items, [$validator, 'isValid']) becomes array_filter($items, $validator->isValid(...)). At this point, the callable is no longer just an array of object and string, but a closure bound directly to the method, whose existence and visibility are checked immediately.

With usort and comparison functions too, first-class callable syntax replaces the often error-prone usort($items, [$this, 'compareByPriority']) with usort($items, $this->compareByPriority(...)). Readability improves because the expression looks like an ordinary method call, just without the trailing arguments.


<?php

declare(strict_types=1);

$prices = [19.99, 5.5, 120.0];

// Old style: function name as string, opaque to static analysis
$roundedOld = array_map('round', $prices);

// First-class callable syntax: parser verifies "round" exists
$roundedNew = array_map(round(...), $prices);

final class Validator
{
    public function isPositive(float $value): bool
    {
        return $value > 0.0;
    }
}

$validator = new Validator();

// Old style: array callable, string method name
$validOld = array_filter($prices, [$validator, 'isPositive']);

// First-class callable syntax: bound instance method, statically checked
$validNew = array_filter($prices, $validator->isPositive(...));

final class Sorter
{
    public function byValueDescending(float $a, float $b): int
    {
        return $b <=> $a;
    }
}

$sorter = new Sorter();
usort($prices, $sorter->byValueDescending(...));

6. Bound $this and visibility for private and protected methods

$object->method(...) creates a closure bound to $object. Inside the closure, $this behaves exactly as if you called the method directly on the object, functionally equivalent to Closure::fromCallable([$object, 'method']). The difference is not in the binding behavior, but in the timing of the visibility check.

With an array callable like [$object, 'privateMethod'], visibility is only checked when the callable is actually invoked, for example deep inside array_map. If the method is not visible from the call context, the call fails with an error that surfaces far removed from where it was actually created. With first-class callable syntax, PHP checks visibility already at the moment $object->privateMethod(...) is evaluated, that is, right at the point where the reference is created. Errors therefore become visible earlier and closer to the actual problem.

Private and protected methods can be referenced via $this->privateHelper(...) inside the class itself, without having to make them publicly accessible. This enables clean internal callback structures, for example for event handlers or pipeline steps, without burdening the class's public interface with additional public methods that were only ever intended for internal use as a callable.

7. First-class callables for static methods and interfaces

Class::method(...) references a static method and creates an unbound closure with no $this context. This corresponds to the classic ['Class', 'method'] callable, but it is just as statically checkable as the instance variant. Inside class methods, self::method(...), parent::method(...), and static::method(...) also work, with static:: respecting late static binding and binding the closure to the class that actually called it, not the class in which the method was originally defined.

A first-class callable reference to an interface method without a concrete context is not possible, since an interface itself has no implementation the parser could resolve. If a variable is typed against an interface, however, $handler->method(...) works without issue, because at runtime the concrete implementation class stands behind the variable. This makes first-class callable syntax compatible with the strategy pattern and with interface implementations wired up via dependency injection.

One detail with static::method(...) in inheritance hierarchies: the closure is bound, at the moment it is created, to whichever class is current at that time. If a child class calls an inherited method that internally uses static::method(...), the resulting closure references the overridden method of the child class, not the parent class's original implementation. Polymorphism is therefore preserved.


<?php

declare(strict_types=1);

interface PaymentHandlerInterface
{
    public function capture(int $orderId): bool;
}

final class StripeHandler implements PaymentHandlerInterface
{
    public function capture(int $orderId): bool
    {
        // ... capture logic omitted
        return true;
    }

    public static function refund(int $orderId): bool
    {
        // ... refund logic omitted
        return true;
    }
}

// Static method reference: unbound closure, no $this
$refundFn = StripeHandler::refund(...);
$refundFn(4711);

// Interface-typed variable: resolves to the concrete implementation at runtime
function processOrder(PaymentHandlerInterface $handler, int $orderId): void
{
    $captureFn = $handler->capture(...);
    $captureFn($orderId);
}

processOrder(new StripeHandler(), 4711);

final class OrderService
{
    public function markPaid(int $orderId): void
    {
        // Late static binding is resolved when the closure is created
        $fn = static::logPayment(...);
        $fn($orderId);
    }

    protected static function logPayment(int $orderId): void
    {
        error_log("Order {$orderId} marked as paid");
    }
}

8. Performance aspects compared to string and array callables

In pure microbenchmarks, calling an already-created closure from first-class callable syntax is exactly as fast as calling a closure from Closure::fromCallable(), since internally both cases produce the same Closure object. The real difference is not in execution, but in resolution: a string or array callable that PHP has not already condensed into a closure has to look up the function or method name again through the internal symbol table on every call. First-class callable syntax resolves the reference once, at creation time, and afterward provides a direct function pointer.

In loops with many iterations, this difference becomes noticeable, especially when a string callable is reassembled from a variable inside the loop each time, instead of being created once outside it. If the first-class callable reference is instead created once before the loop and only invoked afterward, the repeated name lookup disappears entirely.

Task Unsafe / string or array callable Recommended first-class callable syntax Benefit
Referencing a global function 'strlen' strlen(...) Resolved immediately by the parser, typos detectable at analysis time
Referencing an instance method [$object, 'method'] $object->method(...) Visibility is checked at creation, not first at the call
Referencing a static method ['Class', 'method'] Class::method(...) IDE refactoring such as rename and find usages works reliably
Use in array_map array_map('strtoupper', $arr) array_map(strtoupper(...), $arr) Static analysis knows the parameter and return type
Creating a reusable closure Closure::fromCallable([$obj,'m']) $obj->m(...) No detour through an additional callable value needed
Callable in a hot loop Name lookup on every call Closure reference resolved once No repeated symbol table access at runtime

In practice, the measurable effect for one-off calls is negligible. It becomes relevant in library code that uses callables in hot paths, for example in template engines, serializers, or ORM hydrators that run array_map and similar constructs thousands of times per request. Anyone who consistently creates first-class callable syntax outside of loops also benefits from OPcache, which does not need to recompile the creating expression.

9. Migration guide: converting existing callable code

The most practical starting point for migration is a targeted search for existing string and array callables, for example via a regex for ['`[^]]+',\s*'[^']+'\] or through a PHPStan rule that flags outdated callable patterns. It makes sense to prioritize hot paths and public APIs first, because that is where the benefit of static analysis and IDE refactoring is greatest, while rarely used legacy code gets a lower priority.

Important for migration planning: first-class callable syntax is purely additive. Existing callable type hints like callable $handler continue to accept strings, arrays, and first-class callables equally, so the conversion can happen file by file without changing a function's public signature. One limitation remains: if the method name itself is only known at runtime from a variable, for example with dynamically resolved handlers, first-class callable syntax cannot be applied, because the name must be fixed at parse time.


<?php

declare(strict_types=1);

final class LegacyReportBuilder
{
    // BEFORE: string and array callables scattered across the codebase
    public function buildLegacy(array $rows): array
    {
        $rows = array_map('trim', $rows);
        $rows = array_filter($rows, [$this, 'isValidRow']);
        usort($rows, ['LegacyReportBuilder', 'compareRows']);

        return $rows;
    }

    // AFTER: first-class callable syntax, statically verifiable
    public function buildModern(array $rows): array
    {
        $rows = array_map(trim(...), $rows);
        $rows = array_filter($rows, $this->isValidRow(...));
        usort($rows, self::compareRows(...));

        return $rows;
    }

    private function isValidRow(string $row): bool
    {
        return $row !== '';
    }

    private static function compareRows(string $a, string $b): int
    {
        return strcmp($a, $b);
    }
}

10. Summary

First-class callable syntax solves a problem that has existed since the earliest PHP versions: method references as opaque strings or arrays that neither the parser nor the IDE could reliably check. With funcName(...), $obj->method(...), and Class::method(...), these references become real expressions that PHPStan, Psalm, and PhpStorm treat like normal calls. Typos, wrong visibility, and broken rename refactors thereby become visible already at build time, not first at runtime in production.

The biggest gain comes from consistent use across the entire codebase, especially in higher-order functions like array_map, array_filter, and usort. Because the syntax is purely additive and accepts existing callable type hints unchanged, the migration can be carried out file by file with low risk, without having to plan a large breaking-change effort.

First-class callable syntax in PHP: the essentials at a glance

Syntax

funcName(...), $obj->method(...), Class::method(...). The three dots are their own token, no arguments allowed.

Difference from Closure::fromCallable()

The parser resolves the reference immediately, instead of interpreting it as a string or array only at runtime.

Visibility & $this

Instance methods bind $this automatically. Visibility is checked already when the reference is created, not first at the call.

Migration

Purely additive, existing callable type hints remain compatible. Migration file by file without a breaking change is possible.

11. FAQ: First-Class Callable Syntax in PHP

1What is first-class callable syntax in PHP?
The funcName(...) notation available since PHP 8.1, which creates method and function references as parser-resolved expressions instead of string or array callables.
2Since which PHP version does it exist?
Since PHP 8.1. The syntax works unchanged in PHP 8.2, 8.3, and 8.4 as well.
3What do the three dots mean?
A distinct parser token, not a spread operator. The parentheses may contain only exactly three dots.
4Difference from Closure::fromCallable()?
fromCallable() still accepts a string or array callable. First-class callable syntax is resolved directly by the parser and is therefore statically analyzable.
5Does it work with private methods?
Yes, within the valid visibility scope. Visibility is already checked when the reference is created.
6Combinable with the nullsafe operator?
No. $object?->method(...) is not allowed and produces a fatal error. Perform the null check separately beforehand.
7How do I reference static methods?
With Class::method(...), which creates an unbound closure with no $this. Inside classes, also self::, parent::, and static::.
8Does it improve performance?
In hot loops, yes, since no repeated name lookup is needed. For one-off calls, the effect is negligible.
9Can I migrate incrementally?
Yes, the syntax is purely additive. Existing callable type hints remain compatible, migration file by file is possible.
10Do PHPStan and Psalm support the syntax?
Yes, both recognize it as a regular call and check existence, visibility, and signature at analysis time.

Mironsoft

PHP 8.4 modernization, codebase audits, and static analysis rollout

PHP code that benefits from static analysis and your IDE?

We review your existing callable code, replace fragile string and array callables with first-class callable syntax, and set up PHPStan and IDE refactoring for your PHP 8.4 project.

Code review

PHPStan analysis and manual review for outdated callable patterns

Migration

Move string and array callables to first-class callable syntax step by step

Tooling

Set up PHPStan level 8, Rector, and IDE configuration for PHP 8.4