PHP 8.0: What's New? (1/4)

6 months ago by David Grudl translated by Pavel Linhart     edit

PHP version 8.0 is being released right now. It's full of new stuff like no other version before. Their introduction deserved four separate articles. In the first one we'll take a look at what it brings at the language level.

Before we delve into PHP, let it be known that the current version of Nette is fully prepared for the eighth version. Furthermore, as a gift, a fully compatible Nette 2.4 has been released, so from the framework standpoint there's nothing preventing you from using it.

Named arguments

Let's start right away with a bomb, which could be boldly designated as a game changer. Arguments can be now passed to functions and methods not only positionally, but according to their name. Which is absolutely cool in case a method has really too much parameters:

class Response implements IResponse
{
	public function setCookie(
		string $name,
		string $value,
		string|DateInterface|null $time,
		string $path = null,
		string $domain = null,
		bool $secure = null,
		bool $httpOnly = null,
		string $sameSite = null
	) {
		...
	}
}

First two arguments are passed positionally, others by their name: (named must follow after the positional ones)

$response->setCookie('lang', $lang, sameSite: 'None');

// instead of the horrible
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Before the introduction of this feature there were plans to create a new API for sending cookies in Nette, because the number of arguments for setCookie() really grew and the positional notation was confusing. That's not needed anymore, because named arguments are in this case the most convenient API. IDE will hint them and there's type safety.

​They are ideally suited even for explaining ​logical arguments, where their usage is not necessary, but a plain true or false doesn't cut it:

// before
$db = $container->getService(Database::class, true);

// now
$db = $container->getService(Database::class, need: true);

​Argument names are now a part of the public API. It's not possible to change them at will anymore. Because of this even Nette is going through an audit determining whether all arguments have a suitable name.​

Named arguments can also be used in combination with variadics:

function variadics($a, ...$args) {
	dump($args);
}

variadics(a: 1, b: 2, c: 3);
// $args will contain ['b' => 2, 'c' => 3]

​The $args array can now contain even non-numerical keys, which is sort of a BC break. The same applies to the behaviour of call_user_func_array($func, $args), where keys in the $args array now play a much more significant role. On the contrary, functions of the func_*() family are shielded from named arguments.

Named arguments are closely related to the the fact that the splat operator ... can now expand associative arrays:

variadics(...['b' => 2, 'c' => 3]);

​Surprisingly it doesn't work inside arrays at the moment:

$arr = [ ...['a' => 1, 'b' => 2] ];
// Fatal error: Cannot unpack array with string keys

​The combination of named arguments and variadics gives the option to finally have a fixed syntax, for example, for the link() method, to which we can pass named arguments as well as positional ones:

// before
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // had to be an array

// now
$presenter->link('Product:detail', $id, page: 1);

The syntax for named arguments is much sexier than ​writing arrays, so Latte immediately adopted it, where it can be used, for example, in the {include} and {link} tags:

{include 'file.latte' arg1: 1, arg2: 2}
{link default page: 1}

We'll return to named arguments in the third part of the series in connection with attributes.

An expression can throw an exception

Throwing an exception is now an expression. You can wrap it in parentheses and add to an if condition. Hmmm, that doesn’t sound very practical. However, this is much more interesting:

// before
if (!isset($arr['value'])) {
	throw new \InvalidArgumentException('value not set');
}
$value = $arr['value'];


// now, when throw is an expression
$value = $arr['value'] ?? throw new \InvalidArgumentException('value not set');

Because arrow functions can so far only contain one expression, they can now throw exceptions thanks to this feature:

// only single expression
$fn = fn() => throw new \Exception('oops');

Match Expression

The switch-case statement has two major vices:

  • it uses non-strict comparison == instead of ===
  • you have to be careful not to forget about break by mistake

Because of this, PHP comes with an alternative in the form of a new match expression, which uses strict comparison and, conversely, doesn’t use break.

switch code example:

switch ($statusCode) {
    case 200:
    case 300:
        $message = $this->formatMessage('ok');
        break;
    case 400:
        $message = $this->formatMessage('not found');
        break;
    case 500:
        $message = $this->formatMessage('server error');
        break;
    default:
        $message = 'unknown status code';
        break;
}

And the same (only with strict comparison) written using match:

$message = match ($statusCode) {
    200, 300 => $this->formatMessage('ok'),
    400 => $this->formatMessage('not found'),
    500 => $this->formatMessage('server error'),
    default => 'unknown status code',
};

Note that match is not a control structure like switch, but an expression. In the example, we assign its resulting value to a variable. At the same time, individual “options” are also expressions, so it is not possible to write more steps, as in the case of switch.

In case of no match (and there is no default clause), the UnhandledMatchError exception is thrown.

By the way, there are also {switch}, {case} and {default} tags in Latte. Their function corresponds exactly to the new match. They use strict comparison, don’t require break and it’s possible to specify multiple values separated by commas in case.

Nullsafe operator

Optional chaining allows you to write an expression, whose evaluation stops if it encounters null. That’s thanks to the new ?-> operator. It replaces a lot of code that would otherwise have to repeatedly check for null:

$user?->getAddress()?->street

// approximately translates to
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Why “approximately”? Because in reality the expression is being evaluated more ingeniously so no step is repeated. For example, $user->getAddress() is called only once, so the problem caused by the method returning something different for the first and second time can’t be encountered.

This feature was brought by Latte a year ago. Now PHP itself is adopting it. Great.

Constructor property promotion

Syntactic sugar that saves writing the type twice and the variable four times. It’s a pity it didn’t come at a time when we didn’t have such clever IDEs that today write it for us 🙂

class Facade
{
	private Nette\Database\Connection $db;
	private Nette\Mail\Mailer $mailer;

	public function __construct(Nette\Database\Connection $db, Nette\Mail\Mailer $mailer)
	{
		$this->db = $db;
		$this->mailer = $mailer;
	}
}
class Facade
{
	public function __construct(
		private Nette\Database\Connection $db,
		private Nette\Mail\Mailer $mailer,
	) {}
}

It works with Nette DI, you can start using it.

Stricter type checks for arithmetic and bitwise operators

What once helped dynamic scripting languages rise to prominence has become their weakest point. Once, PHP got rid of “magic quotes”, global variables registration and now relaxed behaviour is being replaced by strictness. The time, when in PHP you could add, multiply etc. almost any data type for which it made no sense, is long gone. Starting with version 7.0, PHP is becoming more and more strict and since version 8.0, an attempt to use any arithmetic/bitwise operators on arrays, objects or resources ends with TypeError. The exception is additions of arrays.

// arithmetic and bitwise operators
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Saner string to number comparisons

Or make loose operator great again.

It would seem that there's no longer room for the loose operator ==, that it’s just a typo when writing ===, but this change returns it back to the map again. If we already have it, let it behave reasonably. As a result of the earlier “unreasonable” comparison, for example, in_array() could troll you unpleasantly:

$validValues = ['foo', 'bar', 'baz'];
$value = 0;

dump(in_array($value, $validValues));
// surprisingly returned true
// since PHP 8.0 returns false

The change in behaviour of == concerns the comparison of numbers and “numeric” strings and is shown in the following table:

Comparison Before PHP 8.0
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42 " true true
42 == "42foo" true false
42 == "abc42" false false
"42" == " 42" true true
"42" == "42 " false true

Surprisingly, it’s a BC break at the very core of the language, that has been approved without any resistance. And that’s good. JavaScript could be very jealous in this regard.

Error reporting

Many internal functions now trigger TypeError and ValueError instead of warnings that were easy to ignore. A number of kernel warnings have been reclassified. The shutup operator @ now doesn’t silence fatal errors. And PDO throws exceptions by default.

Nette always tried to solve these things in some way. Tracy modified the behaviour of the shutup operator, Database switched PDO behaviour, Utils contain replacement for standard functions, which throw exceptions instead of easy-to-miss warnings, etc. It’s nice to see that the strict direction Nette has in its DNA becomes the native direction of the language.

Negative array key increments

$arr[-5] = 'first';
$arr[] = 'second';

What will be the key of the second element? It used to be 0, since PHP 8 it’s -4.

Trailing comma

The last place, where the trailing comma couldn’t be, was the definition of function arguments. This is a thing of the past:

	public function __construct(
		Nette\Database\Connection $db,
		Nette\Mail\Mailer $mailer, // trailing comma
	) {
		....
	}

$object::class

The magic constant ::class also works with objects $object::class, completely replacing the get_class() function.

Catch exceptions only by type

And finally: it’s not necessary to specify a variable for the exception in the catch clause:

try {
	$container->getService(Database::class);

} catch (MissingServiceException) {  // no $e
	$logger->log('....');
}

In the next parts, we’ll see major innovations regarding data types, we’ll show what attributes are, what new functions and classes have appeared in PHP and we’ll introduce the Just in Time Compiler.

Further reading