O ?: (Elvis) usa o valor da esquerda só se ele for verdadeiro: qualquer valor "falso", como 0, '', '0', [], false ou null, cai no padrão. O ?? (null coalescing) usa o valor da esquerda sempre que ele existe e não é null: 0 e '' passam direto. Além disso, o ?? não emite o warning Undefined array key quando a chave não existe. Para valor padrão, o operador certo quase sempre é o ??.
O mecanismo: cada operador faz uma pergunta diferente
Os dois parecem fazer a mesma coisa, mas cada um faz uma pergunta diferente ao valor da esquerda.
$a ?: $b é a forma curta de $a ? $a : $b (existe desde o PHP 5.3). O PHP converte $a para booleano e pergunta: "isto é verdadeiro?". A conversão segue a tabela de verdade do PHP, e nela os valores abaixo viram false:
| Valor | Vira booleano |
|---|---|
0, 0.0 |
false |
'' (string vazia) |
false |
'0' (string com o caractere zero) |
false |
[] (array vazio) |
false |
null, false |
false |
'0.0', ' ', 'false' |
true (string não vazia e diferente de '0') |
$a ?? $b (PHP 7.0+) equivale a isset($a) ? $a : $b. A pergunta agora é "isto existe e não é null?". Não há conversão para booleano nenhuma, então 0 é só um inteiro que existe.
$qtd = 0;
var_dump($qtd ?: 10); // int(10) ← o 0 foi trocado
var_dump($qtd ?? 10); // int(0) ← o 0 foi mantido
$nome = '';
var_dump($nome ?: 'anônimo'); // string(7) "anônimo"
var_dump($nome ?? 'anônimo'); // string(0) ""
Por que o ?? não gera "Undefined array key"
Como o ?? herda a semântica do isset(), ele não lê a chave à moda normal. Primeiro ele verifica se ela existe. O ?: faz uma leitura comum de $a, e ler uma chave inexistente é exatamente o que dispara o aviso (um Warning no PHP 8; no 7.x era Notice):
$dados = [];
$x = $dados['k'] ?: 'padrão';
// Warning: Undefined array key "k"
// $x vale 'padrão', mas o log ganhou uma linha
$y = $dados['k'] ?? 'padrão';
// sem aviso nenhum
A proteção vale para a cadeia inteira de acesso, seja variável inexistente, chave aninhada ou propriedade:
var_dump($naoExiste ?? 'x'); // "x", sem "Undefined variable"
var_dump($dados['a']['b']['c'] ?? 'x'); // "x", nenhum nível precisa existir
var_dump($obj->campo ?? 'x'); // "x" se a propriedade não existe
var_dump($usuario?->perfil ?? 'x'); // combina com o nullsafe ?->
O ?? não é "lazy" na esquerda nem esconde erros dentro dela. Em buscar() ?? 'x' a função é chamada normalmente, e qualquer erro lá dentro continua aparecendo. O silêncio vale só para a pergunta final "isto existe?".
Lado a lado: o mesmo dado, dois resultados
Pense num formulário de pedido em que o usuário pode querer 0 unidades de brinde:
// O cliente escolheu 0 brindes...
$brindes = $_POST['brindes'] ?: 1;
// ...e recebe 1. O 0 era falsy e foi "corrigido".
// E se o campo não vier, ainda sai um Warning no log.// Só usa o padrão se o campo não veio (ou veio null)
$brindes = $_POST['brindes'] ?? 1;
// 0 continua 0; campo ausente vira 1, sem warning.Encadeamento: a ?? b ?? 'x'
O ?? é associativo à direita e pode ser encadeado à vontade. O PHP testa da esquerda para a direita e para no primeiro valor que existe e não é null. O padrão clássico é configuração em camadas:
O ponto de partida
A config do usuário tem a chave, mas com null. A variável de ambiente diz 0 (sem limite). O padrão do código é 50.
$cfg = ['limite' => null];
$env = ['LIMITE' => 0];
$limite = $cfg['limite'] ?? $env['LIMITE'] ?? 50;
1º teste: $cfg['limite']
A chave existe, mas vale null. Para o ?? isso é o mesmo que não existir (é a semântica do isset), então ele passa para o próximo operando.
isset($cfg['limite']); // false → segue
2º teste: $env['LIMITE']
Vale 0. Existe e não é null, então a cadeia para aqui. O 50 nem chega a ser avaliado.
$limite; // int(0) ✔ o "sem limite" foi respeitado
O mesmo com ?:
O ?: pergunta "é verdadeiro?". null é falso, segue. 0 também é falso, segue. Sobra o 50, e a escolha explícita de 0 foi descartada sem aviso.
$limite = $cfg['limite'] ?: $env['LIMITE'] ?: 50;
$limite; // int(50) ✘
O operador ??= (PHP 7.4+)
$a ??= $b é a forma curta de $a = $a ?? $b: só atribui se $a não existe ou é null. É ideal para preencher padrões em arrays de opções sem atropelar o que o chamador passou:
function paginar(array $opcoes): array
{
$opcoes['pagina'] ??= 1;
$opcoes['por_pagina'] ??= 20;
$opcoes['offset'] ??= 0; // se veio 0, continua 0
return $opcoes;
}
paginar(['offset' => 0, 'por_pagina' => 50]);
// ['offset' => 0, 'por_pagina' => 50, 'pagina' => 1]
Não existe ?:=. Se você quer "atribua se estiver vazio", escreva $a = $a ?: $b explicitamente, de preferência sabendo que 0 e '' serão substituídos.
Diagnóstico: qual dos dois o seu caso pede?
Antes de escolher, pergunte-se uma coisa só: 0, '' ou '0' são valores válidos para este dado?
- Quantidade, preço, desconto, nota, índice, booleano vindo do banco: sim. Use
??. - Termo de busca, nome de exibição, título opcional, onde string vazia é "não preenchido": não. O
?:faz sentido, mas veja a combinação abaixo.
Para descobrir o que realmente está chegando, inspecione o tipo, não só o valor:
var_dump($_POST['brindes'] ?? 'AUSENTE');
// string(1) "0" → veio do form, como string
// string(0) "" → campo enviado vazio
// "AUSENTE" → campo nem foi enviado
Note que tudo que vem de $_GET/$_POST é string. O '0' do formulário é falsy para o ?:, e é exatamente aí que quantidades "somem".
A combinação que cobre os dois mundos
Quando a chave pode faltar e string vazia deve virar padrão, combine os dois. O ?? evita o warning, e o ?: trata o vazio:
$busca = ($_GET['q'] ?? '') ?: 'todos';
Esse padrão também troca '0' por 'todos'. Se buscar "0" for legítimo, seja explícito:
$busca = trim($_GET['q'] ?? '');
$busca = $busca === '' ? 'todos' : $busca;
Pegadinhas
1. O ?: come o 0 e a string vazia
2. O ?? trata null como "não existe"
$perfil = ['bio' => null];
$perfil['bio'] ?? 'sem bio'; // "sem bio"
array_key_exists('bio', $perfil); // true
Se você precisa distinguir "a chave veio com null" de "a chave não veio" (num PATCH de API, por exemplo, onde null significa "apague"), o ?? não serve. Use array_key_exists().
3. Precedência: o ?? é fraco
O ?? tem precedência mais baixa que ., + e quase tudo, então o PHP agrupa a conta antes:
Com $u = [], o que 'Olá, ' . $u['nome'] ?? 'visitante' produz?
Ver resposta
Resposta certa: "Olá, " e um Warning de chave inexistente
O PHP lê como ('Olá, ' . $u['nome']) ?? 'visitante'. A concatenação roda primeiro, lendo $u['nome'] "na mão" (é ela que gera o warning), e o resultado "Olá, " existe e não é null, então o ?? nunca usa o padrão. O conserto é colocar parênteses:
'Olá, ' . ($u['nome'] ?? 'visitante');
A mesma armadilha vale para contas: $total + $extra['frete'] ?? 10 soma primeiro, e o 10 nunca entra. Regra prática: sempre que o ?? estiver dentro de uma expressão maior, ponha parênteses.
4. Campo obrigatório: falhe em vez de inventar padrão
No PHP 8, throw virou expressão e pode ficar à direita do ??:
$id = $dados['id'] ?? throw new InvalidArgumentException('id obrigatório');
É melhor do que um ?? 0 que deixa um id inválido seguir adiante.
Consertando código que já usa ?:
Não troque todos os ?: por ?? com busca-e-substituição. Alguns foram escritos de propósito para tratar string vazia. Faça uma triagem:
grep -rn '?:' app/ src/ --include='*.php'
Para cada ocorrência, aplique o diagnóstico acima. Se 0/'' forem válidos, troque por ??. Se o vazio deve virar padrão e a chave pode faltar, use a combinação ($x ?? '') ?: $padrao. Se é intencional e a chave sempre existe, deixe como está. Linhas de Undefined array key no log apontando para um ?: são um bom indício do lugar a corrigir primeiro.
Tabela de decisão
| Situação | Use |
|---|---|
| Valor padrão quando a chave/variável pode não existir | $a ?? $padrao |
0, '' ou false são valores válidos |
$a ?? $padrao |
| Várias fontes em ordem de prioridade | $a ?? $b ?? $padrao |
| Preencher opção só se não foi passada | $op['x'] ??= $padrao |
| String vazia deve virar padrão e a chave pode faltar | ($a ?? '') ?: $padrao |
| Distinguir "veio null" de "não veio" | array_key_exists() |
| Campo obrigatório | $a ?? throw new … |
| Teste genuíno de "é verdadeiro?" com a variável sempre definida | $a ?: $b |
O manual documenta o ?? e o ternário ?: juntos, na página dos operadores de comparação, com a tabela de como o PHP compara cada tipo. Vale estudar a página inteira: é o que explica por que '0' é falso e '0.0' não.
A tabela de precedência mostra por que 'Olá, ' . $x ?? 'y' não faz o que parece, e quando os parênteses são obrigatórios.