PHP 8.0: Complete Overview of News (1/4)
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.
Sign in to submit a comment