Variadic Functions and the Spread Operator: ...$args in Detail
AI generated
8.4
PHP · Functions · Spread Operator
Variadic Functions and the Spread Operator in Detail
...$args as a parameter versus ...$array at the call site

The same three dots mean something different in a function signature than they do at a function call, and if you don't keep those two apart, definition and usage get confused fast. We cover how variadic parameters and the spread operator at a call site interact, how named arguments combine with variadics, and where wrapper functions and fluent builders with a variable argument count actually pay off in practice.

12 min read Variadic Functions Spread Operator Named Arguments func_get_args

1. Parameter declaration versus call site: two different meanings

The three dots ... show up in PHP in two syntactically similar but semantically quite different places. In a function signature before a parameter, for example function log(string ...$messages), they mark a variadic parameter that collects any number of arguments and exposes them as an array inside the function. At the call site, though, for example log(...$messages), the same three dots unpack an existing array into individual arguments.

This double meaning isn't accidental, it's mirror-image by design: one case gathers several values into an array, the other unpacks an array back into several values. Anyone combining both cases in the same expression, for example a wrapper function that forwards its own variadic arguments unchanged to another variadic function, is effectively using both meanings back to back.


function sum(int ...$numbers): int
{
    return array_sum($numbers); // $numbers is a plain array here
}

echo sum(1, 2, 3); // 6, variadic parameter collects arguments

$values = [4, 5, 6];
echo sum(...$values); // 15, spread at the call site unpacks

2. Variadic parameters as a type-safe replacement for func_get_args

Before variadic parameters were introduced in PHP 5.6, func_get_args() was the only tool for accessing a variable number of passed arguments inside a function, regardless of the declared signature. The problem was that the function signature itself gave no information about the expected argument count or type, so IDEs and static analysis tools couldn't offer any meaningful help.

A variadic parameter like string ...$messages fully solves that problem: the signature itself documents that any number of arguments of a specific type is expected, PHPStan and similar tools can check every individual argument against the declared type, and the IDE offers correct autocompletion. func_get_args() should practically never appear in modern PHP 8.4 code anymore, except for rare cases where you genuinely need to access arguments passed by reference without a declared parameter.

3. Named arguments combined with variadics

Since PHP 8.0, named arguments can also be combined with variadic parameters, though with one important caveat: named arguments that don't correspond to a regular parameter declared before the variadic parameter end up in the variadic array keyed by their name as a string instead of a numeric index. That effectively turns the function into one that can process both positional and named variable arguments at the same time.

This property is particularly useful for functions meant to accept arbitrary extra options as key-value pairs without defining a separate array parameter with its own structure. It matters that both positional and named entries end up in the same array inside the function, distinguishing between them is only possible via array_is_list() or an explicit check of the keys.


function buildUrl(string $base, string ...$queryParams): string
{
    $query = http_build_query($queryParams);
    return $query === '' ? $base : "{$base}?{$query}";
}

echo buildUrl('/search', q: 'php', page: '2', sort: 'relevance');
// /search?q=php&page=2&sort=relevance
// the named arguments end up as associative entries in the variadic array

4. Practice: wrapper functions with transparent argument forwarding

A classic use case for combining a variadic parameter with the spread operator is a wrapper function that adds extra behavior around an existing function without having to copy its signature exactly. Typical examples are logging wrappers that capture timing information before and after the actual call, or caching wrappers that store the result of an expensive call before returning it.

The key advantage over a manually enumerated parameter list is that the wrapper function no longer needs to change when the wrapped function's signature changes, as long as only additional parameters are introduced. That noticeably reduces maintenance effort, especially in library code meant to wrap several different target functions with varying signatures.


function withTiming(callable $fn, mixed ...$args): mixed
{
    $start = microtime(true);
    $result = $fn(...$args); // forward all collected arguments
    $elapsed = microtime(true) - $start;

    error_log(sprintf('Call took %.4f seconds', $elapsed));

    return $result;
}

$result = withTiming('array_sum', [1, 2, 3, 4, 5]);
$result = withTiming(fn (int $a, int $b) => $a * $b, 6, 7);

5. Fluent builders with a variable argument count

A second practical use case is fluent builders whose methods need to accept a variable number of values, for example a query builder that accepts any number of columns for a SELECT clause or any number of values for a WHERE IN condition. Without variadic parameters, the caller would either always have to pass an explicit array, or the method would need to be replicated with several overloaded signatures for different argument counts, something PHP doesn't support at all.

With a variadic parameter, the same builder can be used flexibly with either individual values or, via the spread operator at the call site, with an already existing array. That makes the API ergonomic for the common case of a handful of values while staying compatible with value lists determined dynamically at runtime.


final class QueryBuilder
{
    private array $wheres = [];

    public function whereIn(string $column, mixed ...$values): self
    {
        $this->wheres[] = [$column, 'IN', $values];
        return $this;
    }
}

$builder = new QueryBuilder();
$builder->whereIn('status', 'active', 'pending'); // individual values

$allowedIds = [3, 7, 12, 19];
$builder->whereIn('id', ...$allowedIds); // spread of an existing array

6. Typing variadic parameters and passing by reference

A variadic parameter can be typed like any other parameter, including union types and nullable types. PHP checks every individual passed argument against the declared type, a type mismatch on just one of ten arguments already triggers a TypeError for the entire call. That makes variadic parameters noticeably safer than an untyped array $args parameter, where individual element types could only be checked at runtime inside the function.

It's less well known that a variadic parameter can also be declared by reference, for example function double(int &...$numbers). In that case, all passed variables are collected by reference, and changes inside the function directly affect the caller's original variables. This combination is rare, but useful for in-place transformations of several variables at once, provided the caller actually passes variables rather than literal values.


function double(int &...$numbers): void
{
    foreach ($numbers as &$number) {
        $number *= 2;
    }
}

$a = 5;
$b = 10;
double($a, $b);
echo "{$a}, {$b}"; // 10, 20

7. Comparison to call_user_func_array: the historical route

Before the spread operator at the call site was introduced in PHP 5.6, call_user_func_array() was the only tool for dynamically passing an array as an argument list to a function. The function takes a callable and an array, then calls the callable with the array's elements as individual arguments, functionally comparable to today's spread operator, but with noticeably higher call overhead due to the extra callable resolution step.

In modern PHP 8.4 code, call_user_func_array() should really only be used where the callable itself exists dynamically at runtime as a variable or string and can't be written directly as a callable, for example in generic dispatch mechanisms. For the normal case of calling a known callable with an array of arguments, spread syntax $fn(...$args) is both more readable and slightly faster, since it skips the function's internal callable-resolution logic entirely.


// Historical, before PHP 5.6:
$result = call_user_func_array('array_sum', [[1, 2, 3]]);

// Modern variant with spread:
$result = array_sum(...[[1, 2, 3]]); // unusual, array_sum([1, 2, 3]) usually suffices

$fn = 'sprintf';
$args = ['%s is %d years old', 'Anna', 32];
echo $fn(...$args); // Anna is 32 years old

8. Limitations: position, default values, and combining with regular parameters

A variadic parameter always has to be the last parameter in a function signature, further parameters after it are not syntactically allowed, since PHP would otherwise be unable to unambiguously determine where the variable argument list ends and another regular parameter begins. Regular parameters before the variadic parameter, on the other hand, are perfectly fine and get resolved first, either positionally or by name, before the remaining arguments flow into the variadic parameter.

A variadic parameter also cannot carry its own default value, since it's implicitly already pre-filled with an empty array when no additional arguments are passed. Trying to declare an explicit default anyway causes a parse error in PHP. This rule is consistent with the fact that a variadic parameter is already conceptually optional, in the sense that it accepts zero or any number of arguments.

9. When variadic parameters genuinely pay off in day-to-day work

Variadic parameters pay off whenever a function conceptually needs to accept an indeterminate but homogeneously typed number of arguments that all play the same role, for example several numbers to sum, several messages to log, or several column names for a database query. They're the clearly better choice over a single array parameter when the typical call passes a few values directly visible in the code, since they avoid extra square brackets while retaining IDE support through typing.

Variadic parameters are less suited to cases where the passed values play different roles, for example a name and an age in the same argument list, since that lacks any semantic structure that an associative array or a dedicated data object would provide. In such cases, named arguments with regular, clearly named parameters or a dedicated value object are the more robust and self-documenting alternative.

Context Meaning of ... Example Result
In a function signature Variadic parameter, collects function f(int ...$n) $n is an array of all arguments
At a function call Spread operator, unpacks f(...$array) Array elements become individual arguments
With named arguments Named extra arguments land in the variadic array f(a: 1, b: 2) with variadic $args $args = ['a' => 1, 'b' => 2]
By reference Variables are collected by reference function f(int &...$n) Changes affect the caller's variables
func_get_args() Legacy access without type safety usable inside any function Untyped array, no IDE support

Mironsoft

PHP modernization, code quality, and legacy refactoring

Grown PHP code nobody wants to touch anymore?

We modernize PHP codebases to current language standards, introduce static analysis and coding standards, and refactor legacy code step by step without endangering live operations.

Legacy Refactoring

Modernize grown PHP code in a structured, low-risk way.

Establishing Code Quality

Anchor PHPStan, coding standards, and CI checks sustainably in the team.

Version Upgrades

Plan and execute PHP major version upgrades safely, without downtime.

10. Summary

Variadic functions: the essentials at a glance

Two meanings

...$args in a signature collects, ...$array at a call site unpacks.

Type safety

Variadic parameters replace func_get_args with full type checking per argument.

Named arguments

Unrecognized named arguments land in the variadic array keyed by name.

Practice

Wrapper functions and fluent builders benefit most from a variable argument count.

11. FAQ: Variadic functions: the essentials at a glance

1What's the difference between ...$args in a signature and at a call site?
In a signature, a variadic parameter collects any number of arguments into an array. At a call site, the same operator unpacks an existing array into individual arguments.
2Does a variadic parameter fully replace func_get_args?
For the vast majority of cases, yes, since it additionally provides type safety and IDE support. func_get_args remains relevant only for rare special cases.
3How do named arguments behave with functions that have a variadic parameter?
Named arguments that don't correspond to a regular parameter end up in the variadic array keyed by their name as a string, instead of being a numerically indexed element.
4Can a variadic parameter be typed?
Yes, like any other parameter, including union types. PHP checks every individual passed argument against the declared type.
5Can a variadic parameter be declared by reference?
Yes, with the syntax type &...$name. All passed variables are then collected by reference, and changes inside the function directly affect the caller's variables.
6Does a variadic parameter have to be the last parameter?
Yes, further parameters after it are not syntactically allowed, since PHP would otherwise be unable to determine unambiguously where the variable argument list ends.
7Can a variadic parameter have its own default value?
No, it's already implicitly pre-filled with an empty array. An explicit default causes a parse error.
8What are wrapper functions with a variadic parameter especially good for?
Logging or caching wrappers that add extra behavior around arbitrary target functions without having to replicate their signature exactly and without needing changes when the signature changes.
9How do fluent builders benefit from variadic parameters?
Methods like whereIn can be called with either individual, directly written values or, via the spread operator, with an already existing array, without needing two separate methods.
10When are variadic parameters the wrong choice?
When the passed values play different semantic roles. In that case, named regular parameters or a dedicated value object are the more robust and self-documenting alternative.