Functions
Writing reusable code in PHP: parameters, default values, type declarations and return types, named arguments, variadics, scope, closures and arrow functions.
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
ifblock or inside another function: it exists only once that code has run. -
Function names are case-insensitive:
ENERGYCOST()callsenergyCost(). 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 barereturn;, returnsnull.
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.