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(...).

Você vai aprender
  • 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_callable e o tipo callable.
  • 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_filter e usort.

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 antigaQuer 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.

Nunca use texto vindo do usuário como callable

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 comTaxa deste objeto". O objeto fica guardado dentro do Closure.
Loja::formatar(...)
"O método estático formatar da classe Loja".
$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";
LinhaO 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

FormaExemploQuando usar
Primeira classe 8.1+strlen(...), $obj->m(...), Classe::m(...)O padrão para funções e métodos que já existem
Arrow functionfn($x) => $x * $taxaQuando é preciso mudar algo: fixar um argumento, combinar duas funções, uma conta pequena
Função anônimafunction ($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 (...)
Cuidado com funções de vários parâmetros

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

must be a valid callback or null, function "strlenn" not found

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.

cannot access private method Carrinho::capitalizar()

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.

Cannot combine nullsafe operator with Closure creation
$ 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;.

Cannot create Closure for new expression
$ 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

ItemO 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 / ClosureTipos 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:

$minuscula = ;

2. O método do objeto

Complete para aplicar o método comTaxa do objeto $loja a cada preço:

$novos = array_map(, $precos);

3. Forma antiga

Escreva, na forma antiga de texto, o callable do método estático formatar da classe Loja:

array_map('', $valores);

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 um Closure: 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.