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:
- The
Epoch doesn't fit in a PHP integer
now returns a newDateRangeError
instead of a genericValueError
, which it does not subclass. This is only an issue for 32-bit platforms. - The
Only non-special relative time specifications are supported for subtraction
warning withDateTime::sub()
anddate_sub()
becomes a newDateInvalidOperationException
. - The
Unknown or bad format (%s) at position %d (%c): %s
andString '%s' contains non-relative elements
warnings that are created while parsing wrong/brokenDateInterval
strings will now throw a newDateMalformedIntervalStringException
when used with the OO interface, instead of showing a warning and returning false.
# 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:
- A
TypeError
is now thrown when passing objects, resources, or arrays as the boundary inputs - A more descriptive
ValueError
is thrown when passing 0 for$step
- A
ValueError
is now thrown when using a negative$step
for increasing ranges - If
$step
is a float that can be interpreted as an int, it is now done so - A
ValueError
is now thrown if any argument isinfinity
orNAN
- An
E_WARNING
is now emitted if$start
or$end
is the empty string. The value continues to be cast to the value 0. - An
E_WARNING
is now emitted if$start
or$end
has more than one byte, only if it is a non-numeric string. - An
E_WARNING
is now emitted if$start
or$end
is cast to an integer because the other boundary input is a number. (e.g.range(5, 'z');
) - An
E_WARNING
is now emitted if $step is a float when trying to generate a range of characters, except if both boundary inputs are numeric strings (e.g.range('5', '9', 0.5);
does not produce a warning) -
range()
now produce a list of characters if one of the boundary inputs is a string digit instead of casting the other input to int (e.g.range('5', 'z');
)
# 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 ofstr_pad()
. Thestr_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 callmb_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:
- Deprecate passing negative
$widths
tomb_strimwidth()
- Deprecate and remove the
NumberFormatter::TYPE_CURRENCY
constant - Deprecate and remove the broken pre-PHP 7.1 Mt19937 implementation (
MT_RAND_PHP
) - Deprecate and remove calling
ldap_connect()
with 2 parameters$host
and$port
- Deprecate remains of string evaluated code assertions
# 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.
- When using FFI, C functions that have a return type of
void
now returnnull
instead of returningFFI\CData:void
-
posix_getrlimit()
now takes an optional$res
parameter to allow fetching a single resource limit. -
gc_status()
has four new fields:running
,protected
,full
, andbuffer_size
. -
class_alias()
now supports creating an alias of an internal class. -
mysqli_poll()
now raises aValueError
when the read nor error arguments are passed. -
array_pad()
is now only limited by the maximum number of elements an array can have. Before, it was only possible to add at most 1048576 elements at a time. - New posix functions:
posix_sysconf()
,posix_pathconf()
,posix_fpathconf()
, andposix_eaccess()
- Executing
proc_get_status()
multiple times will now always return the right value on posix systems. -
opcache.consistency_checks
ini directive was removed -
Improved
array_sum()
andarray_product()
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.