Injeção de dependência
Nesta aula, você aprenderá o que é injeção de dependência (DI), como ela se relaciona com a inversão de controle (IoC), como implementar DI manualmente em PHP e como containers de dependência podem gerenciar automaticamente a resolução de dependências, além de exercícios práticos com respostas.
A injeção de dependência (DI) é um padrão de design essencial no desenvolvimento de software moderno, especialmente em aplicações PHP. Ela permite que as dependências de uma classe sejam fornecidas de fora, em vez de a classe criá-las internamente. Isso promove um código mais desacoplado, testável e fácil de manter. Nesta aula, vamos explorar desde os conceitos fundamentais até a implementação prática, incluindo a utilização de containers de dependência.
Para entender a importância da DI, imagine uma classe que precisa de uma conexão com banco de dados. Se ela instanciar diretamente a conexão, fica difícil substituí-la por um mock em testes ou alterar a configuração sem modificar a classe. Com a injeção de dependência, a classe recebe a conexão pronta, tornando-a flexível e reutilizável.
O que é
Injeção de dependência é uma técnica onde um objeto recebe suas dependências (outros objetos ou valores) em vez de criá-las. Isso pode ser feito via construtor, setter ou propriedade pública. O objetivo é reduzir o acoplamento entre classes e aumentar a coesão, facilitando a manutenção e os testes.
Por exemplo, uma classe UserRepository pode precisar de um objeto Database. Sem DI, ela faria algo como $this->db = new Database(). Com DI, o construtor recebe a instância: public function __construct(Database $db). Isso permite que o chamador decida qual implementação de Database usar (por exemplo, MySQL, PostgreSQL ou um mock).
Existem três formas comuns de injeção:
- Injeção por construtor: as dependências são passadas no construtor e geralmente são obrigatórias.
- Injeção por setter: as dependências são definidas por métodos setter, tornando-as opcionais.
- Injeção por propriedade: as dependências são atribuídas diretamente a propriedades públicas (menos recomendado).
A injeção por construtor é a mais utilizada, pois garante que o objeto esteja totalmente inicializado após a criação.
Inversão de controle
Inversão de controle (IoC) é um princípio mais amplo do qual a injeção de dependência é uma implementação. O conceito básico é inverter o fluxo de controle do programa: em vez de o código controlar a criação e o ciclo de vida das dependências, um contêiner ou framework controla isso. Isso é frequentemente chamado de princípio de Hollywood: "Não nos chame, nós chamaremos você".
Na prática, IoC significa que a responsabilidade de instanciar e injetar dependências é transferida para um container ou para o ponto de composição do aplicativo (por exemplo, o arquivo de configuração). Isso permite que o código de negócio se concentre na lógica, sem se preocupar com a criação de objetos.
Um exemplo simples de IoC sem container: em vez de a classe UserService criar um UserRepository, ela recebe a instância via construtor. O código que monta a aplicação cria as instâncias e as injeta. A inversão de controle está no fato de que o UserService não controla mais a criação da dependência.
O IoC pode ser implementado de várias formas: injeção de dependência, service locator, factory patterns, entre outros. A injeção de dependência é a mais popular por sua simplicidade e testabilidade.
DI manual
A injeção manual de dependência consiste em passar as dependências explicitamente, sem usar um container. Isso é feito no código que cria os objetos (geralmente no "composite root"). Vamos ver um exemplo prático.
Suponha que temos uma classe Logger e uma classe UserService que usa o logger. Primeiro, definimos as classes:
<?php
class Logger {
public function log(string $message): void {
echo "[LOG] $message\n";
}
}
class UserService {
private Logger $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function createUser(string $name): void {
$this->logger->log("Criando usuário: $name");
// Lógica para criar usuário...
}
}
No composite root (por exemplo, em um arquivo index.php), instanciamos as dependências e as injetamos:
<?php
require 'Logger.php';
require 'UserService.php';
$logger = new Logger();
$userService = new UserService($logger);
$userService->createUser('João');
Aqui, a injeção é feita manualmente. Isso é simples e explícito, mas pode se tornar trabalhoso quando há muitas dependências ou quando a aplicação cresce. Para mitigar isso, podemos usar um container.
Container (visão geral)
Um container de dependência (ou container de IoC) é uma ferramenta que gerencia a criação e a injeção de dependências automaticamente. Ele conhece as classes e suas dependências e pode instanciá-las sob demanda. Existem muitos containers para PHP, como PHP-DI, Laravel Container, Symfony Service Container, entre outros.
O container funciona como um registro de serviços. Você define como criar cada classe (ou deixa o container auto-resolver via reflection). Ao pedir um serviço, o container verifica as dependências, cria-as recursivamente e retorna a instância pronta.
Vamos ver um exemplo com PHP-DI, um container popular e simples. Primeiro, instale via Composer: composer require php-di/php-di. Depois, podemos usar o container:
<?php
use DI\ContainerBuilder;
use DI\Container;
require 'vendor/autoload.php';
// Definindo as classes
class Logger {
public function log(string $message): void {
echo "[LOG] $message\n";
}
}
class UserService {
private Logger $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function createUser(string $name): void {
$this->logger->log("Criando usuário: $name");
}
}
// Criando o container
$container = new Container();
// Resolvendo UserService automaticamente
$userService = $container->get(UserService::class);
$userService->createUser('Maria');
O container usa a reflexão para descobrir que UserService precisa de Logger, instancia Logger e injeta no construtor. Isso elimina a necessidade de criar manualmente as dependências.
Além disso, containers permitem configurar aliases, parâmetros e até definir instâncias únicas (singleton) ou novas a cada chamada. Eles são essenciais em aplicações grandes, como frameworks MVC, para gerenciar a complexidade.
Boas práticas e observações finais
Ao usar injeção de dependência, é importante:
- Prefira injeção por construtor para dependências obrigatórias.
- Evite criar dependências dentro de classes de negócio.
- Use interfaces para permitir troca de implementações.
- No caso de containers, cuidado com over-engineering: nem sempre é necessário, mas em projetos grandes ajuda.
- Para testes, a DI facilita a substituição por mocks.
Dominar a injeção de dependência é um passo importante para escrever código profissional e sustentável em PHP.
Referências
- PHP: Programação Orientada a Objetos
- Dependency Injection – Wikipedia
- Inversion of Control Containers and the Dependency Injection pattern – Martin Fowler
- PHP-DI – Documentação oficial
- Laravel Container – Documentação oficial
- Symfony Service Container – Documentação oficial
Exercícios
Crie uma classe
EmailServiceque depende de um objetoMailer. Implemente a injeção por construtor e mostre como instanciar manualmente.✓ Resposta:class Mailer { public function send(string $to, string $subject, string $body): void { // envio real } } class EmailService { private Mailer $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function sendWelcome(string $email): void { $this->mailer->send($email, 'Bem-vindo', 'Olá!'); } } $mailer = new Mailer(); $emailService = new EmailService($mailer);Explique a diferença entre injeção de dependência e inversão de controle, dando um exemplo conceitual.
✓ Resposta:Inversão de controle (IoC) é um princípio mais amplo que diz que o fluxo de controle do programa é invertido: em vez de o código controlar a criação de objetos, um container ou framework faz isso. Injeção de dependência (DI) é uma implementação específica de IoC, onde as dependências são fornecidas ao invés de criadas internamente. Exemplo: em vez de a classe
Servicecriar umRepository, ela recebe via construtor. O container (se houver) fornece a instância, invertendo o controle.Escreva um código que demonstre injeção por setter para uma classe
Configque pode ser definida opcionalmente.✓ Resposta:class Config { private ?string $driver = null; public function setDriver(string $driver): void { $this->driver = $driver; } public function getDriver(): ?string { return $this->driver; } } $config = new Config(); $config->setDriver('mysql');Usando PHP-DI, como você faria para registrar uma implementação concreta para uma interface? Dê um exemplo.
✓ Resposta:use DI\Container; interface LoggerInterface {} class FileLogger implements LoggerInterface {} $container = new Container(); $container->set(LoggerInterface::class, DI\create(FileLogger::class)); $logger = $container->get(LoggerInterface::class); // retorna FileLoggerCite pelo menos três benefícios de usar injeção de dependência e explique cada um.
✓ Resposta:- Testabilidade: facilita a substituição por mocks em testes unitários.
- Baixo acoplamento: classes dependem de abstrações, não de implementações concretas.
- Manutenibilidade: alterações em dependências são feitas em um único ponto de configuração.