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.4

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);.
  • ?type for nullable, A|B for unions, void for 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.