PHP

Closures e funções anônimas no PHP

Conheça mais sobre as funções anônimas, como se comportam no PHP e em qual momento se tornam closures. Além disso, entenda como funcionam os métodos bind(), call() e bindTo() da classe especial Closure.

há 5 anos 7 meses

Formação Desenvolvedor PHP
Conheça a formação em detalhes

Primeiro é preciso que entendamos o que é uma função anônima, que nada mais é que uma função sem nome, que não é invocada pelo seu nome.

Magento - Criação de lojas virtuais Intermediário
Curso Magento - Criação de lojas virtuais Intermediário
Conhecer o curso

Uma função padrão no PHP tem a seguinte sintaxe:

<?php

function foo (string $arg1, int $arg2, ..., type $arg_n) : bool
{
    return true;
}

Um nome, um ou mais argumentos e um retorno.

Ou, pode ter um nome, nenhum argumento e nenhum retorno:

<?php

function helloWorld() : void {
    echo 'Olá mundo!';
}

helloWorld(); // Olá mundo!

As funções podem ser invocadas (maligno isso) de diversas formas:

<?php

function isEven(int $number) : bool
{
    return $number % 2 === 0;
}

isEven(8); // true

$isEven = 'isEven';
$isEven(8); // true

'isEven'(8); // true

call_user_func('isEven', 8); // true

Não pretendo entrar no mérito do que você deve ou não fazer, a melhor ou pior forma. A linguagem oferece diversos mecanismos para esse objetivo e você usa o que achar mais conveniente para o que estiver desenvolvendo. Mas, há de se ressaltar, a invocação direta, pelo nome da função, sempre será a opção mais performática e legível.

Uma função anônima (também conhecida como Lambda) segue a mesma sintaxe de uma função padrão, exceto que ela não possui um nome:

<?php

function (int $number) : bool {
    return $number % 2 === 0;
}

Ela, dessa forma (sem nome), não é possível de ser utilizada. Normalmente, as funções anônimas são atribuídas à variáveis (variable as a function), em retornos de funções/métodos ou utilizadas como callback.

No entanto, se as envolvermos com parênteses, conseguimos resolvê-las e utilizá-las, por exemplo:

<?php

(function (int $number) : bool {
    return $number % 2 === 0;
})(8); // true

Mas, vamos combinar que se for pra usar uma função anônima assim, é melhor que definamos um nome pra ela ou que a atribuemos a uma variável, concorda? Apesar de ser possível, não se vê com frequência a aplicação dessa sintaxe.

Vamos armazená-la em uma variável:

<?php

$isEven = function (int $number) : bool {
    return $number % 2 === 0;
};

$isEven(8); // true

Perfeito. Agora temos como chamar a nossa função anônima, enviá-la como argumento para outras funções ou métodos de classes. O leque de opções já começa a abrir.

Já se perguntou qual é o tipo da variável $isEven uma vez que foi atribuída a ela uma função anônima? Use var_dump() e veja:

var_dump($isEven);

O resultado:

object(Closure)[1]
  public 'parameter' => 
    array (size=1)
      '$number' => string '<required>' (length=10)

É um objeto do tipo Closure. Essa é uma classe especial do PHP que é utilizada para representar uma função anônima. Quando uma função anônima é criada, o seu tipo é Closure. Na prática, uma closure é uma função anônima. Se a variável é do tipo Closure, para o PHP ela representa/é uma função anônima, podendo ser invocada de todas àquelas formas possíveis que mostramos anteriormente.

Não é possível instanciar um objeto da classe Closure, o seu uso é apenas interno, no contexto da criação de funções anônimas. O seu construtor define a visibilidade private e ela é declarada como final (não podendo ser estendida).

Portanto, uma construção dessa:

$myFunction = new Closure();

Gera o seguinte erro fatal:

Instantiation of 'Closure' is not allowed

O Laravel (Framework PHP) faz uso intensivo de closures em todos os seus componentes para os mais diversos casos de uso possíveis.

Laravel - Eloquent ORM
Curso Laravel - Eloquent ORM
Conhecer o curso

Por exemplo tem esse caso aqui onde é possível usar uma closure ao invés de um objeto de regra de validação:

$validator = Validator::make($request->all(), [
    'title' => [
        'required',
        'max:255',
        function($attribute, $value, $fail) {
            if ($value === 'foo') {
                return $fail($attribute.' is invalid.');
            }
        },
    ],
]);

Há diversos outros em casos em que closures são retornadas a partir de métodos:

public function lazy(array $attributes = [])
{
    return function () use ($attributes) {
        return $this->create($attributes);
    };
}

Se você tem uma classe ou função que necessite de um callback (nome ou referência de uma função passada como argumento de outra, ou de um método, para execução em um dado momento), você pode fazer assim, por exemplo:

<?php

function handleSomething(bool $foo, Closure $callback) : void {
    $message = 'Hello World';

    if ($foo === true) {
        $callback($message);
    }
}

handleSomething(true, function($message) {
    echo $message;
}); // Hello World

Teríamos o exato mesmo resultado se tivéssemos passado uma variável que contenha uma função anônima ao invés da própria função anônima:

$callback = function($message) {
    echo $message;
};

handleSomething(true, $callback);

Passada a apresentação inicial, que tal discorrermos um pouco sobre escopo? Uma função anônima, assim como uma função padrão, não consegue utilizar variáveis fora do seu escopo. Ou seja:

<?php

$bar = 'Hello World';

$foo = function () {
    echo $bar;
};

$foo();

Isso vai gerar um erro do tipo Notice, informando que a variável $bar não existe ali no escopo da função anônima. No entanto, existe um construtor chamado use que pode ser usado para importar as variáveis do escopo onde a função anônima está inserida, para dentro do escopo dela.

O exemplo acima pode ser refatorado para:

<?php

$bar = 'Hello World';

$foo = function () use($bar) {
    echo $bar;
};

$foo(); // Hello World

Agora sim temos uma construção válida. As variáveis também podem ser importadas por referência usando o operador & (mas, referências não são assunto para esse artigo.).

Funções anônimas podem retornar outras funções anônimas. É possível, se você usar a criatividade pro mal, criar um “buraco negro”:

<?php

$bar = 'Hello World';

$foo = function () use($bar) {
    return function($message) use($bar) {
        echo $message;
        return function($message) use($bar) {
            echo $message;
            return function() use($bar) {
                echo $bar;
            };            
        };
    };
};

$foo()('Tenho que ')('te dizer: ')(); // Tenho que te dizer: Hello World

(Não façam isso em casa. Foi só uma brincadeira.)

Há uma infinidade de usos para as funções anônimas como, por exemplo, como callbacks da maioria das funções de arrays do PHP:

<?php

$nomes = [
    'Pedro',
    'Jonas',
    'Letícia',
    'Amanda',
    'Patrícia',
];

$nomes = array_filter($nomes, function($nome) {
    return strpos($nome, 'P') !== 0;
});

var_dump($nomes); // Apenas nomes que não iniciam com 'P'

Outros métodos da classe Closure

A classe Closure Implementa quatros métodos: __construct(), bindTo(), bind() e call().

Além disso, ela emula (não o implementa diretamente) o método mágico __invoke() de uma forma indireta, por reflexão. Quando uma classe implementa esse método, ela ganha a capacidade de ser invocada como se fosse uma função:

<?php

class HelloWorld
{
    public function __invoke()
    {
        echo 'Hello World';
    }
}

(new HelloWorld)(); // Hello World

Instanciamos e invocamos o objeto como se fosse uma função, fazendo com que o método __invoke fosse acionado.

É importante notar isso pois, no PHP, quando criamos uma função anônima:

<?php

$message = 'Hello World';

$helloWorld = function() use($message) {
    echo $message;
};

$helloWorld(); // Hello World

Podemos invocar diretamente o método __invoke() que o corpo da função será executado:

<?php

$message = 'Hello World';

$helloWorld = function() use($message) {
    echo $message;
};

$helloWorld->__invoke(); // Hello World

Veja que invocamos o método público __invoke() diretamente, ao invés de $helloWorld();. Isso é possível, mas não é o recomendado, por motivos de performance. Se forçamos o uso de __invoke() numa closure, ela tem muito mais trabalho pra resolver do que uma invocação direta $helloWorld();.

Outra característica das closures é que herdam o contexto do objeto quando trabalhadas dentro de uma classe:

<?php

class Person
{
    protected $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName() : Closure
    {
        return function() {
            return $this->name;
        };
    }
}

$kennedy = new Person('Kennedy');
echo $kennedy->getName()();

Esse exemplo demonstra que a função anônima teve acesso ao objeto por meio da pseudo-variável $this.

A classe Closure implementa um método estático chamado bind(). Ele duplica uma Closure e vincula a ela um objeto e opcionalmente um escopo de classe. Por exemplo, vamos reconstruir o exemplo anterior, dessa forma:

<?php

class Person
{
    protected $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName() : ?string
    {
        return $this->name;
    }
}

$kennedy = new Person('Kennedy');

$getNameClosure = function() {
    return $this->getName();
};

$newClosure = Closure::bind($getNameClosure, $kennedy);
echo $newClosure(); // Kennedy

Veja que, mesmo com a Closure estando fora do escopo da classe Person, conseguimos vinculá-la ao objeto $kennedy. O método bind() vinculou o objeto $kennedy à $getNameClosure gerando uma nova Closure, a $newClosure. Ao invocarmos a $newClosure, $this->getName() é uma instrução válida, demais, agora a pseudo-variável $this da $newClosure tem o escopo do objeto $kennedy.

No entanto, ficamos apenas com acesso a membros públicos do objeto. Altere a visibilidade do método getName() para protected:

protected function getName() : ?string
{
    return $this->name;
}

Execute novamente o exemplo. O seguinte erro será gerado:

Fatal error: Uncaught Error: Call to protected method Person::getName() from context 'Closure'

Veja que na assinatura do método estático bind() temos um terceiro argumento:

public static Closure Closure::bind (Closure $closure , object $newthis [,mixed $newscope = "static"])

O terceiro parâmetro é opcional, é através dele que definimos o novo escopo da closure, que por padrão, é static. Se informarmos um objeto nesse argumento, o escopo dele será utilizado. Isso é o que determina a visibilidade dos métodos protegidos e privados do objeto vinculado.

Portanto, altere a $newClosure para:

$newClosure = Closure::bind($getNameClosure, $kennedy, $kennedy);
echo $newClosure(); // Kennedy

Passamos o objeto $kennedy no terceiro argumento, agora o escopo dele será usado pela closure. Agora o método $this->getName() fica acessível pela closure.

Se você alterar a visibilidade do getName() para private:

private function getName() : ?string
{
    return $this->name;
}

Ainda assim a closure terá acesso a ele. Isso acontece pois, quando vinculamos um objeto à closure e alteramos o seu escopo, como fizemos anteriormente, a lógica do corpo da closure se comporta como se fosse um “método anônimo” (sem nome) dentro daquele objeto.

Claro que isso só deve ser usado quando realmente fizer sentido, ademais, a closure fica com muito poder, não só de acessar membros privados, como também de alterá-los:

<?php

final class Box
{
    private $id = 1;

    public function getId() : int
    {
        return $this->id;
    }
}

$box = new Box();

echo "{$box->getId()} <br>";

$changeIdClosure = function() {
    $this->id = 2;
};

$bindedClosure = Closure::bind($changeIdClosure, $box, $box);
$bindedClosure();

echo "{$box->getId()} <br>";

Veja que alteramos um membro privado do objeto em tempo de execução.

PHP - Orientação a Objetos - Parte 1
Curso PHP - Orientação a Objetos - Parte 1
Conhecer o curso

A classe Closure também implementa um método chamado bindTo(). Ele se comporta exatamente da mesma forma que o bind(), com a diferença que ele não é estático, ele faz uma cópia da closure em que ele faz parte, enquanto que no bind() é preciso que informemos no primeiro argumento qual closure será duplicada com um novo escopo.

Podemos usar o mesmo exemplo anterior, mas usando bindTo(). Basta que alteremos esse trecho:

$bindedClosure = Closure::bind($changeIdClosure, $box, $box);
$bindedClosure();

Para:

$bindedClosure = $changeIdClosure->bindTo($box, $box);
$bindedClosure();

Por fim, ainda temos o método call() , que se comporta exatamente como o bindTo() mas que nos tira a necessidade de criarmos variáveis temporárias. Adaptando o exemplo anterior, teríamos:

<?php

final class Box
{
    private $id = 1;

    public function getId() : int
    {
        return $this->id;
    }
}

$changeIdClosure = function() {
    $this->id = 2;
};

$box = new Box();
echo "{$box->getId()} <br>";

$changeIdClosure->call($box);

echo "{$box->getId()} <br>";

Também poderíamos resolver usando essa sintaxe:

(function() {
    $this->id = 2;
})->call($box);

Se a closure tem a função de retornar algum membro protegido da classe, poderíamos simplificar para:

final class Box
{
    private $id = 1;
}

$closure = function() {
    return $this->id;
};

echo $closure->call(new Box());

A trait Macroable do Laravel (para criação de macros) utiliza bindTo() pra mudar o contexto das closures que ela opera. Vamos testar rapidamente?

Crie um novo projeto, também crie um arquivo composer.json com o seguinte conteúdo:

{
    "require": {
        "illuminate/support": "^5.6"
    }
}

Instale essa dependência:

composer install

Agora, no mesmo diretório, crie um arquivo index.php com o seguinte conteúdo:

<?php

require __DIR__.'/vendor/autoload.php';

use IlluminateSupportTraitsMacroable;

class UserAgent
{
    use Macroable;

    protected $userAgent;

    public function __construct(string $userAgent)
    {
        $this->userAgent = $userAgent;
    }
}

UserAgent::macro('isUsingChrome', function() {
    return preg_match('/Chrome[/s](d+.d+)/', $this->userAgent);
});

$userAgent = new UserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.32 Safari/537.36');

if ($userAgent->isUsingChrome()) {
    echo 'Está usando o Chrome.';
} else {
    echo 'Está usando outro navegador.';
}

Percebeu que aplicamos o que aprendemos anteriormente? A função anônima que definimos para a macro isUsingChrome usa um membro protegido da classe UserAgent. Macros são uma conveniente forma de acrescentar comportamentos às classes em tempo de execução, sem a necesside de compô-las ou estendê-las. O estudo aprofundado da trait Macroable foge um pouco do escopo desse artigo, poderíamos ter um só sobre ela, como ela funciona internamente e como as coisas são resolvidas. Um bom assunto para um próximo artigo. Quem sabe?

Concluindo

Os métodos bind() , bindTo() e call() são formas bem interessantes de se extrair informações de um objeto sem modificar a interface deles. Mas, reforçando o que já havíamos comentado anteriormente, o uso deles precisa ser bem restrito e específico, a fim de manter a previsibilidade do comportamento das nossas classes. Frameworks de testes, bem como os que tratam de assincronismo com PHP, costumam tirar bom proveito desses comportamentos e, principalmente, das closures ou funções anônimas (como você preferir chamar daqui pra frente =D).

Até a próxima!

Desenvolvedor PHP
Formação Desenvolvedor PHP
Conhecer a formação

Autor(a) do artigo

Kennedy Tedesco
Kennedy Tedesco

Head de desenvolvimento. Vasta experiência em desenvolvimento Web com foco em PHP. Graduado em Sistemas de Informação. Pós-graduando em Arquitetura de Software Distribuído pela PUC Minas. Zend Certified Engineer (ZCE) e Coffee Addicted Person (CAP). @KennedyTedesco

Todos os artigos

Artigos relacionados Ver todos