Get "PHP 8 in a Nuthshell" (Now with PHP 8.4)
Amit Merchant

Amit Merchant

A blog on PHP, JavaScript, and more

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

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) is DateTimeZone::PER_COUNTRY
      • “must be one of SUNFUNCS_RET_TIMESTAMP, SUNFUNCS_RET_STRING, or SUNFUNCS_RET_DOUBLE”
    • 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 a ValueError)
        • “Epoch doesn’t fit in a PHP integer”
  • 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“

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 generic ValueError, 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() and date_sub() becomes a new DateInvalidOperationException. Leaving this with a warning and a NULL 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 new DateMalformedIntervalStringException 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.
  • 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.
  • #[\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.
Learn the fundamentals of PHP 8 (including 8.1, 8.2, 8.3, and 8.4), the latest version of PHP, and how to use it today with my new book PHP 8 in a Nutshell. It's a no-fluff and easy-to-digest guide to the latest features and nitty-gritty details of PHP 8. So, if you're looking for a quick and easy way to PHP 8, this is the book for you.

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.

Comments?