<< Home

Chapter 6: Functions

Functions package work behind a name so you can reuse logic, return results, and keep code small and clear. PHP supports traditional functions, closures, and concise arrow functions. You can add types, defaults, attributes, and more for precise behavior.

Defining functions and return values

Define a function with the function keyword, then write parameters in parentheses and the body in braces. Return a value with return; omit it when you only need side effects.

Writing a basic function with a return value

Declare the function once, then call it wherever you need the result.

<?php
function areaOfCircle(float $r): float {
  $pi = 3.14159;
  return $pi * $r * $r;
}

echo areaOfCircle(2.5);
?>

Adding parameter and return types for clarity

Use type declarations on parameters and a return type after the signature. Union types allow several accepted types.

<?php
function toIntOrNull(int|string $v): ?int {
  if (is_int($v)) {
    return $v;
  }
  if (ctype_digit($v)) {
    return (int)$v;
  }
  return null;
}
?>
💡 Use declare(strict_types=1); at the top of files where you want strict parameter type checks; strict typing improves predictability.

Returning early to keep branching simple

Return as soon as you have the answer; this keeps nesting shallow and intent obvious.

<?php
function firstPositive(array $nums): ?int {
  foreach ($nums as $n) {
    if ($n > 0) {
      return $n;
    }
  }
  return null;
}
?>

Parameters, defaults, and named arguments

Parameters accept input. Defaults provide fallback values. Named arguments improve readability by pairing names with values at call sites.

Providing sensible defaults in the signature

Place defaulted parameters after required ones. A default must match the declared type or be null when the type allows it.

<?php
function greet(string $name, string $salutation = "Hello"): string {
  return "$salutation, $name";
}

echo greet("Kai");             // Hello, Kai
echo greet("Kai", "Welcome");  // Welcome, Kai
?>

Calling with named arguments to improve clarity

Use name: value pairs at the call site. Order is flexible when you supply names.

<?php
function rectangle(int $width, int $height, bool $border = false): array {
  return compact("width", "height", "border");
}

$config = rectangle(height: 200, width: 300, border: true);
?>
⚠️ Do not mix positional arguments after named ones; once you start naming, all following arguments must be named as well.

Accepting null with nullable types

Prefix the type with ? to allow null. Use a default of null when absence is meaningful.

<?php
function findUser(?int $id = null): array {
  if ($id === null) {
    return ["mode" => "guest"];
  }
  return ["id" => $id, "mode" => "member"];
}
?>

Variadics and unpacking

Variadics collect many values into one parameter. Unpacking spreads an array or Traversable into individual arguments where a list is required.

Gathering many inputs with a variadic ... parameter

Place ... before the parameter name to capture remaining arguments as an array.

<?php
function sumAll(int ...$nums): int {
  $total = 0;
  foreach ($nums as $n) {
    $total += $n;
  }
  return $total;
}

echo sumAll(2, 4, 6);
?>

Unpacking arrays into parameters with ...

Use ... at the call site to unpack an array into separate arguments.

<?php
function joinWith(string $sep, string ...$parts): string {
  return implode($sep, $parts);
}

$words = ["a", "b", "c"];
echo joinWith("-", ...$words);
?>
💡 Named arguments and unpacking can be combined. Supply named options, then unpack a list for the rest; keep the call readable.

Unpacking arrays by key for named-like behavior

When a function expects an associative array, the spread operator can merge arrays to build the final input.

<?php
$base = ["host" => "localhost"];
$overrides = ["port" => 5432];

$config = [...$base, ...$overrides, "user" => "app"];
?>

Closures and arrow functions

Anonymous functions capture surrounding variables and can be passed around like values. Arrow functions offer compact syntax for single expressions.

Creating a Closure and capturing with use

Use use to import variables from the surrounding scope by value; add & to import by reference.

<?php
$factor = 10;

$scale = function (int $n) use ($factor): int {
  return $n * $factor;
};

echo $scale(3);  // 30
?>

Writing concise fn arrow functions

Arrow functions automatically capture variables by value and evaluate a single expression. The expression result becomes the return value.

<?php
$nums = [1, 2, 3, 4];

$doubled = array_map(fn($n) => $n * 2, $nums);
?>
⚠️ Arrow functions cannot contain statements; when you need multiple statements or complex logic, use a full function closure.

Returning closures from functions for customization

Generate specialized behavior by returning a closure configured with parameters.

<?php
function makePrefixer(string $prefix): callable {
  return fn(string $s): string => $prefix . $s;
}

$warn = makePrefixer("[WARN] ");
echo $warn("Low memory");
?>

Scope, static variables, and recursion

Scope controls where names are visible. Static variables persist their value across calls. Recursion lets a function call itself to solve problems in smaller steps.

Understanding local scope and the global symbol table

Variables inside a function are local. To access a global, prefer passing it in; if you must, read it via the global keyword or $GLOBALS array.

<?php
$rate = 0.2;

function vat(float $net): float {
  global $rate;  // prefer dependency injection instead
  return $net * (1 + $rate);
}
?>

Persisting data between calls with static

A static local variable keeps its value across calls. Use this for simple memoization or counters.

<?php
function nextId(): int {
  static $id = 0;
  $id++;
  return $id;
}

echo nextId();  // 1
echo nextId();  // 2
?>
💡 For complex shared state, prefer a small object with private properties; this keeps state explicit and testable.

Applying recursion with a safe base case

Every recursive function needs a base case to stop calling itself. Verify argument sizes and use iteration when recursion depth might be large.

<?php
function factorial(int $n): int {
  if ($n <= 1) {
    return 1;
  }
  return $n * factorial($n - 1);
}
?>

Attributes on functions

Attributes attach structured metadata to declarations. Frameworks and tools read attributes to modify behavior at runtime or during reflection.

Declaring a #[Attribute] above a function

Place attributes in square brackets with a leading hash. Supply arguments like regular constructor calls.

<?php
#[Deprecated("Call newProcess() instead")]
function oldProcess(): void {
  // …
}
?>

Reading attributes with reflection

Use the Reflection API to inspect attributes and their arguments at runtime.

<?php
$func = new ReflectionFunction('oldProcess');

foreach ($func->getAttributes() as $attr) {
  $name = $attr->getName();
  $args = $attr->getArguments();
  echo "$name: " . json_encode($args) . PHP_EOL;
}
?>
⚠️ Attributes do not enforce behavior by themselves; a library, framework, or your code must read them and act on their presence.

Defining a custom attribute class for precise semantics

Mark your own attribute class with #[Attribute] so PHP recognizes it as an attribute target.

<?php
#[Attribute(Attribute::TARGET_FUNCTION)]
class Route {
  public function __construct(
    public string $method,
    public string $path
  ) {}
}

#[Route(method: "GET", path: "/status")]
function status(): array {
  return ["ok" => true];
}
?>