Novidades do PHP 7

Novidades sobre o PHP 7, discutindo os novos recursos.

Introdução

No final de 2015 foi lançado finalmente o PHP 7.0.0, que estabeleceu uma nova "era" para o PHP e deixou as versões 5.X no passado. As versões 5.X (que começaram em 2004) tiveram uma grande importância na história do PHP, embora alguns tropeços também.

O que algumas pessoas podem se perguntar é por que a versão 5.X saltou para 7.X sem passar pela 6.X? Acontece que no passado houve a implementação do que seria o PHP 6. O projeto iniciou em 2005, mas acabou sendo "abandonado" em 2010. Chegaram até a lançar algumas versões alpha ou beta que cheguei a testar, mas a versão final nunca foi lançada por conta da complexidade que agregou ao núcleo do PHP. A feature que gerou toda a confusão foi o suporte nativo a Unicode. Porém, mesmo abandonando a versão 6, os desenvolvedores da linguagem optaram por lançar algumas das outras features previstas nas versões 5.3.X em diante mesmo. Com isso, vimos as versões 5.X terem uma enorme mudança desde o lançamento da versão 5.0.0 até as versões 5.6.X atuais. Afinal, foram mais de uma década desta versão.

Enfim, como o PHP 6 chegou a ser arquitetado e divulgado com um conjunto de features específicas e até artigos e livros sobre o assunto foram publicados (antes mesmo da versão final ser lançada), optaram por saltar esse número na contagem de versões do PHP para evitar confusões. Para mais detalhes sobre isso, você pode ler o RFC sobre o próximo nome do PHP.

Bem, mas estamos aqui para falar de PHP 7. Estou um pouco enferrujado para escrever artigos, mas vamos ver se esse ano volto ao ritmo.


Novidades do PHP 7

Primeiramente, vamos dar uma olhada na lista das principais novidades do PHP 7, na ordem de importância na minha opinião:

Além destas melhorias, o PHP 7 corrigiu uma enorme lista de bugs conhecidos. Para mais detalhes, você pode consultar a lista completa de recursos do PHP 7.

Algo importante a se destacar é que, quando o número de revisão mais importante (o primeiro número) é incrementado, então aquela versão pode conter recursos que são incompatíveis com as versões anteriores. Ou seja, um código que está funcionando com PHP 5.6 pode não funcionar no PHP 7 em algumas ocasiões. Porém, este tipo de inconveniência é sempre muito debatido e evitado pelos desenvolvedores do PHP. Falaremos sobre estas incompatibilidades a diante.

A velocidade do PHP 7

Performance

Certamente o que mais me chama a atenção no PHP 7 é a velocidade. Nos últimos anos começaram a surgir várias iniciativas para utilizar o PHP com mais velocidade. Por exemplo, a iniciativa do Facebook na criação da linguagem Hack, que é praticamente um fork do PHP, porém executado por uma HHVM através de um processo conhecido como "JIT compilation". Antes disso, o próprio Facebook também havia tentado o HPHPc, que tinha como objetivo converter código PHP em código em C, então compilá-lo.

Creio que os desenvolvedores do PHP começaram a se preocupar com o rumo que a linguagem PHP poderia ter, frente às iniciativas do Facebook, especialmente nos cenários em que a performance é algo crítico.

No final das contas, o PHP 7 foi lançado com um novo motor com o código refatorado, a "Zend Engine 3", que passou a ser tão rápida quanto a HHVM. Se você procurar na Internet alguns benchmarks comparando as duas, vai notar que em algumas ocasiões o PHP 7 supera o HHVM e o contrário também acontece. Isso porque o benchmark depende muito do cenário que se está testando e isso leva em conta muitos fatores (a forma como o código PHP testado foi implementado, quais recursos foram usados, qual servidor HTTP foi usado, quantas requisições em paralelo foram feitas, etc). Pra mim, o importante é que o PHP 7 é muito superior ao PHP 5 em performance, e atingiu um nível aceitável frente a iniciativas como o HHVM.

Declaração de tipos escalares no PHP 7

A declaração de tipos escalares é a possibilidade de utilizar as palavras reservadas "int", "string", "float" e "bool" como indução de tipo (type hinting) em funções. Continua sendo possível utilizar as palavras reservadas "array", "callback", "callable" e "self", além de nomes de classes e nomes de interfaces neste propósito.

O benefício deste recurso é que códigos passam a rodar de forma mais consistente e segura, do ponto de vista de funcionamento. Afinal, se algum programador escreve um código que chama uma função que espera receber tipos específicos, caso seja passado um valor com tipo inesperado ou valor inesperado, então uma exceção é lançada. Isso força os desenvolvedores a prestarem mais atenção ao que estão passando às funções, e façam as devidas conversões, caso necessário. Vejamos um exemplo:

function proximoNumero(int $a) {
    return $a + 1;
}

A indução de tipo possui dois modos de avaliação: o frouxo (que é o padrão) e o estrito.

No modo estrito (strict), os parâmetros passados precisam ser exatamente do tipo esperado (ou NULL, caso o parâmetro permita receber NULL como valor default).

No modo frouxo (coercive), os parâmetros passados podem ter um tipo diferente do esperado, mas precisam ser "aceitáveis", segundo estas regras:

  • int:
    • aceita valores inteiros
    • aceita valores float (são apenas convertidos para inteiro)
    • aceita valores booleanos (true vira 1, e false vira 0)
    • aceita strings, desde que respeitem a expressão regular /^[+-]?\d+/ (os símbolos subsequentes são ignorados)
  • float:
    • aceita valores float
    • aceita valores inteiros (são apenas convertidos para float)
    • aceita valores booleanos (true vira 1.0, e false vira 0.0)
    • aceita strings, desde que respeitem a expressão regular /^[+-]?(\d+|\d*\.\d+)(e\d+)?/ (os símbolos subsequentes são ignorados)
  • bool:
    • aceita valores booleanos
    • aceita valores inteiros (inteiro 0 vira false, o resto vira true)
    • aceita valores float (float 0.0 vira false, o resto vira true)
    • aceita strings (string "" ou "0" viram false, o resto vira true)

Para utilizar o modo estrito é preciso incluir uma declaração no início do arquivo em que se deseja aplicar este modo (para cada arquivo é necessário incluir esta declaração):

<?php
declare(strict_types=1);

Outra observação importante é que os parâmetros com indução de tipo escalar podem também ter um valor default (respeitando o tipo declarado), ou o valor default NULL. Portanto, se uma função precisa saber se um parâmetro de tipo escalar foi informado pelo programador ou não, então ela precisa ter valor default NULL e dentro da função o valor é checado se vale NULL ou não.

Por fim, mas não menos importante, é importante dizer que quando um parâmetro com indução de tipo escalar é declarado para ser recebido por referência (ao invés do padrão, que é por cópia), então se a função precisou aplicar o casting, ele será refletido na variável passada por referência. Veja o exemplo abaixo para entender:

function proximoNumero(int &$a) {
    return $a + 1;
}

$meuNumero = "123";
var_dump($meuNumero); // vai mostrar: string(3) "123"
proximoNumero($meuNumero);
var_dump($meuNumero); // vai mostrar: int(123)

Note que, mesmo que a função não tenha modificado explicitamente o valor do parâmetro $a, quando passamos uma variável com uma string para esta função, ela modificou o tipo da variável para int por causa do casting automático sofrido durante a execução da função.

Declaração de retornos de funções

Diferente da indução de tipo citada na seção anterior (que foi melhorada para suportar tipos escalares no PHP 7), a declaração de retornos de funções é algo totalmente novo no PHP, pois não era possível até então.

A forma como se especifica o tipo de dado retornado por uma função é colocando o sinal de dois-pontos seguido do tipo de dado devolvido pela função antes da implementação da função (ou seja, antes de abrir chaves da função), conforme o exemplo:

function soma(int $a, int $b): int {
    return $a + $b;
}

Os nomes que podem ser usados como tipos incluem "int", "float", "bool", "string", "array", "self", nomes de classes ou nomes de interfaces.

O funcionamento é similar à indução de tipo, ou seja, uma exception é emitida caso algum valor seja incompatível com o esperado. A diferença é que neste caso a exception é emitida quando o valor retornado pela função é incompatível com o esperado. Isso é avaliado em tempo de execução. Portanto, não ocorrerá um erro de sintaxe se você cria uma função assim:

function teste(): int {
    return "abc";
}

Porém, assim que esta função for chamada, ela tentará retornar uma string com valor inválido para o tipo de retorno esperado (inteiro), então a exception será emitida.

A única observação a se destacar é que esta declaração de retorno de funções também respeita a configuração definida por "strict_types", ou seja, possui o modo frouxo por padrão, mas pode assumir o modo estrito se desejado.

Hierarquia de Exceptions no PHP 7

Historicamente, funções do PHP sempre utilizaram um mecanismo de erro próprio, que comentei no artigo Controle de Erros em PHP. Porém, no paradigma orientado a objetos é comum a utilização de controle de Exceptions, que também comentei nos artigos Try catch finally em PHP e Utilizando as Exceptions da SPL do PHP.

Para botar ordem na casa, padronizar a forma como o PHP lida com erros fatais, mas manter certa compatibilidade com o legado, o PHP 7 introduziu uma nova hierarquia de exceptions que é a seguinte:

  • A interface Throwable (nova no PHP 7) é a interface base para exceptions e erros.
    • A classe Exception (existente desde o PHP 5.1) passou a implementar a interface Throwable.
    • A classe Error (nova no PHP 7) também implementa a interface Throwable e é disparada no lugar dos antigos mecanismos de disparo de erro considerados fatais (trigger_error).
      • A classe TypeError extende a classe Error e é disparada em casos de erros de tipo.
      • A classe ParserError extende a classe Error e é disparada em casos de erros de parser (ao executar um código dinâmico com eval, por exemplo).
      • A classe AssertionError extende a classe Error e é disparada em casos de erros de assert.

Com esta nova hierarquia, os "catches" existentes nos códigos anteriores ao PHP 7 continuam capturando apenas o que já capturavam antes, ou seja, as Exceptions. Como a nova classe Error não extende a classe Exception, então eles não são capturados pelo catch (Exception $e).

Porém, agora podemos capturar os erros com um catch (Error $e). E também é possível capturar qualquer coisa, ou seja, montar um catch (Throwable $e), para que tanto Exceptions quanto Errors sejam capturados. Veja os exemplos:

try {
    ...
} catch (Exception $e) {
    // captura apenas exceptions
}

try {
    ...
} catch (Exception $e) {
    // captura exceptions aqui
} catch (Error $e) {
    // captura erros aqui
}


try {
    ...
} catch (Throwable $e) {
    // captura exceptions ou errors
}

Uma observação importante é que a interface Throwable é especial e o programador PHP não pode implementá-la diretamente. Só é possível extender a classe Exception ou a classe Error, garantindo assim que só haverá esses dois tipos básicos de erros.

Talvez seja útil informar que o construtor da classe Exception mudou de assinatura. Antes ele recebia o parâmetro $previous do tipo Exception e agora passou a ser do tipo Throwable para ser mais abrangente.

Operador null coalescing

Este operador é um novo recurso da linguagem muito prático. Até o PHP 5.6.X, era comum criarmos instruções com operador ternário para checar se um valor foi definido com algo diferente de NULL e atribuir este valor ou algum valor padrão a uma variável. Isso era feito desta forma:

// Usando operador ternario
$exemplo = isset($_GET['exemplo']) ? $_GET['exemplo'] : 123;

O operador null coalescing permite fazer isso de forma simplificada, através da sintaxe usando "??", conforme o exemplo:

// Usando operador null coalescing
$exemplo = $_GET['exemplo'] ?? 123;

Operador Spaceship

O operador spaceship (que tem este nome por parecer uma nave espacial) foi criado especialmente para simplificar a criação de funções usadas como callback para comparações. Este tipo de função é muito comum quando estamos ordenando um array de uma forma muito peculiar, através da função usort, por exemplo.

O que o operador spaceship faz é retornar um valor -1, 0 ou 1 dependendo da comparação entre os elementos colocados à esquerda e à direita dele. Quando o elemento da esquerda é considerado menor (ou anterior) ao da direita, o spaceship devolve -1. Quando o elemento da esquerda é considerado maior (ou posterior) ao da direita, o spaceship devolve 1. E quando os elementos da esquerda e da direita são idênticos (ou correspondentes), o spaceship devolve 0 (zero). Veja um exemplo:

$a = 1 <=> 2;              // $a = -1 pois 1 vem antes do 2
$b = 1 <=> 1;              // $b = 0 pois 1 eh igual a 1
$c = 3 <=> 1;              // $c = 1 pois 3 vem depois do 1
$d = 5.4 <=> 5.2;          // $d = 1 pois 5.4 vem depois do 5.2
$e = "PHP" <=> "Java";     // $e = 1 pois "P" vem depois do "J"
$f = "amarelo" <=> "azul"; // $f = -1 pois "m" vem antes do "z"

Classes Anônimas

O recurso de classes anônimas tem importância similar às funções anônimas que surgiram no PHP 5.3. No caso das funções, elas eram criadas sem nome e, normalmente, passadas para outras funções como callback. Elas não eram declaradas em arquivos separados e nem como métodos de alguma classe pois eram usadas em contextos específicos em que não se utilizaria mais aquela função em nenhum outro lugar da aplicação. Ou então, eram armazenados em variáveis (ou arrays), para serem passadas quando necessário para outra função.

O caso das classes é muito semelhante. Cria-se uma classe anônima quando não há intenção de se instanciar objetos dessa classe em vários pontos do código. E então, por praticidade, é criada a classe no mesmo ponto do código em que se espera receber um objeto dela.

É preciso tomar um pouco de cuidado ao usar este recurso, afinal, pode tornar o código pouco legível dependendo do tamanho da classe, além de causar problemas de arquitetura se usado em locais inadequados. Um exemplo extraído do site do php.net demonstra a utilização em um suposto arquivo onde se configura um logger:

$util->setLogger(new class {
    public function log($msg)
    {
        echo $msg;
    }
});

Ou seja, ao invés de criar uma classe em um arquivo separado e então instanciar um objeto para passar para o método setLogger, foi criado um objeto de uma classe anônima que tem o método log. O ideal é que o método setLogger esperasse uma interface Logger, por exemplo, então poderíamos reescrever o código criando uma classe anônima, mas que implementa a interface Logger:

$util->setLogger(new class implements Logger {
    public function log($msg)
    {
        echo $msg;
    }
});

Ou então criar uma classe anônima que extende outra:

$util->setLogger(new class extends BasicLogger {
    public function log($msg)
    {
        echo $msg;
    }
});

Também é possível criar classes anôninimas com construtor. Neste casso, a forma como se cria a classe anônima muda um pouco:

$a = fopen('/tmp/app.log', 'w');
$util->setLogger(new class($a) implements Logger {
    private $arquivo;
    public function __construct($arquivo)
    {
        $this->arquivo = $arquivo;
    }
    public function log($msg)
    {
        fwrite($this->arquivo, $msg);
    }
});

Asserts sem custo

Para quem não sabe, assert é uma função do PHP que já existe desde o PHP 4. Ele recebe dois parâmetros: o primeiro é "algo para ser avaliado" e o segundo é uma mensagem de notificação caso a avaliação seja falsa. Ou seja, ele é útil para se colocar alguns pontos de debug no código. Por exemplo, um programador determina que em certo ponto do código uma variável precisaria obrigatoriamente valer mais que 10, segundo sua lógica, então ele coloca a seguinte linha:

...
assert($a > 10, 'A variavel $a não é maior que 10');
...

Assim, caso a afirmativa "$a > 10" retorne false, então é emitida uma exceção com a mensagem informada. É uma forma simplificada de se fazer isso:

...
if (($a > 10) === false) {
    throw new Exception('A variavel $a não é maior que 10');
}
...

Porém, assert tem uma vantagem sobre o if, que é a posssibilidade de ligá-lo e desligá-lo via configurações do PHP. Assim, você pode deixar esses debugs ligados em ambiente de desenvolvimento e teste, mas desligado em produção.

Importante: note que o assert não foi feito para se avaliar condições que podem ser modificadas pelo usuário final. Não devemos colocar um assert checando se uma variável recebida por GET ou POST possui valor maior que 10, por exemplo. Ou seja, precisam ser condições que o programador entende que só daria errado se ele (programador) ou outro colega de trabalho usasse sua função ou classe de forma incorreta e que isso deveria ser detectado durante o próprio desenvolvimento. Por isso é que em produção essa checagem (em teoria) nunca precisaria ser feita.

O assert também podia receber uma string com um código PHP a ser avaliado. Como o exemplo abaixo:

...
assert('is_callable($f)', "A variável precisa ser invocável");
...

A vantagem de usar uma string é que, quando você desliga as checagens de asserts (em produção), o código não precisa executar a função is_callable($f) para obter um valor booleano que só depois será ignorado pela função assert. Isso por causa da ordem com que as coisas precisam ocorrer para a função ser executada, ou seja, antes do assert ser executado, todas as expressões passadas nos parâmetros precisam ser processadas primeiro. Mas no caso de strings, elas já são valores prontos para serem passados para o assert e então o próprio "assert" é que pega essa string e a executa (caso ele esteja habilitado). Portanto, se a instrução que está sendo avaliada no assert for muito custosa (em tempo ou memória), então haveria um ganho em ambiente de produção, onde o assert estaria desligado.

Finalmente podemos dizer que a grande mudança no assert no PHP 7 foi que ela deixou de ser uma função e passou a ser uma construção da linguagem e que, mesmo passando as expressões sem estarem na forma de string, o seu valor não será processado caso a configuração de asserts estiver desligado (por isso, passaram a ser "sem custo"). Isso é bom, pois montar expressões dentro de string é arriscada como instruções eval e precisam de cuidados de escape de alguns caracteres em determinadas circunstâncias. Além disso, no PHP 7 a construção assert também passou a permitir receber uma exceção no lugar de uma mensagem, como no exemplo:

...
assert($a > 10, new Exception('A variável $a não é maior que 10'));
...

Remoção de SAPIs e extensões velhas e sem suporte

Quanto à remoção de SAPIs e extensões velhas e sem suporte, o que considero mais significativo é a remoção da extensão "mysql". A partir do PHP 7 passa a ser necessário utilizar MySQLi, PDO ou, se você for maluco, pode implementar sua própria biblioteca para comunicar com o MySQL pelo protocolo binário, via socket numa porta específica, usando a função fsockopen. Para continuar usando a extensão "mysql", precisa instalar via Pecl: Extensão MySQL no Pecl

Outra extensão que foi removida foi a ereg. Agora é necessário usar as funções PCRE, como mostrei no artigo Expressões Regulares em PHP.

Para mais informações do que saiu no PHP 7, veja: RFC de funcionalidades removidas no PHP 7.

Gerador de números aleatórios seguro

Foram criadas as funções random_bytes e random_int para gerar uma sequência de bytes ou um número aleatório de forma segura. São úteis para se montar sal para senhas criptografadas ou números que não podem ser gerados de forma influenciada pelo tempo.

Árvore de sintaxe abstrata

A Árvore de sintaxe abstrata, ou AST, foi um recurso introduzido ao núcleo do PHP e que muda significativamente o processo de compilação do código intermediário do PHP. Os benecífios deste recurso estão relacionados diretamente à manutenabilidade do código do parser e compilador, além de desacoplar algumas decisões de sintaxe que, no passado, causavam algum transtorno.

Por exemplo, no passado, as instruções "yield" só podiam ser usadas em expressões (por exemplo, em uma atribuição) se estivessem entre parênteses, por questões de limitação da linguagem:

// Funciona a partir do PHP 5.5 (surgimento dos generators)
$resultado = (yield 123);

// Funciona apenas a partir do PHP 7
$resultado = yield 123;

Com a introdução do AST, os parênteses passaram a ser desnecessários.

Além disso, o processo de compilação passou a ser um pouquinho mais rápido, embora isso só seja relevante quando a aplicação não utiliza opcache. Afinal, quando se usa opcache, a compilação só é realizada uma única vez.

Para os usuários finais do PHP (programdores PHP), pode-se dizer que a introdução do AST no processo de compilação poderá trazer construções de linguagem (sintaxes novas) que antes eram impossíveis. Para mais informações sobre o assunto, veja o RFC sobre AST.

Suporte consistente a 64 bits

O interpretador de PHP é escrito em linguagem C, que possui o tipo "long". Até a versão 7, o PHP utilizava o tipo "long" do C para armazenar números inteiros. O problema é há diferenças do tipo "long" em plataformas Windows (LLP64) e Linux (LP64), pois a primeira usa 32 bits e a segunda 64 bits. Com isso, havia limitações para manipulação de inteiros maiores que 32 bits no Windows, por exemplo. Com o suporte consitente a 64 bits, os desenvolvedores do PHP abstrairam este tipo, tornando a linguagem mais independente da plataforma e mais consistente. Para mais detalhes, veja o RFC sobre melhorias no suporte a 64 bits.


As incompatibilidades com versões anteriores ao PHP 7

Como comentei anteriormente, quando uma versão do PHP incrementa sua revisão mais importante (primeiro número da versão), podem surgir algumas incompatibilidades com versões anteriores. Com isso, eventualmente códigos precisam ser reescritos para resolver estas incompatibilidades.

Seguem as incompatibilidades que considero mais relevantes:

  • Derreferenciamento de variáveis foi refatorado, tornando-se mais consistente e permitindo o uso de "->", "[]", "()", "{}" e "::" arbitrariamente.
  • A declaração de funções construtoras no estilo do PHP4 (função com o mesmo nome da classe) foi oficialmente depreciado.
  • O foreach não afeta mais a posição do cursor de um array (ele se mantém na mesma posição após o foreach encerrar). Além disso, teve várias modificações de comportamento.
  • Remoção de SAPIs e extensões velhas.
  • O comportamento da construção list passou a popular as variáveis da esquerda pra direita e não aceitar mais strings como origem da atribuição.
  • Conversões de int e float passaram a ser mais consistente independente de plataforma.

Conclusão

O PHP 7 trouxe muitas novidades e uma excelente melhoria em performance. As principais características foram discutidas neste artigo, mas você também pode continuar lendo este outro com outras novidades do PHP 7.

E, para ainda mais informações, veja a Lista completa de novidades e incompatibilidades no PHP 7

10 comentários

Gustavo Verzola disse...

Muito bom Rubens! :)
Somente um adendo a parte da depreciação dos construtores do PHP4: parece que ele só será removido totalmente no PHP8. Por enquanto o PHP7 está disparando E_DEPRECATED.

Unknown disse...

Qual é o real problema dos números randômicos serem influenciados pelo tempo? Seria porque eles se tornariam previsíveis demais?

Rubens Takiguti Ribeiro (autor do blog) disse...

Michael, creio que as novas funções para geração de strings e inteiros aleatórios sejam apenas por conveniência de uso mesmo. Porém, existe sim um tipo de ataque baseado em tempo (timming attack). Dê uma lida nesse outro artigo:
http://rubsphp.blogspot.com.br/2014/03/prevencao-de-timing-attack-no-php.html