PHP 8.0: Visão geral completa das novidades (1/4)

há 4 anos De David Grudl  

A versão 8.0 do PHP foi lançada. Está tão cheia de novidades como nenhuma versão anterior. A apresentação delas exigiu quatro artigos separados. Neste primeiro, veremos o que há de novo em termos de linguagem.

Antes de mergulharmos no PHP, saiba que a versão atual do Nette está totalmente preparada para o PHP 8. Além disso, como presente, foi lançado também o Nette 2.4, que é totalmente compatível com ele, então, do ponto de vista do framework, nada impede que você comece a usar a nova versão.

Argumentos nomeados

E começamos logo com uma bomba, que pode ser considerada um game changer. Agora é possível passar argumentos para funções e métodos não apenas posicionalmente, mas também pelos seus nomes. O que é absolutamente ótimo no caso de um método ter muitos parâmetros:

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
	) {
		...
	}
}

Passamos os dois primeiros argumentos posicionalmente, os seguintes pelo nome: (os nomeados devem sempre vir depois dos posicionais)

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

// em vez do insano
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Antes da chegada deste recurso, estava planeado criar uma nova API no Nette para enviar cookies, porque o número de parâmetros de setCookie() realmente cresceu e a notação posicional era confusa. Agora isso não é mais necessário, porque os argumentos nomeados são, neste caso, a API mais prática. O IDE irá sugeri-los e eles têm verificação de tipo.

Também são ótimos para esclarecer parâmetros lógicos, onde seu uso não é necessário, mas true ou false por si só não dizem muito:

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

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

Os nomes dos parâmetros agora se tornam parte da API pública. Não é possível alterá-los arbitrariamente como antes. Por esse motivo, o Nette também está passando por uma auditoria para verificar se todos os parâmetros têm nomes apropriados.

Argumentos nomeados também podem ser usados em combinação com variadics:

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

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

Agora, o array $args pode conter chaves não numéricas, o que é uma certa quebra de compatibilidade (BC break). O mesmo se aplica ao comportamento da função call_user_func_array($func, $args), onde as chaves no array $args agora desempenham um papel significativo. Por outro lado, as funções da família func_*() estão isoladas dos argumentos nomeados.

Com os argumentos nomeados também está intimamente relacionado o fato de que o operador splat ... agora pode desempacotar arrays associativos:

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

Curiosamente, isso ainda não funciona dentro de arrays:

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

A combinação de argumentos nomeados e variadics oferece a possibilidade de finalmente ter uma sintaxe fixa, por exemplo, para o método link() do presenter, ao qual agora podemos passar argumentos nomeados da mesma forma que os posicionais:

// antes
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // tinha que ser um array

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

A sintaxe dos argumentos nomeados é muito mais sexy do que escrever arrays, portanto Latte adotou-o imediatamente, onde pode ser usado, por exemplo, nas tags {include} e {link}:

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

Voltaremos aos parâmetros nomeados na terceira parte em conexão com atributos.

Uma expressão pode lançar uma exceção

Lançar uma exceção agora é uma expressão. Você pode, por exemplo, envolvê-la em parênteses e adicioná-la a uma condição if. Hmmm, isso não parece muito prático. Mas isto já é mais interessante:

// antes
if (!isset($arr['value'])) {
	throw new \InvalidArgumentException('valor não definido');
}
$value = $arr['value'];


// agora, quando throw é uma expressão
$value = $arr['value'] ?? throw new \InvalidArgumentException('valor não definido');

Como as arrow functions até agora só podiam conter uma única expressão, graças a este recurso, elas podem lançar exceções:

// apenas uma expressão
$fn = fn() => throw new \Exception('ops');

Expressões Match

A construção switch-case tem duas grandes desvantagens:

  • usa comparação não estrita == em vez de ===
  • você precisa ter cuidado para não esquecer acidentalmente o break

Portanto, o PHP apresenta uma alternativa na forma da nova construção match, que usa comparação estrita e, por outro lado, não usa break.

Exemplo de código switch:

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;
}

E o mesmo (apenas com comparação estrita) escrito usando match:

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

Note que match não é uma estrutura de controle como switch, mas uma expressão. No exemplo, atribuímos seu valor resultante a uma variável. Ao mesmo tempo, as “opções” individuais também são expressões, portanto, não é possível escrever vários passos, como no caso de switch.

Se nenhuma das opções corresponder (e não houver cláusula default), uma exceção UnhandledMatchError será lançada.

A propósito, no Latte também existem as tags {switch}, {case} e {default}. Seu funcionamento corresponde exatamente ao novo match. Elas usam comparação estrita, não exigem break e em case é possível indicar vários valores separados por vírgulas.

Operador Nullsafe

O encadeamento opcional (optional chaining) permite escrever uma expressão cuja avaliação para se encontrar null. E isso graças ao novo operador ?->. Ele substitui muito código que, de outra forma, verificaria repetidamente por null:

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

// significa aprox.
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Por que “significa aprox.”? Porque, na realidade, a expressão é avaliada de forma mais inteligente e nenhum passo é repetido. Por exemplo, $user->getAddress() é chamado apenas uma vez, portanto, não pode ocorrer o problema causado pelo método retornar algo diferente na primeira e na segunda vez.

Esta novidade fresca foi trazida pelo Latte há um ano. Agora chega ao próprio PHP. Ótimo.

Promoção de propriedade do construtor

Açúcar sintático que economiza a escrita dupla do tipo e a escrita quádrupla da variável. Pena que não chegou na época em que não tínhamos IDEs tão inteligentes, que hoje escrevem isso por nós 🙂

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,
	) {}
}

Funciona com o Nette DI, você pode começar a usar imediatamente.

Comportamento estrito de operadores aritméticos e bitwise

O que antes catapultou as linguagens de script dinâmicas para o estrelato, com o tempo se tornou seu ponto mais fraco. O PHP se livrou das “magic quotes”, do registro de variáveis globais e agora o comportamento relaxado está sendo substituído pela estrita. A época em que você podia somar, multiplicar, etc., em PHP quase qualquer tipo de dados para os quais isso não fazia o menor sentido, já passou há muito tempo. A partir da versão 7.0, o PHP está se tornando cada vez mais estrito e, a partir da versão 8.0, a tentativa de usar qualquer operador aritmético/bitwise em arrays, objetos ou resources resulta em um TypeError. A exceção é a adição de arrays.

// operadores aritméticos e bitwise
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Comparação mais sensata de strings e números

Ou seja, make loose operator great again.

Pareceria que para o operador frouxo == já não há lugar, que é apenas um erro de digitação ao escrever ===, mas esta mudança o coloca de volta no mapa. Já que o temos, que se comporte de forma sensata. A consequência da comparação “insensata” anterior foi, por exemplo, o comportamento de in_array(), que poderia te pegar de surpresa:

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

dump(in_array($value, $validValues));
// surpreendentemente retornava true
// a partir do PHP 8.0 retorna false

A mudança no comportamento de == diz respeito à comparação de números e strings “numéricas” e é mostrada na tabela a seguir:

Comparação Antes 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

Surpreendente é que se trata de uma quebra de compatibilidade (BC break) na própria base da linguagem, que foi aprovada sem qualquer resistência. E isso é bom. Aqui o JavaScript poderia ter muita inveja.

Relatório de erros

Muitas funções internas agora lançam TypeError e ValueError em vez de avisos, que podiam ser facilmente ignorados. Vários avisos do núcleo foram reclassificados. O operador shutup @ agora não silencia erros fatais. E o PDO no modo padrão lança exceções.

O Nette sempre tentou resolver essas coisas de alguma forma. O Tracy modificava o comportamento do operador shutup, o Database alternava o comportamento do PDO, o Utils contém substitutos para funções padrão que lançam exceções em vez de avisos discretos, etc. É bom ver que a direção estrita, que está no DNA do Nette, está se tornando a direção nativa da linguagem.

Arrays com índice negativo

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

Qual será a chave do segundo elemento? Antes era 0, a partir do PHP 8 é -4.

Vírgula final

O último lugar onde não podia haver uma vírgula final era na definição de parâmetros de função. Isso já é passado:

	public function __construct(
		Nette\Database\Connection $db,
		Nette\Mail\Mailer $mailer, // vírgula final
	) {
		....
	}

$object::class

A constante mágica ::class também funciona com objetos $object::class, substituindo completamente a função get_class().

catch sem variável

E finalmente: na cláusula catch não é necessário indicar uma variável para a exceção:

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

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

Nas próximas partes, veremos novidades significativas nos tipos de dados, mostraremos o que são atributos, quais novas funções e classes apareceram no PHP e apresentaremos o Just in Time Compiler.