Passando Dependências

Argumentos, ou na terminologia de DI “dependências”, podem ser passados para classes das seguintes maneiras principais:

  • passagem pelo construtor
  • passagem por método (chamado setter)
  • configuração de variável
  • método, anotação ou atributo inject

Agora mostraremos cada variante com exemplos concretos.

Passagem pelo construtor

As dependências são passadas no momento da criação do objeto como argumentos do construtor:

class MyClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass($cache);

Esta forma é adequada para dependências obrigatórias que a classe necessita essencialmente para sua função, pois sem elas a instância não poderá ser criada.

A partir do PHP 8.0, podemos usar uma forma mais curta de notação (constructor property promotion), que é funcionalmente equivalente:

// PHP 8.0
class MyClass
{
	public function __construct(
		private Cache $cache,
	) {
	}
}

A partir do PHP 8.1, a variável pode ser marcada com o sinalizador readonly, que declara que o conteúdo da variável não mudará mais:

// PHP 8.1
class MyClass
{
	public function __construct(
		private readonly Cache $cache,
	) {
	}
}

O contêiner de DI passa as dependências para o construtor automaticamente usando autowiring. Argumentos que não podem ser passados dessa forma (por exemplo, strings, números, booleanos) escrevemos na configuração.

Constructor hell

O termo constructor hell descreve a situação em que um descendente herda de uma classe pai cujo construtor requer dependências, e ao mesmo tempo o descendente requer dependências. Ele também deve receber e passar as dependências do pai:

abstract class BaseClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

final class MyClass extends BaseClass
{
	private Database $db;

	// ⛔ CONSTRUCTOR HELL
	public function __construct(Cache $cache, Database $db)
	{
		parent::__construct($cache);
		$this->db = $db;
	}
}

O problema surge no momento em que queremos alterar o construtor da classe BaseClass, por exemplo, quando uma nova dependência é adicionada. Então, é necessário modificar também todos os construtores dos descendentes. O que torna tal modificação um inferno.

Como evitar isso? A solução é dar preferência à composição em vez de herança.

Ou seja, projetaremos o código de forma diferente. Evitaremos classes abstratas Base*. Em vez de MyClass obter certas funcionalidades herdando de BaseClass, essa funcionalidade será passada como dependência:

final class SomeFunctionality
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

final class MyClass
{
	private SomeFunctionality $sf;
	private Database $db;

	public function __construct(SomeFunctionality $sf, Database $db) // ✅
	{
		$this->sf = $sf;
		$this->db = $db;
	}
}

Passagem por setter

As dependências são passadas chamando um método que as armazena em uma variável privada. A convenção usual de nomenclatura para esses métodos é a forma set*(), por isso são chamados de setters, mas podem, é claro, ter qualquer outro nome.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass;
$obj->setCache($cache);

Este método é adequado para dependências opcionais que não são essenciais para a função da classe, pois não há garantia de que o objeto realmente receberá a dependência (ou seja, que o usuário chamará o método).

Ao mesmo tempo, este método permite chamar o setter repetidamente e, assim, alterar a dependência. Se isso não for desejado, adicionamos uma verificação ao método ou, a partir do PHP 8.1, marcamos a propriedade $cache com o sinalizador readonly.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		if (isset($this->cache)) {
			throw new RuntimeException('A dependência já foi definida');
		}
		$this->cache = $cache;
	}
}

A chamada do setter é definida na configuração do contêiner de DI na chave setup. Aqui também se utiliza a passagem automática de dependências por autowiring:

services:
	-	create: MyClass
		setup:
			- setCache

Configuração de variável

As dependências são passadas escrevendo diretamente na variável de membro:

class MyClass
{
	public Cache $cache;
}

$obj = new MyClass;
$obj->cache = $cache;

Este método é considerado inadequado porque a variável de membro deve ser declarada como public. E, portanto, não temos controle sobre se a dependência passada será realmente do tipo especificado (válido antes do PHP 7.4) e perdemos a capacidade de reagir à dependência recém-atribuída com código próprio, por exemplo, para impedir alterações subsequentes. Ao mesmo tempo, a variável se torna parte da interface pública da classe, o que pode não ser desejável.

A configuração da variável é definida na configuração do contêiner de DI na seção setup:

services:
	-	create: MyClass
		setup:
			- $cache = @\Cache

Inject

Enquanto os três métodos anteriores se aplicam geralmente em todas as linguagens orientadas a objetos, a injeção por método, anotação ou atributo inject é específica puramente para presenters no Nette. Eles são discutidos em um capítulo separado.

Qual método escolher?

  • o construtor é adequado para dependências obrigatórias que a classe necessita essencialmente para sua função
  • o setter, por outro lado, é adequado para dependências opcionais, ou dependências que podem ser alteradas posteriormente
  • variáveis públicas não são adequadas
versão: 3.x