PHP 8.2 is set to be released at the end of 2022, but final date will be announced at some point in the future. In this post I will evaluate all the features, improvements and deprecations. I’ll try to keep this article up-to-date with RFCs that will be accepted after publishing.

The type system in PHP is not perfect, but it is being gradually enriched and developed. This time the change is a typical evolution of existing types, or rather the way how we can use types - it will be possible to combine union types (logical OR: |) and intersection types (logical AND: &), thanks to which it will be possible to model expected / returned types in a more flexible manner.

It’s hard to talk about unequivocal profit here because this change has an infinite number of usages. At the same time, it brings a lot of challenges for developers of tools operating on Abstract Syntax Tree, such as #PHPStan or #Rector - all of them have the difficult task of adapting to changes in syntax (although a large part will of course be done in the parser) and adding rules to verify the correctness of the code or to allow its refactoring.

It is worth noting that the RFC introduces DNF types, the syntax of which is clearly defined: it is a OR set, in which individual elements can take the form of the AND set. So A|(B&C) is the correct type, while A&(B|C) is not, and will cause a parsing error.

I am really happy that PHP follows the blow and introduces ability to use readonly on class level. It’ll be now possible to model data transferring objects (DTO) even easier than before. In PHP 8.1 usage was like this:

class Foo
{
    public function __construct(
        readonly public string $foo,
        readonly public string $bar,
        readonly public string $baz
    ) {}
}

Starting with 8.2, in order to achieve full object immutability, readonly will have to be used only once:

readonly class Foo
{
    public function __construct(
        public string $foo,
        public string $bar,
        public string $baz
    ) {}
}

No doubts here — it’s a great addition that will be widely used both in standard PHP library and in open source packages. Of course, you should read the full RFC, especially section about restrictions, so you’ll know what readonly usage is not allowed.

Interesting point from Frank de Jonge:

It means that readonly disallows modification of properties’ values after their initialisation, so it’s harder to create fluid API that returns immutable object created from another immutable object (e.g. (new \DateTimeImmutable())->modify('+1 day')) based on read only properties. In my opinion it’s not something that readonly is intended for - it is great for modeling simple, immutable data structures (DTO), but for objects containing logic and API you should use private properties and methods (modifiers and getters).

A trait is a symbol that allows the same code to be reused in many places without interfering with the inheritance tree or the composition applied. Class can inherit from other class, implement interfaces, and at the same time it can use traits completely independently (of course there is a risk of name conflicts, but these can be resolved at the import level). Unfortunately, the lack of support for constants resulted in the developer being faced with a choice:

  • ignore the encapsulation rule and use, within trait, constants declared outside of it (in places where the trait is used)
  • do not use trait and replace them with another architectural solution

The first option is unacceptable to me. Personally, I strictly adhere to the fact that the trait is a closed set of properties and methods, so that it does not use anything declared outside the trait. If needed, trait can define abstract methods that must be implemented in a class that uses that trait.

So, as you can guess, the accepted RFC brings a change that I support and which I will probably use sooner or later 🙂 From version 8.2, the following code will be correct:

trait Example {
    public const ONE = 1;
    protected const TWO = 2;
    private const THREE = 3;

    public function count(): string
    {
        return implode(', ', [self::ONE, self::TWO, self::THREE]);
    }
}

This will allow better symbol modeling, and while traits aren’t one of my favorites, I’m glad that the number of potential bad uses will be limited. The rest is in the hands of the developers 😉.

Introduction to this section is a bit enigmatic, doesn’t it? No different with enums, their usage seems to be not-so-easy for some people. The problem is a fact that enums are objects - their usage is based on instances of specific enum symbol, which improves type system for arguments and return values. Unfortunately, it’s not possible to fetch properties in constant expressions. Below you can see examples of usages that will be allowed in PHP 8.2:

enum E: string {
    case Foo = 'foo';
}

const C = E::Foo->name;

function f() {
    static $v = E::Foo->value;
}

#[Attr(E::Foo->name)]
class C {}

function f(
    $p = E::Foo->value,
) {}

class C {
    public string $p = E::Foo->name;
}

// The rhs of -> allows other constant expressions
const VALUE = 'value';
class C {
     const C = E::Foo->{VALUE};
}

Voting ended with 24:11 result, so decision wasn’t unambiguous. Discussion was barely existent, at least not in official channel 😉. Main arguments against was inconsistency in the language, by in my opinion deviations from conventions are acceptable if they resolve actual community problems.

Personally, I would go with support based on constant-like usage, so it would be possible to use Some::thing instead of Some::thing->value. But as far as I know it wouldn’t be fully compatible with declare(strict_types=1); (well, not without some internal hacking…). Accepted format enforces some boilerplate code, but it solves the problem 🤷.

Probably each of us, while wandering around the Internet, encountered an error connecting to the database like:

PDOException: SQLSTATE[HY000] [2002] No such file or directory in /var/www/html/test.php:3
Stack trace:
#0 /var/www/html/test.php(3): PDO->__construct('mysql:host=loca...', 'root', 'password')
#1 {main}

With a bit of luck (or common sense), the owner of the site used a password long enough that the error displayed was truncated and didn’t open access to the database for every visitor (well, at least made it more difficult). Of course, the problem does not apply only to databases and errors displayed explicitly to the user - the proposed attribute hides the values in stack traces, and these can also be sent to external services, e.g. #Sentry, which can also anonymise various data. But firstly, it must be properly configured, and secondly, these mechanisms most probably will not work with stack traces. So it’s good to be able to hide sensitive data on the application side.

The new attribute works as follows:

<?php

function test(
    $foo,
    #[\SensitiveParameter] $bar,
    $baz
) {
    throw new \Exception('Error');
}

test('foo', 'bar', 'baz');

/*
Fatal error: Uncaught Exception: Error in test.php:8
Stack trace:
#0 test.php(11): test('foo', Object(SensitiveParameterValue), 'baz')
#1 {main}
  thrown in test.php on line 8
*/

I believe that this is a useful feature that can improve the #security of an application. I don’t quite like the way Object(SensitiveParameterValue) is presented because this form is also used for scalar values, which introduces some discrepancy between the actual type and the one displayed in stack trace. I would prefer something like SensitiveParameter<string>, SensitiveParameter<Foo> etc. so essential information about the passed parameter is not lost. However, it is a detail that does not reduce the usefulness of the attribute itself.

Anyone who has worked with legacy systems (or simply in teams where code quality was not a priority…) knows that dynamically defined object properties are a real nightmare. Sure, modern IDEs can warn us and point out such places, but without the support of tools, these properties are difficult to work with. They can be quite confusing, because when looking at the code in which some properties are used and at the class definition, we can get the impression that the code is referring to non-existent data, while it has just been dynamically declared somewhere else. On the other hand, the fact that PHP implicitly creates new properties means that we are not protected against common typos or mistakes, which, as we know, can happen even to the best 😉

Let me demonstrate example straight from RFC:

class User {
    public $name;
}

$user = new User();

// Assigns declared property User::$name.
$user->name = 'foo';

// Oops, a typo:
$user->nane = 'foo';

This kind of usage up to PHP 8.1 would just create new property, which could lead to several problems (especially in the projects not taking advantage from #static analysis). From version 8.2 E_DEPRECATED warning will be raised, while PHP9 will just throw an Error exception.

At the same time this RFC introduces new #[AllowDynamicProperties] attribute, which allows you to maintain the current behavior (no deprecation warning in 8.2 and no exception in PHP9), so it will be possible to migrate existing project to newer PHP versions without significantly modifying the code. Note, however, that it will not be possible to use the #[AllowDynamicProperties] attribute in classes marked as readonly - such usage will throw an exception.

This kind of language-level enhancements improve code quality and help build good practices. I can only applaud 👏

There is currently a callable meta-type in PHP that can be used to type arguments and function returns. Unfortunately, not all the data accepted by the callable type can actually be called as$callable(), which is very inconsistent and introduces the risk of serious errors. The purpose of this RFC is to remove support for those callables that cannot actually be called - as of PHP 8.2, functions that use callable (e.g. call_user_func()) will trigger an E_DEPRECATED warning informing about the deprecated usage. Ultimately, in version 9.0, such support will be completely removed and the callable type will not accept the following formats:

"self::method"
"parent::method"
"static::method"
["self", "method"]
["parent", "method"]
["static", "method"]
["Foo", "Bar::method"]
[new Foo, "Bar::method"]

Most of theme have their own replacements (details in the RFC), I also suspect that sooner or later there will be rules in #Rector that will enable automatic code refactoring.

I am glad that PHP cleans up these kinds of things, because each change like this improves the quality of the language and increases its stability from the developers’ perspective. More strictness on the language level also means fewer needs in tools like #PHPStan.

The purpose of this RFC is to warn people early that their code will stop working on PHP 9.0. As some callable formats were dropped in the earlier RFC, but some specific usage paths were not taken into account in the context of generating E_DEPRECATED errors, this could lead to a situation where code working correctly on PHP 8.2 and not generating any deprecation warnings about abandoned formats, could stop working in PHP 9.0.

I am glad that the PHP developers care about the language users and have decided to change their previous decision and introduce warnings in these places. Image-wise, it is definitely a good decision, as any potential problems after the 9.0 release would have a negative impact on the perception of PHP.

This change should interest those who use multilingualism in their systems and operate on strings containing characters outside of the standard A-Z range - in PHP 8.2, functions that operate on strings will correctly handle diacritics. This means that strtoupper ('ą') will finally return Ą, not ą as before. The sort functions will also start behaving as expected.

As we know perfectly well, PHP has a long backwards-compatibility tail behind it, which in some way creates a bad image of the language and can be a source of the jokes. In any case, many PHP built-in functions historically have signatures that do not fit with modern technology. For example, strpos() returns a numeric value indicating where the specified string occurs OR just false when the specified string is not present in another string. The problem with this signature is that until version 8.0 it could not be described other than with the @return int|false in phpDoc, which was not really of great value. PHP8 made it possible to use false in the union type, so signatures like strpos(/* ... */): int|false are fully valid. This RFC goes a step further and allows the use of false and null as standalone types.

At first glance, this change is completely useless (who would want to use false as the return type 🤔?). However, when reading the RFC, we can see examples describing covariance and contravariance, and then this change makes sense (a little, but still). Let’s look at an example:

class User {}

interface UserFinder
{
    function findUserByEmail(): User|null;
}

class AlwaysNullUserFinder implements UserFinder
{
    function findUserByEmail(): null
    {
        return null;
    }
}

With changes from this RFC, it is possible to implement the interface and comply with LSP, while limiting the original signature of the return type User|null to only null. In my opinion, this is not something that will find wide adoption - personally, I see a field for showing off in tests or development environments, where some parts of the system will simply be replaced with “empty” implementations. Though maybe I just haven’t encountered scenarios where this change would solve the real problem 😉

There is not much to write about it: in PHP 8.2, true will become a standalone type, just like false and null were accepted before. Like in the previous RFC’s context, I personally do not see any real added value here. I suspect that this has implications in the standard PHP library and may apply to specific open source libraries. However, even if I do not see the value, it does not mean that this change is not needed - it is good to have more room for maneuver.

Currently, PHP has four ways to dynamically inject values into strings: "$foo", "{$foo}", "${foo}" and "${$foo}" (the last case is the so-called variable variable: a dynamic reference to a variable by a value assigned to another variable - quite niche syntax and rather rarely used). This RFC is deprecating the last two methods and support for them will be completely removed in PHP9.

The reason that ${} interpolation was deprecated is that they both had nearly identical syntax but completely different behavior. Although it seems that such conflicts are edge cases, it is good that PHP cleans it up and reduces the amount of supported interpolation syntax. New types of interpolation may be added in the future (such as that suggested in RFC "{$:func()}"), but before that it would be great to standardise what is already available in the language.

This change is so low-level, that most PHP users will never see the difference. However, there is a significant backward incompatibility aspect of this change - mysqlnd does not and will not support automatic reconnection. Libraries or systems that have used this functionality will have to find other ways to achieve this behavior.

The motivation behind this RFC is that the mentioned functions are not well-defined. They have a limited functionality while their name might suggest that they are more universal. In general, there are many encoding formats and this is a complicated topic as a whole, so people often make mistakes when using these functions. They will therefore be marked as deprecated in PHP 8.2 and then removed in PHP 9.0.