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.