Timeline Taxi Out now: my sci-fi novel Timeline Taxi is published!

What's new in PHP 8.3

PHP 8.3 is released on November 23, 2023; it has improvements to readonly classes, the new json_validate() function, additions to the recently added Randomizer class, stack overflow detection, and more.

In this post, we'll go through all features, performance improvements, changes and deprecations one by one. If you want to stay up to date, you can subscribe to my newsletter, follow me on Twitter, or subscribe to my RSS feed.

# Readonly amendments RFC

This RFC proposed two changes, only one was accepted: being able to reinitialize readonly properties while cloning. That might sound like a very big deal, but this RFC only addresses a very specific (but important) edge case: overwriting property values within __clone(), in order to allow deep cloning readonly properties.

readonly class Post
{
    public function __construct(
        public DateTime $createdAt,
    ) {}
    
    public function __clone()
    {
        $this->createdAt = new DateTime(); 
        // This is allowed,
        // even though `createdAt` is a readonly property.
    }
}

You can read an in-depth post about this RFC and some sidenotes here.


# Typed class constants RFC

You can now typehint class constants:

class Foo
{
    const string BAR = 'baz'; 
} 

# #[Override] attribute RFC

The new #[Override] attribute is used to show a programmer's intent. It basically says "I know this method is overriding a parent method. If that would ever change, please let me know".

Here's an example:

abstract class Parent
{
    public function methodWithDefaultImplementation(): int
    {
        return 1;
    }
}

final class Child extends Parent
{
    #[Override]
    public function methodWithDefaultImplementation(): int
    {
        return 2; // The overridden method
    }
} 

Now, let's imagine at one point the parent method changes its method name:

abstract class Parent
{
    public function methodWithNewImplementation(): int
    {
        return 1;
    }
}

Thanks to the #[Override] attribute, PHP will be able to detect that Child::methodWithDefaultImplementation() doesn't override anything anymore, and it will throw an error.

You can read more about the #[Override] attribute here.


# Negative indices in arrays breaking

If you have an empty array, add an item with a negative index, and then add another item, that second item would always start at index 0:

$array = [];

$array[-5] = 'a';
$array[] = 'b';

var_export($array);

//array (
//  -5 => 'a',
//  0 => 'b',
//)

Starting from PHP 8.3, the next item will be added at index -4:

//array (
//  -5 => 'a',
//  -4 => 'b',
//)

# Anonymous readonly classes UPGRADING

Previously, you weren't able to mark anonymous classes as readonly. That's fixed in PHP 8.3:

$class = new readonly class {
    public function __construct(
        public string $foo = 'bar',
    ) {}
};

# The new json_validate() function RFC

Previously, the only way to validate whether a string was valid JSON, was to decode it and detect whether any errors were thrown. This new json_validate() function is beneficial if you only need to know whether the input is valid JSON, since it uses less memory compared to decoding the string.

json_validate(string $json, int $depth = 512, int $flags = 0): bool

# Randomizer additions RFC

PHP 8.2 added the new Randomizer class. This update brings some small additions:

Randomizer::getBytesFromString(string $string, int $length): string

This method allows you to generate a string with a given length that consists of randomly selected bytes from a given string.

Randomizer::getFloat(
    float $min,
    float $max,
    IntervalBoundary $boundary = IntervalBoundary::ClosedOpen
): float

getFloat() returns a float between $min and $max. You can define whether $min and $max should be included thanks to the IntervalBoundary enum. Closed means the value is included, while Open means excluded.

Randomizer::nextFloat(): float {}

Finally, nextFloat() is a shorthand for getFloat(0, 1, IntervalBoundary::ClosedOpen), in other words: it'll give you a random float between 0 and 1, where 1 is excluded.


# Dynamic class constant fetch RFC

PHP 8.3 allows you to fetch constants with a more dynamic syntax:

class Foo 
{
    const BAR = 'bar';
}

$name = 'BAR';
 
// Instead of this:
constant(Foo::class . '::' . $name);

// You can now do this:
Foo::{$name};

# More Appropriate Date/Time Exceptions RFC breaking

In many cases, PHP would simply throw an Exception or Error object; or emit a warning or error when something went wrong in dealing with dates and times. This RFC goes through all those edge cases and adds proper, dedicated exceptions for them.

We now have exceptions like DateMalformedIntervalStringException, DateInvalidOperationException, and DateRangeError.

In general, these additions won't break any code, since these newly added exceptions and errors subclass the generic Exception and Error classes. However, there are three small breaking changes that come with this RFC:


# Improved unserialize() error handling RFC

unserialize() will now always emit a E_WARNING when running into problems instead of sometimes an E_NOTICE.

This RFC also proposed adding more exceptions when running unserialize(), but that part didn't get accepted.


# Changes to the range() function breaking

From the changelog:


# Traits and static properties breaking

From the changelog:

Uses of traits with static properties will now redeclare static properties inherited from the parent class. This will create a separate static property storage for the current class. This is analogous to adding the static property to the class directly without traits.


# Stack overflow detection PR

PHP 8.3 adds two new ini directives called zend.max_allowed_stack_size and zend.reserved_stack_size. Programs that are close to overflowing the call stack may now throw an Error when using more than the difference between zend.max_allowed_stack_size and zend.reserved_stack_size.

The benefit of this feature is that stack-overflow-induced segmentation faults won't result in segfaults anymore, making debugging a lot easier.

The default for zend.max_allowed_stack_size is 0, meaning PHP will automatically determine a value. You can also provide -1 to indicate there isn't a limit, or a specific number of bytes. The zend.reserved_stack_size directive is used to determine the "buffer zone", so that PHP is able to still throw an error instead of actually running out of memory. The value here should be a number of bytes, but PHP will determine a reasonable default for you, so you don't necessarily need to set it, unless you're running into edge cases for specific programs.

On a final note, for fibers, the existing fiber.stack_size directive is used as the max allowed stack size.

zend.max_allowed_stack_size=128K

# New mb_str_pad function RFC

From the RFC:

In PHP, various string functions are available in two variants: one for byte strings and another for multibyte strings. However, a notable absence among the multibyte string functions is a mbstring equivalent of str_pad(). The str_pad() function lacks multibyte character support, causing issues when working with languages that utilize multibyte encodings like UTF-8. This RFC proposes the addition of such a function to PHP, which we will call mb_str_pad().

The function looks like this:

function mb_str_pad(
    string $string, 
    int $length, 
    string $pad_string = " ", 
    int $pad_type = STR_PAD_RIGHT, 
    ?string $encoding = null,
): string {}

# Magic method closures and named arguments PR

Let's say you have a class that supports magic methods:

class Test {
    public function __call($name, $args) 
    {
        var_dump($name, $args);
    }
    
    public static function __callStatic($name, $args) {
        var_dump($name, $args);
    }
}

PHP 8.3 allows you to create closures from those methods, and then pass named arguments to those closures. That wasn't possible before.

$test = new Test();

$closure = $test->magic(...);

$closure(a: 'hello', b: 'world'); 

# Invariant constant visibility breaking

Previously, visibility for constants weren't checked when implementing an interface. PHP 8.3 fixes this bug, but it might lead to code breaking in some places if you weren't aware of this behaviour.

interface I {
    public const FOO = 'foo';
}

class C implements I {
    private const FOO = 'foo';
}

# The small deprecations RFC rfc

As is usual with every release, there's a single RFC that adds a bunch of small deprecations. Keep in mind that deprecations are no errors, and these are generally a good thing for the language to move forward. These are the deprecations that passed, you can read more details about them in the RFC:


# Small, but notable changes

Not every change in PHP passes the RFC process. In fact, the majority of changes include maintenance and bugfixing, and don't require an RFC. All of these changes are listed in the UPGRADING document. I'll list some of the most prominent ones, but you should definitely read throughout the whole list if you want to know about the tiniest details.


That's it for now, but this list will grow over time. If you want to stay up to date, you can subscribe to my newsletter, follow me on Twitter, or subscribe to my RSS feed.