Bem-vindo à aula sobre Injeção de Dependência (DI) em PHP. Este é um dos princípios mais importantes no desenvolvimento de software moderno, pois promove a separação de interesses, torna o código mais testável e flexível, e é a base de frameworks como Laravel e Symfony. Nesta aula, você vai entender o que é DI, como ela se relaciona com a Inversão de Controle (IoC), como implementar DI manualmente em seus projetos e, por fim, terá uma visão geral sobre containers de DI, que automatizam esse processo.

Vamos começar do básico: o problema que a DI resolve. Imagine uma classe que precisa de outra para funcionar, como um controlador que precisa de um serviço de email. Se o controlador cria o serviço internamente, ele fica acoplado a uma implementação específica, dificultando testes e mudanças. A Injeção de Dependência resolve isso fornecendo as dependências de fora, geralmente através do construtor. Ao longo da aula, você verá exemplos práticos e como evoluir de uma abordagem ingênua para uma solução profissional com container.

O que é

Injeção de Dependência é um padrão de design onde as dependências de uma classe são fornecidas externamente, em vez de serem criadas internamente. Em vez de fazer new EmailService() dentro da classe, você recebe uma instância já pronta via construtor, setter ou propriedade. Isso inverte o controle: a classe não decide como criar suas dependências, mas apenas declara o que precisa.

A principal vantagem é o desacoplamento. A classe depende apenas de interfaces ou abstrações, não de implementações concretas. Isso permite trocar implementações facilmente (por exemplo, de um serviço de email real para um mock em testes) sem alterar o código da classe. Além disso, promove o princípio da responsabilidade única, pois a classe foca em sua lógica, e a composição das dependências fica a cargo de quem as injeta.

Exemplo simples de DI por construtor:

<?php

class UserController {
    private UserRepository $repository;

    public function __construct(UserRepository $repository) {
        $this->repository = $repository;
    }

    public function show(int $id): User {
        return $this->repository->find($id);
    }
}

// Uso: a dependência é criada fora e injetada
$repository = new UserRepository(new PDO('...'));
$controller = new UserController($repository);

Note que UserController não sabe como o repositório é construído; ele apenas o recebe. Isso torna o controlador testável, pois podemos passar um repositório falso nos testes.

Inversão de controle

Inversão de Controle (IoC) é um princípio mais amplo que DI é uma forma de implementar. IoC significa que o fluxo de controle de um programa é invertido: em vez de o código chamar bibliotecas, o framework ou container assume o controle e chama o seu código. Em termos de dependências, IoC significa que a classe não controla a criação de suas dependências; algo externo (como um container) o faz.

Um exemplo comum é o padrão de localizador de serviços (Service Locator), mas DI é preferível porque torna as dependências explícitas. Com DI, a classe declara suas necessidades via construtor ou propriedades, e o container as resolve e injeta. Isso torna o código mais previsível e fácil de testar, pois você sabe exatamente o que a classe precisa.

Na prática, IoC é frequentemente implementada com um container de DI, que mapeia interfaces para implementações concretas. Quando você pede uma classe ao container, ele resolve todas as dependências recursivamente, criando as instâncias necessárias. Isso é especialmente útil em grandes aplicações, onde a composição manual se torna inviável.

Para ilustrar, considere uma aplicação com várias camadas:

<?php

interface MailerInterface {
    public function send(string $to, string $subject, string $body): void;
}

class SmtpMailer implements MailerInterface {
    public function send(...) { /* implementação */ }
}

class UserRegistration {
    public function __construct(private MailerInterface $mailer) {}

    public function register(User $user): void {
        // salvar no banco...
        $this->mailer->send($user->email, 'Bem-vindo', '...');
    }
}

Sem IoC, o código teria que criar SmtpMailer diretamente, o que acopla a classe a uma implementação específica. Com IoC, o container injeta a implementação configurada, permitindo trocar para LogMailer ou MockMailer sem alterar UserRegistration.

DI manual

A injeção manual é a forma mais simples de DI: você cria as dependências no ponto de composição (geralmente no script principal ou em um arquivo de configuração) e as passa para as classes. Isso é adequado para projetos pequenos ou quando você quer entender o mecanismo.

Vamos criar um exemplo completo com DI manual. Suponha um sistema de notificação que usa um serviço de log e um serviço de email. Vamos definir as interfaces e implementações:

<?php

interface LoggerInterface {
    public function log(string $message): void;
}

interface MailerInterface {
    public function send(string $to, string $subject, string $body): void;
}

class FileLogger implements LoggerInterface {
    public function log(string $message): void {
        file_put_contents('app.log', date('Y-m-d H:i:s') . ' ' . $message . PHP_EOL, FILE_APPEND);
    }
}

class SmtpMailer implements MailerInterface {
    public function send(string $to, string $subject, string $body): void {
        // Simulação de envio
        echo "Enviando email para $to: $subject - $body\n";
    }
}

class NotificationService {
    public function __construct(
        private LoggerInterface $logger,
        private MailerInterface $mailer
    ) {}

    public function notifyUser(string $email, string $message): void {
        $this->logger->log("Notificando $email");
        $this->mailer->send($email, 'Notificação', $message);
    }
}

Agora, para usar, criamos as instâncias e injetamos manualmente:

<?php

$logger = new FileLogger();
$mailer = new SmtpMailer();
$notification = new NotificationService($logger, $mailer);

$notification->notifyUser('usuario@exemplo.com', 'Olá!');

Isso funciona, mas imagine se você tiver dezenas de classes com dependências complexas. A composição manual se torna repetitiva e propensa a erros. Além disso, se você quiser trocar a implementação (por exemplo, usar um mailer em memória para testes), precisará alterar o ponto de criação. Por isso, em aplicações reais, usamos um container de DI.

Container (visão geral)

Um container de DI é uma ferramenta que gerencia a criação e a injeção de dependências automaticamente. Ele funciona como um registro central onde você define como criar cada classe e quais dependências ela precisa. Quando você solicita uma classe, o container resolve todas as dependências recursivamente, criando as instâncias conforme necessário.

Existem vários containers para PHP, como o PHP-DI, o Laravel Container e o Symfony DependencyInjection. Todos seguem a mesma ideia: você registra serviços (classes) e suas dependências, e o container resolve. Muitos oferecem auto-wiring, que é a capacidade de resolver dependências automaticamente com base nos type hints dos construtores, sem configuração explícita.

Vamos ver um exemplo usando PHP-DI, um container popular e simples. Primeiro, você instala via Composer: composer require php-di/php-di. Depois, pode usar o container assim:

<?php

use DI\ContainerBuilder;

require 'vendor/autoload.php';

// Cria o container com auto-wiring
$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true);
$container = $containerBuilder->build();

// Agora, pedimos o NotificationService ao container
$notification = $container->get(NotificationService::class);
$notification->notifyUser('outro@exemplo.com', 'Mensagem via container');

O container, vendo que NotificationService precisa de LoggerInterface e MailerInterface, tentará resolver essas interfaces. Como são interfaces, precisamos dizer ao container qual implementação usar. Podemos configurar isso:

<?php

use DI\ContainerBuilder;
use function DI\autowire;

$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true);

// Define as implementações para as interfaces
$containerBuilder->addDefinitions([
    LoggerInterface::class => autowire(FileLogger::class),
    MailerInterface::class => autowire(SmtpMailer::class),
]);

$container = $containerBuilder->build();

$notification = $container->get(NotificationService::class);

Assim, o container resolve automaticamente as dependências, e você pode trocar as implementações apenas alterando a configuração, sem tocar nas classes. Isso traz imensa flexibilidade e facilita testes, pois você pode configurar o container com mocks.

Em resumo, o container de DI é a evolução natural da injeção manual para projetos maiores, centralizando a configuração e automatizando a resolução de dependências.

Boas práticas

  • Prefira injeção por construtor sempre que possível, pois torna as dependências obrigatórias e explícitas.
  • Dependa de interfaces (abstrações) e não de implementações concretas, para facilitar a troca e os testes.
  • Evite o Service Locator, que esconde dependências e dificulta testes.
  • Use um container de DI em projetos maiores, mas entenda o que ele faz por baixo dos panos.
  • Mantenha as classes coesas: se uma classe tem muitas dependências, talvez seja melhor dividi-la.

Referências

Exercícios

  1. Explique com suas palavras a diferença entre Injeção de Dependência e Inversão de Controle.

    ✓ Resposta: Inversão de Controle (IoC) é um princípio mais amplo que inverte o fluxo de controle do programa, delegando a criação e injeção de dependências a um contêiner externo. A Injeção de Dependência (DI) é uma técnica específica para implementar IoC, onde as dependências são fornecidas a uma classe de fora, geralmente via construtor, em vez de a classe criá-las internamente.
  2. Escreva um código em PHP com uma classe PedidoService que dependa de PedidoRepository e EmailService, usando injeção por construtor.

    ✓ Resposta:
    class PedidoService {
        private PedidoRepository $repository;
        private EmailService $emailService;
    
        public function __construct(PedidoRepository $repository, EmailService $emailService) {
            $this->repository = $repository;
            $this->emailService = $emailService;
        }
    
        public function processar(int $id): void {
            $pedido = $this->repository->find($id);
            $this->emailService->enviar($pedido->getClienteEmail(), 'Pedido processado', 'Seu pedido foi processado.');
        }
    }
  3. O que é auto-wiring em um container de DI? Dê um exemplo de como funciona.

    ✓ Resposta: Auto-wiring é a capacidade do container de resolver automaticamente as dependências de uma classe com base nos type hints de seu construtor, sem precisar de configuração explícita. Por exemplo, se ClienteService tem um construtor __construct(ClienteRepository $repo), o container pode instanciar ClienteRepository automaticamente se ele também não tiver dependências ou se estas forem resolvíveis. Isso reduz a configuração necessária e agiliza o desenvolvimento.
  4. Quais as vantagens de usar uma interface para as dependências em vez de uma classe concreta?

    ✓ Resposta: Usar interfaces permite que a classe dependa de uma abstração, facilitando a troca de implementações sem alterar o código cliente. Isso é essencial para testes (podemos injetar um mock), para suportar múltiplas implementações (ex.: diferentes drivers de banco) e para reduzir o acoplamento, tornando o sistema mais flexível e de fácil manutenção.
  5. Crie um exemplo de configuração de um container PHP-DI que associe uma interface a uma implementação concreta.

    ✓ Resposta:
    use DI\ContainerBuilder;
    use function DI\autowire;
    
    $builder = new ContainerBuilder();
    $builder->useAutowiring(true);
    $builder->addDefinitions([
        LoggerInterface::class => autowire(FileLogger::class),
    ]);
    $container = $builder->build();