Functions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05. It covers named functions; anonymous functions, arrow functions and first-class callables are the subject of Closures and Callables, and methods are covered with classes in Classes and Objects.
Defining and calling functions
A function is declared with function, a name, a parameter list, an optional return type and a body. Function names
are case-insensitive, live in the current namespace, and are global: a function defined anywhere can be called from
anywhere, even before its definition in the same file, as long as it is declared unconditionally.
<?php
declare(strict_types=1);
echo loanDays('ebook'), "\n"; // 14 -- called before its definition: fine
function loanDays(string $format): int
{
return $format === 'ebook' ? 14 : 21;
}
Functions declared inside an if or inside another function only exist after that code has run. This conditional
definition is how polyfills provide functions missing from older PHP versions:
<?php
declare(strict_types=1);
if (!function_exists('array_first')) { // array_first() is native since PHP 8.5
function array_first(array $array): mixed
{
foreach ($array as $value) {
return $value;
}
return null;
}
}
var_dump(array_first(['Solaris', 'Kindred'])); // string(7) "Solaris"
A function cannot be redeclared, and nested function declarations (a function statement inside another function
body) declare global functions when the outer one runs, which fails the second time it runs. Use closures for
local helpers.
Source: User-defined functions
Arguments
By value and by reference
Arguments are passed by value by default: the function gets its own copy (arrays and strings share memory until
written, thanks to copy-on-write; objects are handles, so the object is shared). Prefixing a parameter with &
passes it by reference, so the function can modify the caller’s variable.
<?php
declare(strict_types=1);
function addCopy(array $stock, string $title): array // by value: returns a new array
{
$stock[$title] = ($stock[$title] ?? 0) + 1;
return $stock;
}
function addCopyInPlace(array &$stock, string $title): void // by reference: modifies the caller's array
{
$stock[$title] = ($stock[$title] ?? 0) + 1;
}
$stock = ['Kindred' => 1];
$newStock = addCopy($stock, 'Kindred');
addCopyInPlace($stock, 'Solaris');
print_r($newStock); // ["Kindred" => 2]
print_r($stock); // ["Kindred" => 1, "Solaris" => 1]
// Only variables can be passed by reference:
// addCopyInPlace(['x' => 1], 'y'); // Error: could not be passed by reference
Prefer returning new values: by-reference parameters hide the fact that a call changes its arguments. Internal
functions that sort or modify arrays (sort(), array_push(), preg_match()'s $matches) are the common exception.
Source: Passing arguments by reference
Default values, including new in initializers
A parameter with a default value is optional. Defaults must be constant expressions; since PHP 8.1 they may also use
new (new in initializers), and since 8.5 closures and first-class callables. Optional parameters should come
after required ones: a required parameter after an optional one is deprecated since 8.0 (the default is ignored).
<?php
declare(strict_types=1);
final class SystemClock
{
public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-10-05'); }
}
function dueDate(int $days = 21, SystemClock $clock = new SystemClock()): string // 8.1: new in initializer
{
return $clock->now()->modify("+{$days} days")->format('Y-m-d');
}
echo dueDate(), ' ', dueDate(14), "\n"; // 2026-10-26 2026-10-19
new in a default creates a fresh object on each call that uses the default, so it is a safe way to give
dependencies a default implementation that tests can replace.
Source: Default argument values
Variadic parameters and argument unpacking
A parameter prefixed with ... collects all remaining arguments into an array (5.6). It must be the last parameter,
and it can be typed and passed by reference. At the call site, ... unpacks an array or Traversable into
arguments; with string keys (8.1) the keys become named arguments.
<?php
declare(strict_types=1);
function shelve(string $shelf, string ...$titles): string
{
return sprintf('%s: %s (%d)', $shelf, implode(', ', $titles), count($titles));
}
echo shelve('A', 'Solaris', 'Kindred'), "\n"; // A: Solaris, Kindred (2)
$returns = ['Dawn', 'Imago'];
echo shelve('B', ...$returns), "\n"; // B: Dawn, Imago (2)
echo shelve(...['shelf' => 'C', 'titles' => 'Ariel']), "\n"; // string keys become named arguments (8.1)
Source: Variable-length argument lists
Named arguments (PHP 8.0)
Arguments can be passed by parameter name, in any order, skipping optional ones. This makes calls to functions with many optional parameters readable and lets you set only the option you need:
<?php
declare(strict_types=1);
function formatLoan(string $title, int $days = 21, bool $renewable = true, string $currency = 'EUR'): string
{
return sprintf('%s: %d days, %s, fines in %s', $title, $days, $renewable ? 'renewable' : 'final', $currency);
}
echo formatLoan('Kindred', renewable: false), "\n"; // skip $days, set $renewable
echo formatLoan(currency: 'GBP', title: 'Solaris'), "\n"; // any order
// Works with internal functions too
echo htmlspecialchars('<b>', double_encode: false), "\n";
var_dump(json_encode(['ñ'], flags: JSON_UNESCAPED_UNICODE));
Kindred: 21 days, final, fines in EUR
Solaris: 21 days, renewable, fines in GBP
<b>
string(6) "["ñ"]"
Rules: positional arguments must come before named ones; a parameter cannot be passed twice; unknown names throw an
Error; and a variadic parameter collects unknown named arguments with their names as string keys.
The consequence for library authors: parameter names are now part of your public API. Renaming a parameter breaks
every caller that used the name, so treat renames like signature changes. Child classes may rename parameters, but
then calls through the child type must use the new name; static analysers warn about it. The #[\NoDiscard] and
#[\SensitiveParameter] attributes, as well as PHPDoc, all refer to parameters by name.
Source: Named Arguments
Nullable parameters
A parameter that may be null is declared with ?T or T|null. A null default does not make the type nullable
any more: function f(Book $b = null) is deprecated since PHP 8.4 (see
Types and the Type System).
Source: Nullable types
Return values
return ends the function and provides its value; a function without return (or with a bare return;) returns
null. The return type, if declared, is checked (and coerced in non-strict files). Several values are returned as an
array and destructured, or better as a small readonly object.
<?php
declare(strict_types=1);
/** @return array{0: int, 1: int} [available, on loan] */
function stockSummary(array $copies): array
{
$onLoan = count(array_filter($copies, fn (bool $lent): bool => $lent));
return [count($copies) - $onLoan, $onLoan];
}
[$available, $onLoan] = stockSummary([true, false, false]);
echo "{$available} available, {$onLoan} on loan\n"; // 2 available, 1 on loan
Source: Returning values
Returning by reference
function &name() returns a reference, which the caller must also bind with =& to benefit from it. It is rarely
needed: PHP’s copy-on-write already avoids copies, and returning references makes data flow hard to follow. It is
used by some collection classes for offsetGet() on nested arrays.
<?php
declare(strict_types=1);
final class Registry
{
private array $items = ['count' => 0];
public function &item(string $key): mixed
{
return $this->items[$key];
}
public function get(string $key): mixed { return $this->items[$key]; }
}
$r = new Registry();
$count = &$r->item('count'); // bind the returned reference
$count++;
var_dump($r->get('count')); // int(1)
Source: Returning References
#[\NoDiscard] and the (void) cast (PHP 8.5)
Some functions return a value that callers must not ignore: a new immutable object, an error flag, a lock handle.
Marking a function or method with the #[\NoDiscard] attribute makes PHP emit a warning when its result is not used.
When discarding is intentional, write the (void) cast to say so explicitly. Since 8.5 several internal methods carry
the attribute, notably DateTimeImmutable::modify(), add(), sub() and the set*() methods, which return a new
object instead of changing the existing one: calling them without using the result is always a bug.
<?php
declare(strict_types=1);
final class Loan
{
public function __construct(public readonly string $isbn, public readonly int $renewals = 0) {}
#[\NoDiscard('Loan is immutable: use the returned instance')]
public function renew(): self
{
return new self($this->isbn, $this->renewals + 1);
}
}
$loan = new Loan('978-0-00-000001-1');
$loan->renew(); // Warning: The return value of method Loan::renew() should either be used or
// intentionally ignored by casting it as (void), Loan is immutable: use the returned instance
$loan = $loan->renew(); // correct
(void) $loan->renew(); // explicitly discarded: no warning
echo $loan->renewals, "\n"; // 1
Variable functions and internal functions
If a variable holds a string, $name() calls the function with that name; arrays [$object, 'method'] and
[ClassName::class, 'method'] work the same way. This is how callbacks were written before closures and first-class
callables. Internal functions are those provided by PHP and its extensions; function_exists() tells whether one is
available, and get_defined_functions() lists them.
<?php
declare(strict_types=1);
$formatter = 'strtoupper';
echo $formatter('kindred'), "\n"; // KINDRED
$checks = ['is_numeric', 'ctype_digit'];
foreach ($checks as $check) {
printf("%s('1979') = %s\n", $check, var_export($check('1979'), true));
}
var_dump(function_exists('array_find')); // bool(true) on PHP 8.4+
var_dump(function_exists('each')); // bool(false): removed in PHP 8.0
Some language constructs look like functions but are not, and cannot be called as variable functions: echo,
print, isset, unset, empty, include, require and eval. Never build a function name from user input.
Source: Variable functions and Internal (built-in) functions
Recursion
A function may call itself. PHP has no tail-call optimisation and every call uses stack space; very deep recursion
(tens of thousands of levels) can crash the process with a segmentation fault, or, since PHP 8.3, end with an Error
when zend.max_allowed_stack_size is reached. Iterate, or use an explicit stack, for unbounded depth.
<?php
declare(strict_types=1);
/** @param array<string, array> $tree */
function countBooks(array $tree): int
{
$total = count($tree['books'] ?? []);
foreach ($tree['sections'] ?? [] as $section) {
$total += countBooks($section); // recursive call
}
return $total;
}
$library = [
'books' => ['Kindred'],
'sections' => [
['books' => ['Solaris', 'Dawn']],
['books' => [], 'sections' => [['books' => ['Ariel']]]],
],
];
echo countBooks($library), "\n"; // 4
Source: Recursive functions
func_get_args and friends
func_get_args(), func_num_args() and func_get_arg($n) return the arguments actually passed, including extra
ones not declared as parameters. Since PHP 7 they reflect the current value of the parameters, not the original
arguments. They predate variadics; use ...$args instead, which is typed, visible in the signature and understood by
tools.
<?php
declare(strict_types=1);
function legacyJoin(): string
{
return implode(', ', func_get_args()); // pre-5.6 style
}
function modernJoin(string ...$parts): string
{
return implode(', ', $parts);
}
echo legacyJoin('Solaris', 'Kindred'), "\n";
echo modernJoin('Solaris', 'Kindred'), "\n";
Source: func_get_args
Common pitfalls
-
Renaming parameters breaks named-argument callers.
-
Optional before required: deprecated; the default is ignored.
-
By-reference parameters with literals: only variables can be passed by reference.
-
Relying on implicit
nulldefaults for nullability: deprecated since 8.4. -
Nested
functiondeclarations create global functions on each call of the outer function and fail the second time. -
Ignoring results of immutable operations:
$date->modify()on aDateTimeImmutablereturns a new object; mark your own such methods with#[\NoDiscard].
Where older books and tutorials are outdated
-
Named arguments (8.0), new in initializers (8.1), string-key unpacking (8.1),
#[\NoDiscard]and(void)(8.5) are absent from books written for PHP 7. -
func_get_args()for variadic functions (Beak ch. 2, Kromann) is superseded by...$args. -
create_function()(Beak, Kromann) was removed in 8.0; use a closure. -
Implicitly nullable parameters (
Type $x = null) are deprecated since 8.4. -
call_user_func()is no longer needed for variable functions or callables:$callable(...$args)works for every callable form.
References
-
PHP Manual — Functions: user-defined functions, function arguments (by reference, defaults, variadics, named arguments), returning values, variable functions, internal functions
-
PHP RFC — Named Arguments and PHP RFC — Marking return values as important