Closures avançado
LidoNesta aula, você aprenderá técnicas avançadas de closures em PHP, incluindo o método bindTo, a relação entre closures e $this, o controle de escopo e casos de uso práticos. Ao final, você terá um entendimento sólido de como manipular closures para criar soluções elegantes e flexíveis.
Bem-vindo à aula sobre Closures Avançado em PHP. Nesta aula, vamos explorar recursos poderosos das closures (funções anônimas) que vão além do básico. Você já sabe que closures podem capturar variáveis do escopo onde foram criadas, mas agora vamos mergulhar em técnicas como redefinir o objeto ao qual a closure está vinculada, entender como $this funciona dentro de uma closure e como controlar o escopo de execução. Essas capacidades tornam as closures extremamente versáteis e são amplamente utilizadas em frameworks e bibliotecas modernas.
Dominar esses conceitos permitirá que você escreva código mais expressivo e reutilizável, especialmente em contextos como programação funcional, injeção de dependência e design de APIs. Vamos começar!
bindTo
O método bindTo é uma funcionalidade avançada das closures que permite alterar o objeto ao qual a closure está vinculada e também o escopo da classe. Em PHP, uma closure criada dentro de um objeto tem acesso ao $this daquele objeto. Com bindTo, você pode reatribuir essa closure para agir como se fosse um método de outro objeto, mesmo que ela tenha sido definida em um contexto diferente.
A assinatura do método é: Closure::bindTo(?object $newThis, object|string|null $newScope = 'static'). O primeiro parâmetro é o novo objeto que será $this dentro da closure; o segundo define o escopo da classe, que afeta a visibilidade de propriedades e métodos protegidos/privados. Se você passar null como primeiro parâmetro, a closure não terá $this (mas ainda pode ter escopo).
class A {
private $segredo = 'segredo de A';
}
class B {
private $segredo = 'segredo de B';
public function getClosure() {
return function() {
return $this->segredo;
};
}
}
$b = new B();
$closure = $b->getClosure();
// Chamada normal: funciona porque a closure está vinculada a $b
var_dump($closure()); // string(14) "segredo de B"
$a = new A();
// Tentativa de chamar a closure com $this = $a, mas escopo ainda é B
$bound = $closure->bindTo($a);
var_dump($bound()); // Erro: Cannot access private property A::$segredo
// Agora com escopo A, conseguimos acessar o segredo de A
$bound = $closure->bindTo($a, A::class);
var_dump($bound()); // string(14) "segredo de A"No exemplo acima, vemos que o escopo é crucial para acessar membros privados. Sem definir o escopo, a closure ainda tenta acessar a propriedade privada da classe B (o escopo original), mesmo que $this seja de A. Ao definir o escopo como A, a closure pode acessar membros privados de A.
Closures e $this
Dentro de uma closure, a variável $this se comporta de forma especial. Se a closure for criada dentro de um método de uma classe, ela automaticamente captura o $this do objeto daquele método. Isso permite que a closure acesse propriedades e métodos do objeto, mesmo que seja chamada fora do contexto original.
Vamos ver um exemplo clássico de uso de $this em closures, comum em callbacks de arrays ou eventos:
class Usuario {
private $nome;
private $idade;
public function __construct($nome, $idade) {
$this->nome = $nome;
$this->idade = $idade;
}
public function getNome() { return $this->nome; }
public function getIdade() { return $this->idade; }
}
$usuarios = [
new Usuario('Ana', 25),
new Usuario('Bruno', 30),
new Usuario('Carlos', 22)
];
// Usando closure dentro de um método que captura $this
function filtrarPorIdade($usuarios, $idadeMinima) {
return array_filter($usuarios, function($u) use ($idadeMinima) {
return $u->getIdade() >= $idadeMinima;
});
}
$resultado = filtrarPorIdade($usuarios, 25);
foreach ($resultado as $u) {
echo $u->getNome() . "\n"; // Ana, Bruno
}Neste caso, a closure não usa $this porque é definida em uma função, não em um método. Mas se a closure for definida dentro de um método, ela pode acessar $this diretamente:
class Relatorio {
private $titulo;
public function __construct($titulo) {
$this->titulo = $titulo;
}
public function gerarCallback() {
return function() {
// $this está disponível e se refere ao objeto Relatorio
return "Relatório: " . $this->titulo;
};
}
}
$rel = new Relatorio('Vendas');
$cb = $rel->gerarCallback();
var_dump($cb()); // string(20) "Relatório: Vendas"É importante notar que, a partir do PHP 5.4, closures podem ser chamadas diretamente após a criação, mas a captura de $this é automática apenas quando a closure é definida dentro de um método de objeto. Se você quiser usar $this em uma closure definida fora de uma classe, você deve usar bindTo ou Closure::call.
Escopo
O escopo de uma closure em PHP é um conceito sutil, mas poderoso. Quando você cria uma closure, ela captura o escopo da classe onde foi definida (se houver). Esse escopo determina quais membros privados e protegidos a closure pode acessar. O método bindTo permite alterar esse escopo, como vimos.
Além disso, closures têm um escopo de variáveis próprio: elas podem capturar variáveis do escopo onde foram criadas usando use. Essas variáveis são copiadas por valor, a menos que você use & para referência. Vamos explorar um exemplo que mostra a diferença entre escopo de classe e escopo de variáveis:
class Exemplo {
private $dado = 'privado';
public $publico = 'publico';
public function getClosure() {
$valorLocal = 42;
// Captura $valorLocal por valor e $this por padrão
return function() use ($valorLocal) {
return [$this->dado, $this->publico, $valorLocal];
};
}
}
$obj = new Exemplo();
$closure = $obj->getClosure();
var_dump($closure());
// array(3) { [0]=> string(7) "privado" [1]=> string(7) "publico" [2]=> int(42) }Se tentarmos chamar a closure fora do objeto, ela ainda tem acesso a $this porque foi definida dentro do método. Mas se quisermos mudar o escopo, podemos usar bindTo. O escopo também afeta a chamada de métodos privados e protegidos:
class A {
private function privado() { return 'A privado'; }
public function getClosure() {
return function() { return $this->privado(); };
}
}
class B {}
$a = new A();
$closure = $a->getClosure();
var_dump($closure()); // string(9) "A privado"
$b = new B();
$bound = $closure->bindTo($b, A::class); // escopo A
var_dump($bound()); // string(9) "A privado" (mesmo com $this = B, mas escopo A permite acesso a privado)
$boundSemEscopo = $closure->bindTo($b); // escopo padrão 'static', não tem acesso a privado
var_dump($boundSemEscopo()); // Erro: Call to private method A::privado() from scope BNesse exemplo, quando usamos bindTo($b, A::class), a closure é vinculada ao objeto $b como $this, mas o escopo é definido como A, permitindo que a closure acesse métodos privados de A. Isso mostra como o escopo pode ser separado do objeto atual.
Casos de uso
As técnicas avançadas de closures têm vários casos de uso práticos em PHP. Vamos ver alguns:
1. Injeção de dependência e contêineres
Em frameworks como Laravel, closures são usadas para registrar serviços em contêineres de injeção de dependência. O bindTo pode ser usado para reutilizar closures em diferentes contextos.
class Container {
private $services = [];
public function set($nome, Closure $closure) {
$this->services[$nome] = $closure;
}
public function get($nome) {
if (!isset($this->services[$nome])) {
throw new Exception("Serviço não encontrado");
}
// Chama a closure com $this = Container (para acesso a outros serviços)
$closure = $this->services[$nome]->bindTo($this, self::class);
return $closure();
}
}
$container = new Container();
$container->set('db', function() {
return new PDO('mysql:host=localhost;dbname=teste', 'user', 'pass');
});
$container->set('repo', function() {
// $this agora é o Container, podemos obter 'db'
$db = $this->get('db');
return new UsuarioRepository($db);
});
$repo = $container->get('repo');2. Middleware e pipelines
Closures são frequentemente usadas para implementar middlewares em aplicações web. O bindTo pode ser usado para compartilhar estado entre middlewares.
function middleware($request, Closure $next) {
// lógica antes
$response = $next($request);
// lógica depois
return $response;
}
$pipeline = function($request) use ($middlewares) {
// Reduz a lista de middlewares em uma única closure
};
3. Encapsulamento e privacidade
Com bindTo, você pode criar closures que acessam membros privados de objetos, permitindo implementar padrões como "friend classes" ou expor funcionalidades específicas sem tornar métodos públicos.
class Banco {
private $saldo = 1000;
public function getSaldo() { return $this->saldo; }
public function getClosure() {
return function() { return $this->saldo; };
}
}
$banco = new Banco();
$consulta = $banco->getClosure();
// Expor a closure para outro objeto, mas manter escopo privado
$auditor = new Auditor();
$consultaAuditoria = $consulta->bindTo($auditor, Banco::class);
echo $consultaAuditoria(); // 10004. Callbacks dinâmicos
Em bibliotecas de ordenação, filtro, etc., closures permitem customização flexível. Por exemplo, você pode criar uma closure que se adapta ao objeto que a chama.
$ordenaPor = function($a, $b) {
// usa $this se definido
};
5. Testes e mocks
Em testes unitários, você pode usar bindTo para injetar dependências falsas ou acessar internals de classes.
$mock = function() { return 'mock'; };
$closure = function() { return $this->metodoReal(); };
$closure = $closure->bindTo($mock, SomeClass::class);
Esses são apenas alguns exemplos. A flexibilidade das closures avançadas permite soluções criativas para problemas complexos.
Boas práticas
Ao usar closures avançadas, lembre-se de:
- Use
bindTocom cuidado: mudar o escopo pode levar a erros de visibilidade se não for bem compreendido. - Prefira
Closure::callpara chamadas temporárias, pois é mais conciso (disponível desde PHP 7.0). - Documente o escopo esperado das closures, especialmente em APIs públicas.
- Evite capturar
$thisdesnecessariamente em closures que não precisam dele, para evitar acoplamento. - Em contêineres de DI, use closures para lazy loading, mas tenha cuidado com referências circulares.
Referências
- PHP: Closures - Manual oficial
- PHP: Closure::bindTo - Manual oficial
- PHP: Anonymous functions - Manual oficial
- PHP: Closure::call - Manual oficial
- PHP: Closures and anonymous functions (PHP 5.3+)
- PHP: Functions - Manual oficial
Exercícios
- Exercício 1: Crie uma classe
Contadorcom um métodogetClosure()que retorna uma closure que incrementa um contador interno (propriedade privada) e retorna o valor. Teste a closure chamando-a várias vezes. - Exercício 2: Usando
bindTo, crie duas classesAeB, cada uma com uma propriedade privadanome. EmA, crie um método que retorna uma closure que retorna o nome do objeto. UsebindTopara fazer a closure retornar o nome de um objeto deB. - Exercício 3: Explique a diferença entre escopo de classe e escopo de variáveis em closures. Dê um exemplo que ilustre a diferença.
- Exercício 4: Implemente um pequeno contêiner de injeção de dependência que use closures e
bindTopara permitir que uma closure registrada acesse outros serviços do contêiner via$this. - Exercício 5: Crie uma closure que seja capaz de acessar uma propriedade privada de uma classe, mas que possa ser usada em um contexto onde o objeto é de outra classe, usando
bindTocom escopo. Mostre o código e explique o resultado.
class Contador {
private $valor = 0;
public function getClosure() {
return function() {
$this->valor++;
return $this->valor;
};
}
}
$c = new Contador();
$closure = $c->getClosure();
echo $closure(); // 1
echo $closure(); // 2
echo $closure(); // 3class A {
private $nome = 'A';
public function getClosure() {
return function() { return $this->nome; };
}
}
class B {
private $nome = 'B';
}
$a = new A();
$b = new B();
$closure = $a->getClosure();
$bound = $closure->bindTo($b, B::class);
echo $bound(); // Buse. Exemplo: uma closure definida em um método de A pode acessar privados de A (escopo de classe A), mas também pode capturar uma variável local por valor (escopo de variáveis). Alterar o escopo de classe com bindTo não afeta as variáveis capturadas.class Container {
private $services = [];
public function set($nome, Closure $closure) {
$this->services[$nome] = $closure;
}
public function get($nome) {
if (!isset($this->services[$nome])) {
throw new Exception("Serviço não encontrado");
}
$closure = $this->services[$nome]->bindTo($this, self::class);
return $closure();
}
}
$container = new Container();
$container->set('db', function() { return new PDO('mysql:...'); });
$container->set('repo', function() {
$db = $this->get('db');
return new Repo($db);
});
$repo = $container->get('repo');class A {
private $segredo = 'segredo de A';
public function getClosure() {
return function() { return $this->segredo; };
}
}
class B {}
$a = new A();
$b = new B();
$closure = $a->getClosure();
$bound = $closure->bindTo($b, A::class);
echo $bound(); // "segredo de A"
// A closure agora usa $b como $this, mas o escopo é A, permitindo acesso à propriedade privada de A.