PHP Tipos Avançados

Na aula Tipos e Retorno, você colocou um tipo em cada parâmetro: int $quantidade, string $nome. Mas a vida real nem sempre cabe num tipo só: um preço pode ser inteiro ou decimal; uma busca pode achar um número ou nada. O PHP 8 trouxe uma "gramática" para descrever esses casos com precisão, e um modo estrito, o strict_types, que proíbe conversões escondidas.

Você vai aprender
  • Union types (int|float): "este ou aquele". PHP 8.0+
  • Nullable (?int): "isto ou null", e o aviso novo sobre = null.
  • mixed (qualquer coisa) PHP 8.0+ e never (nunca volta) PHP 8.1+.
  • Intersection types (Countable&ArrayAccess): "este e aquele". PHP 8.1+
  • DNF types ((A&B)|null) e true, false, null como tipos. PHP 8.2+
  • static como tipo de retorno.
  • declare(strict_types=1) em profundidade: o que muda, onde vale, o que não muda.

A ideia: a etiqueta da caixa

Um tipo é a etiqueta de uma caixa: "aqui só entra parafuso". Até agora, cada caixa tinha uma etiqueta simples. Os tipos avançados permitem etiquetas mais ricas:

EtiquetaEscritaExemplo da vida
Este ou aqueleint|float"Pague em dinheiro ou cartão"
Isto ou nada?string"Apelido (opcional)"
Qualquer coisamixedA caixa de "achados e perdidos"
Este e aquele ao mesmo tempoCountable&ArrayAccess"Vaga para quem fala inglês e espanhol"
Nunca devolve nada (nem volta)neverA porta de saída de emergência

Union types: int|float PHP 8.0+

A barra vertical | (lê-se "ou") junta vários tipos. O valor precisa ser de pelo menos um deles:

<?php
function dobro(int|float $n): int|float
{
    return $n * 2;
}

var_dump(dobro(5));
var_dump(dobro(2.5));
var_dump(dobro("3"));
var_dump(dobro("três"));
int|float $n
O parâmetro aceita um inteiro ou um decimal.
: int|float
O retorno também pode ser um ou outro.
$ php uniao.php
int(10)
float(5)
int(6)             « "3" foi convertido para int (o arquivo não está no modo estrito)

Fatal error: Uncaught TypeError: dobro(): Argument #1 ($n) must be of type int|float, string given, called in /home/ana/tipos/uniao.php on line 10 and defined in /home/ana/tipos/uniao.php:2
Stack trace:
#0 /home/ana/tipos/uniao.php(10): dobro('tr\xC3\xAAs')
#1 {main}
  thrown in /home/ana/tipos/uniao.php on line 2

Repare em duas coisas. Primeiro: o tipo é preservado: dobro(5) devolveu int(10) e dobro(2.5) devolveu float(5). Segundo: "três" não é número de jeito nenhum, e o PHP recusou com um TypeError ("erro de tipo"): "o argumento 1 ($n) precisa ser int|float, veio uma string". O tr\xC3\xAAs no rastro é a palavra "três" com o ê mostrado byte a byte (veja Acentos e mbstring).

Antes do PHP 8.0, a única forma de dizer "int ou float" era não pôr tipo nenhum e escrever no comentário @param int|float $n. Se você vir isso em código antigo, já sabe traduzir.

Regras dos union types

  • Um tipo não pode se repetir: int|string|int dá Fatal error: Duplicate type int is redundant.
  • void não entra em união (não faz sentido "devolve nada ou um int").
  • Use com moderação: int|string|array|bool geralmente indica uma função fazendo coisas demais.

Nullable: ?int e int|null

O caso de união mais comum é "um tipo ou null" (null = "nada", "sem valor"). Ele tem um atalho: o ponto de interrogação na frente. ?float é exatamente o mesmo que float|null.

<?php
function buscarPreco(string $produto): ?float
{
    $tabela = ["Caneca" => 19.9, "Camiseta" => 49.9];
    return $tabela[$produto] ?? null;
}

var_dump(buscarPreco("Caneca"));
var_dump(buscarPreco("Boné"));
$ php nullable2.php
float(19.9)
NULL               « "não achei"

Quem chama a função precisa tratar os dois casos: $preco = buscarPreco($p) ?? 0.0; ou um if ($preco === null). Esse é o valor do tipo: ele avisa que o null pode vir.

Nullable não é opcional

?string quer dizer "aceito null", não "pode deixar de passar":

<?php
function saudacao(?string $nome): string
{
    return $nome === null ? "Olá, visitante!" : "Olá, $nome!";
}

echo saudacao("Ana"), "\n";
echo saudacao(null), "\n";
echo saudacao(), "\n";
$ php nullable.php
Olá, Ana!
Olá, visitante!

Fatal error: Uncaught ArgumentCountError: Too few arguments to function saudacao(), 0 passed in /home/ana/tipos/nullable.php on line 9 and exactly 1 expected in /home/ana/tipos/nullable.php:2
Stack trace:
#0 /home/ana/tipos/nullable.php(9): saudacao()
#1 {main}
  thrown in /home/ana/tipos/nullable.php on line 2

"Poucos argumentos: 0 passados, exatamente 1 esperado". Para o parâmetro ser opcional, dê um valor padrão: ?string $nome = null.

Cuidado com string $nome = null (sem o ?)

Código antigo escrevia function f(string $nome = null), e o PHP transformava o tipo em ?string por baixo dos panos. Desde o PHP 8.4 isso dá aviso:

$ php nullable2.php        « com a linha: function implicito(string $nome = null) {}

Deprecated: implicito(): Implicitly marking parameter $nome as nullable is deprecated, the explicit nullable type must be used instead in /home/ana/tipos/nullable2.php on line 11
float(19.9)
NULL

"Marcar o parâmetro como nullable implicitamente está obsoleto; use o tipo nullable explícito". O aviso aparece antes de qualquer saída, porque é dado quando o arquivo é compilado. Conserto: ?string $nome = null.

Missão: a função formatarPreco só aceita float. Mude o tipo do parâmetro para aceitar int, float ou null (use int|float|null) e faça ela devolver "sem preço" quando receber null. A saída deve ter R$ 19,90, sem preço e R$ 50,00.

<?php
function formatarPreco(float $valor): string
{
    return "R$ " . number_format($valor, 2, ",", ".");
}

echo formatarPreco(19.9), "\n";
echo formatarPreco(null), "\n";
echo formatarPreco(50), "\n";
<?php
function formatarPreco(int|float|null $valor): string
{
    if ($valor === null) {
        return "sem preço";
    }
    return "R$ " . number_format($valor, 2, ",", ".");
}

echo formatarPreco(19.9), "\n";
echo formatarPreco(null), "\n";
echo formatarPreco(50), "\n";

mixed: qualquer coisa PHP 8.0+

mixed ("misturado") aceita qualquer valor: número, texto, array, objeto, null. É o mesmo que array|bool|callable|int|float|null|object|resource|string.

<?php
function descrever(mixed $valor): string
{
    return get_debug_type($valor);
}

foreach ([42, 3.14, "oi", true, null, [1, 2], new DateTime()] as $v) {
    echo descrever($v), "\n";
}
$ php mixed.php
int
float
string
bool
null
array
DateTime

A função get_debug_type() diz o tipo de qualquer valor (inclusive o nome da classe de um objeto). E se mixed aceita tudo, para que escrever? Para dizer que foi de propósito: um parâmetro sem tipo pode ser esquecimento; mixed é uma decisão. Use em funções realmente genéricas (como descrever ou um var_dump caseiro); no resto, prefira um tipo preciso.

Como mixed já inclui null, ?mixed dá erro:

$ php mixed2.php

Fatal error: Type mixed cannot be marked as nullable since mixed already includes null in /home/ana/tipos/mixed2.php on line 2
Stack trace:
#0 {main}

never: a função que nunca volta PHP 8.1+

void quer dizer "termina normalmente, mas não devolve valor". never quer dizer mais: "esta função nunca termina normalmente": ou ela encerra o programa (exit), ou lança uma exceção, ou fica num laço eterno.

<?php
function abortar(string $motivo): never
{
    echo "Erro: $motivo\n";
    exit(1);
}

function buscar(array $estoque, string $item): int
{
    if (!isset($estoque[$item])) {
        abortar("$item não existe");
    }
    return $estoque[$item];
}

$estoque = ["Caneca" => 3];
echo buscar($estoque, "Caneca"), "\n";
echo buscar($estoque, "Boné"), "\n";
echo "nunca chega aqui\n";
$ php never.php; echo "código: $?"
3
Erro: Boné não existe
código: 1          « o exit(1) encerrou o programa

Para que serve? Para quem lê (e para ferramentas de análise): depois de abortar(…), o código "acabou". Exemplos reais: uma função redirecionar(string $url): never que faz header("Location: …") e exit (veja header e Redirecionamento), ou falhar(): never que sempre lança uma exceção.

O PHP confere a promessa. Se a função terminar normalmente:

<?php
function abortar(string $motivo): never
{
    echo "Erro: $motivo\n";
}

abortar("teste");
$ php never2.php
Erro: teste

Fatal error: Uncaught TypeError: abortar(): never-returning function must not implicitly return in /home/ana/tipos/never2.php:5
Stack trace:
#0 /home/ana/tipos/never2.php(7): abortar('teste')
#1 {main}
  thrown in /home/ana/tipos/never2.php on line 5

"Uma função never não pode retornar implicitamente" (chegar ao } final). Um return; escrito dentro dela é pego antes mesmo de rodar: Fatal error: A never-returning function must not return.


Intersection types: A&B PHP 8.1+

O & (lê-se "e") exige que o objeto seja de todos os tipos ao mesmo tempo. Só faz sentido com interfaces e classes: um objeto pode implementar várias interfaces.

Exemplo: a função quer "algo que dê para contar (count()) e acessar com colchetes ($x[0])". O PHP tem uma interface para cada coisa: Countable e ArrayAccess. A classe ArrayObject implementa as duas.

<?php
function resumo(Countable&ArrayAccess $lista): string
{
    return count($lista) . " itens; o primeiro é " . $lista[0];
}

$itens = new ArrayObject(["Caneca", "Camiseta", "Boné"]);
echo resumo($itens), "\n";
echo resumo(["Caneca", "Camiseta"]), "\n";
$ php intersecao.php
3 itens; o primeiro é Caneca

Fatal error: Uncaught TypeError: resumo(): Argument #1 ($lista) must be of type Countable&ArrayAccess, array given, called in /home/ana/tipos/intersecao.php on line 9 and defined in /home/ana/tipos/intersecao.php:2
Stack trace:
#0 /home/ana/tipos/intersecao.php(9): resumo(Array)
#1 {main}
  thrown in /home/ana/tipos/intersecao.php on line 2

O ArrayObject passou. O array comum, não: arrays não são objetos e não implementam interfaces, mesmo funcionando com count() e colchetes. Tipos de interseção não aceitam tipos simples (int&string não existe: nada é as duas coisas).


DNF types: (A&B)|null PHP 8.2+

E se a função quiser aceitar "um objeto contável e acessável, ou um array comum, ou null"? É preciso misturar & e |. O PHP 8.2 permite, desde que as interseções fiquem entre parênteses. O nome técnico é DNF (Disjunctive Normal Form, "forma normal disjuntiva"): uma lista de alternativas (|), onde cada alternativa pode ser um grupo (A&B).

<?php
function resumo((Countable&ArrayAccess)|array|null $lista): string
{
    if ($lista === null) {
        return "lista vazia";
    }
    return count($lista) . " itens; o primeiro é " . $lista[0];
}

echo resumo(new ArrayObject(["Caneca", "Camiseta", "Boné"])), "\n";
echo resumo(["Caneca", "Camiseta"]), "\n";
echo resumo(null), "\n";
$ php dnf.php
3 itens; o primeiro é Caneca
2 itens; o primeiro é Caneca
lista vazia

Sem os parênteses, o PHP nem entende a linha:

$ php dnf-erro.php        « function f(Countable&ArrayAccess|null $x) {}

Parse error: syntax error, unexpected token "|", expecting variable in /home/ana/tipos/dnf-erro.php on line 2

Na prática, DNF aparece mais em bibliotecas do que em código do dia a dia. O importante é saber ler: parênteses agrupam o "e"; a barra separa os "ou".


true, false e null como tipos PHP 8.2+

Várias funções do PHP devolvem "um valor ou false" quando falham: array_search, strpos, file_get_contents. Para descrever isso, existe o tipo false (desde o 8.0 em uniões; sozinho desde o 8.2). O 8.2 também trouxe true e null como tipos próprios.

<?php
function salvar(string $texto): true
{
    file_put_contents('/tmp/nota.txt', $texto);
    return true;
}

function procurar(array $lista, string $item): int|false
{
    return array_search($item, $lista, true);
}

function nada(): null
{
    return null;
}

var_dump(salvar("oi"));
var_dump(procurar(["a", "b"], "b"));
var_dump(procurar(["a", "b"], "z"));
var_dump(nada());
$ php literais.php
bool(true)
int(1)
bool(false)
NULL
: true
Só pode devolver true. Útil quando a função sempre dá certo (e lança exceção quando dá errado), mas precisa manter a "cara" de uma função que devolve bool.
: int|false
O padrão "achou a posição ou false", o mesmo de array_search.
: null
Só pode devolver null (raro; aparece ao estender classes).
$ php literais-erro.php     « function salvar(): true { return false; }

Fatal error: Uncaught TypeError: salvar(): Return value must be of type true, false returned in /home/ana/tipos/literais-erro.php:4
Stack trace:
#0 /home/ana/tipos/literais-erro.php(6): salvar()
#1 {main}
  thrown in /home/ana/tipos/literais-erro.php on line 4
$ php literais-erro2.php    « function f(): true|false {}

Fatal error: Type contains both true and false, bool must be used instead in /home/ana/tipos/literais-erro2.php on line 2
Stack trace:
#0 {main}

A segunda mensagem é um conselho: "o tipo contém true e false; use bool".


static como tipo de retorno

Algumas classes permitem encadear chamadas ($c->onde(…)->ordenarPor(…)), devolvendo o próprio objeto em cada método (return $this;). Qual tipo de retorno escrever? static quer dizer "um objeto da classe de quem chamou", que pode ser uma classe filha.

<?php
class Consulta
{
    protected array $partes = [];

    public static function nova(): static
    {
        return new static();
    }

    public function onde(string $condicao): static
    {
        $this->partes[] = "WHERE $condicao";
        return $this;
    }

    public function sql(): string
    {
        return "SELECT * FROM produtos " . implode(" ", $this->partes);
    }
}

class ConsultaOrdenada extends Consulta
{
    public function ordenarPor(string $campo): static
    {
        $this->partes[] = "ORDER BY $campo";
        return $this;
    }
}

$c = ConsultaOrdenada::nova()->onde("preco < 50")->ordenarPor("nome");
echo get_class($c), "\n";
echo $c->sql(), "\n";
$ php estatico.php
ConsultaOrdenada
SELECT * FROM produtos WHERE preco < 50 ORDER BY nome
RetornoPrometeNo exemplo, chamado em ConsultaOrdenada
self"Um objeto da classe onde o método foi escrito" (ou filha)Promete só uma Consulta: o editor não sugere ordenarPor() depois de onde()
static"Um objeto da classe de quem chamou"Promete uma ConsultaOrdenada: o encadeamento é entendido inteiro

Com self, o exemplo também roda (o objeto é uma ConsultaOrdenada, que é uma Consulta), mas a promessa é mais fraca. Com static, o PHP cobra a promessa mais forte. Se o método criar o objeto com new self() em vez de new static():

$ php static-erro.php      « nova(): static { return new self(); }

Fatal error: Uncaught TypeError: Consulta::nova(): Return value must be of type ConsultaOrdenada, Consulta returned in /home/ana/tipos/static-erro.php:6
Stack trace:
#0 /home/ana/tipos/static-erro.php(11): Consulta::nova()
#1 {main}
  thrown in /home/ana/tipos/static-erro.php on line 6

Repare que a mensagem já mostra o static "resolvido": ConsultaOrdenada. Mais sobre self e static em static e Herança.


declare(strict_types=1) em profundidade

Você já viu essa linha no topo dos arquivos modernos. Ela liga o modo estrito de tipos. Para entender o que muda, compare os dois modos com a mesma função.

Modo padrão (coercivo): o PHP tenta converter

<?php
function total(int $quantidade, float $preco): float
{
    return $quantidade * $preco;
}

var_dump(total(3, 10.5));
var_dump(total("3", "10.5"));
var_dump(total(3.0, 20));
var_dump(total(true, 10));
var_dump(total(2.5, 10));
var_dump(total("3 canecas", 10));
$ php fraco.php
float(31.5)
float(31.5)        « "3" e "10.5" viraram números
float(60)          « 3.0 virou 3 (sem parte decimal, sem problema)
float(10)          « true virou 1 (!)

Deprecated: Implicit conversion from float 2.5 to int loses precision in /home/ana/tipos/fraco.php on line 2
float(20)          « 2.5 virou 2: meia caneca sumiu, com um aviso

Fatal error: Uncaught TypeError: total(): Argument #1 ($quantidade) must be of type int, string given, called in /home/ana/tipos/fraco.php on line 12 and defined in /home/ana/tipos/fraco.php:2
Stack trace:
#0 /home/ana/tipos/fraco.php(12): total('3 canecas', 10)
#1 {main}
  thrown in /home/ana/tipos/fraco.php on line 2

No modo padrão, o tipo funciona como um conversor: o PHP converte o que der ("3" → 3, true → 1) e só recusa o que não dá ("3 canecas"). É confortável, mas esconde erros: total(true, 10) provavelmente é um engano, e o 2.5 perdeu metade.

Modo estrito: o tipo tem que bater

<?php
declare(strict_types=1);

function total(int $quantidade, float $preco): float
{
    return $quantidade * $preco;
}

var_dump(total(3, 10.5));
var_dump(total(3, 20));
var_dump(total("3", "10.5"));
$ php forte.php
float(31.5)
float(60)          « int → float é a única conversão permitida

Fatal error: Uncaught TypeError: total(): Argument #1 ($quantidade) must be of type int, string given, called in /home/ana/tipos/forte.php on line 11 and defined in /home/ana/tipos/forte.php:4
Stack trace:
#0 /home/ana/tipos/forte.php(11): total('3', '10.5')
#1 {main}
  thrown in /home/ana/tipos/forte.php on line 4

No modo estrito, "3" não é int: é string, e pronto. A única exceção é passar um int onde se espera float (o 20 virou 20.0), porque isso nunca perde informação.

As cinco regras do strict_types

  1. Vale por arquivo. Cada arquivo escolhe o seu modo. Não existe "ligar para o projeto todo"; por isso a linha vai no topo de todos os arquivos.
  2. Vale para as chamadas feitas naquele arquivo. O que conta é o arquivo de quem chama a função, não o de quem a declarou (veja abaixo).
  3. Vale também para os retornos das funções declaradas no arquivo: com o modo estrito, function preco(): int { return "10"; } dá TypeError; sem ele, devolve int(10).
  4. Vale para as funções do PHP: com o modo estrito, strlen(12345) dá TypeError; sem ele, devolve 5.
  5. Precisa ser a primeira instrução do arquivo: só o <?php e comentários podem vir antes.

A regra 2 é a que mais confunde. A Ana tem uma biblioteca no modo estrito e dois arquivos que a usam:

<?php
declare(strict_types=1);

function dobrar(int $n): int
{
    return $n * 2;
}
<?php
// este arquivo NÃO tem strict_types
require 'biblioteca.php';

var_dump(dobrar("21"));
$ php chamador.php
int(42)            « quem chamou está no modo padrão: "21" foi convertido
$ php chamador2.php        « o mesmo, mas com declare(strict_types=1) no topo

Fatal error: Uncaught TypeError: dobrar(): Argument #1 ($n) must be of type int, string given, called in /home/ana/tipos/chamador2.php on line 6 and defined in /home/ana/tipos/biblioteca.php:4
Stack trace:
#0 /home/ana/tipos/chamador2.php(6): dobrar('21')
#1 {main}
  thrown in /home/ana/tipos/biblioteca.php on line 4

A biblioteca é a mesma; o que mudou foi o arquivo que chamou. A ideia é: cada arquivo decide o quanto confia nos próprios valores. E as regras 3 e 4, conferidas:

$ php forte-retorno.php    « strict_types + function preco(): int { return "10"; }

Fatal error: Uncaught TypeError: preco(): Return value must be of type int, string returned in /home/ana/tipos/forte-retorno.php:6
Stack trace:
#0 /home/ana/tipos/forte-retorno.php(9): preco()
#1 {main}
  thrown in /home/ana/tipos/forte-retorno.php on line 6
$ php fraco-retorno.php    « a mesma função, sem strict_types
int(10)
$ php interna.php          « strict_types + var_dump(strlen(12345));

Fatal error: Uncaught TypeError: strlen(): Argument #1 ($string) must be of type string, int given in /home/ana/tipos/interna.php:4
Stack trace:
#0 {main}
  thrown in /home/ana/tipos/interna.php on line 4
$ php interna-fraco.php    « sem strict_types: strlen(12345) e str_repeat("ab", "3")
int(5)
string(6) "ababab"

O que o strict_types não muda

  • Operadores continuam convertendo: "3" + 2 dá 5, "10" == 10 dá true. Para comparações, use === (veja Comparação).
  • Propriedades tipadas de classes seguem o modo do arquivo onde a atribuição é feita.
  • Dados de formulário ($_POST, $_GET) sempre chegam como string. No modo estrito, você converte explicitamente ((int) $_POST['quantidade'], ou melhor, com validação) antes de passar para funções tipadas. É isso que a missão final treina.
Qual modo usar?

Em código novo, sempre declare(strict_types=1);. Os erros aparecem na hora, na linha certa, em vez de virarem um valor errado lá na frente (a meia caneca que sumiu). É uma das recomendações de Boas Práticas.


Todos os tipos, num quadro

TipoAceitaDesde
int, float, string, bool, arrayO tipo simples7.0 (array já existia antes)
?TT ou null7.1
void(retorno) não devolve nada7.1
object, iterable, callableQualquer objeto; array ou Traversable; algo chamável7.x
A|BA ou B8.0
mixedQualquer valor8.0
static(retorno) a classe de quem chamou8.0
never(retorno) nunca volta8.1
A&BObjeto que é A e B8.1
(A&B)|CDNF: grupos com "e", separados por "ou"8.2
true, false, nullSó esse valor8.2 (false em união desde 8.0)

Quando dá errado

TypeError: … must be of type int, string given

O que significa: o valor não é do tipo pedido. A mensagem diz a função, o número e o nome do argumento, o tipo esperado, o tipo recebido, a linha que chamou (called in) e a linha onde a função foi declarada (defined in). O erro está quase sempre em called in.

Como consertar: converta ou valide o valor antes de chamar (dados de formulário são sempre string), ou amplie o tipo com |, se a função realmente deve aceitar os dois.

Deprecated: Implicitly marking parameter $x as nullable is deprecated

O que significa: string $x = null, sem o ?. Funcionou por anos; desde o PHP 8.4, avisa.

Como consertar: ?string $x = null.

Too few arguments … exactly 1 expected

O que significa: você achou que ?string tornava o parâmetro opcional. Não torna.

Como consertar: acrescente o valor padrão: ?string $nome = null.

strict_types declaration must be the very first statement in the script
$ php lugar.php

Fatal error: strict_types declaration must be the very first statement in the script in /home/ana/tipos/lugar.php on line 3
Stack trace:
#0 {main}

O que significa: havia código (aqui, um echo) antes do declare.

Como consertar: <?php, depois declare(strict_types=1);, depois o namespace, depois o resto.

Parse error: syntax error, unexpected token "|"

O que significa: você misturou & e | sem parênteses, ou está num PHP anterior ao 8.0 (sem union types) ou ao 8.2 (sem DNF).

Como consertar: (A&B)|null; confira a versão com php -v.


Exercícios

1. O atalho por extenso

?int é um atalho. Complete a forma por extenso, com union type:

function mostrarCodigo(int| $codigo): void

2. Talvez um objeto

Escreva o retorno "um Produto ou null" usando o atalho:

function buscar(int $id):

3. A primeira linha

Escreva a linha que liga o modo estrito de tipos:

<?php

4. Com declare(strict_types=1), qual destas chamadas a function f(float $x) funciona?

5. Qual a diferença entre void e never?

6. O arquivo a.php tem strict_types=1 e declara function f(int $n). O arquivo b.php, sem strict_types, chama f("5"). O que acontece?

Missão final: este arquivo está no modo estrito, e os dados chegaram de um formulário, como texto. Rode e leia o TypeError. Conserte sem tirar o declare(strict_types=1): converta os valores antes de chamar a função (dica: (int) e (float), veja Conversão de Tipos). A saída deve ser Total: R$ 31,50.

<?php
declare(strict_types=1);

function total(int $quantidade, float $preco): float
{
    return $quantidade * $preco;
}

// valores que vieram de um formulário: sempre texto
$form = ['quantidade' => '3', 'preco' => '10.5'];

$valor = total($form['quantidade'], $form['preco']);
echo "Total: R$ ", number_format($valor, 2, ",", "."), "\n";
<?php
declare(strict_types=1);

function total(int $quantidade, float $preco): float
{
    return $quantidade * $preco;
}

// valores que vieram de um formulário: sempre texto
$form = ['quantidade' => '3', 'preco' => '10.5'];

$valor = total((int) $form['quantidade'], (float) $form['preco']);
echo "Total: R$ ", number_format($valor, 2, ",", "."), "\n";

Resumo

  • A|B: um ou outro. ?T = T|null, mas não torna o parâmetro opcional; use ?T $x = null (nunca T $x = null, obsoleto no 8.4).
  • mixed: qualquer coisa, de propósito. never: a função nunca volta (exit ou exceção).
  • A&B: objeto que é os dois; (A&B)|null: DNF, com parênteses obrigatórios.
  • true, false, null podem ser tipos; int|false é o padrão "achou ou falhou".
  • static no retorno: "a classe de quem chamou"; combine com new static().
  • declare(strict_types=1): primeira linha, vale por arquivo, para chamadas feitas nele (inclusive às funções do PHP) e para os retornos das funções dele; só int → float é aceito. Use sempre em código novo.