---
title: Functions
source: 'https://academia.sh/en/courses/php-fundamentals/functions'
course: 'PHP Fundamentals'
language: en
updated: '2026-10-21T05:03:33+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows by measurement that an extra argument given to a user function is swallowed silently, while built-in functions reject it'
    - 'Tells apart which error a missing, unknown or colliding named argument produces'
    - 'Verifies the behavioral difference between a default value and a static variable across calls'
    - 'Shows by counting that ...$x can make the surplus visible, and shows how return types coerce'
---

# Functions

Argument and return behavior in PHP functions: silent swallowing of extra arguments, errors for missing and unknown arguments, default values and static, passing by value and by reference, return types, and making the surplus visible with ...$x.

The Control Flow lesson measured that an unmatched branch passes silently in `switch` and halts execution in `match`. This lesson asks the same question of calls: when a function is given more arguments than it expects, where does the surplus go, and what happens when too few are given? The channel of this lesson is silent: an argument given in excess to a function the user wrote is ignored without producing any notice.

In PHP, a function declared with the `function` keyword returns a value with `return`; its parameters can carry types and default values. The concept of a function was established in the Programming Fundamentals course (M01/K02); here only how PHP behaves toward that concept is measured. The question of whether a change is visible to the caller and to others was measured separately for objects in M11/K03; this lesson mentions it only as far as needed for scalar and array values.

## Extra Arguments

If a user function is given more arguments than it has parameters, PHP does not count the surplus as an error: the function still runs and the extra arguments are ignored. Built-in functions are strict on this. The file below gives three arguments to a two-parameter function and tries the same thing with the built-in `strlen()`; the error handler keeps a counter.

```php
<?php
// extra.php — a user function swallows the extra argument, a built-in rejects it
$notices = 0;
set_error_handler(function (int $level) use (&$notices): bool {
    $notices++;
    return true;
});

function add_two($a, $b)
{
    return $a + $b;
}

echo 'add_two(1, 2, 3):  ', add_two(1, 2, 3), "\n";

try {
    $length = strlen('abc', 'x');
    echo 'strlen(abc, x):    ', $length, "\n";
} catch (ArgumentCountError $error) {
    echo 'strlen(abc, x):    ', get_class($error), "\n";
}
echo 'notices: ', $notices, "\n";
```

```
add_two(1, 2, 3):  3
strlen(abc, x):    ArgumentCountError
notices: 0
```

**LE43.** The expression `add_two(1, 2, 3)` returned `3`, that is, the third argument took no part in the sum and nothing was said about it: the counter stayed at zero. The built-in `strlen()` answered the same surplus with an `ArgumentCountError`. This asymmetry is a design decision: not counting extra arguments as an error in user functions allows a function to later gain an additional optional parameter. The cost is that a typo in a call can be swallowed.

## Missing and Unknown Arguments

Unlike the surplus, a missing argument is an error: if a required parameter is not filled, an `ArgumentCountError` is thrown. Besides passing arguments by position, PHP allows passing them by name (a named argument). The file below makes five calls: a missing one, a complete one, a named one, one with an unknown name, and one that renames a parameter already filled by position.

```php
<?php
// missing.php — ArgumentCountError rejects a missing argument, Error rejects an unknown name
function greet(string $name, string $title = 'mr'): string
{
    return $title . ' ' . $name;
}

$calls = [
    'greet()'                          => fn() => greet(),
    "greet('Ada')"                     => fn() => greet('Ada'),
    "greet(title: 'dr', name: 'Ada')"  => fn() => greet(title: 'dr', name: 'Ada'),
    "greet('Ada', surname: 'X')"       => fn() => greet('Ada', surname: 'X'),
    "greet('Ada', name: 'Ece')"        => fn() => greet('Ada', name: 'Ece'),
];
foreach ($calls as $label => $call) {
    try {
        $text = var_export($call(), true);
    } catch (Error $error) {
        $text = get_class($error);
    }
    printf("%-34s%s\n", $label, $text);
}
```

```
greet()                           ArgumentCountError
greet('Ada')                      'mr Ada'
greet(title: 'dr', name: 'Ada')   'dr Ada'
greet('Ada', surname: 'X')        Error
greet('Ada', name: 'Ece')         Error
```

**LE44.** The missing argument gave an `ArgumentCountError`. When `$title`, which has a default value, was omitted, `'mr Ada'` was produced. Named arguments worked independently of order: `'dr Ada'` came out although `title` was written first. The name `surname`, which is not defined in the function, and the `name` parameter already filled by position each threw an `Error`. So an extra positional argument is silent and an extra named argument is loud; naming gives the user a stricter contract than the swallowing of extra arguments.

## Default Values and static

A default value is recomputed on every call; that is, an empty array written as the default of a parameter is a new empty array on every call and does not carry the elements added by the previous call. A variable declared with `static` inside a function, on the other hand, lives across calls. The file below puts the two behaviors side by side.

```php
<?php
// defaults.php — the default is renewed on every call, static lives across calls
function append(int $item, array $list = []): array
{
    $list[] = $item;
    return $list;
}

function count_calls(): int
{
    static $counter = 0;
    $counter++;
    return $counter;
}

echo 'append(1):     ', count(append(1)), "\n";
echo 'append(2):     ', count(append(2)), "\n";

count_calls();
count_calls();
echo 'count_calls(): ', count_calls(), "\n";
```

```
append(1):     1
append(2):     1
count_calls(): 3
```

**LE45.** `append` returned one element in both calls: the default empty array was renewed on every call. `count_calls` gave `3` on the third call: the `static` variable was not erased each time the function finished. The two show the same distinction from two directions: parameters and local variables belong to the call, a `static` variable belongs to the function. Neither produces a notice.

## By Value and by Reference

PHP passes arguments by value by default: the function works on its own copy and does not touch the caller's variable. If a parameter is declared with `&`, the function changes the caller's variable. The file below makes the same change with two declarations and counts the caller's array.

```php
<?php
// by_value.php — value copies, reference changes the caller's variable
function by_value(array $list, int $number): void
{
    $list[] = 99;
    $number++;
}

function by_reference(array &$list, int &$number): void
{
    $list[] = 99;
    $number++;
}

$items = [1, 2];
$n = 10;

by_value($items, $n);
echo 'by_value:     ', count($items), ' elements, n: ', $n, "\n";

by_reference($items, $n);
echo 'by_reference: ', count($items), ' elements, n: ', $n, "\n";
```

```
by_value:     2 elements, n: 10
by_reference: 3 elements, n: 11
```

**LE46.** After the call passed by value, the array stayed at two elements and the number at ten: the function changed the copy. After the call passed by reference, the array had three elements and the number was eleven. The consequence of this distinction for objects was measured in M11/K03; what is observed here is that unless `&` is written, the language does not make a change visible to the caller. A reference parameter is read from the function's signature: there is no mark at the call.

## Return Values

A function that has no `return` returns `null`. If a return type is declared, PHP coerces the returned value to that type, just as with parameters: the coercion rule in the Variables and Data Types lesson works here too. The file below shows return behavior in four cases.

```php
<?php
// returns.php — null without return, the return type coerces or rejects
function nothing()
{
}

function to_int(): int
{
    return '5';
}

function broken(): int
{
    return 'five';
}

function blank(): void
{
}

echo 'nothing(): ', var_export(nothing(), true), "\n";
echo 'blank():   ', var_export(blank(), true), "\n";

$result = to_int();
echo 'to_int():  ', var_export($result, true), ' ', get_debug_type($result), "\n";

try {
    broken();
} catch (TypeError $error) {
    echo 'broken():  ', get_class($error), "\n";
}
```

```
nothing(): NULL
blank():   NULL
to_int():  5 int
broken():  TypeError
```

**LE47.** The function without `return` returned `null`, and so did the one declared `void`. The function declared `: int` silently converted the string `'5'` to `5`, while `'five'` gave a `TypeError` because it could not be coerced. The return type therefore follows the same rule as the parameter type: a numeric string is coerced, otherwise it is rejected.

## The Moment a Function Is Defined

PHP can know a function before a file starts executing, but not every function. A function declared unconditionally at the top level of the file is recognized while the file is read and can be called on a line before its declaration. A function declared inside an `if` block exists only when that block is executed. A call to a function that was never declared throws an `Error`. The file below measures the three cases with `function_exists()`.

```php
<?php
// definition.php — a top-level function is known beforehand, a conditional one is defined when it runs
echo 'before:       ', var_export(function_exists('top_level'), true), "\n";
echo 'conditional:  ', var_export(function_exists('conditional'), true), "\n";
echo 'call:         ', top_level(), "\n";

function top_level(): string
{
    return 'ran';
}

if (true) {
    function conditional(): string
    {
        return 'x';
    }
}
echo 'after:        ', var_export(function_exists('conditional'), true), "\n";

try {
    no_such_thing();
} catch (Error $error) {
    echo 'undefined:    ', get_class($error), "\n";
}
```

```
before:       true
conditional:  false
call:         ran
after:        true
undefined:    Error
```

**LE48.** `top_level`, declared unconditionally, existed before its declaration line and the call worked. `conditional`, declared conditionally, did not exist before the block was executed and did exist afterward. An undefined name gave an `Error`, so it belongs to the halts class. What is silent is that, depending on the line on which the call is made, the same name works in one place and not in another: the existence of a function depends on the structure of the file, and the existence of a conditional one depends on execution order.

## Closures and Captured Variables

An anonymous function (closure) cannot reach a variable in the outer scope on its own; it has to capture it explicitly with `use`. `use ($x)` copies the value at declaration time, while `use (&$x)` binds the variable itself. The arrow function `fn` captures the outer variable by value at the moment it is created. The file below measures the three capture forms by calling them after the captured variable has been changed.

```php
<?php
// closure.php — use captures the value, & binds the variable, an arrow function takes the value at creation
$x = 1;
$by_value = function () use ($x) {
    return $x;
};
$by_reference = function () use (&$x) {
    return $x;
};
$arrow = fn() => $x;

$x = 2;
echo 'use ($x):  ', $by_value(), "\n";
echo 'use (&$x): ', $by_reference(), "\n";
echo 'fn:        ', $arrow(), "\n";

$increase = fn() => $x++;
$increase();
echo 'x after increment in fn: ', $x, "\n";
```

```
use ($x):  1
use (&$x): 2
fn:        1
x after increment in fn: 2
```

**LE49.** After `$x` was set to two, the two forms that capture by value saw the old value, `1`; only the one bound with `&` saw `2`. The increment inside the arrow function did not change the outer `$x`: the variable entered the arrow function as a copy. The distinction is the counterpart of passing by value and by reference in closures, and it produces no notice: code that picks the wrong form does not fail, it works with an old value.

## A Parameter That Accepts Null

Being able to give `null` to a parameter is two separate things: the parameter's type accepting `null`, and the parameter being omittable. `?int $x` accepts null but is required; `?int $x = null` both accepts null and can be omitted. A union type (`int|string`), in loose mode, chooses by an order which type to coerce to. The file below measures seven calls in a table.

```php
<?php
// nullable.php — ?int is required, = null can be omitted, a union type picks the coercion order
function required(?int $x)
{
    return var_export($x, true);
}

function optional(?int $x = null)
{
    return var_export($x, true);
}

function union(int|string $x)
{
    return get_debug_type($x);
}

$calls = [
    'required(null)'  => fn() => required(null),
    'required()'      => fn() => required(),
    'optional()'      => fn() => optional(),
    "union('5')"      => fn() => union('5'),
    'union(5.0)'      => fn() => union(5.0),
    'union([])'       => fn() => union([]),
    'union(true)'     => fn() => union(true),
];
foreach ($calls as $label => $call) {
    try {
        $text = $call();
    } catch (Error $error) {
        $text = get_class($error);
    }
    printf("%-20s%s\n", $label, $text);
}
```

```
required(null)      NULL
required()          ArgumentCountError
optional()          NULL
union('5')          string
union(5.0)          int
union([])           TypeError
union(true)         int
```

**LE50.** `required(null)` worked but `required()` gave an `ArgumentCountError`: `?` provides null acceptance, not omittability. `optional()` returned `NULL`. In the union type a value with an exact match stayed as it was; where there was no exact match PHP followed an order and chose the result silently; and a value such as an array, which cannot be converted to any member, gave a `TypeError`. This is the counterpart in union types of the loose mode observation in the type declaration lesson.

## A Called Function

A function given to another function as an argument (a callback) can be called by the caller with more arguments than its declaration expects. A user function swallows this surplus silently again; a built-in function does not. The file below runs `array_filter()` in the mode that sends two arguments with a single-parameter closure, and makes the same test with the built-in `strlen` through `array_map()`.

```php
<?php
// callback.php — a called user function swallows the extra argument, a built-in does not
$numbers = [1, 2, 3, 4];

$single = array_filter($numbers, fn($v) => $v > 2, ARRAY_FILTER_USE_BOTH);
echo 'single parameter: ', implode(',', $single), "\n";

$double = array_filter($numbers, fn($v, $k) => $k % 2 === 0, ARRAY_FILTER_USE_BOTH);
echo 'two parameters:   ', implode(',', $double), "\n";

try {
    $result = array_map('strlen', ['a'], ['b']);
    echo 'strlen: ', count($result), "\n";
} catch (ArgumentCountError $error) {
    echo 'strlen: ', get_class($error), "\n";
}
```

```
single parameter: 3,4
two parameters:   1,3
strlen: ArgumentCountError
```

**LE51.** The single-parameter closure, called with two arguments, ran without ever seeing the key and gave `3,4`. The two-parameter one used the key and selected the two elements with even indexes, `1,3`. `array_map()`, called with two arrays, sent two arguments to `strlen` and received an `ArgumentCountError`. The same extra argument was silent in one place and halting in another, depending on the kind of callback.

## The Bounding Measurement

The limit of silent swallowing in a user function is that making the surplus visible is possible. There are two ways: `func_num_args()` says how many arguments were actually given inside the function, while the `...$x` syntax gathers all remaining arguments into an array, and the surplus shows up in the length of that array. The file below measures the ways with the same call.

```php
<?php
// visible.php — func_num_args and ...$x make the surplus visible
function add_two($a, $b)
{
    return $a + $b;
}

function count_args($a, $b)
{
    return func_num_args();
}

function sum(int ...$numbers): int
{
    return array_sum($numbers);
}

function count_all(...$x): int
{
    return count($x);
}

echo 'add_two(1, 2, 3):   ', add_two(1, 2, 3), "\n";
echo 'count_args(1, 2, 3): ', count_args(1, 2, 3), "\n";
echo 'sum(1, 2, 3):       ', sum(1, 2, 3), "\n";
echo 'count_all(1, 2, 3): ', count_all(1, 2, 3), "\n";
```

```
add_two(1, 2, 3):   3
count_args(1, 2, 3): 3
sum(1, 2, 3):       6
count_all(1, 2, 3): 3
```

**LE52.** `add_two` swallowed the third argument again and gave `3`. `func_num_args()` counted `3` in the same call: the swallowed argument can be measured from inside the function. The function written with `...$numbers` added all three numbers and gave `6`; `...$x` delivered the surplus as an array and `count` said `3`. The silent channel is therefore not unavoidable: the author of a function can choose a signature in which the surplus will be visible.

## Summary

- The channel of functions is silent: an extra positional argument given to a user function is ignored without a notice, while a built-in function throws an `ArgumentCountError`.
- A missing required argument gives an `ArgumentCountError`, and an unknown or colliding named argument gives an `Error`; naming is stricter than position.
- A default value is renewed on every call, and a `static` variable lives across calls.
- Arguments are passed by value; the caller's variable changes only if `&` is written in the signature.
- A function without `return` returns `null`; a return type coerces a numeric string and rejects one that is not.
- `func_num_args()` and `...$x` make the swallowed surplus visible.

## Next Step

Where the extra argument goes was seen: it was ignored or gathered into an array. The Indexed and Associative Arrays lesson looks at that array: when two arrays are merged, how do the keys combine for elements that carry the same key?
