PHP Function Arguments
Function arguments are the values you hand to a function so it can do its work. PHP gives you far more control over arguments than just listing them in order: you can set default values, declare their types, pass them by reference so the function can modify the caller’s variable, accept a variable number of arguments, and even name your arguments at the call site. Understanding these mechanisms is essential for writing functions that are safe, flexible, and easy to call correctly.
Overview / How It Works
When PHP calls a function, it creates a new local scope (a new symbol table) for that function’s execution. Each parameter listed in the function’s signature becomes a local variable inside that scope, bound to the value supplied at the call site. By default, PHP copies the value into the parameter — this is called pass by value. If the argument is a scalar (int, string, bool, float), the copy is a genuine independent copy. If the argument is an array, PHP uses a technique called copy-on-write: the array isn’t physically duplicated in memory until either the caller or the function actually modifies it, at which point the engine forks the data. Either way, from the programmer’s point of view, changes made to a by-value parameter inside the function never affect the caller’s original variable.
Objects behave differently in one specific sense: a variable holding an object actually holds a handle (an internal reference) to the object stored in memory. When you pass an object as an argument, PHP copies the handle, not the object itself — so both the caller’s variable and the parameter point to the very same object. Calling a mutating method or setting a property through the parameter will be visible to the caller, even though the object was technically “passed by value.” Only if you reassign the parameter to a brand-new object does the caller’s variable remain untouched.
PHP also supports true pass by reference, where the parameter becomes an alias for the caller’s variable itself, using an & in the function signature. And since PHP 5.6, a function can declare a final variadic parameter (...$args) to accept an unlimited number of trailing arguments, collected into an array. Since PHP 8.0, callers can supply arguments by name (argument: value) instead of position, and the same ... syntax can spread an array or generator’s values into individual arguments at a call site.
Syntax
function functionName(type $param1, type $param2 = defaultValue, type &$refParam, type ...$restParams): returnType {
// function body
}
// calling with named arguments and the spread operator
functionName(param1: $value1, param2: $value2);
functionName(...$arrayOfArguments);
| Part | Meaning |
|---|---|
type $param |
A type declaration (e.g. int, string, ?array, Foo|Bar) constraining the accepted value. Omit the type to accept anything. |
$param = defaultValue |
Makes the parameter optional; if the caller omits it, the default is used. Parameters with defaults must come after required ones (unless using named arguments). |
&$refParam |
Pass-by-reference: the parameter becomes an alias for the caller’s variable, so changes propagate back to the caller. |
...$restParams |
A variadic parameter; it must be the last parameter and collects any remaining arguments into an array. |
: returnType |
An optional return type declaration for the function itself. |
name: $value |
A named argument at the call site — matched by parameter name, not position, letting you skip earlier optional parameters. |
...$array (call site) |
The spread operator, unpacking an array (or Traversable) into individual positional arguments. |
Examples
Example 1: Default values and type declarations
<?php
function greet(string $name, string $greeting = "Hello"): string {
return "{$greeting}, {$name}!";
}
echo greet("Maria"), PHP_EOL;
echo greet("Chen", "Welcome"), PHP_EOL;
Output:
Hello, Maria!
Welcome, Chen!
The $greeting parameter has a default value of "Hello", so the first call only needs to supply $name. The second call overrides the default by passing a second argument explicitly. The string type declarations mean PHP will attempt to coerce non-string scalars into strings (or throw a TypeError in strict-types mode) if you pass the wrong type.
Example 2: Passing arrays by reference
<?php
function addTax(array &$prices, float $rate): void {
foreach ($prices as &$price) {
$price += $price * $rate;
}
unset($price);
}
$cart = [10.00, 25.50, 3.75];
addTax($cart, 0.08);
foreach ($cart as $price) {
echo number_format($price, 2), PHP_EOL;
}
Output:
10.80
27.54
4.05
Because $prices is declared with &, the array inside addTax() is the very same array as $cart in the caller — there is no copy. The inner foreach loop also uses a reference (as &$price) so it can modify each element in place, and the unset($price) afterward breaks that reference to avoid a classic bug where the last reference lingers and corrupts a later loop over the same variable.
Example 3: Variadic parameters, spread, and named arguments
<?php
function buildReport(string $title, int ...$scores): string {
$total = array_sum($scores);
$count = count($scores);
$average = $count > 0 ? $total / $count : 0;
return sprintf("%s: total=%d, average=%.1f", $title, $total, $average);
}
echo buildReport("Quiz 1", 8, 9, 7, 10), PHP_EOL;
$scores = [6, 7, 8];
echo buildReport("Quiz 2", ...$scores), PHP_EOL;
function createUser(string $name, int $age = 18, string $role = "student"): string {
return "{$name} ({$age}) - {$role}";
}
echo createUser(name: "Ada", role: "admin"), PHP_EOL;
Output:
Quiz 1: total=34, average=8.5
Quiz 2: total=21, average=7.0
Ada (18) - admin
buildReport() gathers any number of trailing integers into the $scores array via the variadic ...$scores parameter. The second call demonstrates the reverse operation: the spread operator ...$scores unpacks an existing array into separate positional arguments at the call site. The final call uses named arguments to set name and role while skipping age entirely, letting its default value of 18 apply — something that’s impossible with plain positional arguments unless age is the last parameter.
Under the Hood
When the Zend Engine compiles a function, each parameter is compiled into a slot in the function’s local variable table, along with metadata describing its type, default value opcode, and whether it’s passed by reference or is variadic. At call time, the engine walks the argument list you provided and, for each one, either copies the zval (PHP’s internal value container) into the parameter slot, or — for by-reference or object parameters — copies a pointer/handle so both sides observe the same underlying data. If a type declaration is present and the value doesn’t match, the engine attempts a coercion (in weak typing mode) following PHP’s scalar coercion rules, or throws a TypeError immediately (in strict_types mode, or when no valid coercion exists). Default values are only evaluated when the caller actually omits the argument — the engine jumps straight past the argument-binding opcode for that parameter and evaluates the default expression at that point, so a default value must be a compile-time constant expression (you can’t set one parameter’s default to reference another parameter’s value). For variadic parameters, the engine collects every remaining positional argument (and the spread contents of any ... unpacked arrays) into a single array, in the order received, before the function body begins executing.
Common Mistakes
Mistake 1: Expecting an array parameter to be modified without a reference
<?php
function addItem(array $items): array {
$items[] = "new item";
return $items;
}
$cart = ["apple", "banana"];
addItem($cart);
print_r($cart);
Output:
Array
(
[0] => apple
[1] => banana
)
Because $items is a normal by-value parameter, addItem() only modifies its own local copy of the array. The return value containing the updated array is discarded since the call result was never assigned to anything, so $cart is left completely unchanged. Fix it by either capturing the return value, or making the parameter a reference:
<?php
function addItem(array &$items): void {
$items[] = "new item";
}
$cart = ["apple", "banana"];
addItem($cart);
print_r($cart);
Output:
Array
(
[0] => apple
[1] => banana
[2] => new item
)
Mistake 2: Putting a variadic parameter anywhere but last
function badExample(int ...$numbers, string $label) {
return $label . ": " . array_sum($numbers);
}
PHP requires the variadic parameter to be the final one in the signature, because it greedily consumes every remaining argument. Writing it earlier causes a fatal compile-time error: Only the last parameter can be variadic. The fix is simply to reorder the parameters so the variadic one comes last:
<?php
function goodExample(string $label, int ...$numbers): string {
return $label . ": " . array_sum($numbers);
}
echo goodExample("Total", 4, 5, 6), PHP_EOL;
Output:
Total: 15
Best Practices
- Always add type declarations to parameters (and a return type) — they document intent and let PHP catch mistakes early with a
TypeErrorinstead of a silent bug. - Enable
declare(strict_types=1);at the top of files where you want PHP to refuse type coercion (e.g. rejecting a string"5"passed to anintparameter) rather than silently converting it. - Reserve pass-by-reference (
&) for cases where mutating the caller’s variable is genuinely the point (like sorting functions); overusing references makes code harder to reason about. - Prefer returning a new value over mutating an argument in place when either approach works — it keeps functions predictable and easier to test.
- Use named arguments to make call sites self-documenting, especially for functions with several boolean or numeric parameters where positional calls are easy to misread.
- Use variadic parameters instead of accepting a raw array when a function conceptually takes “one or more of X” — it makes the call site read more naturally.
- Keep required parameters before optional ones in the signature; if you must skip an earlier optional parameter, use named arguments rather than passing its default value explicitly.
Practice Exercises
- Write a function
formatPrice(float $amount, string $currency = "USD")that returns a string like"USD 19.99". Call it once with both arguments and once relying on the default. - Write a function
appendLog(array &$log, string $message): voidthat pushes$messageonto the$logarray by reference. Call it three times with the same$logvariable and print the final array to confirm all three messages were recorded. - Write a variadic function
maxOf(int ...$numbers): intthat returns the largest number passed to it (hint: look at the built-inmax()function, and remember you can spread an array intomaxOf()with...). Call it once with individual integers and once by spreading an array like[3, 9, 4].
Summary
- Scalars and arrays are passed by value by default; PHP copies the value (arrays use copy-on-write) so changes inside the function don’t affect the caller.
- Objects are passed by handle, so mutating an object’s properties or calling its methods through a parameter does affect the caller’s object, but reassigning the parameter to a new object does not.
- Use
&before a parameter name to make it a true reference to the caller’s variable. - Default values make parameters optional and are only evaluated when the caller omits the argument; they must be constant expressions.
- A variadic parameter (
...$name) must be last and collects any extra arguments into an array; the spread operator (...$array) does the reverse at a call site. - Named arguments (PHP 8+) let you call by parameter name, in any order, and skip optional parameters cleanly.
