Nullable types e o operador ?->

1. Introdução aos Nullable Types no PHP

Antes do PHP 7.1, lidar com valores nulos era uma tarefa ambígua e propensa a erros. Funções que podiam retornar null não tinham uma forma explícita de declarar essa possibilidade na assinatura, forçando os desenvolvedores a confiar em documentação ou inspeção manual do código. O programador precisava adivinhar se um retorno poderia ser nulo.

Com a introdução dos Nullable Types no PHP 7.1, esse cenário mudou drasticamente. Agora é possível declarar explicitamente que um tipo pode aceitar null usando o prefixo ? antes do tipo:

function saudacao(?string $nome): string {
    return "Olá, " . ($nome ?? "visitante") . "!";
}

echo saudacao(null); // Olá, visitante!
echo saudacao("João"); // Olá, João!

A partir do PHP 8.0, uma sintaxe alternativa usando union types também está disponível:

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

Ambas as formas são equivalentes, mas a sintaxe com ? é mais concisa para casos simples.

2. Nullable Types em Parâmetros e Retornos

Parâmetros nullable são especialmente úteis quando um argumento é opcional e seu valor padrão é null:

function configurarEmail(
    string $destinatario,
    ?string $assunto = null,
    ?string $corpo = null
): string {
    $assunto = $assunto ?? "Sem assunto";
    $corpo = $corpo ?? "Sem conteúdo";

    return "Enviando para $destinatario: $assunto - $corpo";
}

echo configurarEmail("user@example.com");
echo configurarEmail("user@example.com", "Olá", "Corpo da mensagem");

Retornos nullable são comuns em funções de busca que podem não encontrar resultados:

function buscarUsuario(int $id): ?array {
    $usuarios = [
        1 => ['nome' => 'Ana', 'email' => 'ana@exemplo.com'],
        2 => ['nome' => 'Carlos', 'email' => 'carlos@exemplo.com'],
    ];

    return $usuarios[$id] ?? null;
}

$usuario = buscarUsuario(3); // null
var_dump($usuario); // NULL

3. Nullable Types em Propriedades de Classe

Propriedades de classe também podem ser declaradas como nullable, permitindo inicialização tardia ou estado opcional:

class Produto {
    public ?string $descricao;
    public ?float $desconto;

    public function __construct(
        public string $nome,
        public float $preco,
        ?string $descricao = null
    ) {
        $this->descricao = $descricao;
        $this->desconto = null;
    }

    public function aplicarDesconto(float $percentual): void {
        $this->desconto = $percentual;
    }
}

$produto = new Produto("Notebook", 3500.00);
echo $produto->descricao; // null (sem erro)
$produto->aplicarDesconto(10.0);

Com promoted properties (PHP 8.0+), a declaração fica ainda mais limpa:

class Pedido {
    public function __construct(
        public string $codigo,
        public ?string $observacao = null,
        public ?\DateTime $dataEntrega = null
    ) {}
}

$pedido = new Pedido("PED-123");
$pedido->dataEntrega = new \DateTime('2025-01-15');

4. O Operador Nullsafe: ?->

O operador nullsafe (?->) foi introduzido no PHP 8.0 e revolucionou a forma como lidamos com encadeamento de chamadas em objetos potencialmente nulos. Em vez de verificar manualmente cada nível, o PHP interrompe automaticamente a execução se encontrar null:

class Endereco {
    public function __construct(
        public string $cidade,
        public string $estado
    ) {}
}

class Usuario {
    public function __construct(
        public string $nome,
        public ?Endereco $endereco = null
    ) {}

    public function getEndereco(): ?Endereco {
        return $this->endereco;
    }
}

$usuario = new Usuario("Maria");
// Sem nullsafe - múltiplas verificações
$cidade = null;
if ($usuario !== null && $usuario->getEndereco() !== null) {
    $cidade = $usuario->getEndereco()->cidade;
}

// Com nullsafe - uma linha
$cidade = $usuario?->getEndereco()?->cidade;
var_dump($cidade); // NULL (sem erro)

5. Casos de Uso do Operador ?->

O grande poder do nullsafe está no encadeamento profundo de objetos:

class Empresa {
    public function __construct(
        public string $nome,
        public ?Departamento $departamento = null
    ) {}
}

class Departamento {
    public function __construct(
        public string $nome,
        public ?Funcionario $gerente = null
    ) {}
}

class Funcionario {
    public function __construct(
        public string $nome,
        public ?string $email = null
    ) {}
}

function obterEmailGerente(?Empresa $empresa): ?string {
    // Encadeamento seguro - interrompe se qualquer nível for null
    return $empresa?->departamento?->gerente?->email;
}

$empresa = new Empresa("TechCorp");
echo obterEmailGerente($empresa); // NULL (sem erro)

$empresaCompleta = new Empresa(
    "TechCorp",
    new Departamento("TI", new Funcionario("Ana", "ana@techcorp.com"))
);
echo obterEmailGerente($empresaCompleta); // ana@techcorp.com

Isso elimina a necessidade de múltiplos blocos if:

// Antes - código verboso
function getCidadeUsuario(?Usuario $usuario): ?string {
    if ($usuario === null) {
        return null;
    }
    $endereco = $usuario->getEndereco();
    if ($endereco === null) {
        return null;
    }
    return $endereco->cidade;
}

// Depois - código elegante
function getCidadeUsuario(?Usuario $usuario): ?string {
    return $usuario?->getEndereco()?->cidade;
}

6. Nullsafe com Arrays e Callables

O operador nullsafe também funciona com acesso a arrays e callables, embora com algumas particularidades:

// Acesso a índices de array com nullsafe
$config = [
    'banco' => [
        'host' => 'localhost',
        'porta' => 3306,
    ]
];

$porta = $config?['banco']?['porta']; // 3306
$senha = $config?['banco']?['senha']; // null (índice não existe)

// Nullsafe em callables
$callable = null;
$resultado = $callable?(); // null (sem erro de "call to null")

$callable = function() {
    return "Executado!";
};
$resultado = $callable?(); // "Executado!"

Limitação importante: O operador ?-> não funciona com métodos estáticos:

class Config {
    public static function get(string $chave): ?string {
        return $_ENV[$chave] ?? null;
    }
}

// Isto NÃO funciona - erro de sintaxe
// $valor = Config?::get('DB_HOST');

// Forma correta
$valor = Config::get('DB_HOST');

7. Boas Práticas e Armadilhas

Quando usar nullable types vs. valores padrão:

// Prefira valores padrão quando possível
function saudacao(string $nome = "Mundo"): string {
    return "Olá, $nome!";
}

// Use nullable quando null tem significado semântico diferente
function buscarOuCriar(string $email): ?Usuario {
    $usuario = Usuario::findByEmail($email);
    return $usuario ?? null; // null significa "não encontrado"
}

Nullsafe não substitui validação completa:

class Logger {
    public function log(string $mensagem): void {
        echo "[LOG] $mensagem\n";
    }
}

$logger = new Logger();
// Nullsafe não valida se o objeto é válido, apenas se não é null
$logger?->log("Teste"); // Funciona

// Mas cuidado com objetos que implementam __call
class Proxy {
    public function __call(string $name, array $args) {
        // Pode retornar null ou lançar exceção
    }
}

Combinação com operadores relacionados:

// Null coalescing (??) - valor padrão para null
$nome = $usuario?->nome ?? "Anônimo";

// Null coalescing assignment (??=) - atribui se for null
$usuario?->apelido ??= "Sem apelido";

// Combinando tudo
$cidade = $usuario?->endereco?->cidade 
    ?? $usuario?->endereco?->estado 
    ?? "Localização desconhecida";

8. Evolução e Comparação com Outros Recursos

Os nullable types e o operador nullsafe representam uma evolução significativa no tratamento de valores nulos no PHP:

// PHP 7.0 - Verificação manual
$cidade = is_null($usuario) ? null : 
         (is_null($usuario->getEndereco()) ? null : 
         $usuario->getEndereco()->cidade);

// PHP 7.1+ - Nullable types + operador ternário
$cidade = $usuario !== null ? 
          ($usuario->getEndereco() !== null ? 
          $usuario->getEndereco()->cidade : null) : null;

// PHP 8.0+ - Nullsafe
$cidade = $usuario?->getEndereco()?->cidade;

// Com match expression (PHP 8.0+)
$status = match(true) {
    $usuario?->ativo => "Ativo",
    $usuario?->inativo => "Inativo",
    default => "Desconhecido"
};

Com a introdução de enums no PHP 8.1, nullable types ganham ainda mais expressividade:

enum StatusUsuario: string {
    case Ativo = 'ativo';
    case Inativo = 'inativo';
}

class Usuario {
    public function __construct(
        public string $nome,
        public ?StatusUsuario $status = null
    ) {}
}

O operador nullsafe, combinado com union types e enums, permite escrever código mais seguro e expressivo, reduzindo drasticamente a quantidade de verificações manuais de null sem sacrificar a segurança de tipos.

Referências