...$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.
Table of Contents
- 1. Parameter declaration versus call site: two different meanings
- 2. Variadic parameters as a type-safe replacement for func_get_args
- 3. Named arguments combined with variadics
- 4. Practice: wrapper functions with transparent argument forwarding
- 5. Fluent builders with a variable argument count
- 6. Typing variadic parameters and passing by reference
- 7. Comparison to call_user_func_array: the historical route
- 8. Limitations: position, default values, and combining with regular parameters
- 9. When variadic parameters genuinely pay off in day-to-day work
- 10. Summary
- 11. FAQ
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.