BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles What's New in PHP 8.3

What's New in PHP 8.3

Bookmarks

Key Takeaways

  • PHP 8.3 extends the readonly features introduced in earlier 8.x versions.
  • The new #[\Override] attribute explicitly marks a method that is  meant to be an overriding method.
  • Class constants can now be explicitly typed.
  • New functions have been added to the Random/Randomizer class.
  • A new function json_validate() helps validating a JSON string.
     

This article is part of the article series "PHP 8.x". You can subscribe to receive notifications about new articles in this series via RSS.

PHP continues to be one of the most widely used scripting languages on  the web with 77.3% of all the websites whose server-side programming language is known using it according to w3tech. PHP 8 brings many new features and other improvements, which we shall explore in this article series.

PHP 8.3 is the latest major update in the PHP 8.x series.

In addition to performance improvements, it brings a wealth of new features, including amendments to the readonly feature introduced in PHP 8.1; explicitly typed class constants; a new #[\Override] attribute for methods intended to be overridden from a superclass; and more.

Setting the Environment

Download and install PHP 8.3 binaries. In previous installments of this series, we used Windows OS. To keep in line with this, download and install the PHP 8.3 Windows binaries. Setup the environment as detailed in PHP 7 - Getting Started and OOP Improvements. Finally, verify that the PHP version is 8.3 by running php --version on the command line.

New Increment/Decrement operators

PHP 8.3 introduces new increment and decrement functions str_increment(string $string) and str_decrement(string $string) that increment/decrement their argument by adding/subtracting 1. In other words $v++ is the same as $v += 1 and $v-- is the same as $v -= 1.

The functions throw a ValueError if any of the following is true:

  • $string is the empty string
  • $string is not comprised of alphanumeric ASCII characters

Additionally, the str_decrement function throws a ValueError if the string cannot be decremented. As an example, "A" or "0" cannot be decremented.
Increment/decrement of non-alphanumeric strings is deprecated.  No type conversion is performed for strings that can be interpreted as a number in scientific notation.

In the following example script, the str_increment(string $string) function call increments an alphanumeric string. The  str_decrement(string $string) function decrements the value of an alphanumeric string. The script also demonstrates that the functions’ argument must be an alphanumeric string, or a ValueError is thrown:

<?php
 
$str = "1";
$str = str_increment($str);
echo var_dump($str);  

$str = "1";
$str = str_decrement($str);
echo var_dump($str);  

$str = "-1";
$str = str_decrement($str);
echo var_dump($str);  
?>

Run the script, with the following output:

 string(1) "2"
 string(1) "0"
Uncaught ValueError: str_decrement(): Argument #1 ($string) must be composed only of alphanumeric ASCII characters ...

Increment/Decrement on type bool has no effect and will produce a warning. Likewise, decrementing or incrementing an empty string is deprecated as non-numeric.  Additionally, it should be noted that both decrementing and incrementing a non-numeric string has no effect and is deprecated. To demonstrate, run the following script:

<?php 
// decrementing empty string
$str = "";
--$str;  
echo var_dump($str);
// decrementing non-numeric string
$str = "input";
--$str;  
echo var_dump($str);
// incrementing empty string
$str = "";
++$str;  
echo var_dump
($str);  
// incrementing non-numeric string string
$str = "input";
++$str;  
echo var_dump($str);  

The output includes  deprecation messages.

Deprecated: Decrement on empty string is deprecated as non-numeric in …
int(-1)
Deprecated: Decrement on non-numeric string has no effect and is deprecated in …
string(5) "input"
Deprecated: Increment on non-alphanumeric string is deprecated in …
string(1) "1"
string(5) "input"

However, alphanumeric strings get incremented/decremented, though the output may not always be predictable. Run the following script:

<?php
$str = "start9";
$str = str_increment($str);
echo var_dump($str);
$str = "end0";
$str = str_decrement($str);
echo var_dump($str);   
$str = "AA";
$str = str_decrement($str);
echo var_dump($str); 

The output is as follows:

string(5) "input"
string(6) "staru0"
string(4) "enc9"
string(1) "Z"

The string argument must be within range so as not to cause an underflow. To demonstrate, run the following script:

<?php
$str = "00";
$str = str_decrement($str);
echo var_dump($str);  

A ValueError is output:

Uncaught ValueError: str_decrement(): Argument #1 ($string) "00" is out of decrement range

The new #[\Override] attribute to mark overriding methods  

PHP 8.3 introduces the  #[\Override] attribute to explicitly declare the intent of overriding methods. The #[\Override] attribute is introduced to remove any ambiguity in method overriding. How could overriding method declarations be ambiguous? PHP already verifies that the signature of an overriding method is compatible with the corresponding overridden method from a parent class. PHP already verifies that implemented methods inherited from an interface are compatible with the given interface. What PHP doesn't verify is whether a method intends to override an existing method in a parent class.

What PHP doesn't verify is whether a method intends to implement a method from an interface. If the intent is declared with the new #[\Override] attribute it becomes easier to debug the code for any code that appears to be an overriding method due to similarity in method signature or due to a typo or an error but was not intended to be an overriding method. Marking overridden methods, whether from a superclass or from  an interface, explicitly serves many purposes, including:

  • Making debugging easier.
  • Refactoring and cleaning up existing code.
  • Detecting a possibly breaking change in a superclass that was provided by a library.

How does the PHP engine interpret the new #[\Override] attribute? If this attribute is added to a method, the engine validates at compile time that a method with the same name exists in a parent class or any of the implemented interfaces. If no such method exists a compile time error is emitted. The #[\Override] attribute does not change the rules and syntax for overriding methods. It only provides a hint to the compiler. The  #[\Override] attribute is satisfied, or usable, with:

  • Public and protected methods of a superclass or an implemented interface including abstract methods and static methods.
  • Abstract methods in a used trait (a used trait is one that is used in a class with the use keyword) as demonstrated with an example subsequently.

In the example script that follows, the class B extends class A and overrides three of its methods fn1, fn2 and fn3.

<?php
class A {
    protected function fn1(): void {}
    public function fn2(): void {}
    private function fn3(): void {}
}
class B extends A {
    #[\Override]
    public function fn1(): void {}
   #[\Override]
    public function fn2(): void {}
    #[\Override]
    public function fn3(): void {}  
}

The script runs and the #[\Override] attribute is satisfied for the first two methods, but not for the fn3 because fn3 is a private method.

B::fn3() has #[\Override] attribute, but no //matching parent method exists ...

In the next example, a class extends another class and implements an interface, overriding its only method. The #[\Override] attribute is placed on the overriding method.

<?php
interface B {
    public function fn(): void;
}
class A {
    public function fn(): void {}  
}
class C extends A implements B {
#[\Override]
public function fn(): void {}
}
?>

A matching method must exist in the superclass. To demonstrate this, run the following script, that has the #[\Override] attribute on a method without a matching method in the superclass.

<?php
class Hello
{
    #[\Override]
    public function hello(): void {}  
}
?>

An error message is generated:

Hello::hello() has #[\Override] attribute, but no matching parent method exists ...

The #[\Override] attribute cannot be applied to the __construct() method, as demonstrated by the following script:

<?php
class Obj  
{
    private $data;
    #[\Override]
    public function __construct() {
        $this->data = "some data";
    }
}

An error message is generated:

Obj::__construct() has #[\Override] attribute, but no matching parent method exists ...

The #[\Override] attribute on a trait’s method is ignored if the trait is not used in a class. The #[\Override] attribute can be declared on a trait’s method as follows:

<?php
trait Hello {
    #[\Override]
    public function hello(): void {}
}
?>

However, if the trait is used in a class, the #[\Override] attribute cannot be declared on a trait’s method without the method also being in a superclass. An example:

<?php
trait Hello {
    #[\Override]
    public function hello(): void {}
}
class C {
    use Hello;  
}
?>

An error message is generated:

C::hello() has #[\Override] attribute, but no matching parent method exists ...

It goes without saying that not all, or any, methods from a parent class, or implemented interface, or used trait, must be overridden. Otherwise, a class inheriting abstract methods from a parent class, interface, or trait could be declared as abstract if not providing an implementation. But when a class does override methods from a used trait, interface, or superclass it is best, though not required, to mark/decorate the overridden methods with the #[\Override] attribute.

Abstract methods from a used trait that are overridden in a class satisfy the #[\Override] attribute. What that means is that an abstract method inherited from a trait that is used in a class can be preceded/marked with the #[\Override] attribute in the class to indicate that this is an overriding method. In the following script, the #[\Override] attribute on the method hello is indicative of an intent to override the hello() abstract method from the used trait.

<?php
trait Hello {
    abstract public function hello();
    public function hello2() {}
}
class HelloClass {
    use Hello;
    #[\Override]
    public function hello() {
        return "hello";
    }
}
?>

However, regular methods inherited from a used trait cannot be preceded or marked with the #[\Override] attribute because it wouldn’t really be overriding any method. It would only be "shadowing" a method from a trait. To demonstrate, consider the following script:

<?php
trait Hello {
    public function hello(){}
    public function hello2() {}
}
class HelloClass {
    use Hello;
    #[\Override]
    public function hello() {
        return "hello";
    }
}
?>

The #[\Override] attribute is indicating an intent to override some method, but the class is only "shadowing" a method with the same name belonging to a trait. The script generates an error message:

HelloClass::hello() has #[\Override] attribute, but no matching parent method exists  

The #[\Override] attribute may be used with enums. As an example, declare an interface and implement the interface in an enum. Override the method from the interface in the enum.

<?php
interface Rectangle {
    public function rect(): string;
}
enum Geometry implements Rectangle {
    case Square;
    case Line;
    case Point;
    case Polygon;

    #[\Override]
    public function rect(): string{
        return "Rectangle";
    }
}

The #[\Override] attribute may be used with anonymous classes. An example:

<?php
class Hello {public function hello() {}}
interface HelloI {public function hello2();}
var_dump(new class() extends Hello implements HelloI {
   #[\Override]
  public function hello() {}
   #[\Override]
  public function hello2() {}
});
?>

The output from the script is as follows:

object(Hello@anonymous)#1 (0) { }

Arbitrary static variable initializers  

PHP 8.3 adds support for non-constant expressions in static variable initializers. In the following example, the static variable initializer in fn2() is a function call instead of being a constant.

<?php
function fn1() {
    return 5;
}
 
function fn2() {
    static $i = fn1();
    echo $i++, "\n";
}
fn2();
?>

The script returns a value of 5 when the function is called.

Re-declaring static variables, which was supported in earlier versions, is not supported in PHP 8.3 anymore. The following script re-declares a static variable initializer.

<?php
function fn1() {
    return 5;
}
function fn2() {
    static $i = 1;
    static $i = fn1();
}
fn2();
?>

An error message is generated when the script is run:

Duplicate declaration of static variable $i ... 

One side-effect of support for non-constant expressions is that the ReflectionFunction::getStaticVariables() method may not be able to determine the value of a static variable at compile-time simply because the static variable initializer makes use of an expression whose value is known only after the function has been called. If a static variable's value cannot be ascertained at compile time, a value of NULL is returned as in the example below:

<?php
function getInitValue($initValue) {
    static $i = $initValue;
}
var_dump((new ReflectionFunction('getInitValue'))->getStaticVariables()['i']);

Next, modify the script to call the getInitValue function, which initializes the static variable:

<?php
function getInitValue($initValue) {
    static $i = $initValue;
}
getInitValue(1);
var_dump((new ReflectionFunction('getInitValue'))->getStaticVariables()['i']);
?>

This time, the same ReflectionFunction call returns the initialized value of int(1).

But once the value has been added to the static variable table it cannot be reinitialized with another function call, such as:

getInitValue(2);

The static variable’s value is still int(1) as indicated by the output of  the following script, which reads: int(1) int(1).

<?php
function getInitValue($initValue) {
    static $i = $initValue;
}
getInitValue(1);
var_dump((new ReflectionFunction('getInitValue'))->getStaticVariables()['i']);
getInitValue(2); 
var_dump((new ReflectionFunction('getInitValue'))->getStaticVariables()['i']);
?>

Another side-effect of allowing non-constant expressions in static variable initializers is that if an exception is thrown during initialization, the static variable does not get explicitly initialized and gets an initial value of NULL, but a subsequent call may be able to initialize the static variable.

Yet another side-effect is that the initial value of a static variable that depends on another static variable is not known at compile-time. In the following script, the static variable $b’s value is known only after setInitValue() is called.

<?php
function setInitValue() {
   static $a = 0;
   static $b = $a + 1;
   var_dump($b);
}
setInitValue();
?>

The output is:

int(1)

Dynamic class constant lookup

PHP 8.3 introduces new syntax to lookup class constants. Prior to PHP 8.3 the constant() function had to be used to lookup class constants as follows:

<?php
class C {
    const  string SOME_CONSTANT = 'SCRIPT_LANG';
}

$some_constant = 'SOME_CONSTANT';

var_dump(constant(C::class . "::{$some_constant}"));

The output is :

string(11) "SCRIPT_LANG"

With PHP 8.3, the syntax to lookup class constants is simplified as follows:

<?php
class C {
    const  string SOME_CONSTANT = 'SCRIPT_LANG';
}

$some_constant = 'SOME_CONSTANT';

var_dump(C::{$some_constant});

Output is:

string(11) "SCRIPT_LANG"

New Readonly features

As we described in an earlier article of this series, readonly properties were introduced in PHP 8.1, while readonly classes were added in PHP 8.2. PHP 8.3 takes the readonly functionality further by adding two new features:

  • Readonly properties can be reinitialized during cloning.
  • Non-Readonly classes can extend  readonly classes.

Readonly properties can be reinitialized during cloning

For deep-cloning of readonly properties, the readonly properties can be reinitialized during cloning. First, we start with an example of deep-cloning that fails when the script is run with PHP 8.2. The readonly property in the following script is RO::$c.  

<?php
class C {
    public string $msg = 'Hello';
}

readonly class RO {
    public function __construct(
        public C $c
    ) {}

    public function __clone(): void {
        $this->c = clone $this->c;
    }
}

$instance = new RO(new C());
$cloned = clone $instance;

When the script is run, an error is generated:

Uncaught Error: Cannot modify readonly property RO::$c ...

The following script demonstrates modifying a readonly property in PHP 8.3.

<?php
class C {
    public string $msg = 'Hello';
}

readonly class RO {
    public function __construct(
        public C $c
    ) {}

    public function __clone(): void {
        $this->c = clone $this->c;
    }
}

$instance = new RO(new C());
$cloned = clone $instance;
$cloned->c->msg = 'hello';
echo $cloned->c->msg;

The output is as follows:

hello

The reinitialization can only be performed during the execution of the __clone() magic method call. The original object being cloned is not modified; only the new instance can be modified. Therefore, technically the object is still invariant. Reinitialization can only be performed once. Unsetting the value of a readonly property can also be performed and is considered reinitializing.  

In the next example, class A declares two readonly properties $a and $b, which are initialized by the __construct() function. The __clone() method reinitializes the readonly property $a,while the readonly property $b is unset with a call to the cloneB() function.

<?php
class A {
   public readonly int $a;
   public readonly int $b;
   public function __construct(int $a,int $b) {     
       $this->a = $a;
       $this->b = $b;
   }
 public function __clone()
    {
        $this->a = clone $this->a;  
        $this->cloneB();
    }
    private function cloneB()
    {
        unset($this->b);  
    }
}

Cloning an object and modifying its readonly properties does not change the value of the original object’s readonly property values.

$A = new A(1,2);
echo $A->a;
echo $A->b;
$A2 = clone $A;
echo $A->a;
echo $A->b;

The readonly properties keep the same values of 1 and 2 respectively.

Reinitializing the same readonly property twice generates an error. For example, if you add the following line to the __clone() method:

$this->a = clone $this->a;

The following error message is generated:

Uncaught Error: __clone method called on non-object ...

Non-Readonly classes can extend readonly classes

With PHP 8.3, non-readonly classes can extend readonly classes. As an example, the following script declares a readonly class A with three properties, which are implicitly readonly. The readonly properties are initialized in the class constructor.

<?php
readonly class A
{
    public int $a;
    public string $b;
    public array $c;
    
    public function __construct() {
        $this->a = 1;
        $this->b = "hello";
        $this->c = ["1" => "one",
                    "2" => "two"];
    }
}

Then, class B, which is non-readonly, extends class A.   

class B extends A {}

The class would have generated an error with PHP 8.2:

Non-readonly class B cannot extend readonly class A ...

But properties in the readonly class A cannot be redefined in the extending class because the properties are implicitly readonly.

class B extends A {public int $a;}

A readonly class still can’t extend a non-readonly class.

A non-readonly class extending a readonly class does not implicitly make the extending class readonly.

While a readonly class cannot declare an untyped property or a static property, a non-readonly class extending a readonly class may declare an untyped property or a static property. The following script demonstrates this:

<?php
trait T {
    public $a1;        // Untyped property
}
class B extends A {
    use T;
    public static $a2; // Static property
}

Typed class constants

PHP 8.3 adds support for typed class constants. Typed class constants  may be added to a class, interface, enum, and trait. Typed class constant implies that the class constant can be associated with an explicit type.

Prior to PHP 8.3, a class constant did not have an explicit type, so a subclass could assign a value of a different type than the one used in the defining class. In PHP 8.3 a constant may be typed, for example with the type string. A constant of type string can only be assigned a string value but not a value of some other type even in a derived class.

In the following example, a constant of type int is assigned a string value.

<?php
interface I {
    const  string SOME_CONSTANT = 'SCRIPT_LANG';
}

class C implements I {
    const int  ANOTHER_CONSTANT = I::SOME_CONSTANT;
}

An error message is generated:

Cannot use int as value for class constant C::ANOTHER_CONSTANT of type string

The mixed type may be assigned to a constant as in the example:

<?php
interface I {
    const  string SOME_CONSTANT = 'SCRIPT_LANG';
}

class C implements I {
    const mixed  ANOTHER_CONSTANT = 1;
}

Any PHP type may be assigned to a class constant except void, never, and callable.

Randomizer Class Additions

PHP 8.3 adds three new methods to  the \Random\Randomizer class. The new methods provide functionality that is commonly needed. One function generates randomly selected bytes from a given string and the other two functions generate random floating point values.

New Randomizer::getBytesFromString() method

The method returns a string of a given length consisting of randomly selected bytes from a given string.

The method definition is as follows:

public function getBytesFromString(string $string, int $length): string {}  

An example script for the method is as follows:

<?php
 
$randomizer = new \Random\Randomizer();

$bytes = $randomizer->getBytesFromString(
        'some string input',
        10);

echo bin2hex($bytes);

The output is:

7467736f7473676e6573

New Randomizer::getFloat() and Randomizer::nextFloat() methods

The getFloat() method returns a random float value between a minimum and a maximum value provided as method arguments.

The $boundary parameter value determines whether the $min and $max values are inclusive. In other words, the $boundary parameter determines whether a returned value could be one of the $min and $max values.

The enum values determine the interval as described in table:

Value     Interval     Description
ClosedOpen (default) [$min, $max) Includes the lower bound
Excludes the upper bound
ClosedClosed [$min, $max] Includes the lower bound
Includes the upper bound
OpenClosed ($min, $max] Excludes the lower bound
Includes the upper bound
OpenOpen ($min, $max) Excludes the lower bound
Excludes the upper bound

As an example, getFloat() returns a random float value between 1 and 2 in the script:

<?php
 
$randomizer = new \Random\Randomizer();

$f = $randomizer->getFloat(1,2); 

echo var_dump($f);

The output is as follows:

float(1.3471317682766972)

The $max arg must be greater than the $min arg but must be finite.

The next example demonstrates the use of different boundaries.

<?php
 
$randomizer = new \Random\Randomizer();

$f = $randomizer->getFloat(1,3,\Random\IntervalBoundary::OpenOpen); 

echo var_dump($f);
$f = $randomizer->getFloat(1,3,\Random\IntervalBoundary::ClosedOpen); 

echo var_dump($f);
$f = $randomizer->getFloat(1,3,\Random\IntervalBoundary::OpenClosed); 

echo var_dump($f);
$f = $randomizer->getFloat(1,3,\Random\IntervalBoundary::ClosedClosed); 

echo var_dump($f);

Outputs generated by calling the script multiple times are:

float(2.121058113021827) float(1.4655805702205025) float(1.8986188544040883) float(1.2991440175378313)

float(2.604249570497203) float(1.8832264253121545) float(2.127150199054182) float(2.5601957175378405)

float(2.0536414161355174) float(2.5310859684773384) float(1.5561747808441186) float(2.4747482582046323)

float(2.8801657134532497) float(1.9661050785744774) float(1.0275149347491048) float(2.6876762894295947)

float(2.0566100272261596) float(1.2481323630515981) float(2.378377362548793) float(2.365791373823495)

The nextFloat() method returns a random float in the interval [0, 1). The method is equivalent to getFloat(0, 1, \Random\IntervalBoundary::ClosedOpen).

Make unserialize() emit a warning for trailing data

The unserialize() function previously only considered the main data and ignored extra data after the trailing delimiter of the serialized value, namely ‘;’ for scalars and ‘}’ for arrays and objects. With PHP 8.3 the trailing bytes are not ignored and a warning message is output. An example script:

<?php
var_dump(unserialize('i:1;'));
var_dump(unserialize('b:1;i:2;'));

A warning message is generated:

unserialize(): Extra data starting at offset 4 of 8 byte ...

New json_validate() function

PHP 8.3 adds a much needed new function to validate if a string argument is valid JSON. The string argument must be a UTF-8 encoded string. The function returns a boolean (true or false) to indicate whether the string is valid JSON. Pre-PHP 8.3, a custom function to validate JSON can be created as follows:

<?php
function json_validate(string $string): bool {
    json_decode($string);

    return json_last_error() === JSON_ERROR_NONE;
}

But the json_validate() function is not a built-in function. The following script that does not include the custom function definition generates an error when run with pre-PHP 8.3:

<?php 
 
var_dump(json_validate('{ "obj": { "k": "v" } }'));

The error message is:

Uncaught Error: Call to undefined function json_validate()

With the support for the new json_validate() function in PHP 8.3, the following script runs ok:

<?php
 
var_dump(json_validate('{ "obj": { "k": "v" } }'));

Output is:

bool(true)

Minor deprecations

PHP 8.3 deprecates certain minor functionality that was not used.

Passing negative $widths to mb_strimwidth() has been deprecated. The multibyte extension must be enabled in the php.ini configuration file to use the function:

extension=mbstring

The following script passes a negative width -2 to the function.

<?php
echo mb_strimwidth("Hello", 0, -2, "...");
?>

When the script is run, the following deprecation message is output:

Deprecated: mb_strimwidth(): passing a negative integer to argument #3 ($width) is deprecate...

Second, the NumberFormatter::TYPE_CURRENCY constant has been deprecated. Enable the internationalization extension to use the constant.

extension=intl

Run the following script:

<?php
$fmt = numfmt_create( 'de_DE', NumberFormatter::TYPE_CURRENCY);
$data = numfmt_format($fmt, 1234567.891234567890000);
?>

A deprecation message is output:

Deprecated: Constant NumberFormatter::TYPE_CURRENCY is deprecated in C:\PHP\scripts\sample.php on line 2

The MT_RAND_PHP constant, which was introduced for a special case implementation is not of any significant use, and therefore has been deprecated. Run the following script that makes use of the constant.

<?php
echo mt_rand(1,  MT_RAND_PHP), "\n";

A deprecation message is output:

Deprecated: Constant MT_RAND_PHP is deprecated ...

The ldap_connect function, which is used to check whether given connection parameters are plausible to connect to LDAP server, has deprecated the function signature that makes use of two arguments for host, and port to be specified separately:

ldap_connect(?string $host = null, int $port = 389): LDAP\Connection|false

Enable the ldap extension in the php.ini to use the function.

extension=ldap

Run the following script:

<?php
$host ='example.com';
$port =389;
ldap_connect($host,$port;
?>

A deprecation message is output:

Deprecated: Usage of ldap_connect with two arguments is deprecated ...

Summary

As a recap, this article discusses the salient new features in PHP 8.3, including some amendments to readonly features introduced in earlier 8.x versions, the #[\Override] attribute to explicitly decorate a method that is meant to be an overriding method, explicitly typed class constants, and the new json_validate() function to validate a JSON string.

 

This article is part of the article series "PHP 8.x". You can subscribe to receive notifications about new articles in this series via RSS.

PHP continues to be one of the most widely used scripting languages on  the web with 77.3% of all the websites whose server-side programming language is known using it according to w3tech. PHP 8 brings many new features and other improvements, which we shall explore in this article series.

About the Author

Rate this Article

Adoption
Style

BT