Chapter 3. Functions

Every computer program in every language is built by tying various components of business logic together. Often, these components need to be somewhat reusable, encapsulating common functionality that needs to be referenced in multiple places throughout an application. The easiest way to make these components modular and reusable is to encapsulate their business logic into functions, specific constructs within the application that can be referenced elsewhere throughout an application.

For example, Example 3-1 illustrates how a simple program might be written to capitalize the first character in a string. Coding without using functions is considered imperative or simple procedural programming as we define exactly what the program needs to accomplish one command (or line of code) at a time.

Example 3-1. Imperative (function-free) string capitalization
$str = "this is an example";

if (ord($str[0]) >= 97 && ord($str[0]) <= 122) {
    $str[0] = chr(ord($str[0]) - 32);
}

echo $str . PHP_EOL; // This is an example

$str = "and this is another";

if (ord($str[0]) >= 97 && ord($str[0]) <= 122) {
    $str[0] = chr(ord($str[0]) - 32);
}

echo $str . PHP_EOL; // And this is another

$str = "3 examples in total";

if (ord($str[0]) >= 97 && ord($str[0]) <= 122) {
    $str[0] = chr(ord($str[0]) - 32);
}

echo $str . PHP_EOL; // 3 examples in total
Note

The references to ord() and chr() are references to native functions defined by PHP itself. ord() returns the binary value of a character as an integer. Similarly, chr() converts a binary value (represented as an integer) into its corresponding character.

When we write code without defining functions, our code ends up rather repetitive as we’re forced to copy and paste identical blocks of code throughout the application. This violates one of the key principles of software development: DRY, or “don’t repeat yourself.”

A common way to describe the opposite of this principle is WET, or “write everything twice.” Writing the same block of code over again leads to two problems:

  1. Our code becomes rather long and difficult to maintain

  2. If the logic within the repeated code block needs to change, we have to update several parts of our program every time

Rather than repeating logic imperatively, as we did in Example 3-1 we can define a function that wraps this logic and later invoke that function directly as in Example 3-2. Defining functions is an evolution of imperative/procedural programming that augments the functions provided by the language itself with those defined by our application.

Example 3-2. Procedural string capitalization
function capitalize_string($str)
{
    if (ord($str[0]) >= 97 && ord($str[0]) <= 122) {
        $str[0] = chr(ord($str[0]) - 32);
    }

    return $str;
}

$str = "this is an example";

echo capitalize_string($str) . PHP_EOL; // This is an example

$str = "and this is another";

echo capitalize_string($str) . PHP_EOL; // And this is another

$str = "3 examples in total";

echo capitalize_string($str) . PHP_EOL; // 3 examples in total

User-defined functions are incredibly powerful and quite flexible. The capitalize_string() function in Example 3-2 is relatively simple — it takes a single string parameter and returns a string. However, there is no indication in the function as defined that the $str parameter must be a string - you could just as easily pass a number or even an array as follows:

$out = capitalize_string(25); // 25

$out = capitalize_string(['a', 'b']); // ['A', 'b']

Recall our discussion of PHP’s loose type system from Chapter 1 — by default, PHP will try to infer what you mean when you pass a parameter into capitalize_string() and, in most cases, will return something useful. In the case of passing an integer, PHP will trigger a warning that you are trying to access elements of an array incorrectly, but it will still return an integer without crashing.

More sophisticated programs can add explicit typing information to both the function parameters and its return to provide safety checks around this kind of usage. Other functions could return multiple values rather than a single item. The recipes that follow cover a variety of ways functions can be used in PHP and begins scratching at the surface of building a full application.

3.1 Accessing function parameters

Problem

You want to access the values passed into a function when it’s called elsewhere in a program.

Solution

Use the variables defined in the function signature within the body of the function itself as follows:

function multiply($first, $second)
{
    return $first * $second;
}

multiply(5, 2); // 10

$one = 7;
$two = 5;

multiply(7, 5); // 35

Discussion

The variable names defined in the function signature are available only within the scope of the function itself and will contain values matching the data passed into the function when its called. Inside the curly braces that define the function, you can use these variables as if you’ve defined them yourself. Just know that any changes you make to those variables will only be available within the function and won’t impact anything elsewhere in the application by default.

Example 3-3 illustrates how a specific variable name can be used both within a function and outside a function while referring to two, completely independent values. Said another way, changing the value of $number within the function will only impact the value within the function, not within the parent application.

Example 3-3. Local function scoping
function increment($number)
{
    $number += 1;

    return $number;
}

$number = 6;

echo increment($number); // 7
echo $number; // 6

By default, PHP passes values into functions rather than passing a reference to the variable. In Example 3-3 this means PHP passed the value 6 into a new $number variable within the function, performed a calculation, and returned the result. The $number variable outside the function was entirely unaffected.

Warning

PHP passes simple values (strings, integers, Booleans, arrays) by value by default. More complex objects, however, are always passed by reference. In the case of objects, the variable inside the function points back to the same object as the variable outside the function rather than to a copy of it.

In some cases, you might want to explicitly pass a variable by reference rather than just passing its value. In that case, we need to modify the function signature as this is a change to its very definition rather than something that can be modified when the function is called. Example 3-4 illustrates how our increment() function would change to pass $number by reference instead of by value.

Example 3-4. Passing variables by reference
function increment(&$number)
{
    $number += 1;

    return $number;
}

$number = 6;

echo increment($number); // 7
echo $number; // 7

In reality, the variable name doesn’t need to match both inside and outside the function - I’m using $number in both cases here to illustrate the difference in scoping. If we stored an integer in $a and passed that variable instead as increment($a) the result would be identical to the example in Example 3-4.

See Also

PHP reference documentation on user-defined functions and passing variables by reference.

3.2 Settings a function’s default parameters

Problem

You want to set a default value for a function’s parameter so invocations don’t have to pass it.

Solution

Assign a default value within the function signature itself. For example:

function get_book_title($isbn, $error = 'Unable to query')
{
    try {
        $connection = get_database_connection();
        $book = $query_isbn($connection, $isbn);

        return $book->title;
    } catch {
        return $error;
    }
}

get_book_title('978-1-098-12132-7');

Discussion

The example in the Solution attempts to query a database for the title of a book based on its ISBN. If the query fails for any reason, the function will return the string passed into the $error parameter instead.

To make this parameter optional, the function signature assigns a default value - when calling get_book_title() with a single parameter, the default $error value is used automatically. You alternatively have the option to pass your own string into this variable when invoking the function, such as get_book_title(978-1-098-12132-7, Oops!);.

When defining a function with default parameters, it’s a best practice to place all parameters with default values last in the function signature. While it is possible to define parameters in any order, doing so makes it difficult to call the function properly.

Example 3-5 illustrates the kinds of problems that can come up by placing optional parameters before required ones.

Note

It is possible to define function parameters with specific defaults in any order. However, declaring mandatory parameters after optional ones is deprecated as of PHP 8.0. Continuing to do so might result in an error in a future version of PHP.

Example 3-5. Mis-ordered default parameters
function brew_latte($flavor = 'unflavored', $shots)
{
    return "Brewing a {$shots}-shot, {$flavor} latte!";
}

brew_latte('vanilla', 2); 1
brew_latte(3); 2
1

Proper execution. Returns “Brewing a 2-shot, vanilla latte!”

2

Triggers an ArgumentCountError exception because $shots is undefined.

There might be cases where the parameters themselves make logical sense to be in a particular order (to make the code more readable, for example). Know that if any parameters are required, every parameter to their left is also effectively required even if you try to define a default value.

See Also

Examples of default arguments in the PHP manual.

3.3 Using named function parameters

Problem

You want to pass arguments into a function based on the name of the parameter rather than its position.

Solution

Use the named argument syntax while calling a function as follows:

array_fill(start_index: 0, count: 100, value: 50);

Discussion

By default, PHP leverages positional parameters in function definitions. The Solution example references the native array_fill() function that has the following function signature:

array_fill(int $start_index, int $count, mixed $value): array

Basic PHP coding must supply arguments to array_fill() in the same order in which they’re defined - $start_index followed by $count followed by $value. While the order itself is not a problem, making sense of what each value means when scanning visually through code can be a challenge. Using the basic, ordered parameters, the Solution example would be written as follows, requiring deep familiarity with the function signature to know which integer represents which parameter:

array_fill(0, 100, 50);

Named function parameters disambiguate which value is being assigned to which internal variable. They also allow for arbitrary reordering of parameters when you invoke the function as said invocation is now explicit as to which value is assigned to which parameter.

Another key advantage of named arguments is that optional arguments can be skipped entirely during function invocation. Consider a verbose activity logging function like in Example 3-6 where multiple parameters are considered optional as they set defaults:

Example 3-6. Verbose activity logging function
activity_log(
    string    $update_reason,
    string    $note           = '',
    string    $sql_statement  = '',
    string    $user_name      = 'anonymous',
    string    $ip_address     = '127.0.0.1',
    ?DateTime $time           = null
): void

Internally, Example 3-6 will use its default values when it’s called with a single argument - if $time is null it will silently replace it with a new DateTime instance representing “now.” However, there might be times where we want to populate one of these optional parameters without wanting to explicitly set all of them.

Consider the use case where we want to rehydrate events from a static log file. User activity was anonymous (so the defaults for $user_name and $ip_address are adequate), but we need to explicitly set the date at which an event occurred. Without named arguments, an invocation in this case would appear as follows:

activity_log(
    'Testing a new system',
    '',
    '',
    'anonymous',
    '127.0.0.1',
    new DateTime('2021-12-20')
);

With named arguments, we can skip setting default parameters to their defaults entirely and explicitly set just the parameters we need to. The preceding code code be simplified to the following:

activity_log('Testing a new system', time: new DateTime('2021-12-20'));

In addition to drastically simplifying our usage of activity_log(), the usage of named parameters has the added benefit of keeping our code DRY. The default values for our arguments are stored directly in the function definition rather than being copied to every invocation of the function as well. If we later need to change a default, we can edit the function definition alone.

See Also

The original RFC proposing named parameters.

3.4 Enforcing function argument and return typing

Problem

You want to force your program to implement type safety and avoid PHP’s native loose type comparisons.

Solution

Add input and return types to function definitions. Optionally add a strict type declaration to the top of each file to enforce values match type annotations (and emit a Fatal error if they don’t match). For example:

declare(strict_types=1);

function add_numbers(int $left, int $right): int
{
    return $left + $right;
}

add_numbers(2, 3); 1
add_numbers(2, '3'); 2
1

This is a perfectly valid operation and will return the integer 5

2

While 2 + '3' is valid PHP code, the string '3' violates the function’s type definitions and will trigger a Fatal error

Discussion

PHP natively supports various scalar types and allows developers to annotate both function input parameters and returns to identify the kinds of values that are allowable for each. In addition, developers can specify their own custom classes as types, or leverage class inheritance within the type system.1

Parameter types are annotated by placing the type directly before the name of the parameter when defining the function. Similarly, return types are specified by appending the function signature with a : and the type that function would return as in the following:

function name(type $parameter): return_type
{
    // ...
}

The simplest types leveraged by PHP are enumerated in Table 3-1.

Table 3-1. Simple single types in PHP
Type Description

array

The value must be an array (containing any type of values)

callable

The value must be a callable function

bool

The value must be Boolean

float

The value must be a floating point number

int

The value must be an integer

string

The value must be a string

iterable

The value must be an array or an object that implements Traversable

mixed

The object can be any value

In addition, both built-in and custom classes can be used to define types as shown in Table 3-2.

Table 3-2. Object types in PHP
Type Description

Class/Interface name

The value must be an instance of the specified class or interface

self

The value must be an instance of the same class as the one in which the declaration is used

parent

The value must be an instance of the parent of the class in which the declaration is used

object

The value must be an instance of an object

PHP also permits simple scalar types to be expanded by either making them nullable or by combining them into union types. To make a specific type nullable, you have to prefix the type annotation with a ?. This will instruct the compiler to allow values to be either the specified type or null as in the following example:

function say_hello(?string $message): void
{
    echo 'Hello, ';

    if ($message === null) {
        echo 'world!';
    } else {
        echo $message . '!';
    }
}

say_hello('Reader'); // Hello, Reader!
say_hello(null); // Hello, World!

A union type declaration combines multiple types into a single declaration by concatenating simple types together with the pipe character (|). If we were to rewrite the type declarations on the Solution example with a union type combining strings and integers, the Fatal error thrown by passing a string in for addition would resolve itself. Consider the possible rewrite in Example 3-7 that would permit either integers or strings as parameters.

Example 3-7. Rewriting our Solution example to leverage union types
function add_numbers(int|string $left, int|string $right): int
{
    return $left + $right;
}

add_numbers(2, '3'); // 55

The biggest problem with the Example 3-7 is that adding strings together with the + operator has no meaning in PHP. If both parameters are numeric (either integers or integers represented as strings) the function will work just fine. If either is a non-numeric string, PHP will throw a TypeError as it doesn’t know how to “add” two strings together. These kinds of errors are what we hope to avoid by adding type declarations to our code and enforcing strict typing - they formalize the contract we expect our code to support and encourage programming practices that naturally defend against coding mistakes.

By default, PHP uses its typing system to hint at what types are allowed into and returned from functions. This is useful to prevent passing bad data into a function, but relies heavily on either developer diligence or additional tooling2 to enforce typing. Rather than rely on humans’ ability to check code, PHP allows for a static declaration in each file that all invocations should follow strict typing.

Placing declare(strict_types=1); at the top of a file tells the PHP compiler you intend for all invocations in that file to obey parameter and return type declarations. Note that this directive applies to invocations within the file where it’s used, not to the definitions of functions in that file. If you call functions from another file, PHP will honor the type declarations in that file as well. However, placing this directive in your file will not force other files that reference your functions to obey the typing system.

See Also

PHP documentation on type declarations and the declare construct.

3.5 Defining a function with a variable number of arguments

Problem

You want to define a function that takes one or more arguments without knowing ahead of time how many values will be passed in.

Solution

Use PHP’s spread operator () to define a variable number or arguments.

function greatest(int ...$numbers): int
{
    $greatest = 0;
    foreach ($numbers as $number) {
        if ($number > $greatest) {
            $greatest = $number;
        }
    }

    return $greatest;
}

greatest(7, 5, 12, 2, 99, 1, 415, 3, -7, 4);
// 415

Discussion

The spread operator automatically adds all parameters passed in that particular position or after it to an array. This array can be typed by prefixing the spread operator with a type declaration (review Recipe 3.4 for more on typing), thus requiring every element of the array to match a specific type. Invoking the function defined in the Solution example as greatest(2, "five"); will throw a type error as we have explicitly declared an int type for every member of the $numbers array.

Your function can accept more than one positional parameter while still leveraging the spread operator to accept an unlimited number of additional arguments. The following example will print a greeting to the screen for an unlimited number of individuals:

function greet(string $greeting, string ...$names): void
{
    foreach($names as $name) {
        echo $greeting . ', ' . $name . PHP_EOL;
    }
}

greet('Hello', 'Tony', 'Steve', 'Wanda', 'Peter');
// Hello, Tony
// Hello, Steve
// Hello, Wanda
// Hello, Peter

greet('Welcome', 'Alice', 'Bob');
// Welcome, Alice
// Welcome, Bob

The spread operator has more utility than just in function definition. While it can be used to pack multiple arguments into an array, it can also be used to unpack an array into multiple arguments for a more traditional function invocation. Example 3-8 provides a trivial illustration of how this array unpacking works by passing an array into a function that does not accept an array by leveraging the spread operator.

Example 3-8. Unpacking an array with the spread operator
function greet(string $greeting, string $name): void
{
    echo $greeting . ', ' . $name  . PHP_EOL;
}

$params = ['Hello', 'world'];
greet(...$params);
// Hello, world!

In some cases, a more complex function might return multiple values (as discussed in the next recipe) so passing the return of one function into another becomes simple with the spread operator. In fact, any array or variable that implements PHP’s Traversable interface can be unpacked into a function invocation in this manner.

See Also

PHP documentation on variable argument lists.

3.6 Returning more than one value

Problem

You want to return multiple values from a single function invocation.

Solution

Rather than returning a single value, instead return an array of multiple values and unpack them using list() outside of the function.

function describe(float ...$values): array
{
    $min = min($values);
    $max = max($values);
    $mean = array_sum($values) / count($values);

    $variance = 0.0;
    foreach($values as $val) {
        $variance += pow(($val - $mean), 2);
    }
    $std_dev = (float) sqrt($variance/count($values));

    return [$min, $max, $mean, $std_dev];
}

$values = [1.0, 9.2, 7.3, 12.0];
list($min, $max, $mean, $std) = describe(...$values);

Discussion

PHP is only capable of returning one value from a function invocation, but that value itself could be an array containing multiple values. When paired with PHP’s list() construct, this array can be easily destructured to individual variables for further use by the program.

While the need to return many different values isn’t common, when the occasion comes up it can be incredibly handy. One example is in web authentication. Many modern systems today use JSON Web Tokens (JWTs), which are period-delimited strings of Base64-encoded data. Each component of a JWT represents a separate, discrete thing - a header describing the algorithm used, the data in the token payload, and a verifiable signature on that data.

When reading a JWT as a string, PHP applications often leverage the built-in explode() function to split the string on the periods delimiting each component. A simple use of explode() might appear as follows:

$jwt_parts = explode('.', $jwt);
$header = base64_decode($jwt_parts[0]);
$payload = base64_decode($jwt_parts[1]);
$signature = base64_decode($jwt_parts[2]);

The preceding code works just fine, but the repeated references to positions within an array can be difficult to follow both during development and debugging later if there is a problem. In addition, developers must manually decode every part of the JWT separately - forgetting to invoke base64_decode() could be fatal to the operation of the program.

An alternative approach would be to unpack and automatically decode the JWT within a function and return an array of the components as follows:

function decode_jwt(string $jwt): array
{
    $parts = explode('.', $jwt);

    return array_map('base64_decode', $parts);
}

list($header, $payload, $signature) = decode_jwt($jwt);

A further advantage of using a function to unpack a JWT rather than decomposing each element directly is that you could build in automated signature verification or even filter JWTs for acceptability based on the encryption algorithms declared in the header. While this logic could be applied procedurally while processing a JWT, keeping everything in a single function definition leads to cleaner, more maintainable code.

The biggest drawback to returning multiple values in one function call is in typing - these functions have an array return type, but PHP doesn’t natively allow for specifying the type of the elements within an array. There are potential workarounds to this limitation by way of documenting the function signature and integrating with a static analysis tool like Psalm, but no native support within the language for typed arrays. As such, if you’re using strict typing (and you should be using strict typing), returning multiple values from a single function invocation should be a rare occurrence.

See Also

Recipe 3.5 on passing a variable number of arguments and Recipe 1.3 for more on PHP’s list() construct. Also reference the phpDocumentor documentation on typed arrays that can be enforced by tools like Psalm.

3.7 Accessing global variables from within a function

Problem

Your function needs to reference a globally-defined variable from elsewhere in the application.

Solution

Prefix any global variables with the global keyword to access them within the function’s scope.

$counter = 0;

function increment_counter()
{
    global $counter;

    $counter += 1;
}

count();

echo $counter; // 1

Discussion

PHP separates operations into various scopes based on the context in which a variable is defined. For most programs, there is a single scope that spans all included or required files. A variable defined in this global scope is available everywhere regardless of which file is currently executing.

$apple = 'honeycrisp';

include 'someotherscript.php'; 1
1

The $apple variable is also defined within this script and available for use.

User-defined functions, however, define their own scope. A variable defined outside of a user-defined function is not available within the body of the function. Likewise, any variable defined within the function is not available outside of the function. Example 3-9 illustrates the boundaries of the parent and function scope in a program.

Example 3-9. Local versus global scoping
$a = 1; 1

function example(): void
{
    echo $a . PHP_EOL; 2
    $a = 2; 3

    $b = 3; 4
}

example();

echo $a . PHP_EOL; 5
echo $b . PHP_EOL; 6
1

The variable $a is initially defined in the parent scope.

2

Inside the function scope, $a is not yet defined. Attempting to echo its value will result in a warning.

3

Defining a variable called $a within the function will not overwrite the value of the same-named variable outside of the function.

4

Defining a variable called $b within the function makes it available within the function, but this value will not escape the scope of the function.

5

Echoing $a outside of the function, even after invoking example(), will print the initial value we’ve set as the function did not change the variable’s value.

6

Since $b was defined within the function, it is undefined in the scope of the parent application.

Note

It is possible to pass a variable into a function call by reference if the function is defined to accept a variable in such a way. However, this is a decision made by the definition of the function and not a runtime flag available to routines leveraging that function after the fact. See Example 3-4 for an example of what pass-by-reference might look like.

In order to reference variables defined outside of its scope, a function needs to declare those variables as “global” within its own scope. To reference the parent scope, we can rewrite Example 3-9 as follows:

$a = 1;

function example(): void
{
    global $a, $b; 1

    echo $a . PHP_EOL; 2
    $a = 2; 3

    $b = 3; 4
}

example();

echo $a . PHP_EOL; 5
echo $b . PHP_EOL; 6
1

By declaring both $a and $b to be global variables, we are telling the function to use values from the parent scope rather than its own scope.

2

With a reference to the global $a variable, we can now actually print it to output.

3

Likewise, any changes to $a within the scope of the function will impact the variable in the parent scope.

4

Similarly, we now define $b but, as it’s global, this definition will bubble out to the parent scope as well.

5

Echoing $a will now reflect the changes made within the scope of example() as we made the variable global.

6

Likewise, $b is now defined globally and can be echoed to output as well.

There is no limit on the number of global variables PHP can support. Additionally, all globals can be listed by enumerating the special $GLOBALS array defined by PHP. This associative array contains references to all variables defined within the global scope. This special array can be useful if you want to reference a specific variable in the global scope without declaring a variable as global as in Example 3-10.

Example 3-10. Using the associative $GLOBALS array
$var = 'global';

function example(): void
{
    $var = 'local';

    echo 'Local variable: ' . $var . PHP_EOL;
    echo 'Global variable: ' . $GLOBALS['var'] . PHP_EOL;
}

example();
// Local variable: local
// Global variable: global
Warning

As of PHP 8.1, the $GLOBALS array is read-only. In past versions of PHP you could both read from and write to this array to update global variables. In version 8.1 and beyond, you must declare a regular variable to be global and modify the global variable that way.

Global variables are a handy way to reference state across your application, but can lead to confusion and maintainability issues if overused. Some large applications leverage global variables heavily - WordPress, a PHP-based project that powers more than 40% of the Internet3 uses global variables throughout its codebase. However, most application developers agree that global variables should be used sparingly, if at all, to help keep systems clean and easy to maintain.

See Also

PHP documentation on variable scope and the special $GLOBALS array.

3.8 Managing state within a function across multiple invocations

Problem

Your function needs to keep track of its change in state over time.

Solution

Use the static keyword to define a locally-scoped variable that retains its state between function invocations.

function increment()
{
    static $count = 0;

    return $count++;
}

echo increment(); // 0
echo increment(); // 1
echo increment(); // 2

Discussion

A static variable exists only within the scope of the function in which it is declared. However, unlike regular local variables, it holds onto its value every time you return to the scope of the function. In this way, a function can become stateful and keep track of certain data (like the number of times it’s been called) in between independent invocations.

In a typical function, using the = operator will assign a value to a variable. When the static keyword is applied, this assignment operation only happens the first time that function is called. Subsequent calls will reference the previous state of the variable and allow the program to either use or modify the stored value as well.

One of the most common use cases of static variables is to track the state of a recursive function. Example 3-11 provides an illustration of a function that recursively calls itself a fixed number of times before exiting.

Example 3-11. Using a static variable to limit recursion depth
function example(): void
{
    static $count = 0;

    if ($count >= 3) {
        $count = 0;
        return;
    }

    $count += 1;

    echo 'Running for loop number ' . $count . PHP_EOL;
    example();
}

The static keyword can also be used to keep track of expensive resources that might be needed by a function multiple times, but might be the kinds of things we only want a single instance of. Consider a function that logs messages to a database - we might not be able to pass a database connection into the function itself, but we want to ensure the function only opens a single database connection. Such a logging function might be implemented as in Example 3-12.

Example 3-12. Using a static variable to hold a database connection
function logger(string $message): void
{
    static $dbh = null;
    if (null === $dbh) {
        $dbh = new PDO(DATABASE_DSN, DATABASE_USER, DATABASE_PASSWORD);
    }

    $sql = 'INSERT INTO messages(message) VALUES(:message)';
    $statement = $dbh->prepare($sql);

    $statement->execute([':message', $message]);
}

logger('This is a test'); 1
logger('This is another message');2
1

The first time logger() is called, it will define the value of the static $dbh variable. In this case it will connect to a database using the PHP Data Objects (PDO) interface. This interface is a standard object provided by PHP for accessing databases.

2

Every subsequent call to logger() will leverage the initial connection opened to the database and stored in $dbh.

Note that PHP automatically manages its memory usage and automatically clears variables from memory when they leave scope. For regular variables within a function, this means the variables are freed from memory as soon as the function completes. Static and global variables are never cleaned up until the program itself exits as they are always in scope. Take care when using the static keyword to ensure you aren’t storing unnecessarily large pieces of data in memory. In Example 3-12 we open a connection to a database that will never be automatically closed by the function we’ve created.

While the static keyword can be a powerful way to reuse state across function calls, it should be used with care to ensure your application doesn’t do anything unexpected. In many cases it might be better to explicitly pass variables representing state into the function. Even better would be to encapsulate the function’s state as part of an overarching object, which will be covered in Chapter 8.

See Also

PHP documentation on variable scoping, including the static keyword.

3.9 Defining dynamic functions

Problem

You want to define an anonymous function and reference it as a variable within your application.

Solution

Define a closure that can be assigned to a variable and passed into another function as needed.

$greet = function($name) {
    echo 'Hello, ' . $name . PHP_EOL;
};

$greet('World!');
// Hello, World!

Discussion

Whereas most functions in PHP have defined names, the language supports the creation of unnamed (so-called anonymous) functions, or closures. These functions can encapsulate either simple or complex logic, but can be assigned directly to variables for reference elsewhere in the program.

Internally, anonymous functions are implemented using PHP’s native Closure class. This class is declared as final, which means no class can descend from it directly yet anonymous functions are all instances of this class and can be used either directly as functions or as objects.

By default, closures do not inherit any scope from the parent application and, like regular functions, define variables within their own scope. Variables from the parent scope can be passed directly into a closure by leveraging the use directive when defining a function. Example 3-13 illustrates how variables from one scope can be passed into another dynamically.

Example 3-13. Passing a variable between scopes with use()
$some_value = 42;

$foo = function() {
    echo $some_value;
};

$bar = function() use ($some_value) {
    echo $some_value;
};

$foo(); // Warning: Undefined variable

$bar(); // 42

Anonymous functions are used in many projects to encapsulate a piece of logic for application against a collection of data. The next recipe covers exactly that use case.

Note

Older versions of PHP used create_function() for similar utility. Developers could create an anonymous function as a string and pass that code into create_function() to turn it into a Closure instance. Unfortunately, this method used eval() under the hood to evaluate the string - a practice that is considered highly unsafe. While some older projects might still use create_function(), the function itself was deprecated in PHP 7.2 and removed from the language entirely in version 8.0.

See Also

PHP documentation on anonymous functions.

3.10 Passing functions as parameters to other functions

Problem

You want to define part of a function’s implementation and pass that implementation as an argument to another function.

Solution

Define a closure that implements part of the logic you need and pass that directly into another function as if it were any other variable.

$reducer = function(?int $carry, int $item): int {
    return $carry + $item;
};

function reduce(array $array, callable $callback, ?int $initial=null): ?int
{
    $acc = $initial;
    foreach($array as $item) {
        $acc = $callback($acc, $item);
    }

    return $acc;
}

$list = [1, 2, 3, 4, 5];
$sum = reduce($list, $reducer); //15

Discussion

PHP is considered by many to be a functional language as functions are first-class elements in the language and can be bound to variable names, passed as arguments, or even returned from other functions. PHP supports functions as variables through the callable type as implemented in the language. Many core functions (like usort(), array_map(), array_reduce(), etc) support passing a callable parameter, which is then used internally to define the function’s overall implementation.

The reduce() function defined in the Solution example is a user-written implementation of PHP’s native array_reduce() function. They both have the same behavior, and the Solution could be rewritten to pass $reducer directly into PHP’s native implementation with no change in the result:

$sum = array_reduce($list, $reducer); // 15

Since functions can be passed around like any other variable, PHP has the ability to define partial implementations of functions. This is achieved by defining a function that, in turn, returns another function that can be used elsewhere in the program.

For example, we can define a function to set up a basic multiplier routine that multiplies any input by a fixed base amount as in Example 3-14. The main function returns a new function each time we call it, so we can create functions to double or triple arbitrary values and use them however we want.

Example 3-14. Partially-applied multiplier function example
function multiplier(int $base): callable
{
    return function(int $subject) use ($base): int {
        return $base * $subject;
    };
}

$double = multiplier(2);
$triple = multiplier(3);

$double(6);  // 12
$double(10); // 20
$triple(3);  // 9
$triple(12); // 36

Breaking functions apart like this is known as currying. This is the practice of changing a function with multiple input parameters and turning it into a series of functions that each take a single parameter, with most of those parameters being functions themselves. To fully illustrate how this can work in PHP, let’s look at Example 3-15 and walk through a rewrite of the multiplier() function:

Example 3-15. Walkthrough of currying in PHP
function multiply(int $x, int $y): int 1
{
    return $x * $y;
}

multiply(7, 3); // 21

function curried_multiply(int $x): callable 2
{
    return function(int $y) use ($x): int { 3
        return $x * $y; 4
    };
}

curried_multiply(7)(3); // 21 5
1

The most basic form of our function takes two values, multiplies them together, and returns a final result.

2

When we curry the function, we want each component function to only take a single value. Our new curried_multiply() only accepts one parameter but returns a function that leverages that parameter internally.

3

The internal function references the value passed by our previous function invocation automatically (with the use directive).

4

The resulting function implements the same business logic as our basic form.

5

Calling a curried function has the appearance of calling multiple functions in series, but the result is the same.

The biggest advantage of currying like in Example 3-15 is that a partially-applied function can be passed around as a variable and used elsewhere. Similar to our multiplier() function, we can create a doubling or tripling function by partially applying our curried multiplier as follows:

$double = curried_multiply(2);
$triple = curried_multiply(3);

Partially-applied, curried functions are themselves callable functions but can be passed into other functions as variables and fully invoked later.

See Also

Details on anonymous functions in Recipe 3.9.

3.11 Using concise function definitions (arrow functions)

Problem

You want to create a simple, anonymous function that references the parent scope without verbose use declarations.

Solution

Use PHP’s short anonymous function (arrow function) syntax to define a function that inherits its parent’s scope automatically.

$outer = 42;

$anon = fn($add) => $outer + $add;

$anon(5); // 47

Discussion

Arrow functions were introduced in PHP 7.4 as a way to write more concise anonymous functions, as in Recipe 3.9. Arrow functions automatically capture any referenced variables and import them (by value rather than by reference) into the scope of the function.

A more verbose version of the Solution example could be written as follows and achieve the same level of functionality:

$outer = 42;

$anon = function($add) use ($outer) {
    return $outer + $add;
}

$anon(5);

Arrow functions always return a value - it is impossible to either implicitly or explicitly return void. These functions follow a very specific syntax and always return the result of their expression: fn (arguments) => expression. This structure makes arrow functions useful in a wide variety of situations.

One example is a very concise, in-line definition of a function to be applied to all elements in an array via PHP’s native array_map(). Assume input user data is an array of strings that each represent an integer value and we want to convert the array or strings into an array of integers to enforce proper type safety. This can easily be accomplished via Example 3-16.

Example 3-16. Convert an array of numeric strings to an array of integers
$input = ['5', '22', '1093', '2022'];

$output = array_map(fn($x) => intval($x), $input);
// $output = [5, 22, 1093, 2022]

Arrow functions only permit a single-line expression. If your logic is complicated enough to require multiple expressions, use a standard anonymous function (see Recipe 3.9) or define a named function in your code. This being said, an arrow function itself is an expression, so one arrow function can actually return another.

The ability to return an arrow function as the expression of another arrow function leads to a way to use arrow functions in in curried or partially applied functions to encourage code reuse. Assume we want to pass a function in our program that performs modulo arithmetic with a fixed modulus. We can do so by defining one arrow function to perform the calculation and wrap it in another that specifies the modulus, assigning the final, curried function to a variable we can use elsewhere as in Example 3-17.

Note

Modulo arithmetic is used to create so-called clock functions that always return a specific set of integer values regardless of the integer input. Specifically, taking the modulus between two integers is to divide them and return the integer remainder. For example, “twelve modulo 3” is written as 12 % 3 and returns the remainder of 12/3 or 0. Similarly, “fifteen modulo 6” is written as 15 % 6 and returns the remainder of 15/6 or 3. The return of a modulo operation is never greater than the modulus itself (3 or 6 in the previous two examples, respectively). Modulo arithmetic is commonly used to group large collections of input values together or to power cryptographic operations, which will be discussed further in Chapter 9.

Example 3-17. Function currying with arrow functions
$modulo = fn($x) => fn($y) => $y % $x;

$mod_2 = $modulo(2);
$mod_5 = $modulo(5);

$mod_2(15); // 1
$mod_2(20); // 0
$mod_5(12); // 2
$mod_5(15); // 0

Finally, just like regular functions, arrow functions can accept multiple arguments. Rather than passing a single variable (or implicitly referencing variables defined in the parent scope), you can just as easily define a function with multiple parameters and freely use them within the expression. A trivial equality function might use an arrow function as follows:

$eq = fn($x, $y) => $x == $y;

$eq(42, '42'); // true

See Also

Details on anonymous functions in Recipe 3.9 and the PHP manual documentation on arrow functions.

3.12 Creating a function with no return value

Problem

You need to define a function that does not return data to the rest of the program after it completes.

Solution

Use explicit type annotations and reference the void return type.

const MAIL_SENDER = '[email protected]';
const MAIL_SUBJECT = 'Incoming from the Wonderful Wizard';

function send_email(string $to, string $message): void
{
    $headers = ['From' => MAIL_SENDER];

    $success = mail($to, MAIL_SUBJECT, $message, $headers);

    if (!$success) {
        throw new Exception('The man behind the curtain is on break.');
    }
}

send_email('[email protected]', 'Welcome to the Emerald City!');

Discussion

The Solution example uses PHP’s native mail() function to dispatch a simple message with a static subject to the specified recipient. PHP’s mail() returns either true (on success) or false when there’s an error. In our example, we merely want to throw an exception when something goes wrong but otherwise want to return silently.

Note

In many cases, you might want to return a flag - a Boolean value or a string or null - when a function completes to provide an indication as to what has happened so the rest of your program can behave appropriately. Functions that return nothing are relatively rare, but do come up when your program is communicating with an outside party and the result of that communication doesn’t impact the rest of the program. Sending a fire-and-forget connection to a message queue or logging to the system error log are both common use cases for a function that returns void.

The void return type is enforced on compile time in PHP, meaning your code will trigger a fatal error if the function body returns anything at all, even if you haven’t executed anything yet. Example 3-18 illustrates both valid and invalid uses of void.

Example 3-18. Valid and invalid uses of the void return type
function returns_scalar(): void
{
    return 1; 1
}

function no_return(): void
{
    2
}

function empty_return(): void
{
    return; 3
}

function returns_null(): void
{
    return null; 4
}
1

Returning a scalar type (a string, integer, Boolean, etc) will trigger a Fatal error

2

Omitting any kind of return in a function is valid

3

Explicitly returning no data is valid

4

Even though null is “empty,” it still counts as a return and will trigger a Fatal error

Unlike most other types in PHP, the void type is only valid for returns. It cannot be used as a parameter type in a function definition; attempts to do so will result in a Fatal error at compile time.

3.13 Creating a function that does not return

Problem

You need to define a function that explicitly exits and ensure other parts of your application are aware it will never return.

Solution

Use explicit type annotations and reference the never return type. For example:

function redirect(string $url): never
{
    header("Location: $url");
    exit();
}

Discussion

Some operations in PHP are intended to be the last thing the engine does before exiting the current process. Calling header() to define a specific response header must be done prior to printing any body to the response itself. Specifically, calling header() to trigger a redirect is usually the last thing you want your application to do - printing any body text or processing any other operation after you’ve told the requesting client to redirect elsewhere has no meaning or value.

The never return type signals both to PHP and to other parts of your code that the function is guaranteed to halt the program’s execution by way of either exit() or die() or throwing an exception.

If a function that leverages the never return type still returns implicitly, as in Example 3-19, PHP will throw a TypeError exception.

Example 3-19. Implicit return in a function that should never return
function log_to_screen(string $message): never
{
    echo $message;
}

Likewise, if a never-typed function explicitly returns a value, PHP will through a TypeError exception. In both situations, an implicit or an explicit return, this exception is enforced at call-time (when the function is invoked) rather than when the function is defined.

1 We will discuss custom classes and objects at length in Chapter 8

2 PHP CodeSniffer is a popular developer tool for automatically scanning a codebase and ensuring all code matches a specific coding standard. It can be trivially extended to enforce a strict type declaration in all files as well.

3 WordPress’ market reach was about 65% of websites using content management systems and more than 43% of all websites as of December 2021 according to W3Techs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.227.251.194