PHP Variadic Functions

A variadic function in PHP is a function that can accept an arbitrary number of arguments instead of a fixed list. Rather than writing separate overloads or manually building an array before calling the function, you mark the last parameter with three dots (...) and PHP automatically collects every extra argument into that parameter as an array. This is the same mechanism behind familiar built-ins like array_merge(), sprintf(), and implode(), and it pairs naturally with the spread operator for unpacking arrays back into calls. Understanding variadics well means understanding both directions: packing arguments into a parameter, and spreading an array out into arguments.

Overview: How Variadic Functions Work

PHP has supported variadic parameters since PHP 5.6 via the ... (splat) token. When you declare a parameter as ...$name, you are telling the compiler that this parameter absorbs every remaining positional (or named) argument passed to the function, packaging them into a single, ordinary PHP array. That array is created fresh on every call — if no extra arguments are passed, the variadic parameter is simply an empty array, never null and never undefined.

Internally, when the Zend engine compiles a function definition, it flags the last parameter (if written with ...) as variadic. At call time, the engine first binds arguments positionally to any preceding fixed parameters, then takes everything left over — whether passed positionally or, since PHP 8.1, by name — and pushes it into the variadic parameter’s array. Positional leftovers are inserted with sequential integer keys starting at 0 (not continuing the count from the fixed parameters), while named arguments matched to the variadic parameter keep their name as a string key. This is why you can safely foreach over a variadic parameter and check is_string($key) to tell positional and named arguments apart.

If you add a type to the variadic parameter, e.g. int ...$numbers, that type applies to every individual element collected, not to the array as a whole. PHP validates (and, outside strict_types mode, coerces) each incoming value against that type exactly as it would for a normal typed parameter. A variadic parameter can also be passed by reference with &...$args, in which case every element in the resulting array is a reference back to the original variable, useful for functions that need to modify their callers’ variables in bulk.

Only one variadic parameter is allowed per function, and it must be the last parameter in the signature — PHP cannot know where a variadic list ends and subsequent named parameters begin, so this is enforced as a fatal, compile-time error.

Syntax

function functionName(type $fixedParam, ..., type ...$variadicParam): returnType {
    // $variadicParam is an array here
}

// Calling with the spread operator to unpack an array:
functionName(...$arrayOfArguments);
Part Meaning
type Optional type declaration applied to each collected argument (e.g. int, string, mixed).
... The splat token. In a function signature it declares a variadic parameter; at a call site it spreads an array or Traversable into individual arguments.
$variadicParam Inside the function body, this is always an ordinary array containing every extra argument.
Position The variadic parameter must be the last one declared; no parameters may follow it.
Return type Declared normally — variadics do not affect the return type.

Examples

Example 1: A Basic Variadic Sum Function

<?php
function sum(int ...$numbers): int {
    return array_sum($numbers);
}

echo sum(1, 2, 3), PHP_EOL;
echo sum(10, 20, 30, 40), PHP_EOL;
echo sum(), PHP_EOL;
Output:
6
100
0

Each call passes a different number of arguments, and PHP packs them all into the $numbers array before the function body runs. Calling sum() with no arguments at all still works because the array simply ends up empty, and array_sum([]) is 0.

Example 2: Mixing Fixed and Variadic Parameters

<?php
function formatMessage(string $level, string ...$parts): string {
    return "[$level] " . implode(' ', $parts);
}

echo formatMessage('INFO', 'User', 'logged', 'in'), PHP_EOL;
echo formatMessage('ERROR', 'Connection failed'), PHP_EOL;
Output:
[INFO] User logged in
[ERROR] Connection failed

$level is a normal required parameter and is bound first; everything passed after it lands in $parts. This pattern — one or two fixed “configuration” parameters followed by a variadic payload — is extremely common in logging, formatting, and query-building helpers.

Example 3: Unpacking Arrays with the Spread Operator

<?php
function average(float ...$numbers): float {
    if (count($numbers) === 0) {
        return 0.0;
    }
    return array_sum($numbers) / count($numbers);
}

$scores = [85.5, 92.0, 78.25, 90.0];

echo average(...$scores), PHP_EOL;
echo average(100, 90), PHP_EOL;

$combined = [...$scores, 100.0];
echo average(...$combined), PHP_EOL;
Output:
86.4375
95
89.15

The ... operator works in two places here: in the function signature it collects arguments, and at each call site it spreads an existing array back out into individual arguments. [...$scores, 100.0] also shows array spreading used to build a new array by unpacking $scores and appending an extra value, a common PHP 7.4+ idiom for combining arrays without array_merge().

Example 4: Variadics with Named Arguments (PHP 8.1+)

<?php
function buildUrl(string $base, string ...$params): string {
    $query = [];
    foreach ($params as $key => $value) {
        $query[] = is_string($key) ? "$key=$value" : $value;
    }
    return $base . '?' . implode('&', $query);
}

echo buildUrl('/search', q: 'php', page: '2'), PHP_EOL;
Output:
/search?q=php&page=2

Since PHP 8.1, named arguments that don’t match a fixed parameter are also collected by the variadic parameter, but with their argument name preserved as the array key instead of a numeric index. Checking is_string($key) lets buildUrl() tell a named page: '2' apart from a plain positional value.

Under the Hood: Step by Step

When PHP compiles a function with a trailing ...$param, it stores a flag on that parameter’s metadata marking it variadic. At call time the engine does roughly the following:

  1. Bind arguments to fixed parameters left to right, by position or by matching name.
  2. Once the fixed parameters are satisfied, collect every remaining argument — positional or named — instead of raising a “too many arguments” error.
  3. Create a new array; assign positional leftovers integer keys starting at 0, and assign named leftovers their argument name as a string key.
  4. If the variadic parameter has a type declaration, validate or coerce each element against it individually as it is inserted.
  5. Bind the finished array to the variadic parameter’s variable before the function body executes.

Before variadics existed (pre-PHP 5.6), the only way to write an “accepts anything” function was func_get_args(), which reaches into the current call frame and reconstructs the argument list — it works, but it is untyped, invisible in the function signature, and unfriendly to IDEs and static analysis tools. The two approaches produce the same array shape for plain positional calls, but only variadics document themselves and support type-checking:

<?php
function oldSum() {
    return array_sum(func_get_args());
}

function newSum(int ...$numbers): int {
    return array_sum($numbers);
}

echo oldSum(1, 2, 3), PHP_EOL;
echo newSum(1, 2, 3), PHP_EOL;
Output:
6
6

Common Mistakes

Mistake 1: Putting the Variadic Parameter Before Others

A variadic parameter absorbs “everything left over,” so PHP requires it to be the very last parameter. Placing anything after it is a fatal compile-time error:

<?php
function broken(...$items, string $label) {
    return $label . ': ' . implode(', ', $items);
}

This fails to compile with Fatal error: Only the last parameter can be variadic, because PHP would have no way to decide where the variadic list ends and $label begins. The fix is simply to reorder the parameters so the variadic one comes last:

<?php
function fixed(string $label, ...$items) {
    return $label . ': ' . implode(', ', $items);
}

echo fixed('Tags', 'php', 'variadic', 'functions'), PHP_EOL;

Output: Tags: php, variadic, functions

Mistake 2: Passing an Array Instead of Spreading It

A variadic parameter always receives individual arguments, never a single array by accident — forgetting the spread operator is one of the most common variadic bugs:

<?php
function sum(int ...$numbers): int {
    return array_sum($numbers);
}

$values = [1, 2, 3];

echo sum($values), PHP_EOL;

Here $values is passed as a single argument, so PHP tries to match it against the int ...$numbers collection and throws TypeError: sum(): Argument #1 ($numbers) must be of type int, array given, because an array is not an int. The fix is to unpack the array with ... so each element becomes its own argument:

<?php
function sum(int ...$numbers): int {
    return array_sum($numbers);
}

$values = [1, 2, 3];

echo sum(...$values), PHP_EOL;

Output: 6

Best Practices

  • Always type variadic parameters (int ...$x, string ...$x) so PHP validates each element instead of silently accepting anything.
  • Prefer variadics over func_get_args() in new code — they are self-documenting in the signature and work with IDE autocompletion and static analyzers.
  • Use the spread operator (...$array) to pass an existing array into a variadic function rather than looping and calling the function repeatedly, or using call_user_func_array().
  • Keep at most one variadic parameter, and always place it last — PHP enforces this, but designing signatures with it in mind up front avoids awkward refactors.
  • When mixing named arguments with a variadic parameter, remember string keys are preserved — guard with is_string($key) if you need to distinguish named from positional extras.
  • Don’t reach for variadics on functions with a genuinely fixed, well-known set of parameters; a normal signature is clearer and gives better editor hints than an under-specified array.
  • Document (in a docblock, if not in the type) what each element of the variadic array is expected to represent, since the array’s own type only tells the reader “array,” not “array of what.”

Practice Exercises

  • Write a variadic function concatAll(string $separator, string ...$parts): string that joins its variadic arguments with the given separator. Calling concatAll('-', 'php', 'is', 'fun') should return "php-is-fun".
  • Write a variadic function maxOf(int ...$numbers): ?int that returns the largest number passed in, or null if none were passed. Test it both with individual arguments and by spreading an existing array like [4, 19, 7] into the call.
  • The following function is broken: function greet($greeting, ...$names, $punctuation) { ... }. Explain why it fails to compile, and rewrite it so it works while still accepting any number of names.

Summary

  • A variadic parameter, written ...$name, must be the last parameter and collects every remaining argument into an ordinary array.
  • Typing a variadic parameter (int ...$x) validates each collected element individually, not the array as a whole.
  • The same ... token spreads an array back out into individual arguments at a call site, and can also spread arrays inside array literals.
  • Since PHP 8.1, named arguments matched to a variadic parameter are stored using their name as a string array key instead of a numeric index.
  • Variadics are the modern, typed, self-documenting replacement for the older func_get_args() pattern.
  • Only one variadic parameter is allowed per function, and it always defaults to an empty array when nothing extra is passed.