PHP Functions, Types and Scope
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
PHP 8 has a complete type system: parameters, return values and properties can all be typed. Good declarations move a large class of bugs from production to the first request.
Defining and calling
function greet(string $name): string
{
return "Hello, " . $name;
}
echo greet("ann"); // Hello, ann
Declarations are hoisted, so a call may appear above the definition — though with Composer autoloading the order rarely matters anyway.
Parameter and return types
function add(int $a, int $b): int
{
return $a + $b;
}
function isAdult(int $age): bool
{
return $age >= 18;
}
function findUser(int $id): ?array // array or null
{
return $id > 0 ? ["id" => $id] : null;
}
| Declaration | Meaning |
|---|---|
int float string bool |
Scalar types |
array |
Array |
?string |
string or null |
int|string |
Union type (PHP 8) |
void |
Returns nothing (return type only) |
never |
Never returns — always throws or exits |
callable |
Any callable |
Combine with declare(strict_types=1); so a wrong type raises TypeError instead of being coerced.
Defaults and named arguments
function createUser(string $name, string $role = "user", bool $active = true): array
{
return ["name" => $name, "role" => $role, "active" => $active];
}
Parameters with defaults must come after parameters without them.
PHP 8 supports named arguments, which let you skip middle parameters and ignore order:
createUser(name: "ann", active: false);
They pay off most when a function takes several booleans or optional settings.
Variadic parameters
function sum(int ...$numbers): int
{
return array_sum($numbers);
}
sum(1, 2, 3); // 6
And you can spread an array into the call:
$nums = [1, 2, 3];
sum(...$nums); // 6
By value, by reference, and objects
Arguments are passed by value by default:
function bump(int $n): void
{
$n++;
}
$count = 1;
bump($count);
echo $count; // 1 — unchanged
Declare & when the function must modify the caller’s variable:
function bump(int &$n): void
{
$n++;
}
$count = 1;
bump($count);
echo $count; // 2
Objects are the exception: an object variable holds a handle, so mutating properties affects the original without &:
function renameUser(object $user): void
{
$user->name = "bob"; // visible to the caller
}
Reference parameters hide side effects from the caller. Prefer returning a value unless you have a concrete reason.
Scope
A function does not see variables from outside:
$tax = 0.13;
function total(float $price): float
{
return $price * (1 + $tax); // Warning: Undefined variable $tax
}
Three fixes, best first:
// 1. pass it in (recommended)
function total(float $price, float $tax): float
{
return $price * (1 + $tax);
}
// 2. global (hard to trace, avoid)
function total(float $price): float
{
global $tax;
return $price * (1 + $tax);
}
// 3. a constant for genuine global configuration
const TAX_RATE = 0.13;
function total(float $price): float
{
return $price * (1 + TAX_RATE);
}
Superglobals ($_GET, $_POST, $_SERVER, …) are reachable inside functions, but passing values in keeps functions testable.
static locals
A static variable keeps its value between calls:
function counter(): int
{
static $count = 0;
return ++$count;
}
counter(); // 1
counter(); // 2
Useful for one-off memoisation, but it adds hidden state; a parameter or a class property is usually clearer.
Closures and arrow functions
$numbers = [1, 2, 3];
// Arrow function (PHP 7.4+): captures outer variables automatically, one expression
$doubled = array_map(fn($n) => $n * 2, $numbers);
// Anonymous function: capture explicitly with use
$factor = 10;
$scaled = array_map(function ($n) use ($factor) {
return $n * $factor;
}, $numbers);
use captures by value; write use (&$total) when the closure must modify the outer variable.
Worked example: input validation
<?php
declare(strict_types=1);
function validateUser(array $input): array
{
$errors = [];
$name = trim($input["name"] ?? "");
if (mb_strlen($name, "UTF-8") < 2) {
$errors[] = "Name must be at least 2 characters";
}
$age = filter_var($input["age"] ?? null, FILTER_VALIDATE_INT);
if ($age === false || $age < 0 || $age > 150) {
$errors[] = "Age must be an integer between 0 and 150";
}
return ["valid" => $errors === [], "errors" => $errors];
}
var_dump(validateUser(["name" => "a", "age" => "200"]));
// ["valid" => false, "errors" => ["Name must be at least 2 characters", "Age must be an integer between 0 and 150"]]
filter_var returns false on failure and 0 is a perfectly valid value, so the check must be === false.
Common mistakes
1. Putting default parameters before required ones
The later required parameter can no longer be omitted, and PHP emits a deprecation.
2. Testing filter_var or strpos with == false
0 is misread as failure. Use === false.
3. Reading an outer variable inside a function
You get null plus a warning. Pass it in, or use a constant.
4. Assuming objects are copied when passed
They are not; property changes are visible to the caller.
5. Forgetting use in a closure
The outer variable is invisible. An arrow function fn() captures it automatically.
6. Reference parameters that surprise the caller
Return a value instead unless mutation is the point.
Summary
- Type parameters and return values; add
declare(strict_types=1);. ?typefor nullable,A|Bfor unions,voidfor no return.- Variadics with
..., both in signatures and at call sites. - Functions do not share outer scope: pass arguments, use constants for config.
- Prefer
fn()for short callbacks.
Next: handling form input safely — superglobals, validation and output escaping.