← Voltar aos artigos

operadores · null · arrays · validação · tipos

Diferença entre ?? (null coalescing) e ?: (Elvis) no PHP

Publicado em 26/10/2026 · 9 min de leitura

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:

evite
// 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.
prefira
// 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
no manualComparison

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.

estudar na árvore →
no manualOperator Precedence

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.

estudar na árvore →

Leve o estudo além do artigo

O manual inteiro do PHP como uma jornada: progresso por tópico, anotações e leitura embutida.