PHP 8.0: Novas funções, classes e JIT (4/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 último, veremos as novas funções e classes e apresentaremos o Just in Time Compiler.

Novas funções

A biblioteca padrão do PHP possui centenas de funções e na versão 8.0 apareceram seis novas. Parece pouco, mas a maioria delas preenche lacunas da linguagem. O que corresponde bem ao tom de toda a versão 8.0, que aprimora e consolida o PHP como nenhuma versão anterior. Você pode encontrar uma visão geral de todas as novas funções e métodos no guia de migração.

str_contains() str_starts_with() str_ends_with()

Funções para verificar se uma string começa, termina ou contém uma substring.

if (str_contains('Nette', 'te')) {
	...
}

Juntamente com a chegada deste trio, o PHP define como lidar com a string vazia ao pesquisar, o que também orienta todas as outras funções relacionadas, de forma que a string vazia é encontrada em todos os lugares:

str_contains('Nette', '')     // true
str_starts_with('Nette', '')  // true
strpos('Nette', '')           // 0 (anteriormente false)

Graças a isso, o comportamento do trio é completamente idêntico aos análogos no Nette:

str_contains()      # Nette\Utils\String::contains()
str_starts_with()   # Nette\Utils\String::startsWith()
str_ends_with()     # Nette\Utils\String::endsWith()

Por que essas funções são tão importantes? As bibliotecas padrão de todas as linguagens são sempre sobrecarregadas pelo desenvolvimento histórico e é impossível evitar o surgimento de inconsistências e erros. Mas, ao mesmo tempo, é o cartão de visita de cada linguagem. É surpreendente que no PHP, com 25 anos de idade, faltem funções para operações tão básicas como retornar o primeiro ou o último elemento de um array, escapar HTML sem truques (htmlspecialchars não escapa apóstrofo) ou justamente pesquisar uma string dentro de outra. Que isso possa ser contornado de alguma forma não se sustenta, porque o resultado não é um código legível e compreensível. É uma lição para todos os autores de API. Quando você vê que uma parte considerável da documentação de uma função é ocupada pela explicação de suas peculiaridades (como os valores de retorno de strpos), é um sinal claro para ajustar a biblioteca e adicionar justamente str_contains.

get_debug_type()

Substitui o já obsoleto get_type(). Em vez de tipos longos como integer, retorna o int usado hoje; no caso de objetos, retorna diretamente o tipo:

Valor gettype() get_debug_type()
'abc' string string
[1, 2] array array
231 integer int
3.14 double float
true boolean bool
null NULL null
new stdClass object stdClass
new Foo\Bar object Foo\Bar
function() {} object Closure
new class {} object class@anonymous
new class extends Foo {} object Foo@anonymous
curl_init() resource resource (curl)
curl_close($ch) resource (closed) resource (closed)

Objetificação de recursos

Valores do tipo resource vêm dos tempos em que o PHP ainda não tinha objetos, mas na verdade precisava deles. Assim surgiram os resources. Hoje temos objetos e, em comparação com os resources, eles funcionam muito melhor com o garbage collector, então o plano é substituí-los gradualmente por objetos.

A partir do PHP 8.0, os resources de imagens, conexões curl, openssl, xml, etc. são alterados para objetos. No PHP 8.1, será a vez das conexões FTP, etc.

$res = imagecreatefromjpeg('image.jpg');
$res instanceof GdImage  // true
is_resource($res)        // false - Quebra de BC

Esses objetos ainda não têm métodos, nem você pode criar instâncias deles diretamente. Por enquanto, trata-se realmente apenas de tirar os resources obsoletos do PHP sem alterar a API. E isso é bom, porque criar uma boa API é uma tarefa separada e exigente. Ninguém deseja que surjam no PHP outras classes como SplFileObject com métodos nomeados fgetc() ou fgets().

PhpToken

O tokenizador também está se movendo para objetos, e, portanto, as funções em torno de token_get_all. Desta vez, não se trata de se livrar de resources, mas obtemos um objeto completo que representa um token PHP.

<?php

$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // instância PhpToken

echo $token->id;             // T_OPEN_TAG
echo $token->text;           // '<?php'
echo $token->line;           // 1
echo $token->getTokenName(); // 'T_OPEN_TAG'
echo $token->is(T_STRING);   // false
echo $token->isIgnorable();  // true

O método isIgnorable() retorna true para os tokens T_WHITESPACE, T_COMMENT, T_DOC_COMMENT e T_OPEN_TAG.

Weak maps

Weak maps estão relacionados ao garbage collector, que libera da memória todos os objetos e valores que não estão mais em uso (ou seja, não há nenhuma variável ou propriedade em uso que os contenha). Como a vida de uma thread PHP é efêmera e hoje temos memória suficiente disponível nos servidores, geralmente não nos preocupamos com questões relacionadas à liberação eficiente de memória. Mas para scripts de execução mais longa, elas são cruciais.

O objeto WeakMap é semelhante ao SplObjectStorage. Em ambos, objetos são usados como chaves e permitem o armazenamento de quaisquer valores sob eles. A diferença é que WeakMap não impede que o objeto seja liberado pelo garbage collector. Ou seja, se o único lugar onde o objeto ainda existe for a chave em um weak map, ele será removido do mapa e da memória.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'data for $obj';

dump(count($map));  // 1
unset($obj);
dump(count($map));  // 0

Para que isso é bom? Por exemplo, para cache. Tenhamos um método loadComments(), ao qual passamos um artigo de blog e ele retorna todos os seus comentários. Como o método é chamado repetidamente com o mesmo artigo, criaremos também getComments(), que armazenará em cache o resultado do primeiro método:

class Comments
{
	private WeakMap $cache;

	public function __construct()
	{
		$this->cache = new WeakMap;
	}

	public function getComments(Article $article): ?array
	{
		$this->cache[$article] ??= $this->loadComments($article);
		return $this->cache[$article]
	}

	...
}

O truque é que, no momento em que o objeto $article for liberado (por exemplo, a aplicação começar a trabalhar com outro artigo), sua entrada no cache também será liberada.

PHP JIT (Just in Time Compiler)

Talvez você saiba que o PHP é compilado para o chamado opcode, que são instruções de baixo nível que você pode ver, por exemplo, aqui e que são executadas pela máquina virtual do PHP. E o que é JIT? O JIT pode compilar transparentemente o PHP diretamente para código de máquina, que é executado diretamente pelo processador, evitando assim a execução mais lenta pela máquina virtual.

O JIT, portanto, visa acelerar o PHP.

A tentativa de implementar JIT no PHP remonta a 2011 e é obra de Dmitry Stogov. Desde então, ele experimentou 3 implementações diferentes, mas nenhuma delas chegou ao PHP estável pelos seguintes motivos: o resultado nunca foi um aumento substancial de desempenho para aplicações web típicas; complica a manutenção do PHP (ou seja, ninguém além de Dmitry entende 😉); existiam outras maneiras de melhorar o desempenho sem precisar usar JIT.

O salto de desempenho do PHP na versão 7 foi um subproduto do trabalho no JIT, embora paradoxalmente ele não tenha sido implantado. Isso só acontece no PHP 8. Mas vou frear imediatamente as expectativas exageradas: provavelmente você não notará nenhuma aceleração.

Então, por que o JIT entra no PHP? Em primeiro lugar, outras formas de melhorar o desempenho estão se esgotando lentamente e o JIT está simplesmente na vez. Embora não traga aceleração em aplicações web comuns, ele acelera significativamente, por exemplo, cálculos matemáticos. Abre-se assim a possibilidade de começar a escrever essas coisas em PHP. E, na verdade, seria possível implementar diretamente em PHP funções que até agora exigiam implementação direta em C por causa da velocidade.

O JIT faz parte da extensão opcache e é ativado junto com ela no php.ini (leia a documentação sobre aqueles quatro dígitos):

zend_extension=php_opcache.dll
opcache.jit=1205              ; configuração usando o quarteto OTRC
opcache.enable_cli=1          ; para funcionar também no CLI
opcache.jit_buffer_size=128M  ; memória reservada para o código compilado

Que o JIT está em execução, você saberá, por exemplo, no painel de informações na Tracy Bar.

O JIT funciona muito bem quando todas as variáveis têm tipos claramente definidos e não podem mudar durante chamadas repetidas do mesmo código. Portanto, estou curioso para saber se um dia declararemos tipos também para variáveis no PHP: string $s = 'Olá, esta é a conclusão da série';