04 · Functions¶
🎥 Video walkthrough¶
Functions group reusable logic under a name. PHP functions can take typed parameters, default values, and return typed values (all optional, but recommended).
Basic function syntax¶
Type declarations and return types¶
<?php
function add(int $a, int $b): int {
return $a + $b;
}
echo add(2, 3) . "\n"; // 5
// add("2", "3") would still work due to PHP's type coercion in non-strict mode
Add declare(strict_types=1); as the very first line of a file to make PHP
reject mismatched types instead of silently coercing them — recommended for
any serious project.
<?php
declare(strict_types=1);
function add(int $a, int $b): int {
return $a + $b;
}
echo add(2, 3) . "\n"; // 5
// add("2", "3") now throws a TypeError instead of coercing
Default and named arguments¶
<?php
function greet(string $name, string $greeting = "Hello"): string {
return "$greeting, $name!";
}
echo greet("Ada") . "\n"; // Hello, Ada!
echo greet("Ada", "Hi") . "\n"; // Hi, Ada!
// Named arguments (PHP 8+) -- skip earlier optional params by name
echo greet(name: "Grace", greeting: "Hey") . "\n"; // Hey, Grace!
Variadic functions (...$args)¶
<?php
function total(int ...$numbers): int {
return array_sum($numbers);
}
echo total(1, 2, 3, 4) . "\n"; // 10
Passing by reference¶
<?php
function doubleIt(int &$n): void {
$n *= 2; // modifies the CALLER's variable directly
}
$value = 5;
doubleIt($value);
echo $value . "\n"; // 10
By default, PHP passes scalars (int, string, bool, float) by value — the
function gets its own copy. The & before the parameter switches to
pass-by-reference, letting the function modify the caller's variable.
Anonymous functions and arrow functions¶
<?php
// Anonymous function (closure)
$square = function (int $n): int {
return $n * $n;
};
echo $square(5) . "\n"; // 25
// Arrow function (PHP 7.4+) -- implicitly captures outer variables
$factor = 3;
$multiply = fn($n) => $n * $factor;
echo $multiply(4) . "\n"; // 12
// Closures over outer variables explicitly, with "use"
$threshold = 10;
$isAboveThreshold = function (int $n) use ($threshold): bool {
return $n > $threshold;
};
var_dump($isAboveThreshold(15)); // bool(true)
fn() => ... auto-captures variables from the surrounding scope by value;
regular function() use (...) {} closures require you to list captured
variables explicitly.
Scope¶
<?php
$counter = 0; // global scope
function increment() {
global $counter; // must explicitly import globals -- unlike JS/Python
$counter++;
}
increment();
increment();
echo $counter . "\n"; // 2
Unlike JavaScript or Python, PHP functions do not see outer-scope
variables automatically — you must global them or pass them in explicitly.
This is a deliberate design choice that avoids accidental variable leakage.
Cheat sheet¶
| Feature | Syntax |
|---|---|
| Basic function | function name($a, $b) { ... return $x; } |
| Typed params/return | function name(int $a): string { ... } |
| Default value | function name($a, $b = 10) { ... } |
| Named argument | name(b: 5) |
| Variadic | function name(...$args) { ... } |
| By reference | function name(&$x) { ... } |
| Anonymous function | $f = function($x) { return $x; }; |
| Arrow function | $f = fn($x) => $x * 2; |
How It Actually Works¶
Every function call allocates a new execution context (an execute_data/call frame) on PHP's internal call stack, with its own symbol table mapping variable names to zval slots — this is why variables inside a function are invisible outside it without global or closures. Type declarations on parameters aren't erased at compile time the way TypeScript's are: the engine inserts a runtime type-check opcode before the call frame is even entered, coercing or rejecting the argument (throwing TypeError in strict mode) before your function body runs a single line. Passing by reference (&$arg) changes what gets bound in the callee's symbol table: instead of the callee getting its own zval pointing at a (possibly COW-shared) value, its symbol table slot points at the same zval reference as the caller's variable, so writes are visible on both sides immediately — no copy is ever made. Closures capture the surrounding scope by creating a Closure object that snapshots the used variables into its own internal property array at closure-creation time (by value, unless you write use (&$var), which stores a reference instead) — the closure does not keep a live pointer into the enclosing function's now-destroyed stack frame.
🔀 See this in another language¶
Exercise¶
Write a function formatCurrency(float $amount, string $symbol = "$"): string
that returns the amount formatted to 2 decimal places with the symbol
prefixed (e.g. formatCurrency(9.5) returns "$9.50"). Then write a
variadic function sumPositive(int ...$numbers): int that sums only the
positive numbers passed in, ignoring zero and negative values.