Variadic functions and the splat operator (...$args)
Concept
Variadic functions accept an arbitrary number of arguments via the ...$args syntax in the parameter list. PHP collects all surplus positional arguments into an indexed array (or an associative array if named arguments are used, as covered in the named arguments lesson). The variadic parameter must be the last in the signature. You may combine typed leading parameters with a typed variadic tail: function add(int ...$numbers): int ensures every collected argument is an integer.
The splat operator ... has a second role at the call site: it unpacks an array (or any Traversable) into individual positional arguments. This is the complement of the variadic definition — ... in a function signature collects; ... at a call site spreads. array_push($stack, ...$newItems) is cleaner than a foreach loop. Spreading also works with string-keyed arrays when calling functions that accept named arguments: createUser(...$data) where $data = ['name' => 'Alice', 'email' => '...'] is the equivalent of calling with all named arguments.
Type-checking applies element-by-element: if you declare int ...$ids and spread an array containing a string, PHP throws a TypeError at the position of the bad element. This makes variadic functions with typed tails as safe as fixed-signature functions.
A practical use for variadic parameters is building pipeline-style APIs. Laravel's Pipeline::through(...$pipes) accepts an array of middleware classes; internally it just stores $this->pipes = $pipes. The splat operator at the call site lets callers pass either individual arguments or a pre-built array with equal readability.
Code Example
<?php
declare(strict_types=1);
// Typed variadic — every element must be int
function sum(int ...$numbers): int
{
return array_sum($numbers);
}
echo sum(1, 2, 3, 4, 5) . PHP_EOL; // 15
// Mixed leading params + variadic
function logMessage(string $level, string ...$messages): void
{
$prefix = strtoupper($level);
foreach ($messages as $msg) {
echo "[{$prefix}] {$msg}" . PHP_EOL;
}
}
logMessage('error', 'Disk full', 'Write failed', 'Aborting');
// Splat at call site — spread array into args
$ids = [10, 20, 30];
$total = sum(...$ids);
echo $total . PHP_EOL; // 60
// Spread into built-ins
$first = [1, 2, 3];
$rest = [4, 5, 6];
$merged = array_merge(...[$first, $rest]); // spread array of arrays
var_dump($merged); // [1,2,3,4,5,6]
// Named spread — string-keyed array unpacked as named args
function createPoint(float $x, float $y, float $z = 0.0): array
{
return compact('x', 'y', 'z');
}
$coords = ['y' => 2.0, 'x' => 1.0];
$point = createPoint(...$coords); // named spread, order irrelevant
var_dump($point); // ['x'=>1.0, 'y'=>2.0, 'z'=>0.0]Interview Q&A
Q: What is the difference between ...$args in a function definition versus ...$array at a call site?
In a function definition, ...$args is the variadic collector: PHP gathers all remaining arguments passed at the call site into an array and binds it to $args. At a call site, the splat operator is an expander (spread): f(...$array) unpacks the array elements as individual positional arguments. Think of them as inverses. If you define function f(int ...$ns) and call f(...[1, 2, 3]), PHP expands the array into three positional arguments, then collects them back into $ns. Both the expansion and the collection step apply type validation, so type safety is preserved end-to-end.
Q: Can you type the variadic parameter, and what happens when an element fails the type check?
Yes. function f(string ...$parts) enforces that every argument collected into $parts is a string. PHP validates each element individually at call time. If argument position 3 is an integer, PHP throws TypeError: f(): Argument #3 must be of type string, int given. Under strict types, no coercion is attempted. This per-element validation is what distinguishes typed variadics from an untyped array $parts parameter — with the latter you would have to validate elements yourself and the type information is invisible to static analysis tools.
Q: How does Laravel use the splat operator internally, and where would you encounter it in Illuminate source?
Laravel uses the splat operator extensively in its pipeline, middleware dispatch, and container resolution layers. In Illuminate\Pipeline\Pipeline, the then() method resolves each pipe closure and calls $carry = $pipe($passable, $stack) — the intermediate closures are built by reducing the pipes array. In Illuminate\Container\Container::call(), after building the resolved dependency list via Reflection, the container spreads the resolved array: $dependencies = $this->getMethodDependencies(...) and then calls the closure with $callable(...$dependencies). This is the runtime bridge between type-hinted constructor injection and PHP's native calling convention.