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.
- Union types (
int|float): "este ou aquele". PHP 8.0+ - Nullable (
?int): "isto ounull", e o aviso novo sobre= null. mixed(qualquer coisa) PHP 8.0+ enever(nunca volta) PHP 8.1+.- Intersection types (
Countable&ArrayAccess): "este e aquele". PHP 8.1+ - DNF types (
(A&B)|null) etrue,false,nullcomo tipos. PHP 8.2+ staticcomo 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:
| Etiqueta | Escrita | Exemplo da vida |
|---|---|---|
| Este ou aquele | int|float | "Pague em dinheiro ou cartão" |
| Isto ou nada | ?string | "Apelido (opcional)" |
| Qualquer coisa | mixed | A caixa de "achados e perdidos" |
| Este e aquele ao mesmo tempo | Countable&ArrayAccess | "Vaga para quem fala inglês e espanhol" |
| Nunca devolve nada (nem volta) | never | A 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|intdáFatal error: Duplicate type int is redundant. voidnão entra em união (não faz sentido "devolve nada ou um int").- Use com moderação:
int|string|array|boolgeralmente 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.
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 devolvebool. - : int|false
- O padrão "achou a posição ou
false", o mesmo dearray_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
| Retorno | Promete | No 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
- 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.
- 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).
- 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, devolveint(10). - Vale para as funções do PHP: com o modo estrito,
strlen(12345)dáTypeError; sem ele, devolve5. - Precisa ser a primeira instrução do arquivo: só o
<?phpe 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" + 2dá5,"10" == 10dá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 comostring. 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.
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
| Tipo | Aceita | Desde |
|---|---|---|
int, float, string, bool, array | O tipo simples | 7.0 (array já existia antes) |
?T | T ou null | 7.1 |
void | (retorno) não devolve nada | 7.1 |
object, iterable, callable | Qualquer objeto; array ou Traversable; algo chamável | 7.x |
A|B | A ou B | 8.0 |
mixed | Qualquer valor | 8.0 |
static | (retorno) a classe de quem chamou | 8.0 |
never | (retorno) nunca volta | 8.1 |
A&B | Objeto que é A e B | 8.1 |
(A&B)|C | DNF: grupos com "e", separados por "ou" | 8.2 |
true, false, null | Só esse valor | 8.2 (false em união desde 8.0) |
Quando dá errado
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.
O que significa: string $x = null, sem o ?. Funcionou por anos; desde o PHP 8.4, avisa.
Como consertar: ?string $x = null.
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.
$ 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.
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:
2. Talvez um objeto
Escreva o retorno "um Produto ou null" usando o atalho:
3. A primeira linha
Escreva a linha que liga o modo estrito de tipos:
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(nuncaT $x = null, obsoleto no 8.4).mixed: qualquer coisa, de propósito.never: a função nunca volta (exitou exceção).A&B: objeto que é os dois;(A&B)|null: DNF, com parênteses obrigatórios.true,false,nullpodem ser tipos;int|falseé o padrão "achou ou falhou".staticno retorno: "a classe de quem chamou"; combine comnew 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.