PHP Callables de Primeira Classe
Muitas funções do PHP recebem outra função como argumento: array_map aplica uma função a cada item, usort usa uma função para comparar, array_filter usa uma para decidir quem fica. Algo que pode ser chamado como função é um callable ("chamável"). O PHP tem várias formas de escrever um callable; a mais moderna, do PHP 8.1, é a mais curta e a mais segura: strlen(...).
- O que é um callable e por que passar uma função como valor.
- As formas antigas:
'strlen',[$objeto, 'metodo'],'Classe::metodo', e os problemas delas. call_user_func,is_callablee o tipocallable.Closure::fromCallable().- A sintaxe de callable de primeira classe:
strlen(...),$objeto->metodo(...),Classe::metodo(...). PHP 8.1+ - A usar tudo isso com
array_map,array_filtereusort.
Antes, veja Funções Anônimas e Arrow Functions: lá você criou funções novas para passar adiante (fn($x) => $x * 2). Aqui o assunto é passar funções e métodos que já existem.
A ideia: passar a receita, não o bolo
Quando a Ana pede ao Bruno "faça um bolo", o Bruno entrega o bolo pronto. Mas, se ela pede "use esta receita em cada um destes 10 ingredientes", ela entrega a receita, e o Bruno a aplica quantas vezes precisar.
Em PHP, strlen("caneca") é o bolo: a função roda e devolve 6. Para o array_map, a Ana precisa entregar a receita, a própria função strlen, sem rodá-la. A dúvida é: como escrever "a função strlen" sem chamá-la? É isso que esta aula responde.
As formas antigas: texto e array
Durante muitos anos, o PHP aceitou como callable um texto com o nome da função, ou um array com o objeto e o nome do método:
<?php
class Loja
{
public function __construct(private float $taxa) {}
public function comTaxa(float $preco): float
{
return round($preco * (1 + $this->taxa), 2);
}
public static function formatar(float $valor): string
{
return "R$ " . number_format($valor, 2, ",", ".");
}
}
$precos = [10, 19.9, 49.9];
$loja = new Loja(0.1);
// 1. string com o nome de uma função
print_r(array_map('round', $precos));
// 2. array [objeto, 'método']
print_r(array_map([$loja, 'comTaxa'], $precos));
// 3. array ['Classe', 'método estático'] ou string 'Classe::método'
print_r(array_map(['Loja', 'formatar'], $precos));
print_r(array_map('Loja::formatar', $precos));
// chamando direto a partir de uma variável
$f = 'strtoupper';
echo $f("caneca"), "\n";
$g = [$loja, 'comTaxa'];
echo $g(100), "\n";
echo call_user_func('str_repeat', "ab", 3), "\n";
| Forma antiga | Quer dizer |
|---|---|
'round' | A função round |
[$loja, 'comTaxa'] | O método comTaxa daquele objeto $loja |
['Loja', 'formatar'] ou 'Loja::formatar' | O método estático formatar da classe Loja |
$f("caneca") | Uma variável com um callable pode ser chamada com parênteses |
call_user_func($f, …) | "Chame este callable com estes argumentos" (o mesmo que $f(…)) |
$ php antigo.php
Array
(
[0] => 10
[1] => 20
[2] => 50
)
Array
(
[0] => 11
[1] => 21.89
[2] => 54.89
)
Array
(
[0] => R$ 10,00
[1] => R$ 19,90
[2] => R$ 49,90
)
Array
(
[0] => R$ 10,00
[1] => R$ 19,90
[2] => R$ 49,90
)
CANECA
110
ababab
Funciona, e você vai ver muito isso em código existente. Mas tem três problemas.
Problema 1: o erro aparece tarde e longe
Para o PHP, 'strlenn' é só um texto. Ninguém confere se a função existe até o array_map tentar usá-la:
<?php
$nomes = ["Ana", "Bruno", "Carla"];
print_r(array_map('strlenn', $nomes));
$ php typo.php
Fatal error: Uncaught TypeError: array_map(): Argument #1 ($callback) must be a valid callback or null, function "strlenn" not found or invalid function name in /home/ana/call/typo.php:3
Stack trace:
#0 /home/ana/call/typo.php(3): array_map('strlenn', Array)
#1 {main}
thrown in /home/ana/call/typo.php on line 3
Aqui o erro ficou perto, mas, se o texto tivesse sido guardado numa variável no começo do programa e usado bem depois, o erro apontaria para a linha do uso.
Problema 2: os editores não enxergam
Para o editor (veja Editores e IDEs), 'comTaxa' é um texto qualquer. Se você usar "Renomear método" para trocar comTaxa por aplicarTaxa, o texto não muda e o programa quebra. "Ir para a definição" e "Encontrar usos" também não funcionam.
Problema 3: a visibilidade depende de onde se chama
O array [$this, 'capitalizar'] com um método private funciona dentro da classe, mas o mesmo array, usado de fora, é recusado:
$ php privado.php … Fatal error: Uncaught TypeError: array_map(): Argument #1 ($callback) must be a valid callback or null, cannot access private method Carrinho::capitalizar() in /home/ana/call/privado.php:31 …
(O arquivo completo está mais abaixo, na seção de métodos privados.)
is_callable e o tipo callable
Para saber se um valor pode ser chamado, existe is_callable():
<?php
class Loja
{
public function publico() {}
private function privado() {}
}
$loja = new Loja();
var_dump(is_callable('strlen'));
var_dump(is_callable('strlenn'));
var_dump(is_callable([$loja, 'publico']));
var_dump(is_callable([$loja, 'privado']));
var_dump(is_callable(fn() => 1));
$ php iscall.php bool(true) bool(false) « a função não existe bool(true) bool(false) « existe, mas é privada (vista de fora) bool(true) « funções anônimas também são callables
E, para exigir um callable num parâmetro, use o tipo callable:
<?php
function aplicarATodos(array $itens, callable $acao): array
{
$resultado = [];
foreach ($itens as $item) {
$resultado[] = $acao($item);
}
return $resultado;
}
print_r(aplicarATodos(["ana", "bruno"], ucfirst(...)));
print_r(aplicarATodos(["ana", "bruno"], 'strtoupper'));
print_r(aplicarATodos(["ana", "bruno"], fn($n) => "Olá, $n"));
print_r(aplicarATodos(["ana"], "nao_existe"));
$ php aplicar.php
Array
(
[0] => Ana
[1] => Bruno
)
Array
(
[0] => ANA
[1] => BRUNO
)
Array
(
[0] => Olá, ana
[1] => Olá, bruno
)
Fatal error: Uncaught TypeError: aplicarATodos(): Argument #2 ($acao) must be of type callable, string given, called in /home/ana/call/aplicar.php on line 14 and defined in /home/ana/call/aplicar.php:2
Stack trace:
#0 /home/ana/call/aplicar.php(14): aplicarATodos(Array, 'nao_existe')
#1 {main}
thrown in /home/ana/call/aplicar.php on line 2
As três formas foram aceitas; o texto "nao_existe", não. (O ucfirst(...) da primeira chamada é a forma nova, explicada daqui a pouco.) O tipo callable só pode ser usado em parâmetros e retornos, não em propriedades de classe; para guardar numa propriedade, use o tipo Closure.
Como qualquer nome de função vira callable, $acao = $_GET['acao']; $acao($dados); deixa o visitante escolher qual função do PHP rodar (unlink, system…). Se precisar escolher uma ação pelo nome, use uma lista fechada: $acoes = ['maiuscula' => strtoupper(...), …]; e confira com isset($acoes[$nome]).
Closure::fromCallable()
O PHP 7.1 trouxe um meio-termo: transformar qualquer callable num objeto Closure (o mesmo tipo das funções anônimas). A conferência acontece na hora da conversão:
<?php
class Loja
{
public function __construct(private float $taxa) {}
public function comTaxa(float $preco): float { return round($preco * (1 + $this->taxa), 2); }
}
$tamanho = Closure::fromCallable('strlen');
$taxa = Closure::fromCallable([new Loja(0.1), 'comTaxa']);
var_dump($tamanho);
echo $tamanho("caneca"), "\n";
echo $taxa(100), "\n";
$ php fromcallable.php
object(Closure)#1 (2) {
["function"]=>
string(6) "strlen"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
6
110
O var_dump mostra o que há dentro do Closure: qual função ele embrulha (strlen) e os parâmetros dela ($string, obrigatório). Ainda assim, o nome continua sendo um texto ('strlen'), com os problemas 1 e 2. A forma nova resolve isso.
Callable de primeira classe: strlen(...) PHP 8.1+
A ideia é simples: escreva a chamada normalmente, mas, no lugar dos argumentos, coloque três pontos. O resultado não é a chamada: é um Closure que fará a chamada depois.
<?php
class Loja
{
public function __construct(private float $taxa) {}
public function comTaxa(float $preco): float
{
return round($preco * (1 + $this->taxa), 2);
}
public static function formatar(float $valor): string
{
return "R$ " . number_format($valor, 2, ",", ".");
}
}
$loja = new Loja(0.1);
$tamanho = strlen(...);
$comTaxa = $loja->comTaxa(...);
$formatar = Loja::formatar(...);
var_dump($tamanho);
echo $tamanho("caneca"), "\n";
echo $comTaxa(100), "\n";
echo $formatar(1234.5), "\n";
$precos = [10, 19.9, 49.9];
print_r(array_map($formatar, array_map($comTaxa, $precos)));
- strlen(...)
- "A função
strlen, ainda não chamada." Os...aqui não são o operador de funções variádicas; dentro dos parênteses, sozinhos, querem dizer "os argumentos virão depois". - $loja->comTaxa(...)
- "O método
comTaxadeste objeto". O objeto fica guardado dentro doClosure. - Loja::formatar(...)
- "O método estático
formatarda classeLoja". - $tamanho("caneca")
- Chama de fato, como qualquer função.
$ php primeira.php
object(Closure)#2 (2) {
["function"]=>
string(6) "strlen"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
6
110
R$ 1.234,50
Array
(
[0] => R$ 11,00
[1] => R$ 21,89
[2] => R$ 54,89
)
O resultado é exatamente o mesmo Closure do Closure::fromCallable('strlen'), mas escrito como código, não como texto. Por isso:
- O erro aparece na hora e no lugar certo. Com o nome errado:
$ php typo2.php « $tamanho = strlenn(...);
Fatal error: Uncaught Error: Call to undefined function strlenn() in /home/ana/call/typo2.php:3
Stack trace:
#0 {main}
thrown in /home/ana/call/typo2.php on line 3
- O editor entende: renomear, "ir para a definição" e autocompletar funcionam.
- A visibilidade é a de onde o callable foi criado (próxima seção).
Missão: troque as três formas antigas (o texto 'strtoupper', o array [$loja, 'comTaxa'] e o texto 'Loja::formatar') pela sintaxe de primeira classe: strtoupper(...), $loja->comTaxa(...) e Loja::formatar(...). A saída não deve mudar.
<?php
class Loja
{
public function __construct(private float $taxa) {}
public function comTaxa(float $preco): float
{
return round($preco * (1 + $this->taxa), 2);
}
public static function formatar(float $valor): string
{
return "R$ " . number_format($valor, 2, ",", ".");
}
}
$loja = new Loja(0.1);
$maiuscula = 'strtoupper';
$taxa = [$loja, 'comTaxa'];
$formatar = 'Loja::formatar';
echo $maiuscula("caneca"), "\n";
echo $formatar($taxa(1122.27)), "\n";
<?php
class Loja
{
public function __construct(private float $taxa) {}
public function comTaxa(float $preco): float
{
return round($preco * (1 + $this->taxa), 2);
}
public static function formatar(float $valor): string
{
return "R$ " . number_format($valor, 2, ",", ".");
}
}
$loja = new Loja(0.1);
$maiuscula = strtoupper(...);
$taxa = $loja->comTaxa(...);
$formatar = Loja::formatar(...);
echo $maiuscula("caneca"), "\n";
echo $formatar($taxa(1122.27)), "\n";
Métodos privados como callable
Dentro de uma classe, $this->metodo(...) cria um Closure que "lembra" que foi criado lá dentro. Ele pode ser entregue para fora e continua funcionando, mesmo com um método private:
<?php
class Carrinho
{
private array $itens = ["caneca", "camiseta", "boné"];
private function capitalizar(string $s): string
{
return ucfirst($s);
}
public function nomes(): array
{
return array_map($this->capitalizar(...), $this->itens);
}
public function nomesAntigo(): array
{
return array_map([$this, 'capitalizar'], $this->itens);
}
public function entregarCallable(): Closure
{
return $this->capitalizar(...);
}
}
$c = new Carrinho();
print_r($c->nomes());
print_r($c->nomesAntigo());
$f = $c->entregarCallable();
echo $f("bolsa"), "\n";
print_r(array_map([$c, 'capitalizar'], ["x"]));
$ php privado.php
Array
(
[0] => Caneca
[1] => Camiseta
[2] => Boné
)
Array
(
[0] => Caneca
[1] => Camiseta
[2] => Boné
)
Bolsa « o Closure criado lá dentro funciona aqui fora
Fatal error: Uncaught TypeError: array_map(): Argument #1 ($callback) must be a valid callback or null, cannot access private method Carrinho::capitalizar() in /home/ana/call/privado.php:31
Stack trace:
#0 /home/ana/call/privado.php(31): array_map(Array, Array)
#1 {main}
thrown in /home/ana/call/privado.php on line 31
A classe decide o que entrega: o Closure é uma "chave" que ela mesma deu. Já o array [$c, 'capitalizar'], montado de fora, é recusado, porque quem tenta usá-lo (o array_map, chamado de fora da classe) não tem acesso. Veja Visibilidade.
Na prática: array_map, array_filter, usort
A Ana recebeu uma lista de produtos digitada às pressas, com espaços sobrando, maiúsculas misturadas e itens vazios. Com funções que já existem no PHP, ela limpa tudo sem escrever nenhuma função nova:
<?php
$entrada = [" caneca ", "CAMISETA", " boné", "12", "", "chaveiro "];
$limpos = array_map(trim(...), $entrada);
$semVazios = array_filter($limpos, strlen(...));
$minusculos = array_map(mb_strtolower(...), $semVazios);
usort($minusculos, strcmp(...));
print_r($minusculos);
echo implode(", ", array_map(ucfirst(...), $minusculos)), "\n";
| Linha | O que faz |
|---|---|
array_map(trim(...), $entrada) | Aplica trim (tira os espaços das pontas) a cada item |
array_filter($limpos, strlen(...)) | Mantém os itens em que strlen dá algo "verdadeiro" (diferente de 0): some o texto vazio. Repare na ordem: no array_filter, o callable vem depois do array |
array_map(mb_strtolower(...), …) | Tudo em minúsculas, inclusive letras acentuadas (veja mbstring) |
usort($minusculos, strcmp(...)) | Ordena usando strcmp para comparar dois textos (veja Ordenar Arrays) |
array_map(ucfirst(...), …) | Primeira letra maiúscula, para mostrar |
$ php uso.php
Array
(
[0] => 12
[1] => boné
[2] => camiseta
[3] => caneca
[4] => chaveiro
)
12, Boné, Camiseta, Caneca, Chaveiro
O "12" ficou em primeiro porque, na comparação de textos do strcmp, dígitos vêm antes das letras. Repare também que array_map(ucfirst(...), …) cabe numa linha, enquanto a versão com função anônima seria array_map(fn($s) => ucfirst($s), …). Quando a função já existe e recebe exatamente um argumento, o callable de primeira classe é a escrita mais limpa. Na próxima aula, o operador pipe |> usa essa mesma sintaxe para encadear transformações da esquerda para a direita.
Todas as formas: qual usar
| Forma | Exemplo | Quando usar |
|---|---|---|
| Primeira classe 8.1+ | strlen(...), $obj->m(...), Classe::m(...) | O padrão para funções e métodos que já existem |
| Arrow function | fn($x) => $x * $taxa | Quando é preciso mudar algo: fixar um argumento, combinar duas funções, uma conta pequena |
| Função anônima | function ($x) use ($taxa) { … } | Lógica com várias linhas |
| Texto / array | 'strlen', [$obj, 'm'] | Só em código antigo (PHP < 8.1) ou quando o nome é escolhido de uma lista fechada |
Closure::fromCallable() | Closure::fromCallable('strlen') | PHP 7.1 a 8.0; hoje, prefira (...) |
O array_map passa um argumento por item. array_map(round(...), $precos) funciona porque o segundo parâmetro de round é opcional. Para arredondar com 1 casa, você precisa fixar o segundo argumento, e aí entra a arrow function: array_map(fn($p) => round($p, 1), $precos).
Quando dá errado
O que significa: um callable em forma de texto tem um nome que não existe.
Como consertar: corrija o nome. Melhor ainda: troque por strlen(...), e o próximo erro de digitação vai aparecer na hora, como Call to undefined function.
O que significa: o callable [$objeto, 'metodo'] aponta para um método privado e foi usado de fora da classe.
Como consertar: se a classe quer oferecer esse comportamento, ela entrega um $this->metodo(...) por um método público; senão, torne o método público.
$ php nullsafe.php « $f = $loja?->comTaxa(...);
Fatal error: Cannot combine nullsafe operator with Closure creation in /home/ana/call/nullsafe.php on line 3
Stack trace:
#0 {main}
O que significa: ?-> (veja ?? e ?->) e (...) não combinam: se $loja fosse null, não haveria método para embrulhar.
Como consertar: confira o null antes: $f = $loja !== null ? $loja->comTaxa(...) : null;.
$ php novo.php « $f = new DateTime(...);
Fatal error: Cannot create Closure for new expression in /home/ana/call/novo.php on line 2
Stack trace:
#0 {main}
O que significa: new Classe(...) não vira callable.
Como consertar: use uma arrow function: fn(string $d) => new DateTime($d).
Referência
| Item | O que faz |
|---|---|
funcao(...) | Closure da função |
$objeto->metodo(...) | Closure do método daquele objeto |
Classe::metodo(...), static::metodo(...), self::metodo(...) | Closure do método estático |
$closure(...) | Também funciona com variáveis que já guardam um callable |
Closure::fromCallable($c) | Converte qualquer callable em Closure |
is_callable($v) | true se $v pode ser chamado daqui |
call_user_func($c, ...$args) | Chama o callable (o mesmo que $c(...$args)) |
callable / Closure | Tipos para parâmetros (callable) e propriedades (Closure) |
Exercícios
1. A função como valor
Complete para guardar a função strtolower (sem chamá-la) na sintaxe de primeira classe:
2. O método do objeto
Complete para aplicar o método comTaxa do objeto $loja a cada preço:
3. Forma antiga
Escreva, na forma antiga de texto, o callable do método estático formatar da classe Loja:
4. O que a variável $f guarda depois de $f = strlen(...);?
5. Qual a principal vantagem de strlen(...) sobre 'strlen'?
6. Para arredondar cada preço com 1 casa decimal com array_map, o que usar?
Missão final: a lista de alunos chegou bagunçada. Usando array_map, array_filter e sort, com callables de primeira classe sempre que possível: (1) tire os espaços das pontas com trim(...); (2) remova os nomes vazios com array_filter e strlen(...); (3) deixe cada nome com a primeira letra de cada palavra maiúscula (dica: ucwords(...) depois de mb_strtolower(...)); (4) ordene com sort. A saída deve ser Ana Souza, Bruno Lima, Carla Dias.
<?php
$alunos = [" carla DIAS ", "", "ANA souza", " ", " bruno lima"];
// seu código aqui
echo implode(", ", $alunos), "\n";
<?php
$alunos = [" carla DIAS ", "", "ANA souza", " ", " bruno lima"];
$alunos = array_map(trim(...), $alunos);
$alunos = array_filter($alunos, strlen(...));
$alunos = array_map(mb_strtolower(...), $alunos);
$alunos = array_map(ucwords(...), $alunos);
sort($alunos);
echo implode(", ", $alunos), "\n";
Resumo
- Callable = algo que pode ser chamado: função, método, função anônima. Serve para passar uma "receita" a
array_map,array_filter,usort… - Formas antigas:
'strlen',[$obj, 'metodo'],'Classe::metodo'. Funcionam, mas o erro aparece tarde e o editor não as entende. - PHP 8.1+
strlen(...),$obj->metodo(...),Classe::metodo(...)criam umClosure: conferido na hora, visível para o editor, com a visibilidade de onde foi criado. is_callable()testa;callableé o tipo de parâmetro;Closure, o de propriedade.- Função já pronta, com um argumento:
(...). Precisa fixar argumentos ou combinar: arrow function.