Análise de Código Fonte do Contêiner de Serviços do Laravel: Vinculação de Contêiner

O contêiner de serviços do Laravel oferece várias maneiras de registrar e gerenciar instâncias de classes. Vamos explorar os detalhes de cada método de vinculação e suas implementações internas.

Vinculação (Bind)

O método bind é a forma mais comum de registrar um serviço no contêiner. Ele suporta três tipos de vinculação:

  • Vinculação de uma classe a si mesma.
  • Vinculação de uma classe a uma Closure (função anônima).
  • Vinculação de uma interface a uma implementação concreta.

Implementação do bind

Vamos analisar o código fonte do método bind:


/**
* Registra uma vinculação no contêiner.
*
* @param string|array $abstract O nome abstrato do serviço.
* @param \Closure|string|null $concrete A implementação concreta ou Closure.
* @param bool $shared Indica se a instância deve ser um singleton.
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
   // Remove instâncias obsoletas associadas a este binding.
   $this->dropStaleInstances($abstract);

   // Se nenhuma implementação concreta foi fornecida, usa o nome abstrato como concreto.
   if (is_null($concrete)) {
       $concrete = $abstract;
   }

   // Se a implementação não é uma Closure, cria uma Closure que a constrói.
   // Isso permite a instanciação preguiçosa (lazy instantiation).
   if (! $concrete instanceof Closure) {
       $concrete = $this->createClosureForBinding($abstract, $concrete);
   }

   // Armazena a configuração da vinculação (Closure e se é compartilhada).
   $this->bindings[$abstract] = compact('concrete', 'shared');

   // Se o serviço abstrato já foi resolvido, dispara um evento de "rebound".
   if ($this->isResolved($abstract)) {
       $this->rebound($abstract);
   }
}
   

Passos da Vinculação

  1. Limpeza de Instâncias Obsoletas: O método dropStaleInstances remove quaisquer instâncias singleton e aliases antigos associados à chave abstrata, preparando o contêiner para a nova vinculação.
  2. Criação da Closure: Se a implementação fornecida não for uma Closure (por exemplo, um nome de classe), o método createClosureForBinding (uma versão adaptada do getClosure original) cria uma Closure que encapsula a lógica de construção dessa classe. Isso garante a instanciação preguiçosa.
  3. Registro da Vinculação: A configuração (a Closure de construção e o flag shared) é armazenada no array $this->bindings, associada à chave abstrata.
  4. Callback de Rebinding: Se o serviço abstrato já tiver sido resolvido anteriormente, o método rebound é chamado. Isso permite que quaisquer objetos já instanciados sejam atualizados para refletir a nova vinculação, através de callbacks registrados.

Limpeza de Instâncias Obsoletas (dropStaleInstances)


protected function dropStaleInstances($abstract)
{
   unset($this->instances[$abstract]); // Remove a instância singleton existente.
   unset($this->aliases[$abstract]); // Remove qualquer alias associado.
}
   

Criação da Closure (createClosureForBinding)

Esta função é crucial para a instanciação preguiçosa e para lidar com dependências recursivas. Ela envolve a construção da classe dentro de uma Closure.


protected function createClosureForBinding($abstract, $concrete)
{
   return function ($container, $parameters = []) use ($abstract, $concrete) {
       // Se a classe abstrata é a mesma que a concreta (auto-vinculação), constrói diretamente.
       if ($abstract === $concrete) {
           return $container->build($concrete);
       }
       // Caso contrário, usa makeWith para resolver a dependência, permitindo recursão.
       return $container->makeWith($concrete, $parameters);
   };
}
   

A lógica aqui é que, para auto-vinculações (ex: bind(A::class, A::class)), a construção direta com build evita um loop infinito. Para outras vinculações, makeWith é usado para resolver a dependência, o que pode levar a um processo recursivo de resolução se a implementação também for uma interface ou classe vinculada.

Callback de Rebinding (rebound)

Este método é acionado quando uma vinculação é modificada e o serviço já foi previamente resolvido. Ele garante que quaisquer instâncias existentes sejam atualizadas.


protected function rebound($abstract)
{
   // Resolve novamente a instância para obter a versão atualizada.
   $instance = $this->make($abstract);

   // Executa todos os callbacks de rebound registrados para esta abstração.
   foreach ($this->getReboundCallbacks($abstract) as $callback) {
       call_user_func($callback, $this, $instance);
   }
}

protected function getReboundCallbacks($abstract)
{
   // Retorna os callbacks registrados para a abstração ou um array vazio.
   return $this->reboundCallbacks[$abstract] ?? [];
}
   

Os callbacks de rebound são registrados usando o método rebinding:


public function rebinding($abstract, Closure $callback)
{
   $abstract = $this->getAlias($abstract); // Resolve aliases para obter o nome final.

   // Adiciona o callback à lista de callbacks de rebound.
   $this->reboundCallbacks[$abstract][] = $callback;

   // Se a abstração já estiver vinculada, resolve-a imediatamente para acionar o callback.
   if ($this->bound($abstract)) {
       $this->make($abstract);
   }
}
   

Singleton (Singleton)

O método singleton é uma forma abreviada de bind onde o parâmetro shared é definido como true.


public function singleton($abstract, $concrete = null)
{
   $this->bind($abstract, $concrete, true); // Chama bind com shared = true.
}
   

Instância (Instance)

O método instance registra uma instância concreta diretamente como um singleton, sem a necessidade de resolver ou construir a classe.


public function instance($abstract, $instance)
{
   // Remove quaisquer bindings ou aliases antigos que possam conflitar.
   $this->removeAbstractAlias($abstract);
   unset($this->aliases[$abstract]);

   // Armazena a instância diretamente.
   $this->instances[$abstract] = $instance;

   // Se a abstração já estava vinculada de alguma forma, dispara o callback de rebound.
   if ($this->isBound($abstract)) {
       $this->rebound($abstract);
   }
}

protected function removeAbstractAlias($searched)
{
   // Remove a entrada do alias das listas internas para evitar conflitos.
   if (!isset($this->aliases[$searched])) {
       return;
   }
   foreach ($this->abstractAliases as $abstract => $aliases) {
       foreach ($aliases as $index => $alias) {
           if ($alias == $searched) {
               unset($this->abstractAliases[$abstract][$index]);
           }
       }
   }
}

public function isBound($abstract)
{
   // Verifica se a abstração está registrada como binding, instância ou alias.
   return isset($this->bindings[$abstract]) ||
          isset($this->instances[$abstract]) ||
          $this->isAlias($abstract);
}
   

Vinculação Contextual (Contextual Binding)

As vinculações contextuais permitem especificar implementações específicas para dependências dentro de um construtor de classe particular. Isso é útil quando uma mesma interface precisa de diferentes implementações dependendo da classe que a requer.

O processo envolve o método when, que retorna um ContextualBindingBuilder.


// Exemplo de uso:
// $this->app->when(PhotoController::class)
//           ->needs(Filesystem::class)
//           ->give(function () {
//               return Storage::disk('local');
//           });

public function when($concrete)
{
   // Retorna um builder para definir vinculações contextuais para a classe $concrete.
   // $concrete é resolvido para seu alias final.
   return new ContextualBindingBuilder($this, $this->getAlias($concrete));
}

// Classe ContextualBindingBuilder
class ContextualBindingBuilder
{
   protected $container;
   protected $concrete; // A classe que requer a dependência.

   public function __construct(Container $container, $concrete)
   {
       $this->container = $container;
       $this->concrete = $concrete;
   }

   public function needs($abstract)
   {
       $this->needs = $abstract; // A dependência abstrata a ser resolvida.
       return $this;
   }

   public function give($implementation)
   {
       // Adiciona a vinculação contextual ao contêiner.
       $this->container->addContextualBinding(
           $this->concrete, $this->needs, $implementation
       );
   }
}

// Método no Container
public function addContextualBinding($concrete, $abstract, $implementation)
{
   // Armazena a implementação contextual.
   // A chave é a classe concreta, o valor é um array mapeando abstrações para implementações.
   $this->contextual[$concrete][$this->getAlias($abstract)] = $implementation;
}
   

Quando o contêiner resolve uma dependência, ele primeiro verifica se existe uma vinculação contextual definida para a classe solicitante e a dependência específica.

Vinculação de Tag (Tag Binding)

As vinculações de tag permitem agrupar múltiplos serviços sob uma mesma tag. Isso é útil para recuperar todos os serviços associados a uma tag específica.


public function tag($abstracts, $tags)
{
   // Garante que $tags seja um array.
   $tags = is_array($tags) ? $tags : array_slice(func_get_args(), 1);

   foreach ($tags as $tag) {
       // Inicializa o array de tags se não existir.
       if (!isset($this->tags[$tag])) {
           $this->tags[$tag] = [];
       }

       // Adiciona cada abstração à lista de tags correspondentes.
       foreach ((array) $abstracts as $abstract) {
           $this->tags[$tag][] = $abstract;
       }
   }
}
   

Para resolver todos os serviços associados a uma tag, você usaria o método make com o nome da tag.

Vinculação via Array

O contêiner implementa a interface ArrayAccess, permitindo o uso de sintaxe de array para vinculações.


public function offsetSet($key, $value)
{
   // Se o valor for uma Closure, vincula diretamente.
   // Caso contrário, cria uma Closure que retorna o valor.
   $this->bind($key, $value instanceof Closure ? $value : function () use ($value) {
       return $value;
   });
}
   

Extensão (Extend)

O método extend permite modifiacr ou estender uma instância já existente no contêiner.


public function extend($abstract, Closure $closure)
{
   $abstract = $this->getAlias($abstract); // Resolve o alias final.

   // Se a instância já existe (não é singleton registrado via bind apenas),
   // modifica a instância existente e dispara o rebound.
   if (isset($this->instances[$abstract])) {
       $this->instances[$abstract] = $closure($this->instances[$abstract], $this);
       $this->rebound($abstract);
   } else {
       // Se não é uma instância existente, armazena a closure de extensão.
       // Ela será aplicada na próxima vez que a instância for criada.
       $this->extenders[$abstract][] = $closure;

       // Se o serviço já foi resolvido, dispara o rebound para aplicar a extensão imediatamente.
       if ($this->resolved($abstract)) { // Nota: 'resolved' vs 'isResolved'
           $this->rebound($abstract);
       }
   }
}
   

As extensões para serviços não instanciados (registrados via bind ou singleton) são armazenadas em $this->extenders e aplicadas quando o serviço é resolvido pela primeira vez.

Eventos do Contêiner

O contêiner dispara eventos durante o processo de resolução de serviços, usando os métodos resolving e afterResolving.


public function resolving($abstract, Closure $callback = null)
{
   $abstract = $this->getAlias($abstract); // Resolve aliases.

   // Se apenas um parâmetro é uma Closure, é um callback global.
   if (is_null($callback) && $abstract instanceof Closure) {
       $this->globalResolvingCallbacks[] = $abstract;
   } else {
       // Caso contrário, registra o callback para a abstração específica.
       $this->resolvingCallbacks[$abstract][] = $callback;
   }
}

public function afterResolving($abstract, Closure $callback = null)
{
   $abstract = $this->getAlias($abstract); // Resolve aliases.

   // Se apenas um parâmetro é uma Closure, é um callback global.
   if ($abstract instanceof Closure && is_null($callback)) {
       $this->globalAfterResolvingCallbacks[] = $abstract;
   } else {
       // Caso contrário, registra o callback para a abstração específica.
       $this->afterResolvingCallbacks[$abstract][] = $callback;
   }
}
   

Callbacks registrados com resolving são executados antes da instanciação do objeto, enquanto os callbacks de afterResolving são executados após a instanciação e injeção de dependências.

Tags: laravel IoC service container dependency injection PHP

Publicado em 6-11 05:35 por Thomas