What's new in PHP 8.3 (Features and Improvements)
PHP 8.3 is the next major release of the PHP programming language. It is scheduled to be released sometime in 2023.
And while PHP 8.2 was released a few days ago while writing this post, PHP 8.3 is already in the works. So, let’s take a look at what has been PHP 8.3 up to so far.
- The new
json_validate()
function - Improved
unserialize()
error handling - New methods in the
Randomizer
class - Fetch class constants dynamically
- Improved Date/Time Exceptions
- Typed Constants
- The
#[\Override]
attribute
The new json_validate()
function
PHP 8.3 will be introducing a new json_validate()
function that can be used to validate a JSON string. So, we can do something like this.
$json = '{"name": "John Doe"}';
$valid = json_validate($json);
if ($valid) {
// Valid JSON
} else {
// Invalid JSON
}
Up until now, if we want to validate a JSON string in PHP, we can use the json_decode()
function. Here’s how it works.
$json = '{"name": "John Doe"}';
$data = json_decode($json);
if (json_last_error() === JSON_ERROR_NONE) {
// Valid JSON
} else {
// Invalid JSON
}
The signature
The json_validate()
function returns true
if the JSON string is valid and false
if it’s invalid. No more relying on the json_last_error()
function. It’s far more readable, straightforward, and easy to use.
Here’s the exact signature of the json_validate()
function.
json_validate(string $json, int $depth = 512, int $flags = 0): bool
$json
- The JSON string to validate.$depth
- The maximum nesting depth of the structure being decoded. Must be greater than zero.$flags
- Bitmask of JSON decode flags. See the json_decode() function for more details.
Improved unserialize()
error handling
PHP 8.3 will be improving the unserialize()
function by throwing an UnserializationFailureException
when the unserialize()
function fails.
Previously, PHP would emit an E_NOTICE
, an E_WARNING
, or throw an arbitrary \Exception
or \Error
This will make it easier to catch the error and handle it accordingly.
Before PHP 8.3, to successfully handle the exception, we had to do something like this.
try {
set_error_handler(static function ($severity, $message, $file, $line) {
throw new \ErrorException($message, 0, $severity, $file, $line);
});
$result = unserialize($serialized);
} catch (\Throwable $e) {
// Unserialization failed. Catch block optional if the error should not be handled.
} finally {
restore_error_handler();
}
var_dump($result);
As you can tell, we need to set an error handler, catch the exception, and then restore the error handler. This is a lot of work. And it’s not even guaranteed that the error handler will be restored correctly in case of an exception.
With PHP 8.3, we can do something like this.
try {
$result = unserialize('B:2:"jon";');
var_dump($result);
// Do something with the $result.
} catch (\UnserializationFailureException $e) {
// unserialization failed.
}
Essentially, a new \UnserializationFailedException
will be added. Whenever a \Throwable
is thrown during unserialize (e.g. within an __unserialize()
handler or because a throwing error handler converts E_NOTICE
/E_WARNING
into an Exception), this \Throwable
will be wrapped in a new instance of \UnserializationFailedException
.
So, it’s a lot easier to handle an exception now. And we also don’t need to worry about using a custom error handler.
New methods in the Randomizer
class
PHP 8.3 will be proposing new methods in the \Random\Randomizer
class. These methods will open up new possibilities for generating random values.
The new getBytesFromString()
method
The method allows you to generate a string with a given length that consists of randomly selected bytes from a given string.
public function getBytesFromString(string $string, int $length): string {}
Here’s how it works.
$randomizer = new \Random\Randomizer();
$randomizer->getBytesFromString('abcdef', 3); // 'bca'
As you can tell, the method will return a string with a given length that consists of randomly selected bytes from the given string.
Generating a multifactor authentication code
One interesting use case of this method is to generate a multifactor authentication code. Here’s how it works.
$randomizer = new \Random\Randomizer();
var_dump(
implode('-', str_split($randomizer->getBytesFromString('0123456789', 20), 5))
);
// string(23) "09898-46592-79230-33336"
Generating random subdomain name
We can also generate a random subdomain name using this method.
$randomizer = new \Random\Randomizer();
var_dump(
$randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz', 10) . '.example.com'
);
// string(20) "jxqzjxqzjx.example.com"
Generating a DNA sequence
We can also generate a DNA sequence using this method.
$randomizer = new \Random\Randomizer();
var_dump(
$randomizer->getBytesFromString('ATGC', 30)
);
// string(30) "TCTGCTGCTGCTGCTGCTGCTGCTGCTGCT"
The getFloat()
method
The getFloat()
method allows you to generate a random float number between two given numbers.
public function getFloat(
float $min,
float $max,
IntervalBoundary $boundary = IntervalBoundary::ClosedOpen
): float {}
Here’s how it works.
$randomizer = new \Random\Randomizer();
$randomizer->getFloat(0, 1); // 0.123456789
As you can tell, the method will return a random float number between two given numbers.
Generating random latitude and longitude
We can use this method to generate random latitude and longitude.
$randomizer = new \Random\Randomizer();
var_dump(
$randomizer->getFloat(-90, 90, \Random\IntervalBoundary::ClosedClosed),
$randomizer->getFloat(-180, 180, \Random\IntervalBoundary::OpenClosed)
);
// Lat: float(-45.123456789)
// Long: float(123.123456789)
The nextFloat()
method
The nextFloat()
method allows you to generate a random float number between zero and one.
public function nextFloat(): float {}
Here’s how it works.
$randomizer = new \Random\Randomizer();
$randomizer->nextFloat(); // 0.123456789
$randomizer->nextFloat(); // 0.987654321
As you can tell, the method will return a random float number between zero and one. So, it’s essentially the same as the getFloat()
method, but with a fixed range.
Simulate a coin flip
We can use this method to simulate a coin flip.
$randomizer = new \Random\Randomizer();
var_dump(
$randomizer->nextFloat() > 0.5 ? 'Heads' : 'Tails'
);
// string(5) "Tails"
Simulate a dice roll
We can also use this method to simulate a dice roll.
$randomizer = new \Random\Randomizer();
var_dump(
(int) ($randomizer->nextFloat() * 6) + 1
);
// int(3)
And that’s it for the new methods in the Randomizer
class.
Fetch class constants dynamically
PHP has been supporting computed/dynamic properties and methods for a while now. I have discussed this in this article.
But to give you a gist of it, this is how it works.
$baz = 'foo';
$$baz = 'bar';
^
// $foo
As you can tell, we can use the variable name as a variable name. So, we can dynamically create a variable name and then use it to create a variable.
Similarly, we can also call class methods dynamically like so.
class Foo
{
public function helloWorld()
{
echo 'Hello world!';
}
}
$dynamicMethod = 'helloWorld';
$a = new Foo();
$a->{$dynamicMethod}(); //prints 'Hello world!'
But PHP did not support computed/dynamic class constants yet. This is something that PHP 8.3 will be proposing.
So, with PHP 8.3, we can access class constants dynamically like so.
class Foo
{
public const BAR = 'bar';
}
$dynamicConstant = 'BAR';
echo Foo::{$dynamicConstant};
//prints 'bar'
It handles the cases where trying to access a non-existent constant will throw an error.
class Foo {}
$bar = 'BAR';
echo Foo::{$bar};
// Error: Undefined constant Foo::BAR
You can read more about this in the RFC.
Improved Date/Time Exceptions
PHP 8.3 will attempt to improve some of the exceptions and errors related to Date/Time extension.
Essentially, the current state of these errors/exceptions is that they are pretty generic and do not provide much information about the error. This does not allow for catching Date/Time exceptions as they are not specific enough.
So, with PHP 8.3, we will be able to catch Date/Time exceptions more specifically.
Here are all the errors and the exceptions that will be thrown for different scenarios.
- Error
- “Cannot modify readonly property
DatePeriod::$%s
” - “Invalid serialization data for
DateTime
object” (used with set_state and wakeup) - “Invalid serialization data for
DateTimeImmutable
object” (used with set_state and wakeup) - “Invalid serialization data for
DateTimeZone
object” (used with set_state and wakeup) - “Invalid serialization data for
DatePeriod
object” (used with set_state and wakeup) - “Unknown or bad format (%s) at position %d (%c) while unserializing: %s” (currently a warning)
- “Trying to compare uninitialized
DateTimeZone
objects” - “An iterator cannot be used with foreach by reference”
- ValueError (all, already like this)
- “must be a two-letter ISO 3166-1 compatible country code when argument #1 (
$timezoneGroup
) isDateTimeZone::PER_COUNTRY
” - “must be one of
SUNFUNCS_RET_TIMESTAMP
,SUNFUNCS_RET_STRING
, orSUNFUNCS_RET_DOUBLE”
- “must be a two-letter ISO 3166-1 compatible country code when argument #1 (
- TypeError
- “
DatePeriod::construct()
accepts (DateTimeInterface
,DateInterval
,int
[,int
]), or (DateTimeInterface
,DateInterval
,DateTime
[,int
]), or (string
[,int
]) as arguments” (already like this)
- “
- DateError
- “Timezone database is corrupt. Please file a bug report as this should never happen”
- “Timezone initialization failed”
- DateObjectError
- “The ” #class_name “ object has not been correctly initialized by its constructor” (DATE_CHECK_INITIALIZED)
- “DatePeriod has not been initialized correctly”
- “Trying to compare uninitialized
DateTimeZone
objects” - “The DateTime object has not been correctly initialized by its constructor”
DateRangeError
(is currently aValueError
)- “Epoch doesn’t fit in a PHP integer”
- “Cannot modify readonly property
- Exception
- DateException
DateInvalidTimeZoneException
(these are not easy to just change/add)- “Timezone must not contain null bytes”
- “Timezone offset is out of range (%s)”
- “Unknown or bad timezone (%s)”
DateInvalidOperationException
- “Only non-special relative time specifications are supported for subtraction” (currently a - WARNING for both procedural and OO variants)
DateMalformedStringException
- Failed to parse time string (%s) at position %d (%c): %s“
DateMalformedIntervalStringException
- “Unknown or bad format (%s)” (date_interval_initialize)
- “Failed to parse interval (%s)” (date_interval_initialize)
- “Unknown or bad format (%s) at position %d (%c): %s” (date_interval_create_from_date_string/OO variant; now: either a warning or exception)
- “String ‘%s’ contains non-relative elements” (date_interval_create_from_date_string/OO variant; now: either a warning or exception)
DateMalformedPeriodStringException
(all DatePeriod’s constructor)- “Unknown or bad format (%s)” (date_period_initialize)
- ”%s(): ISO interval must contain a start date, \“%s\” given“
- ”%s(): ISO interval must contain an interval, \“%s\” given“
- ”%s(): ISO interval must contain an end date or a recurrence count, \“%s\” given“
- ”%s(): Recurrence count must be greater than 0“
- DateException
Backward compatibility
With these changes, there will be some backward compatibility issues that you should be aware of.
- The “Epoch doesn’t fit in a PHP integer” now returns a new
DateRangeError
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 with
DateTime::sub()
anddate_sub()
becomes a newDateInvalidOperationException
. Leaving this with a warning and aNULL
return is not useful behaviour. - The “Unknown or bad format (%s) at position %d (%c): %s” and “String ‘%s’ contains non-relative elements” warnings that are created while parsing wrong/broken
DateInterval
strings will now throw a newDateMalformedIntervalStringException
when used with the OO interface, instead of showing a warning and returning false.
Typed Constants
The perpetual longing of making the type system of PHP more robust is still going on. And going in that direction, PHP 8.3 will be proposing typed constants.
Essentially, up until now, you could not specify the type of constants. But with PHP 8.3, it won’t be the case anymore.
So, from PHP 8.3, you would be able to specify a type to class, interface, trait, as well as enum constants.
Here are some examples of how you can use typed constants.
enum Car
{
const string NAME = "Car"; // Car::NAME is a string
}
trait Base
{
const string NAME = "Base"; // Base::NAME is a string
}
interface Adapter
{
const string NAME = Car::NAME; // Adapter::NAME is a string as well
}
class Audi implements Adapter
{
use Base;
const string NAME = Car::NAME; // Foo::TEST must also be a string
}
class Tesla extends Audi
{
const string NAME = "Model X"; // Tesla::NAME must also be a string, but the value can change
}
Constant values have to match the type of the class constant. In case it does not, a TypeError
will be thrown.
Apart from this, like all the other type checks (property types), constant type checks are always performed in the strict mode.
This is a great addition to the type system of PHP. It will make the type system more robust and will help in catching bugs at compile-time.
The #[\Override]
attribute
The #[\Override]
attribute is a new attribute that will be introduced in PHP 8.3. It will be used to mark a method as overriding a parent method.
Essentially, when a class extends another class, it can override the methods of the parent class.
Check this simple example.
class ParentClass
{
public function foo(int $a, string $b)
{
return "Parent";
}
}
class ChildClass extends ParentClass
{
public function foo(int $a, string $b)
{
return "Child";
}
}
$child = new ChildClass();
$child->foo(1, "2");
// Child
As you can tell, here, the foo()
method of the ChildClass
is overriding the foo()
method of the ParentClass
. And when a child class is overriding a method of the parent class, it is important to make sure that the child class method has the same signature as the parent class method.
So, if we change the signature of the foo()
method of the ChildClass
to something like this,
class ChildClass extends ParentClass
{
public function foo(int $a)
{
return "Child";
}
}
Then, it will result in a fatal error that says the declaration of the child class method is not compatible with the parent class method.
PHP Fatal error: Declaration of ChildClass::foo(int $a)
must be compatible with ParentClass::foo(int $a, string $b)
in /workspace/index.php on line 13
This is how PHP makes sure that the child class method is overriding the parent class method.
But there isn’t a way to check this in reverse. Meaning, if the child class is overriding the parent class method and the signature of the method in the parent class changes, then there is no way to verify if the child class method is overriding any method of the parent class or not.
This is where the #[\Override]
attribute comes into the picture.
The
#[\Override]
attribute can be used to mark a method as overriding a parent method. And if the method is not overriding the method of the same name or signature of the parent class, then it will result in a compile-time error.
So, we can add the #[\Override]
attribute to the foo()
method of the ChildClass
like this in our previous example.
class ChildClass extends ParentClass
{
#[\Override]
public function foo(int $a, string $b)
{
return "Child";
}
}
Now, if the signature of the foo()
method of the ParentClass
changes, then it will result in a compile-time error.
class ParentClass
{
public function foo(int $a)
{
return "Parent";
}
}
The error would look something like this.
Fatal error: ChildClass::foo() has #[\Override] attribute,
but no matching parent method exists
The same goes when a class is implementing an interface and marking a method as overriding a method of the interface.
interface Foo
{
public function foo(int $a, string $b);
}
class Bar implements Foo
{
#[\Override]
public function foo(int $a, string $b)
{
return "Bar";
}
}
Here are all the rules for using the #[\Override]
attribute.
- Public and protected methods of a parent class or implemented interface satisfy
#[\Override]
.- Abstract methods satisfy
#[\Override]
. - Static methods behave as instance methods.
- Abstract methods satisfy
- Private methods of a parent class do not satisfy
#[\Override]
, because they are no part of the externally visible API. __construct()
of a parent class do not satisfy#[\Override]
, because it’s not part of the API of an already-constructed object.- The attribute is ignored on traits, but:
- Abstract methods in a used trait satisfy
#[\Override]
. - Regular methods in a used trait that are “shadowed” by a method in the class using the trait do not satisfy
#[\Override]
. - Methods from a used trait behave as if the method definition was copied and pasted into the target class. Specifically the
#[\Override]
attribute on a trait method requires the existence of a matching method in a parent class or implemented interface.
- Abstract methods in a used trait satisfy
#[\Override]
works as expected on enums and anonymous classes.#[\Override]
works as expected on an interface. A matching method needs to exist in a parent interface.
Like this article?
Buy me a coffee👋 Hi there! I'm Amit. I write articles about all things web development. You can become a sponsor on my blog to help me continue my writing journey and get your brand in front of thousands of eyes.