PHP 8.3 is out, now! Here's what's new and changed.
Introduction
PHP is an open-source project. Knowing what’s going on for the next version only takes a minute of research. For instance, this page lists all the accepted RFCs for PHP 8.3.
Below, you will find a condensed list of what’s new, with code samples that make sense.
Oh and by the way, I already started writing about PHP 8.4’s release date, new features and changes.
When was PHP 8.3 released?
PHP 8.3 was released on November 23, 2023. It has been tested through three alpha releases, three beta, and six release candidates.
Date | Release |
---|---|
June 8, 2023 | Alpha 1 |
June 22, 2023 | Alpha 2 |
July 6, 2023 | Alpha 3 |
July 18, 2023 | Feature freeze |
July 20, 2023 | Beta 1 |
August 03, 2023 | Beta 2 |
August 17, 2023 | Beta 3 |
August 31, 2023 | RC 1 |
September 14, 2023 | RC 2 |
September 28, 2023 | RC 3 |
October 12, 2023 | RC 4 |
October 26, 2023 | RC 5 |
November 9, 2023 | RC 6 |
November 23, 2023 | GA |
How to install and test PHP 8.3 on Mac
If you are using Laravel Herd, this only takes one click!
If you are using Laravel Valet, here’s how to proceed:
- Install the Homebrew package manager if it’s not done already.
- Run
brew update
to make sure Homebrew and the formulae are up to date. - Add a new tap (basically a GitHub repository) for PHP 8.3’s formula:
brew tap shivammathur/php
. - Install the pre-compiled binary for PHP 8.3 (also called “a bottle” in Homebrew’s context). This will make the install so much faster.
brew install php@8.3
. - Link it to make sure that the
php
alias targets the right binary:brew link --overwrite --force php@8.3
.
If you want to learn more about how to install PHP on your Mac, I wrote something for you: PHP for Mac: get started fast using Laravel Valet
What’s new in PHP 8.3: new features and changes
The new Override attribute
When you are using PHP 8.3’s new Override
attribute, here’s what you tell other developers or your future self:
- I’m overriding a method from the parent class or one of the implemented interfaces.
- I have correctly overridden the method. If, for instance, I made a typo, the code would throw an error.
I had so much trouble understanding this one. But in fact, it’s dead simple. 😅
Here’s an example:
class SomeClass { public function someMethod() { } } class SomeOtherClass extends SomeClass { public function someMethod() { } }
In this example, SomeOtherClass
overrides the someMethod
method from its parent class, SomeClass
. But what happens if we change someMethod
in SomeClass
? Our override will be completely nullified.
But as you guessed it, the solution to this problem is the Override
attribute:
class SomeClass { // We renamed the method. public function someRenamedMethod() { } } class SomeOtherClass extends SomeClass { // But now, thanks to Override, we get an error message saying // that our override doesn't override any method anymore. // // Fatal error: SomeOtherClass::someMethod() has #[\Override] attribute, but no matching parent method exists #[Override] public function someMethod() { } }
How neat is that? 👌
Learn more: PHP RFC: Marking overridden methods (#[\Override])
Validate JSON with PHP 8.3 using the new json_validate() function
PHP 8.3 introduces a new json_validate()
function. Its purpose is to check if a given string is a valid JSON object.
Currently, most PHP programmers use the json_decode()
function for this task, but this uses memory and processing power that isn’t required for just checking validity.
json_validate()
will use the existing JSON parser in PHP, ensuring that it will be fully compatible with json_decode()
.
The function accepts a JSON string, a maximum nesting depth and flags, and returns a Boolean value indicating whether the string represents valid JSON.
If there are any errors during validation, these can be fetched using the json_last_error()
or json_last_error_msg()
functions.
$json = '{ "foo": "bar" }'; if (json_validate($json)) { // Valid JSON. } else { // Invalid JSON. }
Learn more: PHP RFC: json_validate
Improved unserialize() error handling
The RFC proposes two main changes to PHP’s unserialize function to enhance its error handling.
The current error handling in unserialize()
is inconsistent, as it may emit an E_NOTICE
, an E_WARNING
, or throw an arbitrary Exception
or Error
depending on how the input string is malformed.
This makes it challenging to reliably handle errors during unserialization.
The first proposal is to introduce a new wrapper exception, UnserializationFailedException
.
Whenever a Throwable
is thrown during unserialize, this Throwable
will be wrapped in a new instance of UnserializationFailedException
.
This allows developers to use a single catch(UnserializationFailedException $e)
to handle all possible Throwable
happening during unserialization.
The original Throwable
is accessible through the $previous property of UnserializationFailedException
, allowing the developer to learn about the actual cause of the unserialization failure.
The second proposal is to increase the error reporting severity in the unserialize()
parser.
In the current state, unserialization can fail due to a syntax error in the input string, out-of-range integers, and similar issues, which result in an E_NOTICE
or E_WARNING
.
This RFC proposes to increase the severity of these notices/warnings, and ultimately have them throw the new UnserializationFailedException
.
Before the proposed changes, handling unserialization errors in PHP could look 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); // Do something with the $result. // Must not appear in the 'try' to not 'catch' unrelated errors.
And several typical cases can look like this:
unserialize('foo'); // Notice: unserialize(): Error at offset 0 of 3 bytes in php-src/test.php on line 3 unserialize('i:12345678901234567890;'); // Warning: unserialize(): Numerical result out of range in php-src/test.php on line 4 unserialize('E:3:"foo";'); // Warning: unserialize(): Invalid enum name 'foo' (missing colon) in php-src/test.php on line 5 // Notice: unserialize(): Error at offset 0 of 10 bytes in php-src/test.php on line 5
After the proposed changes, handling unserialization errors in PHP would be as follows:
function unserialize(string $data, array $options = []): mixed { try { // The existing unserialization logic happens here. } catch (\Throwable $e) { throw new \UnserializationFailedException(previous: $e); } }
And the implementation of the new exception:
class UnserializationFailedException extends \Exception { }
The RFC has been partly accepted for PHP 8.3.
Learn more: PHP RFC: Improve unserialize() error handling
Randomizer additions
The RFC for PHP titled “Randomizer Additions” proposes three new methods and an accompanying enum for the \Random\Randomizer
class.
The getBytesFromString()
method allows you to generate a string of a specified length using randomly selected bytes from a given string.
The getFloat()
method returns a float between a defined $min
and $max
, with the interval boundaries being open or closed depending on the value of the $boundary
parameter.
The nextFloat()
method is equivalent to ->getFloat(0, 1, \Random\IntervalBoundary::ClosedOpen)
.
The enum IntervalBoundary
is used to determine the nature of the interval boundaries in the getFloat()
method.
For example, with the getBytesFromString()
method:
Before:
$randomString = ''; $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; $charactersLength = strlen($characters); for ($i = 0; $i < 16; $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; } $randomDomain = $randomString . ".example.com";
After:
$randomizer = new \Random\Randomizer(); $randomDomain = $randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16) . ".example.com";
Learn more: PHP RFC: Randomizer Additions
Dynamic class constant fetch
This RFC proposes to allow class constants to be accessed dynamically using variables.
Instead of accessing class constants with a static string value (e.g. ClassName::CONSTANT
), you could use a variable containing the constant name.
$constant = 'CONSTANT'; ClassName::{$constant}
This change would make it easier to access class constants dynamically and programmatically.
Learn more: PHP RFC: Dynamic class constant fetch
More appropriate Date/Time exceptions
The RFC proposes to introduce Date/Time extension specific exceptions and errors in PHP, instead of the more generic warnings, errors, and exceptions currently used.
This aims to provide better specificity and handling for Date/Time related exceptions.
The proposal includes various specific exceptions, such as DateInvalidTimeZoneException
, DateInvalidOperationException
, DateMalformedStringException
, DateMalformedIntervalStringException
, and others.
Notably, this change only affects object-oriented style use of date/time functions, as procedural style usage will continue to use warnings and errors as it currently does.
Below are examples of how the new exceptions work:
Before the change:
For creating a DateInterval
with a malformed string, the function call would return a warning and false:
try { $i = DateInterval::createFromDateString("foo"); } catch (Exception $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
For trying to subtract a non-special relative time specification from a DateTimeImmutable
object, the function call would return a warning and null
:
$now = new DateTimeImmutable("2022-04-22 16:25:11 BST"); var_dump($now->sub($e));
After the change:
For creating a DateInterval
with a malformed string, the function now throws a DateMalformedIntervalStringException
:
try { $i = DateInterval::createFromDateString("foo"); } catch (DateMalformedIntervalStringException $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
For trying to subtract a non-special relative time specification from a DateTimeImmutable
object, the function now throws a DateInvalidOperationException
:
$now = new DateTimeImmutable("2022-04-22 16:25:11 BST"); try { var_dump($now->sub($e)); } catch (DateInvalidOperationException $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
Learn more: PHP RFC: More Appropriate Date/Time Exceptions
Read only amendments
This is a two parts RFC and only the part where read only properties can now be reinitialized during cloning has been accepted.
This proposal aims to eliminate the limitation that prevents readonly properties from being “deep-cloned”.
It allows readonly properties to be reinitialized during the execution of the __clone()
magic method call. Here is an example of this change:
Before:
class Foo { public function __construct( public readonly DateTime $bar, public readonly DateTime $baz ) {} public function __clone() { $this->bar = clone $this->bar; // Doesn't work, an error is thrown. } }
After:
class Foo { public function __construct( public readonly DateTime $bar, public readonly DateTime $baz ) {} public function __clone() { $this->bar = clone $this->bar; // Works. $this->cloneBaz(); } private function cloneBaz() { unset($this->baz); // Also works. } } $foo = new Foo(new DateTime(), new DateTime()); $foo2 = clone $foo; // No error, Foo2::$bar is cloned deeply, while Foo2::$baz becomes uninitialized.
Learn more: PHP RFC: Read only amendements
Saner array_sum() and array_product()
This RFC introduces changes to the behavior of array_sum()
and array_product()
functions to make them more consistent with their userland implementations using array_reduce()
.
Currently, array_sum()
and array_product()
skip array or object entries, while their userland implementations using array_reduce() throw a TypeError
.
The proposed changes include:
- Using the same behavior for
array_sum()
andarray_product()
as thearray_reduce()
variants. - Emitting an
E_WARNING
for incompatible types. - Supporting objects with numeric cast to be added/multiplied.
Learn more: PHP RFC: Saner array_(sum|product)()
PHP RFC: Typed class constants
Typed class constants are finally coming in PHP 8.3!
They will help reduce bugs and confusion arising from child classes overriding parent class constants.
Typed class constants can be declared in classes, interfaces, traits, and enums.
Key points:
- Class constant type declarations support all PHP type declarations except void, callable, and never.
- Strict_types mode does not affect the behavior since type checks are always performed in strict mode.
- Class constants are covariant, meaning that their types are not allowed to widen during inheritance.
- Constant values must match the type of the class constant, with the exception that float class constants can also accept integer values.
- ReflectionClassConstant is extended with two methods: getType() and hasType().
As you may imagine, typed constants are super easy to use, just like typed properties:
interface Foo { public const string BAR = 'baz'; } class Bar extends Foo { // Doesn't work anymore! You cannot change the // type and assign a value of another type. public const array BAR = ['foo', 'bar', 'baz']; // OK. public const string BAR = 'foo'; }
Learn more: PHP RFC: Typed class constants
Arbitrary static variable initializers
The RFC proposes a change that would enable the static variable initializer to contain arbitrary expressions, rather than just constant expressions.
This makes the language more flexible and less confusing for users. For example, before this change, a static variable could only be initialized with a constant value:
function foo() { static $i = 1; echo $i++, "\n"; } foo(); foo(); foo();
This would output:
1 2 3
With the proposed change, a static variable can be initialized with a function call:
function bar() { echo "bar() called\n"; return 1; } function foo() { static $i = bar(); echo $i++, "\n"; } foo(); foo(); foo();
The proposal also introduces several changes to the semantics of static variable initialization, including the treatment of exceptions during initialization, the order of operations when destructors are involved, and the handling of recursion during initialization.
Redeclaring static variables would not be allowed, though, and it changes how ReflectionFunction::getStaticVariables()
works with static variables.
Learn more: PHP RFC: Arbitrary static variable initializers
Improvements to the range() function
This RFC proposes changes to the semantics of the range()
function, which currently generates an array of values from a start value to an end value with stipulated behavior for integers, floats, and strings.
The current range()
function has shown to produce unexpected results with certain kinds of inputs and the RFC seeks to make the behavior more predictable and consistent.
The proposed changes include:
- Checking for and throwing errors when unusable arguments are passed to
range()
. - Handling non-finite float values such as
-INF
,INF
,NAN
. - Throwing a more descriptive error when stepping is zero.
- Emit warnings when trying to create a range of characters with a float step.
- Conduct a stricter check on the start and end parameters to ensure they are int, float, or string. Any other types would result in a TypeError.
- Existing calls to
range()
that return arrays of floats when the step can be an integer will now return an array of integers.
Learn more: PHP RFC: Define proper semantics for range() function
I'm using Laravel 11 as an API, along with Breeze, to handle authentication in my application. Overall, the login process works correctly. However, when attempting to submit the login form again, instead of receiving a response, the application automatically redirects me. This is not the desired behavior, as I require a direct response instead of a redirection. Currently, I'm using the following code to manage this process: