PHP Funções Variádicas

Até agora, cada função que você criou recebia um número fixo de argumentos: dois para soma($a, $b), um para saudar($nome). Mas e uma função que soma quantos números você quiser? Ou que recebe uma lista de compras de qualquer tamanho? Para isso existem as funções variádicas (de "variável" + "ádica", que aceita um número variável de argumentos), escritas com três pontinhos: ....

Você vai aprender
  • Como criar uma função que aceita qualquer quantidade de argumentos: function soma(...$numeros).
  • Como misturar parâmetros fixos com o variádico, e como dar um tipo a ele.
  • O jeito antigo, func_get_args(), e por que hoje se usa o ....
  • O spread ("espalhar"): passar um array inteiro como argumentos separados, soma(...$lista).
  • Como a função variádica recebe argumentos nomeados extras.
  • Os erros reais que aparecem quando os pontinhos estão no lugar errado.

Você precisa já conhecer funções, parâmetros (padrão e nomeados) e arrays. Os exemplos rodam na máquina da Ana, na pasta ~/site.


A ideia antes do código: a comanda do garçom

Pense na comanda de um restaurante. O garçom anota o número da mesa (sempre um) e depois os pedidos: pode ser um prato, podem ser sete, pode ser nenhum ainda. A comanda não tem "espaço para exatamente 3 pedidos"; ela tem uma área "pedidos" onde cabe quantos vierem.

Uma função variádica é assim: alguns parâmetros fixos (a mesa) e, no fim, um parâmetro que junta todo o resto num array (os pedidos).

O problema: funções com número fixo de argumentos

Veja o que acontece com uma função comum que soma dois números quando você passa mais, ou menos, do que ela espera:

<?php
function soma($a, $b)
{
    return $a + $b;
}

echo soma(2, 3), "\n";
echo soma(10, 20, 30, 40), "\n";
echo soma(7), "\n";
$ php dois.php
5
30                       « o 30 e o 40 foram ignorados em silêncio!

Fatal error: Uncaught ArgumentCountError: Too few arguments to function soma(), 1 passed in /home/ana/site/dois.php on line 9 and exactly 2 expected in /home/ana/site/dois.php:2
Stack trace:
#0 /home/ana/site/dois.php(9): soma(7)
#1 {main}
  thrown in /home/ana/site/dois.php on line 2

Dois problemas. Com argumentos a mais, o PHP não reclama: só usa os dois primeiros e joga o resto fora, e o resultado sai errado (30 em vez de 100). Com argumentos a menos, ele para com Too few arguments ("argumentos de menos"). A solução não é criar soma3, soma4, soma5… é a função variádica.


O parâmetro variádico: ...$numeros

<?php
function soma(...$numeros)
{
    $total = 0;
    foreach ($numeros as $n) {
        $total += $n;
    }
    return $total;
}

echo soma(2, 3), "\n";
echo soma(10, 20, 30, 40), "\n";
echo soma(7), "\n";
echo soma(), "\n";
...
Três pontos, colados no nome do parâmetro. Querem dizer: "junte aqui todos os argumentos que sobrarem".
$numeros
Dentro da função, é um array comum com os argumentos, na ordem em que vieram: [10, 20, 30, 40].
foreach ($numeros as $n)
Percorre esse array, como qualquer outro (veja foreach).
soma()
Sem nenhum argumento: $numeros é um array vazio, o foreach não dá nenhuma volta e o total fica 0. Não é erro.
$ php soma.php
5
100
7
0

Para ter certeza de que $numeros é mesmo um array, olhe dentro dele com var_dump:

<?php
function mostra(...$numeros)
{
    var_dump($numeros);
}

mostra(10, 20, 30);
mostra();
$ php dentro.php
array(3) {
  [0]=>
  int(10)
  [1]=>
  int(20)
  [2]=>
  int(30)
}
array(0) {            « sem argumentos: array vazio, nunca null
}

Como é um array, tudo o que você sabe de arrays vale: count($numeros) diz quantos vieram, $numeros[0] é o primeiro, array_sum($numeros) soma todos de uma vez.

Experimente: rode e depois chame soma com outros números. Troque o foreach inteiro por return array_sum($numeros); e veja que o resultado é o mesmo. Acrescente echo count($numeros), " números\n"; no começo da função.

<?php
function soma(...$numeros)
{
    $total = 0;
    foreach ($numeros as $n) {
        $total += $n;
    }
    return $total;
}

echo soma(2, 3), "\n";
echo soma(10, 20, 30, 40), "\n";
echo soma(), "\n";

Parâmetros fixos + variádico (e com tipo)

Na comanda, a mesa vem primeiro e os pedidos depois. Na função, os parâmetros fixos vêm primeiro e o variádico sempre por último. A Ana quer a média de um aluno com quantas notas ele tiver:

<?php
function media(string $aluno, float ...$notas): string
{
    $quantas = count($notas);
    if ($quantas === 0) {
        return "$aluno: sem notas";
    }
    $media = array_sum($notas) / $quantas;
    $palavra = $quantas === 1 ? "nota" : "notas";
    return "$aluno: $quantas $palavra, média " . number_format($media, 1, ",", ".");
}

echo media("Ana", 9.5, 8, 10), "\n";
echo media("Bruno", 7), "\n";
echo media("Carla"), "\n";
string $aluno
Parâmetro fixo: o primeiro argumento vai sempre para cá.
float ...$notas
O variádico, com tipo. O tipo vale para cada argumento, não para o array: cada nota precisa ser um número. Os inteiros 8 e 10 viram 8.0 e 10.0.
: string
A função devolve um texto (veja Tipos e Retorno).
media("Carla")
Só o fixo. $notas fica vazio e o if trata esse caso.
$ php media.php
Ana: 3 notas, média 9,2
Bruno: 1 nota, média 7,0
Carla: sem notas

Com o tipo declarado, o PHP confere cada argumento. Um texto no meio dos números para o programa, e a mensagem diz exatamente qual argumento estava errado:

<?php
function soma(int ...$numeros): int
{
    return array_sum($numeros);
}

echo soma(1, 2, 3), "\n";
echo soma(1, "dois", 3), "\n";
$ php tipo.php
6

Fatal error: Uncaught TypeError: soma(): Argument #2 must be of type int, string given, called in /home/ana/site/tipo.php on line 8 and defined in /home/ana/site/tipo.php:2
Stack trace:
#0 /home/ana/site/tipo.php(8): soma(1, 'dois', 3)
#1 {main}
  thrown in /home/ana/site/tipo.php on line 2

"O argumento nº 2 precisa ser do tipo int, foi dado um string". O Argument #2 é a posição na chamada: "dois" é o segundo.


O jeito antigo: func_get_args()

Os três pontinhos chegaram no PHP 5.6 (2014). Antes disso, a única forma era declarar a função sem parâmetros e, lá dentro, pedir a lista do que veio com func_get_args() ("função: pegar os argumentos"). Você ainda vai encontrar isso em código antigo:

<?php
function soma()
{
    $total = 0;
    foreach (func_get_args() as $n) {
        $total += $n;
    }
    return $total;
}

echo soma(2, 3), "\n";
echo soma(10, 20, 30, 40), "\n";
$ php antigo.php
5
100

Funciona, mas olhe a primeira linha: function soma(). Quem lê (ou o editor de código) acha que a função não recebe nada. Também não dá para declarar tipos. Existe ainda func_num_args(), que devolve só a quantidade.

...$numerosfunc_get_args()
A assinatura mostra que aceita vários?SimNão: parece não receber nada
Aceita tipo (int ...$n)SimNão
Recebe argumentos nomeados extrasSim (com as chaves)Não
Quando usarSempre, em código novoSó para entender código antigo

Spread: espalhar um array na chamada

Agora o caminho inverso. A Ana já tem os preços das compras num array e quer passá-los para soma. Se passar o array direto, a função recebe um argumento (o array inteiro), e não três números. Os mesmos três pontos, escritos na chamada, "espalham" o array: cada elemento vira um argumento separado. Esse uso se chama spread ("espalhar") ou argument unpacking ("desempacotar argumentos").

<?php
function soma(int ...$numeros): int
{
    return array_sum($numeros);
}

$compras = [12, 5, 30];
echo soma($compras[0], $compras[1], $compras[2]), "\n";
echo soma(...$compras), "\n";
echo soma(...$compras, ...[100, 200]), "\n";
echo soma(1, 2, ...$compras), "\n";
ChamadaÉ o mesmo que
soma($compras[0], $compras[1], $compras[2])Escrever um por um. Funciona, mas só se você souber o tamanho do array.
soma(...$compras)soma(12, 5, 30)
soma(...$compras, ...[100, 200])soma(12, 5, 30, 100, 200): dá para espalhar mais de um array.
soma(1, 2, ...$compras)soma(1, 2, 12, 5, 30): argumentos comuns antes do spread.
$ php espalhar.php
47
47
347
50
Mesmos pontinhos, papéis opostos

Na declaração (function soma(...$numeros)), os ... juntam vários argumentos num array. Na chamada (soma(...$compras)), eles espalham um array em vários argumentos. Os mesmos três pontos também espalham um array dentro de outro: [...$frutas, ...$verduras] (veja Funções de Array).

Spread em funções com parâmetros fixos

O spread não é só para funções variádicas. Ele serve para qualquer função: cada elemento do array ocupa um parâmetro, na ordem. E, desde o PHP 8.1, um array com chaves de texto é espalhado como argumentos nomeados: cada chave vai para o parâmetro de mesmo nome, em qualquer ordem PHP 8.1+.

<?php
function apresentar(string $nome, int $idade, string $cidade): string
{
    return "$nome, $idade anos, de $cidade";
}

$ana = ["Ana", 29, "Recife"];
echo apresentar(...$ana), "\n";

$bruno = ["cidade" => "Curitiba", "nome" => "Bruno", "idade" => 34];
echo apresentar(...$bruno), "\n";

$carla = ["Carla", 22];
echo apresentar(...$carla), "\n";
$ php fixos.php
Ana, 29 anos, de Recife
Bruno, 34 anos, de Curitiba          « as chaves acharam os parâmetros, fora de ordem

Fatal error: Uncaught ArgumentCountError: Too few arguments to function apresentar(), 2 passed in /home/ana/site/fixos.php on line 14 and exactly 3 expected in /home/ana/site/fixos.php:2
Stack trace:
#0 /home/ana/site/fixos.php(14): apresentar('Carla', 22)
#1 {main}
  thrown in /home/ana/site/fixos.php on line 2

O array da Carla tem só dois elementos, e a função precisa de três. O spread não inventa valores: é como se você tivesse escrito apresentar('Carla', 22), e o stack trace mostra exatamente isso.

Missão: a Ana tem os preços num array e a chamada está passando o array inteiro, o que dá erro. Rode, leia a mensagem e corrija a chamada usando o spread, para aparecer Total: R$ 82.

<?php
function soma(int ...$numeros): int
{
    return array_sum($numeros);
}

$compras = [12, 5, 30, 35];
echo "Total: R$ ", soma($compras), "\n";
<?php
function soma(int ...$numeros): int
{
    return array_sum($numeros);
}

$compras = [12, 5, 30, 35];
echo "Total: R$ ", soma(...$compras), "\n";

Rodando a versão sem spread, o PHP real mostra (repare no soma(Array): um único argumento, o array):

$ php sem-spread.php
Total: R$          « o echo já tinha começado a escrever quando o erro aconteceu
Fatal error: Uncaught TypeError: soma(): Argument #1 must be of type int, array given, called in /home/ana/site/sem-spread.php on line 8 and defined in /home/ana/site/sem-spread.php:2
Stack trace:
#0 /home/ana/site/sem-spread.php(8): soma(Array)
#1 {main}
  thrown in /home/ana/site/sem-spread.php on line 2

Variádicas e argumentos nomeados

Desde o PHP 8.0 você pode chamar funções com argumentos nomeados (nome: valor) PHP 8.0+. Se a função tem um parâmetro variádico, os argumentos nomeados que não correspondem a nenhum parâmetro fixo vão para dentro dele, com o nome como chave:

<?php
function etiqueta(string $texto, string ...$opcoes): string
{
    print_r($opcoes);
    return $texto;
}

etiqueta("Promoção", "negrito", "grande");
etiqueta("Promoção", cor: "vermelho", tamanho: "grande");
$ php nomeados.php
Array
(
    [0] => negrito          « argumentos por posição: chaves 0, 1…
    [1] => grande
)
Array
(
    [cor] => vermelho       « argumentos nomeados: a chave é o nome
    [tamanho] => grande
)

Isso permite funções que aceitam "opções" livres. A Ana usa assim para montar etiquetas HTML para os produtos da lojinha (com htmlspecialchars em tudo o que vai para o HTML; veja Escapar a Saída):

<?php
function etiqueta(string $texto, string ...$opcoes): string
{
    $html = "<span";
    foreach ($opcoes as $nome => $valor) {
        $html .= " data-$nome=\"" . htmlspecialchars($valor) . "\"";
    }
    return $html . ">" . htmlspecialchars($texto) . "</span>";
}

echo etiqueta("Bolo de cenoura", cor: "laranja", preco: "25"), "\n";
echo etiqueta(texto: "Pudim", cor: "amarelo"), "\n";
echo etiqueta("Torta", "salgada"), "\n";
$ php nomeados2.php
<span data-cor="laranja" data-preco="25">Bolo de cenoura</span>
<span data-cor="amarelo">Pudim</span>          « texto: foi para o parâmetro fixo $texto
<span data-0="salgada">Torta</span>           « sem nome, a chave foi 0

Repare na segunda chamada: texto: é o nome de um parâmetro fixo, então foi para $texto, não para $opcoes. E na terceira: um argumento por posição a mais virou a chave 0, o que gerou o atributo estranho data-0. Se a sua função espera opções nomeadas, documente isso (ou confira as chaves dentro dela).


Funções do próprio PHP que são variádicas

Você já usou funções variádicas sem saber. Várias funções prontas do PHP aceitam quantos argumentos você quiser, e com o spread elas trabalham direto com arrays:

<?php
$notas = [7.5, 9.5, 6, 8];
echo max(7.5, 9.5, 6, 8), "\n";
echo max(...$notas), "\n";
echo min(...$notas), "\n";
printf("%s tem %d anos e mora em %s\n", "Ana", 29, "Recife");
$lista = ["pão"];
array_push($lista, "leite", "café", "ovos");
echo implode(", ", $lista), "\n";
$ php prontas.php
9.5
9.5
6
Ana tem 29 anos e mora em Recife
pão, leite, café, ovos

max e min também aceitam um array direto (max($notas)), mas printf (printf e sprintf) e array_push são variádicas de verdade: depois do primeiro argumento, vem "quantos você quiser".

Qual usar?

Você quer…Use
Uma função que aceite qualquer quantidade de valores do mesmo tipofunction f(int ...$valores)
Alguns valores obrigatórios e depois "quantos vierem"function f(string $fixo, float ...$resto)
Passar um array que você já tem para uma função que espera argumentos separadosSpread: f(...$array)
Opções livres com nomeVariádico + argumentos nomeados: f("x", cor: "azul")
A quantidade é sempre a mesma (2, 3…)Parâmetros comuns. Não use variádico só porque pode.
A função recebe uma lista que já nasce como array (linhas de um arquivo, resultados do banco)Um parâmetro array $itens comum costuma ser mais claro

Quando dá errado

Only the last parameter can be variadic
<?php
function media(float ...$notas, string $aluno): string
{
    return $aluno;
}
$ php ultimo.php

Fatal error: Only the last parameter can be variadic in /home/ana/site/ultimo.php on line 2
Stack trace:
#0 {main}

O que significa: "só o último parâmetro pode ser variádico". O variádico junta tudo o que sobrar; se viesse antes, nunca sobraria nada para $aluno. Repare que o erro aparece antes de rodar qualquer linha: o PHP recusa o arquivo inteiro.

Como consertar: mova o variádico para o fim: function media(string $aluno, float ...$notas). Também só pode existir um variádico por função.

Variadic parameter cannot have a default value
<?php
function soma(int ...$numeros = [0]): int
{
    return array_sum($numeros);
}
$ php padrao.php

Fatal error: Variadic parameter cannot have a default value in /home/ana/site/padrao.php on line 2
Stack trace:
#0 {main}

O que significa: "parâmetro variádico não pode ter valor padrão". Ele já tem um: sem argumentos, vale [].

Como consertar: tire o = [0] e trate o array vazio dentro da função, se precisar (if ($numeros === []) …).

Argument #1 must be of type int, array given: faltou o spread
$ php sem-spread.php
Total: R$ 
Fatal error: Uncaught TypeError: soma(): Argument #1 must be of type int, array given, called in /home/ana/site/sem-spread.php on line 8 and defined in /home/ana/site/sem-spread.php:2
…

O que aconteceu: soma($compras) passa o array como um argumento. Sem o tipo int, o erro só apareceria lá dentro, na conta: $numeros seria [[12, 5, 30, 35]] (um array dentro de outro) e o $total += $n pararia com Unsupported operand types: int + array ("tipos de operando não suportados: int + array"), uma mensagem bem menos clara.

Como consertar: soma(...$compras).

Cannot use positional argument after argument unpacking
<?php
function soma(int ...$numeros): int
{
    return array_sum($numeros);
}

$compras = [12, 5, 30];
echo soma(...$compras, 100), "\n";
$ php espalhar-erro.php

Fatal error: Cannot use positional argument after argument unpacking in /home/ana/site/espalhar-erro.php on line 8
Stack trace:
#0 {main}

O que significa: "não é possível usar argumento por posição depois de um desempacotamento". Depois de um ...$array, só podem vir outros ...$array (ou argumentos nomeados).

Como consertar: coloque o valor solto antes (soma(100, ...$compras)) ou dentro de um array espalhado (soma(...$compras, ...[100])).

Cannot use positional argument after named argument
$ php nomeados3.php     « com a linha echo etiqueta(cor: "amarelo", "Pudim");

Fatal error: Cannot use positional argument after named argument in /home/ana/site/nomeados3.php on line 13
Stack trace:
#0 {main}

O que significa: depois do primeiro argumento com nome, todos os seguintes também precisam ter nome. Por posição, o PHP não saberia onde encaixar "Pudim".

Como consertar: argumentos por posição primeiro, nomeados depois: etiqueta("Pudim", cor: "amarelo").

Named parameter $texto overwrites previous argument
$ php sobrescreve.php      « echo etiqueta("Pudim", texto: "Bolo");

Fatal error: Uncaught Error: Named parameter $texto overwrites previous argument in /home/ana/site/sobrescreve.php:6
Stack trace:
#0 {main}
  thrown in /home/ana/site/sobrescreve.php on line 6

O que significa: "o parâmetro nomeado $texto sobrescreve um argumento anterior". "Pudim" já ocupou $texto por posição, e texto: tentou ocupá-lo de novo. Um nome de parâmetro fixo nunca vai para o variádico.

Como consertar: use nomes de opções diferentes dos nomes dos parâmetros fixos.


Referência

FormaO que fazExemplo
function f(...$xs)Junta todos os argumentos num arrayf(1, 2, 3) → $xs = [1, 2, 3]
function f($a, ...$xs)Fixos primeiro; o variádico pega o restof(1, 2, 3) → $a = 1, $xs = [2, 3]
function f(int ...$xs)Tipo conferido em cada argumentof(1, "a") → TypeError
f(...$array)Spread: cada elemento vira um argumentof(...[1, 2]) = f(1, 2)
f(...["a" => 1])Chaves de texto viram argumentos nomeados PHP 8.1+= f(a: 1)
f("x", cor: "azul")Nomeados extras entram no variádico com a chave$xs = ["cor" => "azul"]
func_get_args()(Antigo) array com todos os argumentos recebidosdentro da função
func_num_args()(Antigo) quantos argumentos vieramdentro da função

Exercícios

1. Declarar

Complete para a função aceitar quantos nomes quiser:

function saudar() { foreach ($nomes as $n) echo "Oi, $n!\n"; }

2. Espalhar

O array $turma tem os nomes. Complete a chamada para passar cada nome como um argumento:

saudar();

3. Quantos vieram?

Dentro de function soma(...$numeros), complete para mostrar quantos números foram passados:

echo ;

4. Com function f($a, ...$resto), o que vale $resto em f(5)?

5. Qual destas declarações é válida?

6. Uma função comum soma($a, $b) é chamada com soma(1, 2, 3). O que acontece?

Missão final

Missão: faça duas mudanças. (1) Transforme o parâmetro $item num variádico ...$itens, conte os itens com count e junte-os com implode(", ", $itens) (que cola os elementos de um array com ", " entre eles). (2) Na chamada, passe o array $compras inteiro com spread. A saída deve ser Ana vai comprar 3 itens: pão, leite, café.

<?php
function lista(string $quem, string $item): string
{
    $quantos = 1;
    return "$quem vai comprar $quantos itens: " . $item;
}

$compras = ["pão", "leite", "café"];
echo lista("Ana", $compras[0]), "\n";
<?php
function lista(string $quem, string ...$itens): string
{
    $quantos = count($itens);
    return "$quem vai comprar $quantos itens: " . implode(", ", $itens);
}

$compras = ["pão", "leite", "café"];
echo lista("Ana", ...$compras), "\n";

Resumo

  • Uma função comum ignora em silêncio os argumentos a mais e para com Too few arguments quando faltam.
  • function f(...$xs) junta todos os argumentos num array (vazio se não vier nenhum).
  • O variádico vem sempre por último, é um só e não tem valor padrão. Pode ter tipo: int ...$xs confere cada argumento.
  • func_get_args() é o jeito antigo; em código novo, use ....
  • Na chamada, f(...$array) espalha o array em argumentos. Chaves de texto viram argumentos nomeados (8.1+). Depois de um spread, nada de argumento por posição.
  • Argumentos nomeados que não são de nenhum parâmetro fixo entram no variádico com o nome como chave.