skip to content

In PHP, how do ...$args variadic parameters and ... argument unpacking work, and how do they interact with named arguments?

level: middleimportance: should knowfreq 42%

answer

  1. same token, two positions
  2. trailing arguments collected into an array
  3. a type on ...$x checks every element
  4. string keys unpack as named arguments
  5. func_get_args skips extra named arguments

basics

~20 s

A ...$args parameter collects the remaining arguments into an array; ... at a call spreads an array or Traversable into arguments. String keys unpack as named arguments, and extra named arguments land in the variadic under string keys.

solid answer

~40 s

`...` means two things. In a declaration, `string ...$tags` is a **variadic parameter**: it must be last, it collects every argument left over after the positional parameters into an array, and a type on it is checked for each element. At a call site, `f(...$list)` **unpacks** an array or `Traversable` into separate arguments. Named arguments meet both: when unpacking, string keys become named arguments, and in a variadic, named arguments that match no parameter are collected under their names as string keys instead of throwing. Rules to remember: no positional argument may follow an unpack, a named one may since PHP 8.1, and `func_get_args()` returns only the positional-style arguments, ignoring extra named ones, so reach for a variadic rather than `func_get_args()` in modern code.

code

php · 17 lines
php
<?php
declare(strict_types=1);

function tagAll(string $prefix, string ...$tags): array
{
    return array_map(fn (string $t): string => "$prefix:$t", $tags);
}

print_r(tagAll('env', 'prod', 'eu'));
// Array ( [0] => env:prod [1] => env:eu )

$list = ['staging', 'us'];
print_r(tagAll('env', ...$list));
// Array ( [0] => env:staging [1] => env:us )

print_r(tagAll(prefix: 'env', team: 'ops'));
// Array ( [team] => env:ops )   extra named argument, string key kept

go deeper

for a junior

Recall that ...$args in a declaration collects extra arguments into an array and ... in a call spreads an array into arguments.

for a middle

Explain how string keys and named arguments flow through unpacking and variadics, and the ordering rules the compiler enforces.

for a senior

Show when a variadic option bag is a good API and when it hides typos, and why you would migrate func_get_args() code to typed variadics.

for a principal

Weigh variadic and array-shaped option parameters against explicit named parameters for a long-lived public API that static analysis must understand.

## One token, two jobs PHP uses the `...` token (sometimes called the splat operator) in two places: - in a **function declaration**, before the last parameter, it declares a **variadic parameter** that gathers extra arguments into an array; - in a **function call**, before an argument, it **unpacks** an array or `Traversable` into individual arguments. Both work with regular parameters around them, and both interact with named arguments (PHP 8.0+). ## Variadic parameters `function tagAll(string $prefix, string ...$tags)` accepts one required argument and then any number of strings. Inside the function `$tags` is always an array, empty when no extra arguments were passed. The engine enforces: 1. **Only the last parameter can be variadic**, and there is only one. 2. **A variadic parameter cannot have a default value**; its default is effectively the empty array. 3. **A type declaration applies to every collected element**: `tagAll('env', 'prod', null)` throws a `TypeError` naming argument #3. 4. **By-reference variadics** are allowed: `function bumpAll(int &...$counters)` binds each argument to the caller's variable. Positional parameters before the variadic are filled first; only the leftovers go into the array. ## Unpacking at the call site `tagAll('env', ...$list)` spreads `$list` into arguments, as if each element had been written out. Any array or `Traversable` works, including a generator. You can unpack several times, `f(...$a, ...$b)`, and mix unpacks after positional arguments. Two rules: - after an unpack, **no positional argument** may follow: `f(...$a, 3)` fails at compile time; - since PHP 8.1 a **named argument** may follow: `buildReport(...$base, format: 'json')`, provided it does not overwrite a parameter the unpack already filled. Unpacking a `Traversable` into a by-reference parameter cannot bind the reference; PHP warns and passes the value instead. Unpacking an array can. ## Where named arguments meet `...` | Situation | What PHP does | |---|---| | Unpacked array has integer keys | values are positional arguments, in order | | Unpacked array has string keys | each key is a **named argument**: `f(...['b' => 2, 'a' => 1])` | | Integer key after a string key while unpacking | `Error: Cannot use positional argument after named argument during unpacking` | | Unknown named argument, function has a variadic | collected into the variadic under its name: `['team' => 'ops']` | | Unknown named argument, no variadic | `Error: Unknown named parameter $team` | So a variadic can hold both list elements (integer keys) and named extras (string keys) in the same array. Code that treats it as a plain list, for example with `implode()` or list destructuring, should expect either. ## `func_get_args()` and friends Before PHP 5.6 introduced variadics, variable arguments were read with `func_get_args()`, `func_num_args()` and `func_get_arg($n)`. They still exist, with rules that surprise: - they report the **current** value of each parameter (since PHP 7.0), so reassigning `$a` in the body changes what `func_get_args()` returns; - after a named call that skips a parameter, the skipped slot is filled with its **default**, as if the call had been positional; - **extra named arguments are ignored**: they are reachable only through the variadic parameter; - calling `func_get_args()` outside a function, at the top level of a script, throws `Error: func_get_args() cannot be called from the global scope`. A declared variadic is visible in the signature, can be typed, and is understood by static analysers, which is why modern code prefers it. ## Choosing between them | Need | Prefer | |---|---| | any number of values of one type | a typed variadic, `int ...$ids` | | forwarding a call's arguments unchanged | a variadic plus unpacking: `function logEvent(string $msg, mixed ...$ctx) { writeLog($msg, ...$ctx); }` | | building a call from data assembled at runtime | unpacking an array, with string keys for named arguments | | inspecting arguments of legacy code with no declared variadic | `func_get_args()`, knowing it skips extra named arguments | Forwarding with `...$ctx` passes named extras on as named arguments, because they sit in the variadic under string keys and unpacking turns string keys back into names. That makes a variadic-plus-unpack wrapper transparent to both call styles, which a wrapper built on `func_get_args()` is not.

  • In PHP, what does func_num_args() return inside function test($a, ...$extra) for the call test('a', color: 'red')?
    It returns 1. The named argument `color` matches no declared parameter, so it is collected into `$extra` as `['color' => 'red']`, and the `func_*()` functions ignore collected named extras. `func_get_args()` returns `['a']`; the only way to see `color` is through `$extra`.
  • Can a PHP call unpack a generator into a function whose parameter is by reference?
    Not as a reference. Unpacking a `Traversable` cannot bind a reference, so PHP emits a warning that it cannot pass the by-reference argument by unpacking a Traversable and passes the value instead; the function's writes do not reach the source. Unpacking a real array binds the reference.

saying these in an interview costs you the question

  • A variadic parameter can sit anywhere in the parameter list.
  • Unpacking with ... works only on arrays, not on iterators or generators.
  • String keys in an unpacked array are dropped and the values passed positionally.
  • func_get_args() returns every argument, including extra named ones.
  • A variadic array always has list keys 0, 1, 2 and so on.