Oh Dear is the all-in-one monitoring tool for your entire website. We monitor uptime, SSL certificates, broken links, scheduled tasks and more. You'll get a notifications for us when something's wrong. All that paired with a developer friendly API and kick-ass documentation. O, and you'll also be able to create a public status page under a minute. Start monitoring using our free trial now.

Introducing PHP 8.2: all new features and changes

Original – by Ruben Van Assche – 9 minute read

PHP 8.2 will be released on 8th December 2022, and is a minor release compared to 8.0 and 8.1. This can be partly attributed to the departure of Nikita Popov as one of the most significant contributors to the PHP language. The PHP language has a lot of maintainers and isn't built by one person, but Nikita was one of the most active of them. Features like union types, typed properties, named arguments, and arrow functions, to name a few, were all added by Nikita.

This blog post is a slightly edited excerpt from Front Line PHP, a beautifully designed book by Spatie on how to build modern applications using PHP 8.2. It covers PHP's type system, static analytics, how to use the latest features, and much more!

The news of the departure echoed through the PHP community, and a significant problem had to be acknowledged. Other languages like JavaScript/TypeScript, C#, and Java were backed by large companies with many assets at their disposal. In contrast to the PHP core team, which primarily relies on volunteers and a few small companies to pay some contributors.

The unclear future for the PHP language due to the recent news and a lack of paid core developers was the primary motivation to establish the PHP Foundation in November 2021. Their mission statement was as follows:

The PHP Foundation will be a non-profit organization whose mission is to ensure the long life and prosperity of the PHP language.

The PHP Foundation is backed by companies and individuals in the PHP community and will fund developers working on the PHP core to keep the language getting forward as it was in the past years.

I can proudly say that Spatie is sponsoring the PHP Foundation, and you can too, for as little as a cup of coffee!

The PHP 8.2 release falls right into the first year of the PHP Foundation, and some organizational structures had to be set up. The first developers were selected and eventually hired to work for the foundation during this period. These things take some time, so we consider the PHP 8.2 release one of the more minor releases because major things moved in the PHP core team. Luckily we know this is going to change thanks to the PHP Foundation. The future is looking bright for PHP!

Let's go to a number of improvements included in PHP 8.2.

Dynamic properties

PHP has always had a dynamic nature. The community decided that some of the language's earliest features were too dynamic of nature in the past years. An excellent example of such a dynamic feature are dynamic properties. It is currently allowed to set a property on any object that doesn't exist in the class. For example, this is perfectly valid in PHP:

class User{}

$user = new User();
$user->email = 'info@spatie.be';

Dynamic properties provide an unseen amount of flexibility but also open the door to unexpected behavior and, thus, bugs that are difficult to fix. Let's say you rename the property here. Then you should remember all the other places where this property is being used because it needs to be renamed. Static analysis and an IDE can assist you a bit here. But defining properties in your class will always provide more insight into these tools than dynamically setting them. And you'll probably write a lot fewer bugs too!

Further, reading an undefined property will give you feedback that something went wrong because you'll receive a warning. Whereas writing to an undefined property can be done without any warning. I always want to have a stable type system where I'm sure that properties exist because I strictly define them. Dynamic properties offend this rule, and that's why I'm not using them.

Starting with PHP 8.2, dynamic properties were deprecated. The example above will now throw a deprecation warning. Notice that when you have implemented the magic __get() or __set() methods, getting and setting dynamic properties on an object is still perfectly valid.

Further, you can still enable the functionality of dynamic properties by adding the AllowDynamicProperties attribute to the class:

#[AllowDynamicProperties]
class User{}

$user = new User();
$user->email = 'info@spatie.be';

This attribute is inherited by all the children of the class and is added by default to the stdClass. So extending your class from stdClass will also enable this dynamic behavior:

class User extends stdClass{}

$user = new User();
$user->email = 'info@spatie.be';

Lastly, readonly classes can never have dynamic properties. Adding the AllowDynamicProperties attribute to a readonly class will result in an error.

Deprecations in string interpolation

String interpolation is a nifty feature that allows you to use variables in strings like this:

"Hello there, {$name}"

Did you know you could also do this:

"Hello there, ${name}"

I certainly did not. Though the syntax almost looks the same, it behaves quite differently. Due to these differences in semantics and enough options to use string interpolation, the feature was deprecated with the release of PHP 8.2.

Sensitive parameters

PHP allows you to look at the stack trace and all the parameters associated with each stack frame when something goes wrong. Which is extremely helpful for debugging but can be disastrous for sensitive data. Let's say you have a function like this:

function login(
    string $name,
    string $password
) {
    throw new Exception('Whoops!');
}

The password is now included in the stack trace created by the exception, which means it is available for anyone to see when you've misconfigured your application. When you've configured an external bug tracking service to which you send your stack traces, the password will be sent to the external service with a bunch of debug data, which you want to avoid.

You can easily check this by catching the error and dumping the arguments from the first stack frame:

try {
    login('Freek', 'secret');
} catch (Exception $exception) {
    var_dump($exception->getTrace()[0]['args']);
}

Which will effectively output the following:

array(2) {
  [0]=> string(5) "Freek"
  [1]=> string(6) "secret"
}

In PHP 8.2, a SensitiveParameter attribute was added, replacing the parameter in stack traces. You can use it like this:

function login(
    string $name,
    #[SensitiveParameter]
    string $password
) {
    throw new Exception('Whoops!');
}

Now the output from catching the exception looks like this:

array(2) {
  [0]=> string(5) "Freek"
  [1]=> object(SensitiveParameterValue)#2 (0) {}
}

The PHP developers chose not to replace the parameter with a dubious string like 'hidden', but they created a new DTO, SensitiveParameterValue. This DTO allows you to retrieve the original sensitive value within your code when that would be required. Still, it makes it hard to expose the value accidentally to users of your application or an external service.

Constants in traits

Before PHP 8.2, it was not allowed to add constants to traits. Which was a language discrepancy since traits had access to the constants of the classes where they were being used. But you could strictly not define that a constant would exist in a class, luckily this has been fixed:

trait WithSpatieApi {
    protected const SPATIE_API_VERSION = 1;
}

PCRE no-capture modifier

A small addition to the PHP 8.2 regex extension is the no-capture modifier which will only capture named capture groups. You can add this modifier by appending n to the end of your regex. Let's say you want to capture the abbreviation in these written numbers: 1st, 2nd, 3rd, and so on. You can write a regex like this:

// [0 => '5th',1 => '5', 2 => 'th']
preg_match('/([0-9]+)(st|nd|rd|th)/', '10th', $matches); 

With named capture groups, you can easily name the desired group:

// [0 => '5th', 1 => 5, 'abbreviation' => 'th', 2 => 'th']
preg_match('/([0-9]+)(?P<abbreviation>:st|nd|rd|th)/', '5th', $matches);

Using the no-capture modifier, PHP will only include the named group:

// [0 => '5th', 'abbreviation' => 'th', 1 => 'th']
preg_match('/([0-9]+)(?P<abbreviation>:st|nd|rd|th)/n', '5th', $matches);

Random extension

PHP 8.2 is bundled with a new extension which adds a more OOP-friendly way to work with random number generation and other randomizing operations. A new Randomizer class was added to enable the functionality. You can, for example, shuffle a string like this:

use Random\Randomizer;

$randomizer = new Randomizer();

$randomizer->shuffleBytes('Hello World'); // example output: "eWrdllHoo l"

Or shuffle an array:

$randomizer->shuffleBytes(['a', 'b', 'c', 'd']); // example output: ['b', 'd', 'a', 'c']

Getting a number in a certain interval can be done as such:

$randomizer->getInt(0, 100); // example output: 42

An added benefit is that you can provide a pseudo-random number generator (PRNG) to the randomizer. PHP has several PRNG engines built-in: Mt19937, PcgOneseq128XslRr64, Xoshiro256StarStar, and Secure. Notice that only the last engine is suitable for cryptographic random number generation. This engine model allows more safe/fast engines to be added quickly in the future. You can even create your engine to be used with the randomizer.

You can set the engine for the randomizer as such:

use Random\Engine\Mt19937;
use Random\Randomizer;

$randomizer = new Randomizer(new Mt19937());

Closing, you can now also set the seed of the engine being used for the randomizer. The seed is mostly a random value from which the other random values are derived. When you run this example twice, the number generated will be different every time:

$randomizer = new Randomizer();

$randomizer->getInt(0, 100);

When you set a value for the seed by using the Mt19937 engine, then the randomly generated value will each time be the same:

$randomizer = new Randomizer(new Mt19937(42));

$randomizer->getInt(0, 100);

This behavior can be helpful within a test or local environment, where it will be helpful if restarting a piece of code during debugging will provide the same outcome every time. Even when randomizing operations are being used.

Disjunctive normal form types

Union and intersection types added lots of benefits to the language. With the introduction of disjunctive normal form types (DNF types), you can construct even more complex types. DNF types were added in PHP 8.2, allowing you to make intersection and single types unions. This allows you to add even more granular type-checking to your code.

Let's take a look at the example we had earlier. What if we had an interface like this:

interface WithUrlSegments
{
    public function getUrlSegments(): array;
}

A class Page can now implement this class:

class Page implements WithUrlSegments { /* … */ }

We can now extend the type of our URL function as follows:

function url((WithUuid&WithSlug)|WithUrlSegments $object): string { /* … */ }

The function now allows objects which implement both the WithUuid and WithSlug or objects implementing the single <hljs type WithUrlSegments interface.

The most prominent use case for DNF types will probably be to make intersection types nullable:

function url((WithUuid&WithSlug)|null $object): ?string { /* … */ }

DNF types must adhere to the rules of the DNF form, which means that you can only construct types as a union of intersections and single types. This means that a type like this is not allowed:

interface A {}
interface B {}
interface C {}

function url((A|B)&C $object): string { /* … */ }

It can be rewritten into a union of intersections as follows:

function url((C&A)&(C&B)$object): string { /* … */ }

Readonly classes

Sometimes you want to create a complete immutable DTO which only contains readonly properties. You could, in this case, create a class and define all the properties as readonly:

class CustomerDTO
{
    public function __construct(
        public readonly string $name, 
        public readonly string $email, 
        public readonly DateTimeImmutable $birth_date,
    ) {}
}

Doing this can be quite time-consuming, and every time you add a new property, you should remember to make it readonly. PHP 8.2 introduced a significantly better solution to this problem: readonly classes. With readonly classes, you declare a class as readonly once, and all properties within it will be readonly from that point on:

public readonly class CustomerDTO
{
    public function __construct(
        string $name, 
        string $email, 
        DateTimeImmutable $birth_date,
    ) {}
}

The individual properties of the readonly class behave as readonly properties, so all the behavior we described earlier still applies. Further, it is also impossible to add static properties to a readonly class, resulting in a fatal error.

You can only create a readonly class that extends from a readonly class. This rule implies that it is prohibited to make a readonly class that extends from a non-readonly class and the other way around where you create a non-readonly class that extends from a readonly class. Both these cases will result in a fatal error.

Improvements to enums

When enums were introduced, it was impossible to fetch the name and value properties from an enum in a constant expression. We've summed up some cases of constant expressions where this was the case:

class Post
{
    #[DefaultValue(Status::Draft->name)]
    public string $status = Status::Draft->name;

    public function updateStatus(string $status = Status::Draft->name): void
    {
        /** … */
    }
}

const STATUS = Status::Draft->name;

As from PHP 8.2, all these syntaxes are valid and will not throw errors anymore.

Deprecations in partially supported callable syntax

PHP 8.2 will deprecate some callable syntaxes because they are inconsistent:

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

These syntaxes could be used with the callable type and the functions is_callable() and call_user_func(). Oddly, these syntaxes could be used in some cases but not with the $callable() syntax like this:

class Manager
{
    public function execute(string $method)
    {
        $callable = "self::{$method}";

        $callable();
    }

    private function destruct(){ /** ... */ }
}

Further, these syntaxes are context-dependent. The types behind self and static can change depending on the location from where they were called, which could result in unexpected behavior when they call private methods.

That's why they've been deprecated from being used with the callable type and is_callable() and call_user_func() functions. Updating the example above can be quickly done as such:

class Manager
{
    public function execute(string $method)
    {
        $callable = self::class . "::{$method}";

        $callable();
    }

    private function destruct(){ /** ... */ }
}

Other callable syntaxes like these are still valid to be used:

'function'
['SomeClass', 'someMethod']
'SomeClass:: someMethod']
[new SomeClass(), 'someMethod']
[$this, 'someMethod']
Closure::fromCallable()
function(...)

A function to reset the memory peak usage

In PHP, it has been possible to measure peak memory usage. It was previously never possible to reset this peak, so measuring a second peak after a first one was impossible.

For example, when you create two arrays and want to measure the memory peak usage for the second array, the value will be equal to the memory usage peak from the creation of the first array:

range(0, 100_000);

memory_get_peak_usage(); // 2509760

range(0, 100);

memory_get_peak_usage(); // 2509760

The memory_reset_peak_usage function was added in PHP 8.2 allows you to reset this peak so you can measure it again:

range(0, 100_000);

memory_get_peak_usage(); // 2509760

memory_reset_peak_usage();

range(0, 100);

memory_get_peak_usage(); // 398792

The mysqli::execute_query method

PHP 8.2 adds a new method to the mysqli extension allowing you to prepare, bind parameters, and execute the SQL statement in one method. In the past, a lot of operations were required to perform a query:

$query = 'SELECT * FROM posts WHERE id = ?';
$statement = $connection->prepare($query);
$statement->bind_param('i', $id);
$statement->execute();
$result = $statement->get_result();

This code can now be rewritten as such:

$result = $mysqli->execute_query('SELECT * FROM posts WHERE id = ?', [$id]);

In closing

Even though PHP 8.2 is a smaller release than usual, it still packs a few niceties and bugfixes. To learn more about modern PHP check out Front Line PHP, a beautifully designed book on how to build modern applications using PHP 8.2. It covers PHP's type system, static analysis, how to use the latestest features, and much more.

We've also created a handy cheat sheet that showcases all modern PHP features.

If you are a visual learner, you can opt to watch this video on new PHP 8.2 by JetBrains.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every month I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

avatar

Thank you, I think there's a typo in the code:

function url((C&A)&(C&B)$object): string { /* … */ }

correction:

function url((C&A)|(C&B)$object): string { /* … */ }


also remove public from public readonly class CustomerDTO

Comments powered by Laravel Comments
Want to join the conversation? Log in or create an account to post a comment.