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.

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.

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.

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)

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
&lt;b&gt;
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

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)

#[\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.

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

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 null defaults for nullability: deprecated since 8.4.

  • Nested function declarations create global functions on each call of the outer function and fail the second time.

  • Ignoring results of immutable operations: $date->modify() on a DateTimeImmutable returns 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.