What's New in PHP 8.1
December 20, 2021

What's New in PHP 8.1?

PHP Development

Each year brings a new major or minor release of PHP, and this year is no different. Released on November 25, 2021, PHP 8.1 is the first minor release in the PHP 8 series, and comes with its own new features and deprecations. Let's see what's in store for you early adopters!

Features

The main reason for a new minor release is to provide new features. PHP 8.1 offers more than two dozen new features, from smaller additions like adding a new $options argument to the various hash_*() functions, to entirely new syntax features such as Enumerations. What follows is a breakdown of some of the larger features that arrived with 8.1.

Enums

Enumerations, also known as enumerated types or enums, are types that have a fixed number of possible values. Enums are available in a wide number of programming languages, and have been an often-requested feature for PHP for many years now. As a basic example, we might define a Vehicle enum with allowed styles of transportation:

enum Vehicle
{
    case Pedestrian;
    case Bicycle;
    case Automobile;
    case Lorry;
    case Ferry;
    case Airplane;
}

Code that consumes or returns enums would provide a typehint using the enum:

function travel(Vehicle $vehicle): void
{
}

When calling the above function, you would pass a specific enumerated value:

travel(Vehicle::Ferry);

Passing anything else would raise an error.

You can also declare a backed enum. A backed enum holds a value for each case, where the value may be either a string or an integer. A classic example is declaring HTTP request methods or response status codes:

namespace HTTP;

enum RequestMethod: string
{
    case DELETE  = 'DELETE';
    case GET     = 'GET';
    case HEAD    = 'HEAD';
    case OPTIONS = 'OPTIONS';
    case PATCH   = 'PATCH';
    case POST    = 'POST';
    case PUT     = 'PUT';
}

enum ResponseStatus: int
{
    case OK           = 200;
    case CLIENT_ERROR = 400;
    case SERVER_ERROR = 500;
}

Note that all values must be of the same type, and each value MUST be unique; the backed type is on the enum declaration itself.

Enums can define methods, and also implement interfaces, which means they can have behavior. The primary differences are:

  • Enums can not have state. This means no properties, and it also means you cannot use new to create an instance. Instead, instance methods apply to individual enumeration cases. As an example, in our HTTP\ResponseStatus enumeration above, if defined a reasonPhrase() method that returned a string (e.g., public function reasonPhrase(): string), you would invoke it using a specific case: echo ResponseStatus::400->reasonPhrase().
  • Enums cannot be extended, and cannot inherit from other classes. Visibility is only useful for hiding internal aspects of how the enum performs work, since inheritance is not possible.

Enums themselves implement one of the following internal interfaces:

// For standard enums:
interface UnitEnum
{
    /**
     * Returns an array with all cases for the enum implementation.
     */
    public static function cases(): array;
}

interface BackedEnum extends UnitEnum
{
    /**
     * Returns the enum case matching the given value, if found.
     * Otherwise, raises a ValueError.
     */
    public static function from(int|string $value): static;

    /**
     * Same as from(), but returns a null if no matching case is found.
     */
    public static function tryFrom(int|string $value): ?static;
}

The methods above can never be explicitly defined; the PHP engine implements these internally.

For more information on enumerations, visit the PHP documentation.

Fibers

While developers have been producing async libraries and extensions for PHP for a number of years now, PHP 8.1 now offers low-level engine support for async execution via its new Fibers functionality. Put most succinctly, a Fiber is a code block that manages its own execution stack (the block's state). The feature is similar to the Generators added in PHP 5.4. With generators, however, once a code block (a function or method) yields a value, you had limited ways to resume execution of that block; in most cases, you were forced to operate within a loop in order to resume or terminate the block. With Fibers, however, you can resume execution at any point, allowing you to pass the Fiber around as a message between different methods.

The majority of developers will not work with Fibers directly. Instead, they will be the building blocks for async libraries, with ReactPHP and AMPHP already teaming up to write a common, low-level async event loop around the feature. If you are interested in how they work, until the PHP documentation is written, php.watch has an excellent article on the feature.

Readonly Properties

PHP has had a number of keywords that influence engine behavior, including final and abstract. New with PHP 8.1 is readonly.

The new readonly keyword only applies to class properties. When present, writing to the property from outside the class, or attempting to overwrite it after it has been first written, raises an exception. As an example:

class KeyValue
{
    public readonly string $key;

    public function __construct(string $key)
    {
        $this->key = $key;
    }
}

If at this point you created an instance and tried to overwrite the key:

$keyValue = new KeyValue('some-key');
$keyValue->key = 'another-key'; // Exception

This is true internally as well. If we added a method that would change the $key to the class:

class KeyValue
{
    /* ... */

    public function alterKey(string $key): void
    {
        $this->key =$key; // Exception
    }

You can also use the keyword when using PHP 8's constructor property promotion feature:

class KeyValue
{
    public function __construct(
        public readonly string $key
    ) {
    }
}

I foresee this being a common way to define immutable value objects once PHP 8.1 has wide adoption.

Intersection Types

PHP 8.0 added the concept of union types. Union types are declared with a "|" separating each type allowed. When used with function parameters, any of the types that satisfy the union type are considered valid. When used for return values, the function is indicating the return may be any of the listed types.

PHP 8.1 adds the concept of intersection types. Intersection types are separated by "&", and indicate that the value must satisfy all of the listed types. Why might you want to do this? One common pattern is to define countable iterators, which is often useful for grabbing slices of collections for purposes of pagination. Previously, you'd need to declare a custom interface that extended each:

interface CountableIterator extends Countable, Iterator
{
}

And from there, typehint on that custom interface:

public function paginate(CountableIterator $collection, int $offset, int $slice): array
{
}

The problem with such an approach is that it means a consumer of this code now needs to implement your interfaces, instead of more generic ones.

With intersection types, you can do exactly that:

public function paginate(
    Countable&Iterator $collection,
    int $offset,
    int $slice,
): array {
}

Never Return Type

In PHP 7.2, we received the void return type, to indicate that a function does not return a value. But what if a function never returns? For instance, what if it will only ever throw an exception, or call exit()?

PHP 8.1 adds the never return type for exactly that. When present, if return is called by the method, PHP will raise an error. Otherwise, you can rely on the fact that once called, program execution will go no further.

Final class constants

PHP has supported constants since 5.0, and added visibility operators for them starting in 5.3. One nuisance, however, is that an interface or class extension can override a constant, effectively redefining its value. This has meant that if you want to depend on a specific value of a constant, you must always refer to it via the specific interface or class that originally defined the value, instead of using $this:: or self:: notation.

To solve this problem, PHP 8.1 introduces the ability to mark a constant final. This has the same semantics as declaring a property or method final; extending or implementing classes cannot redefine it.

The final keyword is only allowed for public and protected constants, as private constants are internal to the given implementation already.

array_is_list

PHP uses arrays for both lists and maps. A list is an array of values where all keys are integers, and where the keys are sequential, starting from 0. A map is an array of values where the keys may be integers or strings; if the keys are integers, they are non-sequential and/or start from a value other than 0. Typically speaking, maps operate as look-up tables, and are most similar to object properties.

Because PHP arrays can be either, developers often need to vary behavior based on what type of array they have received. As an example, many libraries accept an array of options for configuring object or application behavior; these are almost always maps, and never lists. As such, it's often expedient to determine which array type you have before you attempt to act on it, in order to raise an error early. The Laminas Project, formerly Zend Framework, has defined such functions/methods since its earliest iterations in 2005!

Thankfully, in PHP 8.1, we now have a dedicated internal function for this: array_is_list(). The function returns true for both empty arrays and arrays representing lists; all other arrays cause the function to return false.

Array unpacking for arrays with string keys

PHP 7.4 introduced the idea of using the spread operator (...) for array unpacking. Prior to 7.4, if you wanted to merge multiple arrays, you would use array_merge():

$array1 = ['dog', 'cat'];
$array2 = ['bird', 'fish'];
$all    = array_merge($array1, $array2, ['lizard', 'ant farm']);

Using the spread operator means we can define an array and unpack other arrays into it; the following is equivalent to the previous:

$all = [...$array1, ...$array2, ...['lizard', 'ant farm']];

This only worked for indexed arrays (lists, if you read the previous section!). With PHP 8.1, it also works with maps (arrays defining string keys).

One thing to note: a key appearing in a later array will overwrite values from previous arrays when using unpacking.

cURL Improvements

PHP 8.1 adds two new features for the cURL extension: the ability to use DNS-Over-HTTPS (DoH), as well as the ability to upload arbitrary string contents as a file (instead of requiring a filehandle or file name).

DNS-Over-HTTPS has become increasingly common as a privacy measure to prevent network sniffs that look to see what domains are being looked up. Normally, DNS requests are done without encryption, meaning a network traffic sniffer can get an idea of what domains a server is trying to access, and, if a bad actor, reply with a different address that directs traffic to a malicious server. DNS-Over-HTTPS encrypts such requests; as long as you trust the DoH provider, you can be assured both of privacy and getting your requests where they belong.

To use this feature, you need to supply the CURLOPT_DOH_URL option to your cURL handle:

$ch = curl_init();
curl_setopt($ch, CURLOPT_DOH_URL, 'https://mozilla.cloudflare-dns.com/dns-query');

Once set, cURL will use the provided DoH provider for all DNS lookups it performs.

When it comes to providing an upload file to cURL, previous versions of PHP offered the CURLFile class, which accepted a local filename, its MIME type, and the filename you want to provide in metadata during the upload. This made uploading a file relatively trivial, but missed an edge case: if the contents you wanted to upload were in-memory (e.g., an in-memory CSV created from a database query), you had to either write the file to a temporary file (and worry about cleanup afterwards), or jump through hoops to create data:// URIs with Base64-encoded values.

PHP 8.1 provides a new class, CURLStringFile, for this purpose. It accepts the same three arguments, with one change: the first argument is the full textual contents you want to upload:

$uploadFile = new CURLStringFile($someCsvText, 'text/csv', 'data.csv');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, ['file' => $uploadFile]);

Breaking Changes and Deprecations

While PHP attempts to follow semantic versioning, each minor release generally introduces deprecations to existing functionality, and often backwards compatibility breaks. The developers attempt to avoid the latter, but occasionally the only way to fix an issue is to introduce a break; other times, it's done to ease long-term maintenance or prevent preventable user issues.

Deprecations are provided to flag to users that functionality they are using will likely change or be removed in an upcoming major version, whether that's the immediate next major version, or a later one. If you exclude E_DEPRECATION from your error logging, which is the suggestion for production systems, you generally will not see these. However, it's good to periodically check your code to see if you are using any deprecated features, so you can prepare for the future.

The following are a number of changes and deprecations that you may encounter as you adopt the new version. This list is not comprehensive; there were more than 2 dozen such changes in 8.1.

Resource to object migrations

PHP 8.0 marked the beginning of a migration away from generic "resource" instances to named class resources. In all previous versions of PHP, resources were a special "resource" type that you could check for using is_resource(); to differentiate between resource types, you would use get_resource_type(). This practice made it difficult for library authors in particular, where they would need to perform a series of checks to validate that they did indeed have a resource of an expected type (e.g., a Postgres connection, a cURL handle, etc.).

Starting in PHP 8.0, the language began converting these various resources into immutable marker classes. This practice allows you to typehint for specific resource types:

public function transform(GdImage $image): void
{
}

PHP 8.1 continues to introduce new dedicated resource types. Note, however, that these new resource types are a breaking change in the language. If you previously were using is_resource() or get_resource_type() to validate your resources, these functions no longer work as expected, and you will need to either update your code to do an instanceof check, add a typehint, or vary the check based on the PHP version in use. Otherwise, they can be used in precisely the same way that the original resources were used. The following resource types were introduced in PHP 8.1::

  • file_info resources are now finfo instances.
  • imap resources are now IMAP\Connection instances.
  • ftp resources are now FTP\Connection instances.
  • GD font resources are now GdFont instances.
  • LDAP resources are now one of either LDAP\Connection, LDAP\Result, or LDAP\ResultEntry instances.
  • PostgreSQL resources are now one of either PgSql\Connection, PgSql\Result, or PgSql\Lob instances.
  • PSpell resources are now one of either PSpell\Dictionary or PSpell\Config instances.

Return types in interfaces and internal classes

Over the past ten years, PHP as a language has been skewing for more explicit, typed usage. In PHP 8.0, the language started adding typehints to function and method arguments, raising fatal errors for incompatible signatures in implementations and extending classes. PHP 8.1 continues this by adding explicit return types to all internal interfaces and classes, and now raises a deprecation notice when implementations or extensions provide incompatible signatures. Moreover, this deprecation notice becomes a fatal error whendeclare(strict_types=1)is enabled.

If your code needs to support multiple PHP versions, or if you absolutely cannot change your return types to conform with the original signatures, you can add the following #[\ReturnTypeWillChange]attribute to each offending method:

/**
 * This is a method that has an incompatible return type.
 *
 * @return null|int
 */
#[\ReturnTypeWillChange]
public function count()
{
}

Note: attribute names are resolved from the current namespace. You must either globally qualify the name (as done above), or import the attribute name into your file (e.g. use ReturnTypeWillChange;).

Mysqli error mode change

In all previous versions of PHP, the mysqli extension would silence errors by default. You could enable error reporting (raising an exception or triggering an error) by setting the error handling mode via the mysqli_report() function prior to making a connection; otherwise, you would need to manually check return statuses of various functions and/or methods.

With PHP 8.1, the mysqli error mode now defaults to MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT, causing it to raise exceptions. If you were manually checking and handling error conditions previously, you will need to call mysqli_report(MYSQLI_REPORT_OFF) prior to making a connection.

Phar default signature algorithm

Previous versions of PHP used the SHA1 algorithm when signing a PHAR file. PHP 8.1 adds two new signature algorithms, SHA256 and SHA512, and now defaults to SHA256. In the majority of cases, this should have no impact on users, unless they are re-packing a previously packed PHAR file. In such cases, use Phar::getSignature() to determine the signature used originally, and provide that value to Phar::setSignatureAlgorithm() before packing the PHAR.

Passing null to non-nullable parameters

Passing null to a non-nullable function or method parameter is not allowed. However, it's only ever been enforced for user-defined functions and methods previously, and not internal PHP functions. Starting in PHP 8.1, passing a null value to a non-nullable internal PHP function or method raises a deprecation warning, or, if declare(strict_types=1) was declared, raises a TypeError.

This is an incredibly subtle error to catch. PHP previously would coerce null values when it could; something marked string would become an empty string, an int would coerce to 0, and so on. Because no warnings were raised, not even with strict types enabled, tests would not even find these previously. Make sure you have a strong, comprehensive testing framework in place to catch these, particularly if you enable strict types.

mhash functions

Prior to PHP 5.3, the language bundled the mhash extension, which provided a number of functions for providing cryptographic hashes of values. Starting in PHP 5.3, the language introduced a new "hash" extension which provided better functionality that also provided a forwards compatible way to adopt new hashing algorithms. Since PHP 7.4, the hash extension has been part of the core distribution, meaning it is no longer possible to compile without it.

With PHP 8.1, the "mhash" extension is now formally deprecated, with scheduled removal in PHP 9.0. If you still rely on mhash, it's time to start migrating to the hash extension.

Implicit float to int conversion

PHP has allowed implicit coercion of float values to int values. When a float value is provided where an integer is required, the language converts it by performing a floor() operation. However, this can lead to data loss, which can lead to hard to trace errors.

Starting in 8.1, these operations now lead to a deprecation notice, or, when strict types is enabled, a TypeError.

One such case is the usage of float array keys. Array keys are expected to be either strings or integers. Floats were allowed previously, but were silently coerced to integers, which would lead to collisions and potential overwriting.

If you are not using strict types, this will simply flag changes you should make to ensure your application is forwards compatible. If you are using strict types, it will be an immediate backwards compatibility break. Make sure your application is comprehensively tested before upgrading so that you can catch these issues.

Summary

PHP 8.1 offers a number of useful new features, particularly to its object model. Enumerations have been long requested, and will help make code more readable and predictable. Intersection types will provide ways to decouple from framework or library-specific interfaces while still providing precise type-hinting for the features needed or provided. Fibers deliver a fantastic low-level, engine feature for creating first-class async features for the language, allowing the language to fully compete with the likes of Node.js.

On the flip side, of course, a number of other changes could impact your applications and potentially cause them to break in unexpected ways. These range from the migration of resources to specific resource object types, changing the default mysqli error mode, and changes to how values are coerced to other types, particularly when strict types are enabled.

As always, if you are unsure if you are ready to upgrade, Zend by Perforce provides LTS versions of PHP, and migration services.

For More Information Contact Us