PHP 8.2 – The Release of Deprecations

The release date for PHP 8.2 has been announced, with General Availability set for the 24th November 2022; the release managers have been elected with Ben Ramsey (@ramsey) as the “veteran” supporting Pierrick Charron (@adoyy) and Sergey Panteleev (@s_panteleev) as the two “rookies”; and the first alpha release is just three weeks away. So now it’s time to start looking ahead at what this new release will bring.

And the honest answer is, very little.

So far we can look forward to three new features (one significant, the other two are relatively minor), an element of new syntactic sugar, and a mass of deprecations.

Probably the most contentious of the deprecations – and one that will likely trigger a slew of deprecation notices in the logs of existing codebases – is deprecating dynamic properties in a class, unless that class explicitly permits their creation. The intent is to reduce typing errors in a case such as:


class User {
    public function __construct(
        protected string $name
    ) {}
    
    public function updateName(string $name): void {
        $this->ame = $name;
    }
}

$user = new User('Mark');
$user->updateName('Mark Baker');

where we’ve mistyped the property name that we’re modifying in the updateName() method. Currently PHP won’t inform us of this, but will dynamically create a new property called ame in this instance of the class. Using a good IDE should have identified this potential problem before we even tried running the code, and unit tests should also identify such errors.

In this case, PHP 8.2 will issue a deprecation notice at runtime when the undefined property is set, although as this is only a depreaction it will still create the property: it’s simply letting us know about it


Deprecated: Creation of dynamic property User::$ame is deprecated

If we do still want to provide functionality that uses dynamic properties within a class, to suppress that deprecation notice, then we need to tell PHP explicitly that this is the case through the use of an attribute:


#[AllowDynamicProperties]
class User {
    ...
}

Note that this doesn’t apply to stdClass, or to properties defined through the magic __set() method.

With the mass of deprecations that we have had in PHP 8.1 and these further deprecations in 8.2, personally I think it shouldn’t be necessary to enforce this in the code itself yet, because there are valid use cases where we do want to create new properties dynamically within the code. I can see the benefits looking forward to PHP 9.0; but I think this behaviour should have been reversed, so classes would initially “opt-in” to this deprecation rather than “opt-out”, and then reverse the “opt-in”/”opt-out” in release 8.3 or 8.4, giving library and tool maintainers some breathing space to handle the change before being swamped with “issues” from early 8.2 adopters who still don’t recognise that a deprecation notice is not an error, and don’t search to see that their “issue” has already been raised a dozen times.


Another deprecation that concerns me is that of partially supported callables. PHP 8.1 introduced first class callable syntax, a feature that I think is excellent. The new proposal in 8.2 was to deprecate the following callables:


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

that are accepted by the callable type, the is_callable() function and call_user_func()/call_user_func_array(), but are not supported by $callable(), and the changes to resolve this are fairly straightforward to implement, so I have no problems with that. The problem lies in the fact that is_callable() was explicitly ignored in the RFC.
Consider the following code:


class Foo {
    public function bar() {
        // Do something conditionally if a child class has implemented a certain method.
        if (is_callable('static::methodName')) {
            static::methodName();
        }
    }
}

This pattern is commonplace in systems that allow callbacks to be registered dynamically for event listeners, but where the basic state is that the callback doesn’t exist.

If methodName hasn’t been implemented in the child class, then this won’t issue any deprecation notice: it will only do so if the method has been defined, and then only when it actually calls that method. In PHP 9.0, that behaviour will change: is_callable() will still return a true if the method exists, but the actual method call will then fail. Where test suites may not have 100% coverage, or may not always test with invalid callbacks, there will be no deprecation notices to alert developers that there is an issue, and the problem will not be discovered until PHP 9.0 when it suddenly starts failing.

As with many of the deprecation RFCs that are being accepted, no impact assessment was made on how this change would affect userland code, particularly tools and libraries, otherwise it should have recognised that the above example is a fairly common code pattern. Fortunately, Juliette Reinders Folmer (who has probably studied more userland code than anybody I know, and whose comments about the userland impact of core changes should always be taken seriously) recognised this problem, conducted her own impact assessment, and has submitted a new RFC to remedy the issue by adding the deprecation notice to is_callable() and when type verification is executed on the callable type.

I just hope that her RFC is accepted when it comes to the vote; but I think that sanity will prevail, and it will be accepted.

Update: Juliette’s RFC is currently in voting, and the initial outlook that it will pass is good; and Rowan Tommins has offered to prepare a patch to implement the proposal.


The syntactic sugar is read-only classes. It’s already possible to implement this since PHP 8.1 by defining all properties in the class as readonly, whether they are explicitly defined, or defined through constructor property promotion (and perhaps also setting the class to final). I’ve written about readonly properties before here and here.


class User {
    public function __construct(
        public readonly string $id,
        public readonly string $name,
        public readonly string $emailAddress
    ) {}
}

With the new syntactic sugar of readonly classes, we define the class itself as readonly, and then every defined property automatically becomes readonly: it saves us from having to specify readonly for every property, so we only have to type it once for the class.


readonly class User {
    public function __construct(
        public string $id,
        public string $name,
        public string $emailAddress
    ) {}
}

But there is slightly more to this: readonly classes can only be extended by other readonly classes.

In our first PHP 8.1 example with the readonly properties, the following extension would be valid:


class Administrator extends User {
}

but it would throw an exception if we tried to extend our readonly User class the same way: if we want to extend readonly User as an Administrator we need to make Administrator a readonly class as well.


readonly class Administrator extends User {
}

Nor can we extend a non-readonly class with a readonly class.
If we have a class that we defined for PHP 8.1 with every property set to readonly such as our original User class, we still can’t extend it with a new PHP 8.2 readonly Administrator class; we’d have to convert our 8.1 User to an 8.2 readonly User class first.
So if you’re already using PHP 8.1 and have started creating your own readonly classes by defining every property as readonly, you’ll probably want to convert them to real readonly classes for PHP 8.2. Hopefully, somebody will have written a rector to handle that conversion before then.

One other point worth noting is that readonly classes forbid dynamic properties (as per the deprecation of dynamic properties described above). Readonly classes don’t simply deprecate, they actually forbid dynamic properties (it will already result in a fatal error), and trying to change this behaviour with the #[AllowDynamicProperties] attribute will also result in a fatal error.


The first real new feature of PHP 8.2 is the ability to use null and false as standalone type-hints; although it’s one that most developers are unlikely ever to use.
We can now declare a method argument that will only accept a value of false (or of null):


function foo(false $falseValue) {
    var_dump($falseValue);
}

foo(true);

and if we try to pass any argument value other than a false to this function, then we’ll get a TypeError:


Fatal error: Uncaught TypeError: foo(): Argument #1 ($falseValue) must be of type false, bool given

Although I can’t come up with a valid use-case where we’d want to restrict a function or method argument to a single false (or null) value; surely the whole point of method arguments is to allow different values to be passed to a method, even if we do apply type restrictions.
This might have more value as a return typehint


function foo(): false {
    return null;
}

Fatal error: Uncaught TypeError: foo(): Return value must be of type false, null returned

But even then, I’m not totally convinced. I can understand using false as part of a union type


function getLastInSet(string $set, string $delimiter): string|false {
    $splitSet = explode($delimiter, $set);
    if (count($splitSet) <= 1) {
        return false;
    }
    
    return array_pop($splitSet);
}

but that has been an option since PHP 8.0

I can actually see a couple of use-cases for true as a standalone value:


class User {
    function isAdmin(): bool
}
 
class Admin extends User
{
    function isAdmin(): true {
        return true;
    }
}

or


function foo(): true {
    // Do something complicated, and throw an exception if it fails
    
    return true;
}

which means that we can return a true on success (which is sensible response for a success). That is good coding practise, because we shouldn’t be relying on Exceptions to control the flow of logic in our caller, but we can use the true return for controlling program flow.
However, the accepted RFC doesn’t allow true as a standalone type. There is a separate RFC for true as a standalone type, which is still “under discussion”; but it doesn’t seem to be going anywhere at the moment.

Update: George Peter Banyard’s RFC for true as a standalone type is currently in voting, and the initial outlook that it will pass is good.


Another relatively minor change is Locale-independent case conversion. The big change here occurred in PHP 8.0, when PHP stopped using the locale setting for functions like strtolower(), stripos() and array_change_key_case(), so this is more of cleanup and rationalisation of the internal PHP code. These basic functions will always assume ASCII unless an explicit call has been made to setlocale()… and calling setlocale() should generally be avoided because it isn’t thread safe.
If you do need access to locale-aware versions of these functions, then you should already be using the intl and/or mbstring extensions, or a polyfill library like symfony/polyfill-mbstring.


The most significant (and useful) new feature of PHP 8.2 is the ability to redact values in backtraces.

So what does this RFC actually do? Let’s look at a (very dirty) example:


class User {
    public string $id;
    public string $name;
    public string $emailAddress;

    public function __construct(
        string $id,
        string $name,
        string $emailAddress,
        string $password
    ) {
        $this->id = $id;
        $this->name = $name;
        $this->emailAddress = $emailAddress;
        if ($password !== 'mySecretPassword') {
            throw new Exception('Invalid Login Credentials');
        };
    }
}

$admin = new User(1, 'Mark', 'admin@domain.net', 'mySecretWord');

And when we trigger that exception:


Fatal error: Uncaught Exception: Invalid Login Credentials
Stack trace:
#0 /in/ZoUnt(24): User->__construct('1', 'Mark', 'admin@domain.ne...', 'mySecretWord')

So argument values that we might consider confidential and that shouldn’t be revealed are clearly visible in the stack trace.

What this RFC gives us is an attribute that we can use to suppress that value from appearing in the stack trace:


class User {
    public string $id;
    public string $name;
    public string $emailAddress;

    public function __construct(
        string $id,
        string $name,
        string $emailAddress,
        #[\SensitiveParameter] string $password
    ) {
        $this->id = $id;
        $this->name = $name;
        $this->emailAddress = $emailAddress;
        if ($password !== 'mySecretPassword') {
            throw new Exception('Invalid Login Credentials');
        };
    }
}

$admin = new User(1, 'Mark', 'admin@domain.net', 'mySecretWord');

and the resulting output now:


Fatal error: Uncaught Exception: Invalid Login Credentials in /in/BR7ca:18
Stack trace:
#0 /in/BR7ca(24): User->__construct('1', 'Mark', 'admin@domain.ne...', Object(SensitiveParameterValue))

So the secret is no longer displayed in the stack trace.

Potentially invaluable for security, especially if this was a stack trace containing credentials for a remote API call or similar, rather than my contrived example.

But there are limitations, let’s see what happens when use a nice PHP 8 feature like constructor property promotion:


class User {
    public function __construct(
        public string $id,
        public string $name,
        public string $emailAddress,
        #[\SensitiveParameter] private string $password
    ) {
        if ($password !== 'mySecretPassword') {
            throw new Exception('Invalid Login Credentials');
        };
    }
}

$admin = new User(1, 'Mark', 'admin@domain.net', 'mySecretWord');

Fatal error: Attribute "SensitiveParameter" cannot target property (allowed targets: parameter)

So we can’t use it in combination with constructor property promotion, not if we wanted that credential to be stored (albeit temporarily) as part of the instance.


And finally we come to yet another deprecation: Deprecations to ${} string interpolation

This RFC looks at the variation in syntax that allow interpolation in double quoted or heredoc strings:


  1 - Directly embedding variables ("$foo")
  2 - Braces outside the variable ("{$foo}")
  3 - Braces after the dollar sign ("${foo}")
  4 - Variable variables ("${expr}", equivalent to (string) ${expr})

and deprecates options #3 and #4

The most interesting aspect of the RFC itself is its examination of what is and isn’t permitted by each option, or the different syntax required within the expression between the options. For example, to access an associative array element using option #1, the syntax is "$foo[bar]", but using option #2 we have to wrap the element index in single quotes "$foo['bar']"; and only option #2 supports calling object methods "{$foo->bar()}" or compound interpolation, or “chaining” "{$foo['bar']->baz()}"
Note that using variable variables ("$$foo") will still work.

I’m not going to complain overmuch at this deprecation, and it a straightforward matter to change syntax #3 to syntax #2. What I would like to see going forward is more consistency in syntax, and also the ability to interpolate static properties and class constants, which would seem a more positive improvement than this deprecation.


class foo {
    public const GREETING = 'Hello';
    public static string $recipient;
    
    public function __construct(string $recipient) {
        self::$recipient = $recipient;
    }
    
    public function sayIt() {
        echo "{self::GREETING} {self::$recipient}";
    }
}

$obj = new foo('World');
$obj->sayIt();

Warning: Undefined variable $recipient in /in/eCT0H on line 12
{self::GREETING} {self::}

without having to extract them to variables first:


class foo {
    public const GREETING = 'Hello';
    public static string $recipient;
    
    public function __construct(string $recipient) {
        self::$recipient = $recipient;
    }
    
    public function sayIt() {
        $greeting = self::GREETING;
        $recipient = self::$recipient;
        echo "{$greeting} {$recipient}";
    }
}

Hello World

otherwise, I might as well use sprintf() instead:


class foo {
    public const GREETING = 'Hello';
    public static string $recipient;
    
    public function __construct(string $recipient) {
        self::$recipient = $recipient;
    }
    
    public function sayIt() {
        echo sprintf('%s %s', self::GREETING, self::$recipient);
    }
}

What these changes particularly highlight for me is the fact that impact assessment isn’t required for an RFC when it should be. Internals make these changes to the language (particularly deprecations) without consideration of the affect that they will have on userland code, especially in libraries, frameworks and developer tools; and it feels as though anybody questioning their wisdom is brow-beaten with calls of “it’s only a deprecation notice”, “it should be identified in unit testing”, “that pattern is just bad coding practise”, or that we’re scaremongering. But the real-world is never that simple; PHP developers don’t always differentiate between a deprecation notice and an error appearing in their logs; libraries don’t always have 100% code coverage (or more importantly N-Path coverage); not all code in the real-world was written by purists, but by people that needed to get a task done. As for scaremongering: warning developers about a forthcoming deprecation, and that it requires work from them to eliminate the deprecation warnings, shouldn’t be considered scaremongering. It’s better to warn about the amount of work that’s expected than simply to pretend that there will be no issues, or that it’s simply a 2-minute code change.

What I do feel (particularly with regard to deprecations) is that end-user developers expect all their libraries, their frameworks, and toolchain components to be full ready on the release day, to have resolved all deprecations so that they won’t appear in logs… and that’s a big ask. It puts a lot of pressure on the maintainers of those libraries in the few weeks lead up to release day. Internals don’t seem to care about the extra workload that this can entail, and end-users aren’t always tolerant if every dependency in their composer.json isn’t perfect for them; and maintainers are caught in the middle.

So please Internals, listen to userland experts when they do raise potential issues about the impact of changes (particularly deprecations); and library/tool/framework users, please be patient with maintainers if you start getting deprecation notices in your logs when you upgrade.

This entry was posted in PHP and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s