New PHP 8 language features that you may have missed
· UpdatedPHP has gained a lot of new language features over the years. Some of them are frankly a bit insane – but in a good way.
PHP has come a long way since Rasmus Lerdorf released its first version in 1993. It used to be a pretty crazy programming language, with a mishmash of naming conventions, nonsensical equality checks, needlessly cryptic error messages (T_PAAMAYIM_NEKUDOTAYIM, anyone?), the ability to use variable variables, and many other weird things that you wouldn’t find in most other programming languages.
The bad news is that almost all that crap still exists and can be used even in 2023. The good news is that the community has spent a lot of effort over the years to turn PHP into a nice programming language.
This blog post summarises all major changes that were introduced in PHP 8.0–8.2.
PHP 8.0
PHP 8.0 was released on 26 November 2020. It’s no longer actively supported, but still receives security support until 26 November 2023.
Named arguments
Named arguments provide a way to explicitly pass arguments to functions based on parameter names. This can make the code more readable, especially if a function accepts many parameters. Unlike positional arguments, named arguments are order-independent, so you can pass them in any order you wish:
class Address( public function __construct( string $street = null, string $postalCode = null, string $city = null, string $state = null, string $country = null ) { // … }) { // …}
// Before$address = new Address( 'Journaalplein 1', null, 'Hilversum', null, 'The Netherlands',);
// After$address = new Address( country: 'The Netherlands', street: 'Journaalplein 1', city: 'Hilversum',);Attributes
Attributes make it possible to annotate classes, properties, functions, methods, parameters and constants with configuration metadata. This replaces hacky PHPDoc-based solutions that had to be used in older versions of PHP:
// Beforeclass PostsController{ /** * @Route("/api/posts/{id}", methods={"GET"}) */ public function get($id) { /* ... */ }}
// Afterclass PostsController{ #[Route("/api/posts/{id}", methods: ["GET"])] public function get($id) { /* ... */ }}Constructor property promotion
Class constructors are often used to assign values to properties. This often means that the same variable names are repeated four times: once as class properties, once in the constructor parameter list, and twice within the constructor body.
PHP 8.0 introduces a shorthand syntax that greatly reduces the amount of boilerplate code. The following two snippets are functionally equivalent:
// Beforeclass Point { public float $x; public float $y; public float $z;
public function __construct( float $x = 0.0, float $y = 0.0, float $z = 0.0 ) { $this->x = $x; $this->y = $y; $this->z = $z; }}
// Afterclass Point { public function __construct( public float $x = 0.0, public float $y = 0.0, public float $z = 0.0, ) {}}Union types
Union types make it possible to declare variables or parameters that can hold different types of values. In previous versions union types could only be described using PHPDoc annotations, but PHP 8.0 introduces native union type declarations that are validated at runtime:
// Before/** * @param string|float|int $value */function printValue($value) {}
// Afterfunction printValue(string|float|int $value) {}Match expression
A match expression can be used to concisely handle multiple conditions by returning values based on an input. Match expressions differ from switch statements in several important ways:
-
The result of the
matchexpression can be immediately returned or assigned to a variable. -
matcharms are evaluated one by one. As soon as an arm matches the subject expression, the evaluation stops and thematchexpression returns a value. Nobreakstatements are needed. -
matchuses strict identity checks (===), whereasswitchuses weak equality checks (==).
$value = 8.0;
$result = match ($value) { '8.0' => 'This won’t match because `match` uses strict equality checks.', 8.0 => 'This will be returned as the result of the match expression.', 'foo', 'bar' => 'A single arm may match one of multiple expressions',};Nullsafe operator
The nullsafe operator (?->) makes it possible to safely access properties and methods of an object, even if the object is null. As soon as the left hand side of the nullsafe operator evaluates to null, the overall result immediately evaluates to null. In older PHP versions this would often require the use of explicit null checks:
// Before$country = null;if ($user !== null) { $address = $user->getAddress(); if ($address !== null) { $country = $address->getCountry(); }}
// After$country = $user?->getAddress()?->country;Saner string to number comparisons
When the equality operator (==) is used to compare values of different types, PHP implicitly converts one of the compared values to a different type.
In previous versions, comparisons between a number and a string would result in the string being implicitly converted to a number. Starting with PHP 8.0, only comparisons between a number and a numeric string will use numeric comparison. In all other cases, the number will be converted to a string.
// Before0 == 'foo'; // true0 == '0'; // true
// After0 == 'foo'; // false0 == '0'; // trueConsistent type errors for internal functions
Calling internal functions with invalid arguments would often cause them to fail silently, i.e. throw a warning and return null. Starting with PHP 8.0 most internal functions now throw Error exceptions regardless of whether you have enabled strict_types. This makes it easier to detect and handle errors.
strlen([]);// TypeError: strlen(): Argument #1 ($str) must be of type string, array givenAlso in this version…
PHP 8.0 also includes many smaller quality-of-life improvements:
-
PHP 8 introduces two JIT compilation engines which show about 3 times better performance on synthetic benchmarks and 1.5–2 times improvement on some specific long-running applications. However, performance is mostly the same as PHP 7.4 for normal web applications, so don’t get your hopes up. 🙃
-
You now have the ability to include trailing commas in parameter lists and closure use lists:
public function connect(string $hostname,string $database,string $username,string $password, // This is now allowed); -
Non-capturing catches make it possible to omit variables for exceptions if you don’t use them:
try {makeBooBoo();} catch (BooBooException) { // No longer need $e (or $ex)// …} -
throwis now an expression instead of a statement. This means that you can now do this:fn () => throw new Exception();$value = $foo ?? throw new Exception();$value = $foo ?: throw new Exception();$value = $foo ? $bar : throw new Exception(); -
$object::classcan now be used instead ofget_class($object)to retrieve the name of a class. -
str_contains(),str_starts_with(), andstr_ends_with()have finally made their way into the standard library.
PHP 8.1
PHP 8.1 was released on 25 November 2021. The active support for this version ends after 25 November 2023. However, it will still receive security support until 25 November 2024.
Enumerations
Until now, it was often necessary to use reflection-based third-party libraries or (ab)use class constants to define enumerations:
// Beforeabstract class State{ const TO_DO = 1; const IN_PROGRESS = 2; const REVIEW = 3; const DONE = 4;}
setState(State::IN_PROGRESS); // This workssetState(-1337); // But so do invalid valuesPHP 8.1 provides native support for enumerations, which can be used in place of a set of constants. A major benefit of enums over constants is that they can be type-checked.
A basic enumeration looks like this:
// Afterenum State{ case ToDo; case InProgress; case Review; case Done;}
function setState(State $state) {...}
setState(State::IN_PROGRESS); // This is type-checkedecho State::InProgress->name; // InProgressEnumerations can also be backed by a value of a certain type, which can be useful if values need to be (de)serialised:
// Afterenum State: string{ case ToDo = 'to_do'; case InProgress = 'in_progress'; case Review = 'review'; case Done = 'done';}
echo State::InProgress->value; // in_progress
State::from('to_do'); // Would throw ValueError if ‘to_do’ did not existState::tryFrom('to_do'); // Would return null if ‘to_do’ did not existReadonly properties
Readonly properties in PHP allow you to set a value only once and prevent further modifications. This reduces the risk of accidental changes in your code. Readonly properties are especially useful for value objects and data transfer objects:
class BlogData{ public readonly Status $status;
public function __construct(Status $status) { $this->status = $status; }}First-class callable syntax
The first-class callable syntax ((...)) allows functions to be treated as data, which makes it easier to pass them into functions, store them in variables, or return them from functions.
$foo = $this->foo(...);$foo();
$fn = strlen(...);$fn('Hello PHP 8.1!');New in initialisers
Objects can now be used as default parameter values, static variables, and global constants, as well as in attribute arguments.
// Beforeclass Service{ private Logger $logger;
public function __construct(Logger $logger = null) { $this->logger = $logger ?? new DefaultLogger(); }}
// Afterclass Service{ private Logger $logger;
public function __construct(Logger $logger = new DefaultLogger()) { $this->logger = $logger; }}Pure intersection types
Union types can be used to indicate that a value can be one of multiple types. Intersection types are similar, except that the value has to satisfy all type constraints. For example, in the snippet below $value must be both instanceof Iterator and Countable:
function count_and_iterate(Iterator&Countable $value) { foreach ($value as $val) { echo $val; }
count($value);}Never return type
The never return type can be used to annotate functions that will not return a value and either throw an exception or end the script’s execution with a die(), exit(), trigger_error() or similar functions like dd().
function redirect(string $uri): never { header('Location: ' . $uri); exit();}
function redirectToLoginPage(): never { redirect('/login'); echo 'Hello'; // Dead code detected by static analysis}Final class constants
Class constants can now be declared final so that they cannot be overridden in child classes.
class Foo{ final public const XX = 'foo';}
class Bar extends Foo{ public const XX = 'bar'; // Fatal error}Explicit octal numerical notation
Octal numbers can now be explicitly prefixed with 0o, which makes them easier to spot:
016 === 16; // false because `016` is octal for `14` and it’s confusing0o16 === 16; // false — not confusing with explicit notation016 === 14; // trueFibers
Fibers allow for more efficient and responsive asynchronous programming, making it easier to handle multiple tasks simultaneously without the need for traditional, resource-intensive threading.
You’re unlikely to do anything with fibers yourself, but PHP 8.1’s support for fibers should mean that if you use a (coroutine-based) concurrency library you’ll need considerably less boilerplate code to get things done.
// Before$httpClient->request('https://example.com/') ->then(function (Response $response) { return $response->getBody()->buffer(); }) ->then(function (string $responseBody) { print json_decode($responseBody)['code']; });
// After$response = $httpClient->request('https://example.com/');print json_decode($response->getBody()->buffer())['code'];Array unpacking support for string-keyed arrays
Array unpacking now also works for arrays with string keys:
$arrayA = ['a' => 1];$arrayB = ['b' => 2];
$foo = array_merge(['a' => 0], $arrayA, $arrayB); // ['a' => 1, 'b' => 2]$bar = ['a' => 0, ...$arrayA, ...$arrayB]; // ['a' => 1, 'b' => 2]Also in this version…
Most other changes that come with PHP 8.1 aren’t really worth mentioning here, except for this one: performance improvements! A benchmark with a Symfony demo app suggests that PHP 8.1 may run up to 23% faster than PHP 8.0.
PHP 8.2
PHP 8.2 was released on 8 December 2022. The active support for this version ends after 8 December 2024, but it will still receive security support until 8 December 2025.
Readonly classes
If you mark a class as readonly, PHP implicitly adds the readonly modifier to every declared property and prevents the creation of dynamic properties.
readonly class Money{ public function __construct( public string $bar, public int $baz, ) {}}Disjunctive normal form (DNF) types
Disjunctive normal form (DNF) types make it possible to arbitrarily combine intersection and union types, as long as intersections are grouped with brackets. This is a nice feature for those who like their code strongly-typed.
function serialise((Arrayable&Entity)|null $entity){}Allow null, false, and true as types
Until now it was possible to use ? to indicate that a function may return null, but not that a function will always return null.
function alwaysReturnsNull(): null;function strpos(): int|false;Similarly, it’s now also possible to use true and false as return types, which is useful for a function like strpos(), which either returns the position of a substring within a string as an int or false if the substring does not appear within the string.
New “Random” extension
PHP 8.2 introduces a new “Random” extension that is more cryptographically secure, promotes safe coding conventions by marking built-in implementations as final, and makes it possible to store state in objects rather than in a hidden, global state.
use Random\Engine\Xoshiro256StarStar;use Random\Randomizer;
$originalRng = new Xoshiro256StarStar(hash('sha256', 'Example seed', true));
$clonedRng = clone $originalRng;$randomizer = new Randomizer($clonedRng);$randomizer->shuffleArray([1, 2, 3, 4, 5, 6, 7, 8]);Constants in traits
Traits allow horizontal reuse of code across classes, but for some reason did not support constants yet. Now they do. Note that constant values can only be accessed from the class that uses a trait.
trait UsesInternalCombustionEngine{ public const SOUND = 'Vroom!';}
class VolkswagenGolf{ use UsesInternalCombustionEngine;}
var_dump(VolkswagenGolf::SOUND); // Vroom!var_dump(UsesInternalCombustionEngine::SOUND); // ErrorDeprecate dynamic properties
The creation of dynamic properties is now deprecated. This should help avoid mistakes and typos. Dynamic properties are still allowed on stdClass objects.
public class User{ public string $name;}
$user = new User();$user->name = 'Anna Kournikova'; // This is perfectly fine$user->namw = 'Kournikova'; // Deprecation notice
$user = new stdClass();$user->namw = 'Kournikova'; // This is still allowedAlso in this version…
PHP 8.2 also contains a few other small changes that are worth mentioning:
-
Now that dynamic properties are deprecated, you can use the
#[\AllowDynamicProperties]attribute to suppress deprecation notices when you try to use dynamic properties anyway.#[\AllowDynamicProperties]class User {}$user = new User();$user->doWhateverTheDuckIWant = true; -
You can now annotate parameters that may contain sensitive information and that should have their values redacted when they appear in a stack trace:
function login($username,#[\SensitiveParameter]$password,); -
String interpolation makes it possible to use variables within strings. The
${}notation has been deprecated, but you can still use{$foo}and$fooin your interpolated strings. -
strtolower()andstrtoupper()are no longer locale-sensitive.
PHP 8.3
PHP 8.3 was released on 23 November 2023. The active support for this version ends after 23 November 2025, but it will still receive security support until 23 November 2026.
Typed class constants
A lot of things in PHP can be typed, but strangely enough this wasn’t possible yet for constants. Starting with PHP 8.3, you can:
public class Dog{ const int LEGS = 4; const string ACTION_TYPE = 'bark';}Dynamic class constant fetch
Another quality-of-life improvement is the ability to retrieve constants dynamically using native PHP syntax. Previously, you had to pass a manually constructed string to the constant() function to dynamically get the value of a constant:
$name = 'ACTION_TYPE';var_dump(constant(Dog::class . '::' . $name));This becomes much easier in PHP 8.3:
var_dump(Foo::$name);New #[\Override] attribute
Java was my first object-oriented language, so I’ve been annotating methods that override a parent method with /** @override */ even though it has no effect in PHP.
The new #[\Override] attribute can be added to methods to ensure that a method with the same name exists in a parent class or an implemented interface. Neat!
class Archer implements JobClass{ #[\Override] public function getHitPointBonus(): int { // To do }}Deep cloning of readonly properties
Read-only classes are read-only, which means that you can never modify any of their values. It turns out that this limits the usefulness of such classes a bit too much.
In PHP 8.3 you are allowed to modify readonly properties at most once when you clone an object, which makes it possible to make deep clones:
readonly class Credit { public function __construct(public Value $value) {}
public function __clone(): void { $this->value = clone $this->value; }}New json_validate() function
PHP 8.3 introduces a json_validate() function that returns true if you pass it a string that is syntactically valid JSON. This is more efficient than using json_decode().
var_dump(json_validate('{ "planet": { "size": "your mom" } }'));New Randomizer methods
PHP 8.2’s Random extension has a three new methods. The first, getBytesFromString() makes it easier to generate random strings that only consist of specific bytes:
$randomizer = new \Random\Randomizer();$randomizer->getBytesFromString('abcdef0123456789', 42);The other two are getFloat() and nextFloat(). These functions make it easier to generate unbiased random floats, which apparently can be pretty hard to implement correctly yourself.
Command line linter supports multiple files
Frankly I didn’t even know that PHP has a built-in linter. Not only does it exist, it has also been improved a bit, as it can now check the syntax of multiple files at the same time:
$ php -l upload.php download.phpNo syntax errors detected in upload.phpNo syntax errors detected in download.phpAlso in this version…
PHP 8.3 also includes a number of other small improvements. Some can be useful for those who work with Document Object Models (DOMs) and calendars, while others like str_increment()… I’m not quite sure what to think about this.