PHP 8.0: Tipos de dados (2/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 segundo, veremos os tipos de dados.

Voltemos à história. O avanço fundamental do PHP 7 foi a introdução de type hints escalares. Quase não aconteceu. A autora da solução incrível, Andrea Faulds, que graças a declare(strict_types=1) era totalmente retrocompatível e opcional, foi rudemente rejeitada pela comunidade. Felizmente, na época, Anthony Ferrara defendeu ela e sua proposta, lançou uma campanha e a RFC passou por muito pouco. Ufa. A maioria das novidades no PHP 8 é obra do lendário Nikita Popov e passaram na votação como manteiga. O mundo está mudando para melhor.

O PHP 8 leva os tipos à perfeição. A grande maioria das anotações phpDoc @param, @return e @var no código desaparecerá e será substituída pela notação nativa e, principalmente, pela verificação do engine PHP. Nos comentários, restarão apenas descrições de estruturas como string[] ou anotações mais complexas para o PHPStan.

Union Types

Union types são uma enumeração de dois ou mais tipos que um valor pode assumir:

class Button
{
	private string|object $caption;

	public function setCaption(string|object $caption)
	{
		$this->caption = $caption;
	}
}

O PHP já conhecia alguns union types especiais antes. Por exemplo, tipos anuláveis como ?string, que é equivalente ao union type string|null, e a notação de interrogação pode ser considerada apenas uma abreviação. Obviamente, também funciona no PHP 8, mas não pode ser combinado com barras verticais, ou seja, em vez de ?string|object, é necessário escrever o string|object|null completo. Além disso, iterable sempre foi equivalente a array|Traversable. Talvez você se surpreenda ao saber que float também é, na verdade, um union type que aceita int|float, mas converte para float.

Não é possível usar os pseudotipos void e mixed em uniões, pois isso não faria sentido algum.

Nette está pronta para os tipos de união. Em Schema, Expect::from() aceita-os, e os apresentadores também os aceitam. Você pode usá-los, por exemplo, nos métodos de renderização e de ação:

public function renderDetail(int|array $id)
{
	...
}

Por outro lado, o autowiring no Nette DI rejeita union types. Ainda falta um caso de uso onde faria sentido, por exemplo, que um construtor aceitasse um ou outro objeto. Claro, se surgir, será possível ajustar o comportamento do contêiner de acordo.

Os métodos getParameterType(), getReturnType() e getPropertyType() em Nette\Utils\Reflection lançam uma exceção no caso de um union type (na versão 3.1; na versão 3.0 mais antiga, retornam null por compatibilidade).

mixed

O pseudotipo mixed diz que o valor pode ser absolutamente qualquer coisa.

No caso de parâmetros e propriedades, na verdade é o mesmo comportamento de quando não indicamos nenhum tipo. Para que serve então? Para poder distinguir quando o tipo simplesmente está ausente e quando é realmente mixed.

No caso do valor de retorno de uma função ou método, não indicar o tipo difere de indicar o tipo mixed. Na verdade, é o oposto de void, pois diz que a função deve retornar algo. A ausência de return é então um erro fatal.

Na prática, você deve usá-lo raramente, porque graças aos union types, você pode especificar o valor com mais precisão. Portanto, é útil em situações excepcionais:

function dump(mixed $var): mixed
{
	// imprime variável
	return $var;
}

false

O novo pseudotipo false só pode ser usado em union types. Surgiu da necessidade de descrever nativamente o tipo do valor de retorno de funções nativas que, historicamente, retornam false em caso de falha:

function strpos(string $haystack, string $needle): int|false
{
}

Por esse motivo, não existe o tipo true, nem é possível usar false sozinho, false|null ou bool|false.

static

O pseudotipo static só pode ser usado como tipo de retorno de um método. Ele diz que o método retorna um objeto do mesmo tipo que o próprio objeto (enquanto self diz que retorna a classe na qual o método está definido). O que é excelente para descrever fluent interfaces:

class Item
{
	public function setValue($val): static
	{
		$this->value = $val;
		return $this;
	}
}

class ItemChild extends Item
{
	public function childMethod()
	{
	}
}

$child = new ItemChild;
$child->setValue(10)
	->childMethod();

resource

Este tipo não existe no PHP 8 e nem será introduzido no futuro. Resources são uma relíquia histórica dos tempos em que o PHP ainda não tinha objetos. Gradualmente, os resources serão substituídos por objetos e, com o tempo, esse tipo desaparecerá completamente. Por exemplo, o PHP 8.0 substitui o resource que representa uma imagem pelo objeto GdImage e o resource de conexão curl pelo objeto CurlHandle.

Stringable

É uma interface que é implementada automaticamente por todo objeto com o método mágico __toString().

class Email
{
	public function __toString(): string
	{
		return $this->value;
	}
}

function print(Stringable|string $s)
{
}

print('abc');
print(new Email);

Na definição da classe, é possível indicar explicitamente class Email implements Stringable, mas não é necessário.

Este estilo de nomenclatura também reflete o Nette\Utils\Html, que implementa a interface Nette\HtmlStringable em vez da anterior IHtmlString. Objetos deste tipo, por exemplo, não são escapados pelo Latte.

Variância de tipo, contravariância, covariância

O Princípio de Substituição de Liskov (Liskov Substitution Principle – LSP) diz que os descendentes de uma classe e as implementações de uma interface nunca devem exigir mais e fornecer menos que o pai. Ou seja, que o método do descendente não deve exigir mais argumentos ou aceitar uma gama mais restrita de tipos para os parâmetros do que o ancestral e, inversamente, não deve retornar uma gama mais ampla de tipos. Mas pode retornar uma gama mais restrita. Por quê? Porque, caso contrário, a herança não funcionaria de forma alguma. Uma função aceitaria um objeto de um determinado tipo, mas não saberia quais parâmetros podem ser passados aos métodos e o que eles realmente retornarão, porque qualquer descendente poderia quebrar isso.

Portanto, em OOP, vale que o descendente pode:

  • aceitar uma gama mais ampla de tipos nos parâmetros (isso é chamado de contravariância)
  • retornar uma gama mais restrita de tipos (covariância)
  • e as propriedades não podem mudar de tipo (são invariantes)

O PHP sabe fazer isso desde a versão 7.4 e todos os tipos recém-introduzidos no PHP 8.0 também suportam contravariância e covariância.

No caso de mixed, o descendente pode restringir o valor de retorno para qualquer tipo, exceto void, porque não se trata de um tipo de valor, mas de sua ausência. Nem o descendente pode omitir o tipo, porque isso também permite a ausência.

class A
{
    public function foo(mixed $foo): mixed
    {}
}

class B extends A
{
    public function foo($foo): string
    {}
}

Também os union types podem ser ampliados nos parâmetros e restringidos nos valores de retorno:

class A
{
    public function foo(string|int $foo): string|int
    {}
}

class B extends A
{
    public function foo(string|int|float $foo): string
    {}
}

Além disso, false pode ser estendido para bool no parâmetro ou, inversamente, bool no valor de retorno pode ser restringido para false.

Todas as violações de covariância/contravariância levam a um erro fatal no PHP 8.0.

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