TS-34: PHP

PHP code MUST follow the PER Coding Style v2.0, published by the PHP Framework Interop Group (PHP-FIG). PER Coding Style is the definitive standard for PHP coding style. This technical standard summarizes and extends it, but where this standard is silent, PER Coding Style is authoritative.

Linting

It is RECOMMENDED to use automated static analysis tools, with automatic code fixing enabled where available, to enforce consistent coding standards.

Type hinting

PHP is a dynamically typed language, but it is possible to add type hints to functions and methods. Type hinting – introduced in PHP 5 but not genuinely useful until PHP 7 – allows developers to specify the expected data types for arguments and return values in function declarations.

The type hints in the following code snippet declare that both $a and $b must be integers, and that the function should also return an integer.

function sum(int $a, int $b): int {
  return $a + $b;
}

Although them use of type hints is optional in the PHP language, it is strongly RECOMMENDED to use them extensively. Specifically:

  • Class properties SHOULD be typed.
  • Method parameters SHOULD be typed.
  • Method return types SHOULD be typed.

Type safety – the practice of using strict typing – has multiple advantages. It helps to catch bugs early, and static analysis tools can pick up on those errors in an automated way. Type hints also serve as a form of inline documentation, helping to make the code easier to understand.

Although PHP’s type hinting capability has improved over time, unfortunately there remain some gaps in PHP’s type system. Most notably is the inability to declare the data types that can be held in arrays and iterables. Also, you can declare a callable type but not the parameters and return types of the callable.

Strict mode

PHP has two type checking modes: coercive (the default) and strict. To enforce strict type checking, you need to add a declare() statement to the beginning of every PHP file.

declare(strict_types=1);

Strict typing mode ensures that the types are exactly as specified. It is strongly RECOMMENDED to opt-in to strict type checking.

Nullable types

Since PHP 7.1 it has been possible to specify an argument or return type as being "nullable", which means the value could be null as an alternative to the declared type. The syntax uses a ? prefixed to the type:

function getUser(int $id): ?User {
  return findByUserId($id) ?: null;
}

The void type

PHP 7.1 also introduced the void return type, which may be used to indicate that a function does not return any value. Although PHP does not require void return declarations, it is RECOMMENDED to include this wherever relevant.

Arrays and iterable types

PHP supports the array type, and the iterable type which covers both arrays and instances of Traversable.

function process(iterable $items): void {
    foreach ($items as $item) {
        // ...
    }
}

Unfortunately, in PHP it is not possible to define the data types that are expected to be stored in list-like structures. To make matters worse, PHP’s array type also doubles as an associative array type and it is widely used to hold arbitrary data structures, the schema of which cannot be typed.

Note

On a side note, an RFC was discussed in 2014 to add "array of" type hint syntax (type[]), but it was ultimately rejected due to the performance costs of the interpreter needing to iterate over all array entries.

For simple arrays passed to functions, you could unpack the values to separate parameters. This requires changing the signature of functions to accept variadic parameters, which is not desirable (or even possible) in all use cases. But where this is doable, you are able to use native PHP type hints to check the type of all parameters.

function test(Foo ...$params): void {
    foreach ($params as $param) {
        // ...
    }
}

If you don’t want to change the signature of a function to implement this solution, you could unpack the array in the function body instead. The type hint in the following example works perfectly:

function test(array $foos) {
    return (function(Foo ...$fooParams) {
        foreach ($fooParams as $foo) {
            // ...
        }
    })(...$foos);
}

Some static analysis tools fill in capability gaps in PHP’s native typing system. Some tools will allow you to use docblocks to extend PHP’s native type hints with additional metadata, which is understood by the analysis tool. It is RECOMMENDED to use these tools to implement stricter type checking. They are particularly useful for defining the types of data held by arrays and iterables.

/** return array<int, MyObject> */
public function toArray(): array {
    return $this->items;
}

Where extended type hints are provided, the docblocks MUST NOT repeat native type declarations - this is redundant.

This can be a good solution where the array type is used to represent hashes (ie. associative arrays). The following phpdoc syntax, originally proposed for PSR-5, is supported by some static analysis tools:

/**
 * Initializes this class with the given options.
 *
 * @param array $options {
 *     This is a description should you wish to add it.
 *
 *     @type boolean $required Whether this element is required
 *     @type string  $label    The display name for this element
 * }
 */
public function __construct(array $options = array())
{
  // ...
}

The above solutions will be adequate for many short-lived, minimally-scoped list-like structures. But the optimal solution is to define your own typed arrays, and so create a library of custom array-like values objects. A RECOMMENDED implementation pattern is as follows:

class ArrayOfFoo extends \ArrayObject {
    public function offsetSet($key, $val) {
        if ($val instanceof Foo) {
            return parent::offsetSet($key, $val);
        }
        throw new \InvalidArgumentException('Value must be of Foo');
    }
}

This defines a new type of "array of foo", which can then be used in type hints:

function workWithFoo(ArrayOfFoo $foos) {
    foreach($foos as $foo) {
        // ...
    }
}

An alternative pattern is as follows:

class Users extends ArrayIterator
{
    public function __construct(User ...$users)
    {
        parent::__construct($users);
    }

    public function current(): User
    {
        return parent::current();
    }

    public function offsetGet($offset): User
    {
        return parent::offsetGet($offset);
    }
}

And another pattern is shown below. This implements the ArrayAccess interface, which allows values to be pushed in the normal way, and IteratorAggregate, which allows us to loop through the array.

class Users implements ArrayAccess, IteratorAggregate
{
    private array $users;

    public function __construct()
    {
        $this->users = [];
    }

    /*
    IteratorAggregate methods.
    */

    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->users);
    }

    /*
    ArrayAccess methods.
    */

    public function offsetExists(mixed $offset): bool
    {
        return isset($this->users[$offset]);
    }

    public function offsetGet($offset): ?User
    {
        return $this->users[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        if (! $value instanceof User) {
            throw new InvalidArgumentException('Expected parameter to be a User);
        }

        $this->users[$offset] = $value;
    }

    public function offsetUnset($offset): void
    {
        if (isset($this->users[$offset])) {
            unset($this->users[$offset]);
        }
    }
}

The optimum design will depend on the particular use case. Whatever the solution pattern, it requires lots of boilerplate code, but there are some open source libraries that abstract this away.

Generics

PHP does not natively support generics. However, many static analysis tools support phpDoc annotation like the following.

/**
 * @template T
 * @param T $item
 * @return T
 */
function duplicate ($item) {
    // ...
}

Callable types

PHP 7.1 introduced the callable type, which means anything that can be executed as a function. It is not currently possible to define the parameter types and return types of callables; for complex callable signatures, the callable’s signature SHOULD be documented in phpdoc.

Union types

PHP 8 introduced union types, which allows programmers to declare variables that could hold any one of several possible types.

function debugInfo(int|string|bool $data): void {
    // ...
}

Union types SHOULD be avoided. Ideally, all variables SHOULD be designed to encapsulate a single discrete data type, and OPTIONALLY null.

Magic methods

PHP has class lifecycle hooks for attaching functionality in dynamic ways. These hooks are called magic methods.

While some application frameworks rely on magic methods to provide elegant ways for application code to interact with the framework’s functions, magic methods – by their nature – make code less explicit and therefore harder to understand.

For this reason, application code SHOULD avoid using magic methods wherever possible. Prefer more direct execution of framework-level logic, and define all your class methods explicitly.

Exceptions

Catch-and-rethrow patterns SHOULD be used to make exceptions more meaningful. Exceptions SHOULD be allowed to "bubble up", perhaps unmodified, until the exception is relevant to the level of abstraction.

This strategy should also make error handling more secure, by avoiding unnecessary disclosure of information about modules underlying the abstraction.

/**
 * @throws UserNotFoundException
 */
public function getUser($username)
{
    // ...

    try {
        $user = $db->query('SELECT ...', $username);
    } catch (DatabaseException $e) {

        /*
        The details of the DatabaseException are unlikely to be relevant
        to the caller of this method. The exception should be re-thrown with
        a more appropriate level of abstraction.
        */
        throw new UserNotFoundException();
    }

    return $user;
}

phpDoc

Exceptions

Where a method explicitly throws something, the thrown type MUST be documented with an @throws annotation.

/**
 * @throws UserNotFoundException
 */
public function getById()
{
    // ...
    throw new UserNotFoundException();
}

Authors MUST NOT use @throws annotations on caller functions to document exception types that may be thrown by other callee functions, unless the caller catches and rethrows those exceptions.

/**
 * @throws UserNotFoundException
 */
public function getUser()
{
    // ...
    try {
      $user = $userRepository->getById($id);
    } catch (UserNotFoundException $e) {
        throw $e;
    }
}

If you tried to document every possible value that could be thrown during the runtime of a function, including those possibly thrown by lower abstraction levels, your phpDocs will quickly get out of control. For the purpose of internal API documentation, only exception types that are relevant to the current abstraction level are relevant.