PHP 8.3 was released on schedule on November 23 and packs many new features and improvements since the launch of PHP 8.2. Even though it’s officially considered a minor release, some of the changes in 8.3 might directly affect your work with PHP — perhaps helping you code faster and with fewer bugs.

Let’s dive in and look at the big — and sometimes not-so-big — changes that come with this latest release.

New Features and Improvements in PHP 8.3

Let’s start by exploring the PHP 8.3 features that grab most of the headlines.

Typed Class Constants

The ability to declare types for class properties has been available to us since PHP 7.4. However, despite numerous tweaks to PHP typing over the years, it hasn’t extended to constants — until now.

Class constants — and that also includes interface, trait, and enum constants — can be typed in PHP 8.3, making it less likely that developers will stray from the intention behind a constant’s initial declaration.

Here’s a basic example using an interface:

// Legal:
interface ConstTest {
    // Declared type and value are both strings
    const string VERSION = "PHP 8.3";
}

// Illegal:
interface ConstTest {
    // Type and value mismatch in this initial declaration
    const float VERSION = "PHP 8.3";
}

The real value of those typed class constants is revealed when working in classes derived from the base declarations. While a child class can frequently assign a new value to a constant, PHP 8.3 can help prevent accidentally changing its type so that it becomes incompatible with the initial declaration:

class ConstTest {
    const string VERSION = "PHP 8.2";
}

class MyConstTest extends ConstTest {

    // Legal:
    // It's OK to change the value of VERSION here
    const string VERSION = "PHP 8.3";

    // Illegal:
    // Type must be declared if it was specified in the base class
    const VERSION = "PHP 8.3";

    // Illegal:
    // In this case, we can't change the type declared in the 
    // base class, even if the new type and its value are compatible.
    const float VERSION = 8.3;
}

Keep in mind that the type assigned to a class constant can vary when “narrowing” multiple types or using an otherwise compatible type:

class ConstTest {
    const string|float VERSION = "PHP 8.2";
}

class MyConstTest extends ConstTest {

    // Legal:
    // Here, it's OK to narrow the type declaration to string or float
    const string VERSION = "PHP 8.3";
    const float VERSION = 8.3;

    // Legal:
    // Value could be an int, but it's compatible with float 
    const float VERSION = 8;

    // Illegal:
    // We can't widen the type options here to include int
    const string|float|int VERSION = 8;
}

Two types supported for other properties when validating return values — void and never — are not supported as class constant types.

A New json_validate() Function

When working with JSON-encoded data, it’s nice to know if the payload is syntactically valid before attempting to do something with it.

In previous releases of PHP, developers have used the json_decode() function and checked for errors while that function attempts to turn JSON data into associative arrays or objects. PHP 8.3’s new json_validate() function does the error checking without using all the memory required to build those array or object structures.

So, in the past, you might have validated a JSON payload something like this:

$obj = json_decode($maybeJSON);

if (json_last_error() === JSON_ERROR_NONE) {
    // Probably do something with $obj   
}

If you aren’t going to do something right away with $obj in the example above, that’s a lot of resources used just to confirm the validity of the original JSON payload. In PHP 8.3, you can do something like this and spare some memory:

if (json_validate($maybeJSON)) {
    // Do something with $maybeJSON   
}

Note: It doesn’t make a lot of sense to use json_validate() and then immediately run the data through json_decode(), using decode’s memory resources anyway. You are more likely to use the new function to validate the JSON before storing it somewhere or delivering it as a request response.

Deep Cloning of readonly Properties

The ability to declare individual class properties as readonly appeared in PHP 8.1. PHP 8.2 introduced the ability to assign that attribute to an entire class. However, many developers felt the constraints imposed when working with classes containing such properties got in the way of useful programming.

An RFC for modifying readonly behavior made two proposals:

  1. Allow classes that are not readonly to extend classes that are
  2. Allow readonly properties to be reinitialized when cloning

It’s the second proposal that has made it into PHP 8.3. The new approach allows instances of a class with readonly properties to be reinitialized within the __clone magic method (including via functions invoked from within __clone).

This code example from the RFC shows how it works:

class Foo {
    public function __construct(
        public readonly DateTime $bar,
        public readonly DateTime $baz
    ) {}
 
    public function __clone() {
        // $bar will get a new DateTime when clone is invoked
        $this->bar = clone $this->bar; 

        // And this function will be called
        $this->cloneBaz();
    }
 
    private function cloneBaz() {
       // This is legal when called from within __clone
        unset($this->baz); 
    }
}
 
$foo = new Foo(new DateTime(), new DateTime());
$foo2 = clone $foo;

New #[\Override] Attribute

When implementing interfaces in PHP, programmers provide detailed functionality for methods named in those interfaces. When creating an instance of a class, programmers can override a parent method by creating an alternate version with the same name and a compatible signature in the child.

One problem is that programmers might think they are implementing an interface method or overriding a parent method when they are not. They might be creating an entirely separate beast because of a typo in the name of the child-class method or because methods have been removed or renamed in the parent code.

PHP 8.3 introduces the #[\Override] attribute to help programmers make it clear that a method must have some lineage within the code.

Here’s a basic example:

class A {
    protected function ovrTest(): void {}
}

// This will work because ovrTest() 
// can be found in the parent class
class B extends A {
    #[\Override]
    public function ovrTest(): void {}
}

// This will fail because ovrBest() 
// (probably a typo) is not in the parent
class C extends A {
    #[\Override]
    public function ovrBest(): void {}
}

Dynamic Fetching of Class Constants and Enum Members

Unlike with other properties in PHP code, fetching class constants and Enum members with variable names has been a little convoluted. Before PHP 8.3, you might have done that using the constant() function like this:

class MyClass {
    public const THE_CONST = 9;
}

enum MyEnum: int {
    case FirstMember = 9;
    case SecondMember = 10;
}

$constantName = 'THE_CONST';
$memberName = 'FirstMember';

echo constant('MyClass::' . $constantName);
echo constant('MyEnum::' . $memberName)->value;

Now, using the same class and Enum definitions above, you can achieve the same result with PHP 8.3’s dynamic fetching of constants like this:

$constantName = 'THE_CONST';
$memberName = 'FirstMember';

echo MyClass::{$constantName};
echo MyEnum::{$memberName}->value;

New getBytesFromString() Method

Have you ever wanted to generate random strings using a pre-approved collection of characters? You can do it easily now with the getBytesFromString() method that has been added to the Random extension in PHP 8.3.

This new method is simple: you pass it a string of characters as source material and specify how many of them you want to use. The method will then select bytes from the string at random until it reaches that specified length.

Here’s a simple example:

$rando = new Random\Randomizer();
$alpha = 'ABCDEFGHJKMNPQRSTVWXYZ';

$rando->getBytesFromString($alpha, 6); //  "MBXGWL"
$rando->getBytesFromString($alpha, 6); //  "LESPMG"
$rando->getBytesFromString($alpha, 6); //  "NVHWXC"

It’s possible for the requested length of the random output to have more bytes than the input string:

$rando = new Random\Randomizer();
$nums = '123456';

$rando->getBytesFromString($nums, 10); //  "2526341615"

With an input string of unique characters, each has an equal chance of being selected for the random result. However, characters can be weighted by having them appear more often than others in the input. For example:

$rando = new Random\Randomizer();
$weighted = 'AAAAA12345';

$rando->getBytesFromString($weighted, 5); //  "1AA53"
$rando->getBytesFromString($weighted, 10); //  "42A5A1AA3A"

New getFloat() and nextFloat() Methods

Also expanding on the Random extension, PHP 8.3 introduces two new methods to generate random float values: getFloat() and nextFloat().

Here’s one example:

$rando = new Random\Randomizer();

// Generate a float value between a minimum 
//  value of 0 and a maximum value of 5
$rando->getFloat(0,5); // 2.3937446906217

The getFloat() method also accepts a third parameter after the minimum and maximum values. Using a Random\IntervalBoundary Enum there can determine whether the min and max values themselves can be returned by the function.

Here are the rules:

  • IntervalBoundary::ClosedOpen: min may be returned, but not max
  • IntervalBoundary::ClosedClosed: both min and max may be returned
  • IntervalBoundary::OpenClosed: min may not be returned, max may
  • IntervalBoundary::OpenOpen: neither min nor max may be returned

When using getFloat() without specifying the Enum as the third parameter, the default is IntervalBoundary::ClosedOpen.

A useful example provided by the documentation for the new function generates random longitude and latitude coordinates, where latitudes can include -90 and 90, but longitude cannot include both -180 and 180 (since they are the same):

$rando = new Random\Randomizer();

printf(
    "Lat: %+.6f Long: %+.6f",
    $rando->getFloat(-90, 90, \Random\IntervalBoundary::ClosedClosed),

    // -180 will not be used 
    $rando->getFloat(-180, 180, \Random\IntervalBoundary::OpenClosed),
);

The new nextFloat() method is essentially the same as using getFloat() to request a random value that ranges from 0 to less than 1:

$rando = new Random\Randomizer();

$rando->nextFloat(); // 0.3767414902847

Other Minor Changes in PHP 8.3

PHP 8.3 also includes a number of other new functions and minor changes. We’ll mention them below with links to additional resources (where available):

Deprecations in PHP 8.3

With each new release of PHP, some functions and settings are flagged for eventual removal. Once deprecated, these features are not recommended for ongoing use and will generate notices in many logs when they appear in executing code.

Here’s a list of deprecations in PHP 8.3, with links to additional information:

Summary

We’ve looked at the significant changes packed into PHP 8.3. For a granular list of every update in this version, you can review the official changelog for the release. If you plan to move your code to a platform running the latest PHP, the 8.2-to-8.3 Migration Guide might help you stay out of trouble.

If it’s your role to install PHP on your development or production servers, 8.3 is ready for download now.

If you’re a Kinsta customer, you can rely on us to make this release available on servers behind your Managed WordPress Hosting or Application Hosting projects.

Steve Bonisteel Kinsta

Steve Bonisteel is a Technical Editor at Kinsta who began his writing career as a print journalist, chasing ambulances and fire trucks. He has been covering Internet-related technology since the late 1990s.