Variadic functions e spread operator

1. Introdução às Funções Variádicas

Funções variádicas são funções capazes de aceitar um número variável de argumentos. Em PHP, esse recurso resolve um problema clássico: como criar funções flexíveis que processam quantidades imprevisíveis de parâmetros sem depender de arrays explícitos ou soluções improvisadas.

Antes do PHP 5.6, a única maneira de lidar com argumentos variáveis era usando func_get_args(), func_num_args() e func_get_arg(). Essas funções internas funcionavam, mas exigiam código mais verboso e não ofereciam type hinting. O operador ... (splat operator) mudou esse cenário, trazendo uma sintaxe limpa e integrada ao sistema de tipos do PHP.

2. Sintaxe Básica com ...$args

A declaração de um parâmetro variádico é feita prefixando o nome do parâmetro com três pontos:

function soma(...$numeros) {
    return array_sum($numeros);
}

echo soma(1, 2, 3, 4, 5); // 15
echo soma(10, 20);        // 30
echo soma();              // 0 (array vazio)

Quando a função é chamada, todos os argumentos passados são automaticamente empacotados em um array. O parâmetro $numeros se torna um array contendo cada argumento na ordem em que foi fornecido.

Regra fundamental: o parâmetro variádico deve ser sempre o último na assinatura da função. Colocá-lo em outra posição gera um erro fatal.

// ERRADO: Parse error
function exemplo(...$args, $extra) { }

// CORRETO
function exemplo($fixo, ...$variavel) { }

3. Tipagem e Type Hinting em Parâmetros Variádicos

Um dos grandes avanços do operador ... é a possibilidade de aplicar type hints diretamente ao parâmetro variádico:

function media(float ...$valores): float {
    if (empty($valores)) {
        return 0.0;
    }
    return array_sum($valores) / count($valores);
}

echo media(10.5, 20.3, 30.1); // 20.3
echo media(1, 2, 3);          // 2.0 (int convertido para float)
// echo media("a", "b");      // TypeError: Argument must be float

A tipagem garante que todos os argumentos passados sejam do tipo especificado. O PHP aplica a coerção normalmente (se em modo estrito) ou converte conforme as regras de tipo. Isso elimina a necessidade de validações manuais dentro da função.

Limitação: o tipo é único para todos os argumentos. Não é possível definir tipos diferentes para argumentos variádicos — para isso, ainda é necessário usar arrays ou objetos específicos.

4. Spread Operator em Chamadas de Funções

O operador ... também funciona no sentido inverso: desempacotar arrays ou objetos Traversable em argumentos individuais ao chamar funções.

function cumprimentar($saudacao, $nome, $pontuacao = ".") {
    return "$saudacao, $nome$pontuacao";
}

$dados = ["Olá", "Maria", "!"];
echo cumprimentar(...$dados); // "Olá, Maria!"

Exemplos práticos com funções nativas:

// array_merge com spread
$parte1 = [1, 2, 3];
$parte2 = [4, 5, 6];
$parte3 = [7, 8, 9];
$tudo = array_merge(...[$parte1, $parte2, $parte3]);
// Resultado: [1, 2, 3, 4, 5, 6, 7, 8, 9]

// sprintf com argumentos dinâmicos
$formato = "Nome: %s, Idade: %d, Cidade: %s";
$valores = ["João", 30, "São Paulo"];
echo sprintf($formato, ...$valores);
// "Nome: João, Idade: 30, Cidade: São Paulo"

5. Combinação de Argumentos Fixos e Variádicos

É comum combinar parâmetros obrigatórios com o variádico para criar APIs expressivas:

function log($nivel, ...$mensagens) {
    $timestamp = date('Y-m-d H:i:s');
    foreach ($mensagens as $msg) {
        echo "[$timestamp][$nivel] $msg\n";
    }
}

log('INFO', 'Sistema iniciado', 'Conexão ok', 'Usuário logado');
// [2025-01-15 10:30:00][INFO] Sistema iniciado
// [2025-01-15 10:30:00][INFO] Conexão ok
// [2025-01-15 10:30:00][INFO] Usuário logado

Também é possível usar valores padrão antes do variádico:

function config($chave, $valor = null, ...$opcoes) {
    $resultado = ['chave' => $chave, 'valor' => $valor];
    if (!empty($opcoes)) {
        $resultado['extra'] = $opcoes;
    }
    return $resultado;
}

print_r(config('timeout', 30, 'cache', 'ssl'));
// Array ( [chave] => timeout [valor] => 30 [extra] => Array ( [0] => cache [1] => ssl ) )

A ordem correta é sempre: parâmetros fixos → parâmetros com valor padrão → parâmetro variádico.

6. Spread Operator em Declarações de Array

Desde o PHP 7.4, é possível usar o spread operator dentro de arrays literais:

$numeros1 = [1, 2, 3];
$numeros2 = [4, 5, 6];
$combinado = [0, ...$numeros1, ...$numeros2, 7];
// Resultado: [0, 1, 2, 3, 4, 5, 6, 7]

Isso funciona com qualquer Traversable:

function gerarPares($limite) {
    for ($i = 2; $i <= $limite; $i += 2) {
        yield $i;
    }
}

$lista = [1, ...gerarPares(10), 11];
// [1, 2, 4, 6, 8, 10, 11]

Cuidados com arrays associativos: arrays com chaves string podem sobrescrever valores se houver chaves duplicadas. Arrays numéricos são reindexados automaticamente:

$a = ['x' => 10, 'y' => 20];
$b = ['y' => 30, 'z' => 40];
$c = [...$a, ...$b];
// ['x' => 10, 'y' => 30, 'z' => 40] (y foi sobrescrito)

7. Funções Variádicas com Referências (&)

Parâmetros variádicos também podem ser passados por referência, permitindo modificar múltiplas variáveis simultaneamente:

function incrementar(&...$numeros) {
    foreach ($numeros as &$n) {
        $n++;
    }
}

$a = 1;
$b = 2;
$c = 3;
incrementar($a, $b, $c);
echo "$a, $b, $c"; // 2, 3, 4

Limitações: não é possível misturar parâmetros por valor e por referência no mesmo variádico. Apenas variáveis podem ser passadas (não literais ou expressões). Use com moderação, pois referências podem tornar o código menos previsível.

8. Casos de Uso Avançados e Boas Práticas

Funções de callback flexíveis:

function aplicar(callable $fn, ...$args) {
    return $fn(...$args);
}

$resultado = aplicar(function($a, $b) {
    return $a * $b;
}, 5, 3);
echo $resultado; // 15

Middlewares e pipelines:

function pipeline($valor, callable ...$etapas) {
    foreach ($etapas as $etapa) {
        $valor = $etapa($valor);
    }
    return $valor;
}

$resultado = pipeline(5,
    fn($n) => $n * 2,
    fn($n) => $n + 1,
    fn($n) => $n ** 2
);
echo $resultado; // 121

Quando evitar:
- Se o número de argumentos for sempre pequeno e fixo, parâmetros nomeados são mais legíveis.
- Em APIs públicas, considere se arrays nomeados (com chaves descritivas) não seriam mais autoexplicativos.
- O empacotamento/desempacotamento tem custo mínimo, mas em loops muito apertados (milhares de iterações) o func_get_args() pode ser marginalmente mais rápido.

Performance: em versões modernas do PHP (8.x), o operador ... é geralmente tão rápido quanto func_get_args() para a maioria dos casos de uso. A diferença é irrelevante em aplicações reais.

Referências