What’s New In PHP 8.2?

Illustration showing the PHP logo

PHP 8.2 was released in December 2022 as the latest minor version in the PHP 8.x release cycle. It adds new features that build upon the capabilities of previous versions, further simplifying the development experience. In this article, we’ll tour each of the major changes and show how they’ll make your code easier to maintain.

Type System Improvements

PHP has been gradually evolving its type system towards a more strongly typed model over the past several releases. 8.2 includes two enhancements that allow types to be even more expressive.

Disjunctive Normal Form: Combined Unions and Intersections

Union and intersection types can now be combined anywhere a type is accepted, such as function parameters and return values. The complete type definition must be written using boolean disjunctive normal form notation (DNF). This means intersection types have to be wrapped in parentheses to be valid.

The following function allows you to pass either an array, or an object that implements both the Countable and Iterator interfaces:

function foo((Countable&Iterator)|array $values) : void {
    // ...
}

This facilitates more powerful type definitions, similar to the example function shown above. Prior to PHP 8.2, you’d have to write two separate functions to achieve the same effect, one for each of the types in the union.

Standalone Types for Null and Booleans

The values true, false, and null are now accepted as standalone types. They can be used in any type definition to indicate that the specific value will be returned:

function alwaysReturnsTrue() : true {}
 
function alwaysReturnsFalse() : false {}
 
function alwaysReturnsNull() : null {}

In realistic use cases, you’ll normally write these values as part of a union type. Handling errors by returning false instead of a regular value is a common pattern in both the PHP core and userland code, for example. Now you can properly describe this behavior in your return type definition:

/**
 * Tries to connect to the database; returns `false` on failure
 */
function connectToDatabase() : DatabaseConnection|false;
 
/**
 * Tries to close the database connection; returns an error code on failure
 */
function closeDatabaseConnection() : true|DatabaseErrorCode;

Readonly Classes

Readonly properties were one of the headline features of PHP 8.1. They let you enforce immutability for class properties after their initial assignation:

final class User {
 
    public function __construct(
 
        public readonly string $Username,
 
        public readonly bool $Admin) {}
 
}
 
$user = new User(
    Username: "howtogeek",
    Admin: true
);
 
// Error - The property is readonly
$user -> Username = "Demo";

In practice, many classes want all their properties to be readonly. Now you can mark the entire class as readonly, which lets you drop the readonly keyword from the individual properties.

readonly final class User {
 
    public function __construct(
 
        public string $Username,
 
        public bool $Admin) {}
 
}
 
$user = new User(
    Username: "howtogeek",
    Admin: true
);
 
// Error - The property is readonly
$user -> Username = "Demo";

Besides saving some repetitive typing, making a class readonly adds a few other constraints:

  • You cannot add any untyped properties to the class. Untyped properties aren’t supported as readonly properties, which readonly classes are syntactic sugar for.
  • The class cannot contain static properties either, because these are similarly incompatible with the readonly keyword.
  • The class cannot be extended by non-readonly children. Inheritance is permitted if the child also includes the readonly keyword.
  • Dynamic properties cannot be created on instances of the class and the AllowDynamicProperties attribute is blocked from overriding this.

Readonly classes make it more convenient to write data transfer objects and other structures that are intended to be immutable. Their use is not obligatory though: you can still create partially mutable classes by continuing to use the readonly keyword on individual properties.

Enum Properties Can Be Consumed In Constant Expressions

Enum properties can now be used in constant expressions. The following code, which was not supported in the version of enums shipped with PHP 8.1, is now legal:

enum PostStatus : int {
 
    case Draft = 1;
 
    case Published = 2;
 
    const VALUES = [self::Published -> value => self::Published];
 
}

The value property of the enum’s Published case is accessed without throwing an error, because PHP’s engine is aware its value can never change.

Traits Can Define Constants

It’s now possible for traits to define constants, something that was omitted from previous PHP releases. Any constants that are written within a trait will be visible in the scope of classes that use it.

This example demonstrates the possibilities for accessing a trait’s constants, within the trait’s code, the code of the class that uses it, and from the global scope:

trait Loggable {
 
    public const LOG_TYPE_JSON = 1;
 
    public function log(string $message) : void {
        if ($this -> getLogType() === self::LOG_TYPE_JSON) {
            // ...
        }
    }
 
    abstract public function getLogType() : int;
 
}
 
 
final class NewUserEvent {
 
    use Loggable;
 
    public function getLogType() : int {
        return self::LOG_TYPE_JSON;
    }
 
}
 
// "1"
echo NewUserEvent::LOG_TYPE_JSON;

Note that you cannot access the constant’s value directly on the trait, from the global scope. The following code will throw a “cannot access trait constant directly” error:

echo Loggable::LOG_TYPE_JSON;

A Modern Object-Oriented Approach to Random Numbers

PHP 8.2 adds a new object-oriented random number generation extension. It allows you to produce random numbers using several modern generation engines. This example demonstrates how to produce a random integer between 1 and 100 with the xoshiro256** engine:

use RandomEngineXoshiro256StarStar;
use RandomRandomizer;
 
$randomizer = new Randomizer(
    new Xoshiro256StarStar(
        hash(
            algo: "sha256",
            data: "256-bit seed value",
            binary: true
        )
    )
);
 
/** Generated with xoshiro256** */
echo $randomizer -> getInt(1, 100);

If you subsequently want to switch to a different engine, you need only replace the parameter that’s passed to your Randomizer instance:

use RandomEngineMt19937;
use RandomRandomizer;
 
$randomizer = new Randomizer(new Mt19937(1234));
 
/** Generated with Mt19937 */
echo $randomizer -> getInt(1, 100);

Prevent Passwords Leaking Into Stack Traces and Error Logs

Code like the following is a standard feature in many PHP codebases:

function connectToDatabase(
    string $host,
    int $port,
    string $username,
    string $password) : void {
    // ...
}

This poses a challenge when the function throws an uncaught exception. PHP’s stack traces include the values of function parameters, so the password ends up being emitted to your error logs. This is a security risk.

PHP 8.2 addresses the problem by providing a new attribute that marks parameters as “sensitive.” Applying the #[SensitiveParameter] attribute to any parameter will redact its value from stack traces:

function connectToDatabase(
    string $host,
    int $port,
    string $username,
    #[SensitiveParameter]
    string $password) : void {
    // ...
}

The modified stack trace will look similar to the following:

Stack trace:
#0 index.php(1): connectToDatabase("localhost", "3306", "demo", Object(SensitiveParameterValue))
#1 {main}

It’ll be worthwhile auditing your codebase to find parameters with sensitive values after you upgrade. Add the attribute to any instances you find. This will help prevent leaked logs from compromising your environment’s security.

Dynamic Class Properties Have Been Deprecated

PHP has historically allowed you to set properties on object instances without first declaring them:

final class User {}
 
$user = new User();
$user -> Username = "howtogeek";

This behavior is often problematic. Neither you nor automated static analysis tools can assert which properties the instances will have.

Dynamic properties also facilitate typos which can be difficult to spot:

final class User {
 
    public string $Username;
 
}
 
$user = new User();
$user -> Usernamee = "howtogeek";

You’ve mistakenly set the Usernamee property, but PHP won’t throw an error or provide any help. This causes you to spend time debugging why the correct Username property doesn’t have the value you expect.

PHP 8.2 addresses these problems by deprecating dynamic properties. Going forwards, you should define all properties a class can accept, either individually or as promoted constructor properties.

As this is a deprecation and not a removal, existing code will continue to function for now. A deprecation notice will be logged each time a dynamic property is read or set. The removal, which could occur in PHP 9.0, will be a breaking change. Any code that relies on dynamic properties will stop working.

There is a way to continue using dynamic properties, however. You can explicitly opt-in a class to dynamic properties by marking it with the new #[AllowDynamicProperties] attribute. This will continue to work in PHP 9.0 too. It solves some of the legitimate use cases for dynamic properties, such as config stores, and temporary maps and caches.

#[AllowDynamicProperties]
final class ConfigStore {}
 
$store = new ConfigStore();
$settings = json_decode(file_get_contents(__DIR__ . "/settings.json"), true);
 
foreach ($settings as $key => $value) {
    $store -> $key = $value;
}

Use of this attribute should be discouraged except in cases similar to this example, where the class is dynamic by nature. The deprecation of dynamic properties is intended to help you write safer code that’s less vulnerable to mistakes.

This change does not affect classes that use the __get() and __set() magic methods. They’ll continue to work as normal, allowing you to implement your own routines when an undefined property is read or set. Instances of stdClass() also continue to support dynamic properties.

Summary

PHP 8.2 is an exciting new release of usability improvements. It includes time-saving readonly classes, more flexible type definitions, and small adjustments that enhance the developer experience, such as constants in traits, enum values in constant expressions, and sensitive parameter value redaction for stack traces.

The upgrade is available now through all supported PHP distribution channels. Moving to 8.2 should be straightforward for most modern codebases that are already written using PHP 8.x features and standards. There are some narrow backward compatibility breaks to be aware of, however, so refer to the official migration guide to learn about all the changes and what you must do to prepare.

Original Article