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

Exercícios

  1. Crie uma classe EmailService que depende de um objeto Mailer. 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);
    
  2. 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 Service criar um Repository, ela recebe via construtor. O container (se houver) fornece a instância, invertendo o controle.

  3. Escreva um código que demonstre injeção por setter para uma classe Config que 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');
    
  4. 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 FileLogger
    
  5. Cite 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.