Defining and calling a function

A function is a named block of code that you define once and call wherever you need it. In PHP you define one with the keyword function, a name, a list of parameters in parentheses and a body in braces. return ends the function and hands a value back to the caller.

<?php
echo energyCost(318.4, 0.27), "\n";    // called before its definition: fine

function energyCost($kwh, $pricePerKwh)
{
    return round($kwh * $pricePerKwh, 2);
}

echo ENERGYCOST(100, 0.27), "\n";      // function names ignore case

function logLine($message)
{
    echo "[portal] $message\n";        // no return: the result is null
}
var_dump(logLine('meter read'));
85.97
27
[portal] meter read
NULL

Three details in this example are typical of PHP:

  • A function defined at the top level of a file can be called before its definition, because PHP reads the whole file before running it. That does not apply to a function defined inside an if block or inside another function: it exists only once that code has run.
  • Function names are case-insensitive: ENERGYCOST() calls energyCost(). Variable names are case-sensitive. Always call a function with the exact name it was defined with; anything else only confuses readers.
  • A function without return, or with a bare return;, returns null.

Parameters and default values

A parameter is a variable in the function definition; an argument is the value you pass for it in a call. A parameter with a default value, written $unit = 'kWh', is optional. Optional parameters come after the required ones.

<?php
function formatReading($kwh, $unit = 'kWh', $decimals = 1)
{
    return number_format($kwh, $decimals) . ' ' . $unit;
}

echo formatReading(1542.66), "\n";
echo formatReading(1.54266, 'MWh', 3), "\n";

function addReading(array $readings, float $kwh)
{
    $readings[] = $kwh;      // changes the local copy only
    return $readings;
}
$week = [318.4];
$newWeek = addReading($week, 296.0);
echo count($week), ' ', count($newWeek), "\n";

function addReadingInPlace(array &$readings, float $kwh)
{
    $readings[] = $kwh;      // & : changes the caller's array
}
addReadingInPlace($week, 301.5);
echo count($week), "\n";
1,542.7 kWh
1.543 MWh
1 2
2

By value and by reference

PHP passes arguments by value: the function receives a copy. That includes arrays, so addReading() above cannot change $week; it returns a new array instead. Putting & before a parameter (array &$readings) passes it by reference: the parameter becomes another name for the caller's variable, and changes are visible outside.

Prefer returning a value. A function that quietly changes its arguments is harder to follow and to test. References make sense for large arrays that really must be updated in place, and some built-in functions use them, such as sort() and preg_match(), which fills its third argument with the matches.

Type declarations

A type declaration states which type a parameter accepts: float $kwh, string $message, array $readings. Unlike Python's type hints, which the interpreter ignores, PHP checks these declarations every time the function is called. A value of the wrong type is converted if the conversion is safe, and otherwise rejected with a TypeError.

Return types

A return type, written after the parameter list as : float, declares what the function gives back. PHP checks the returned value the same way: function total(): int { return 'abc'; } fails with Return value must be of type int, string returned. The special return type void means the function returns nothing.

<?php
function energyCost(float $kwh, float $pricePerKwh): float
{
    return round($kwh * $pricePerKwh, 2);
}

function findCustomer(int $id): ?string      // a string or null
{
    $customers = [501 => 'Lucía Romero', 502 => 'Joost Bakker'];
    return $customers[$id] ?? null;
}

function formatId(int|string $id): string    // union type (PHP 8.0)
{
    return 'CUST-' . $id;
}

function logLine(string $message): void      // returns nothing
{
    echo "[portal] $message\n";
}

var_dump(energyCost(318.4, 0.27));
var_dump(energyCost(100, 0.27));      // int is accepted for float
var_dump(energyCost('318.4', 0.27));  // numeric string is converted
var_dump(findCustomer(502), findCustomer(999));
echo formatId(501), ' ', formatId('A17'), "\n";
logLine('done');
float(85.97)
float(27)
float(85.97)
string(12) "Joost Bakker"
NULL
CUST-501 CUST-A17
[portal] done

Nullable and union types

A nullable type is a type with a question mark in front, such as ?string: the value may be a string or null. It is the honest return type for a lookup that can find nothing, like findCustomer() above. A union type (PHP 8.0) lists several accepted types with |, as in int|string $id; ?string is short for string|null.

Type Accepts
int, float, string, bool The scalar types; an int is also accepted where a float is declared
array, iterable, callable An array; an array or other iterable; anything that can be called
A class or interface name An object of that class, or of a class that extends or implements it
?string, int|string Nullable and union types
mixed Any value, including null (PHP 8.0)
void (return only) The function returns nothing
never (return only) The function never returns: it always throws or exits (PHP 8.1)

Coercive and strict mode

By default PHP runs in coercive mode: a scalar argument of the wrong type is converted when that is possible without guessing. The numeric string '318.4' becomes the float 318.4, as the example above showed. A string that is not a number cannot be converted, so the call fails:

<?php
function energyCost(float $kwh, float $pricePerKwh): float
{
    return round($kwh * $pricePerKwh, 2);
}

echo energyCost(318.4, 0.27), "\n";
echo energyCost('318.4 kWh', 0.27), "\n";   // not a number
85.97
Fatal error: Uncaught TypeError: energyCost(): Argument #1 ($kwh) must be of type float, string given, called in example.php on line 8 and defined in example.php:2

With declare(strict_types=1); as the very first statement of a file, calls made from that file are checked strictly: a string is no longer converted to a number at all. The single exception is that an int is still accepted for a float. The setting also applies to built-in functions called from the file, so strlen(5) becomes a TypeError. Variables, Types and Type Juggling explains strict_types in full.

<?php
declare(strict_types=1);

function energyCost(float $kwh, float $pricePerKwh): float
{
    return round($kwh * $pricePerKwh, 2);
}

echo energyCost(100, 0.27), "\n";       // int to float: still allowed
echo energyCost((float) '318.4', 0.27), "\n";
echo energyCost('318.4', 0.27), "\n";   // strict: no conversion
27
85.97
Fatal error: Uncaught TypeError: energyCost(): Argument #1 ($kwh) must be of type float, string given, called in example.php on line 11 and defined in example.php:4

Named arguments

Since PHP 8.0 you can pass named arguments: name: value, using the parameter's name without the dollar sign. They make a call self-explanatory (thousands: false says more than a bare false) and let you skip optional parameters you do not need. This is PHP's version of Python's keyword arguments.

<?php
function formatReading(float $kwh, string $unit = 'kWh', int $decimals = 1, bool $thousands = true): string
{
    $text = number_format($kwh, $decimals, '.', $thousands ? ',' : '');
    return "$text $unit";
}

echo formatReading(1542.66, decimals: 2), "\n";               // skip $unit
echo formatReading(thousands: false, kwh: 1542.66), "\n";     // any order
echo formatReading(1542.66, 'kWh', 0, false), "\n";           // positional

// Built-in functions accept named arguments too
echo str_pad('42', 6, '0', STR_PAD_LEFT), "\n";
echo str_pad('42', length: 6, pad_string: '0', pad_type: STR_PAD_LEFT), "\n";
1,542.66 kWh
1542.7 kWh
1543 kWh
000042
000042

The rules: positional arguments come first, named ones after them (the reverse is a compile error); each parameter may receive only one value; and an unknown name is an Error (Unknown named parameter $precision). Named arguments work with built-in functions too, which helps with functions that have many optional parameters, such as str_pad() or json_encode().

Variadic functions and spreading arguments

A variadic function accepts any number of arguments. In PHP, the last parameter gets three dots in front of it (float ...$readings) and receives all remaining arguments as an array, much like a JavaScript rest parameter or Python's *args. A type declaration on it applies to every element.

The same three dots in a call do the opposite: they spread an array into separate arguments. String keys in the spread array become named arguments (PHP 8.0); since PHP 8.1 you may also add named arguments after the spread.

<?php
function totalKwh(string $meter, float ...$readings): string
{
    // $readings is an array of all remaining arguments
    return sprintf('%s: %d readings, %.1f kWh', $meter, count($readings), array_sum($readings));
}

echo totalKwh('MTR-100231', 318.4, 296.0, 301.5), "\n";
echo totalKwh('MTR-100232'), "\n";

$week = [12.5, 11.0, 13.0];
echo totalKwh('MTR-100233', ...$week), "\n";          // spread a list into arguments
echo totalKwh(...['meter' => 'MTR-100234']), "\n";    // string key = named argument
MTR-100231: 3 readings, 915.9 kWh
MTR-100232: 0 readings, 0.0 kWh
MTR-100233: 3 readings, 36.5 kWh
MTR-100234: 0 readings, 0.0 kWh

Scope: what a function can see

The scope rules are where PHP differs most from Python and JavaScript. A PHP function has its own local scope, and that is all it sees: its parameters and the variables it creates. Variables from the rest of the file are not visible inside, even when they were defined before the function.

<?php
$tariff = 0.27;

function costWithoutAccess(float $kwh): float
{
    return $kwh * $tariff;     // this $tariff is a new, empty local variable
}

function costWithParameter(float $kwh, float $tariff): float
{
    return $kwh * $tariff;     // pass in what the function needs
}

echo costWithoutAccess(100), "\n";
echo costWithParameter(100, $tariff), "\n";
0
27

Inside costWithoutAccess(), $tariff is a new local variable that was never assigned, so PHP treats it as null (which becomes 0 in the multiplication) and writes a warning to the error output:

Warning: Undefined variable $tariff in /var/www/portal/cost.php on line 5

The fix is to pass the value as a parameter, as costWithParameter() does. PHP also has the global keyword (global $tariff; inside the function) and the $GLOBALS array, which give access to variables of the main script. You will meet them in older code, including WordPress plugins, but they hide the dependencies of a function and make it hard to test.

Static variables

A local variable declared with static is initialized only once and keeps its value between calls. It is handy for a counter or a small cache, but it is hidden state: use it sparingly.

<?php
function nextTicketNumber(): string
{
    static $counter = 31900;   // initialized once, kept between calls
    $counter++;
    return "INC-$counter";
}

echo nextTicketNumber(), "\n";
echo nextTicketNumber(), "\n";
echo nextTicketNumber(), "\n";
INC-31901
INC-31902
INC-31903

Closures and arrow functions

A function does not need a name. An anonymous function, function (…) { … } without a name, is a value: you can store it in a variable, call it as $cost(100), and pass it to another function as a callback. Because of the scope rules, it does not see outer variables by itself. You list the ones it needs after use, which copies their values into it at the moment it is created. That makes it a closure. In PHP every anonymous function is an object of the class Closure.

An arrow function (PHP 7.4), fn($kwh) => …, is the short form for a function whose body is a single expression. It captures every outer variable it uses automatically, so it needs no use. It is the natural partner of array_map() and array_filter().

<?php
$tariff = 0.27;
$readings = ['Zwolle' => 1210, 'Deventer' => 1542];

// Anonymous function: outer variables only via use (copied)
$cost = function (float $kwh) use ($tariff): float {
    return round($kwh * $tariff, 2);
};
print_r(array_map($cost, $readings));

// Arrow function: one expression, outer variables captured automatically
print_r(array_map(fn($kwh) => round($kwh * $tariff, 2), $readings));

$tariff = 0.30;           // changing it later does not affect $cost
echo $cost(100), "\n";
Array
(
    [Zwolle] => 326.7
    [Deventer] => 416.34
)
Array
(
    [Zwolle] => 326.7
    [Deventer] => 416.34
)
27

The last line prints 27, not 30: use ($tariff) copied the value 0.27 when the closure was created. To share the variable itself, capture it by reference with use (&$total). Arrow functions always capture by value, so they cannot change an outer variable:

<?php
$total = 0;

$addCopy = function (float $kwh) use ($total) {
    $total += $kwh;       // changes the closure's own copy
};
$addRef = function (float $kwh) use (&$total) {
    $total += $kwh;       // & : changes the outer variable
};
$addArrow = fn($kwh) => $total += $kwh;   // arrow functions copy too

$addCopy(10);
$addArrow(20);
echo $total, "\n";
$addRef(30);
echo $total, "\n";
0
30

Functions as values: callables

Anything PHP can call is a callable: a closure, an arrow function, the name of a function as a string ('toUpperId'), or an array with an object or class and a method name. Older code passes function names as strings, which editors and static analysis tools cannot follow reliably. Since PHP 8.1, the first-class callable syntax turns any function or method into a Closure object by writing its call with three dots in the parentheses: toUpperId(...), strlen(...) or $meter->read(...).

<?php
function toUpperId(string $id): string
{
    return strtoupper(trim($id));
}

$ids = [' mtr-100231', 'mtr-100232 '];

print_r(array_map(toUpperId(...), $ids));    // first-class callable (PHP 8.1)
print_r(array_map('toUpperId', $ids));       // older style: the name as a string

$length = strlen(...);                       // works for built-ins too
echo $length('Deventer'), "\n";
var_dump($length instanceof Closure, is_callable('toUpperId'));
Array
(
    [0] => MTR-100231
    [1] => MTR-100232
)
Array
(
    [0] => MTR-100231
    [1] => MTR-100232
)
8
bool(true)
bool(true)

Here toUpperId(...) does not call the function: the three dots are literal syntax, not an omission. The result is a closure that calls toUpperId() when it is invoked. A parameter declared as callable accepts all these forms, and is_callable() tests whether a value is one.