Injeção de dependência e containers

1. Fundamentos da Injeção de Dependência

A Injeção de Dependência (DI) é um padrão de design onde as dependências de uma classe são fornecidas externamente, em vez de serem criadas internamente. Em PHP, isso significa que uma classe não deve instanciar suas dependências usando new, mas sim recebê-las prontas.

Problema do acoplamento forte:

class UserService {
    private Database $db;

    public function __construct() {
        $this->db = new Database('localhost', 'root', 'senha');
    }
}

Este código é difícil de testar (não podemos mockar o Database), inflexível (toda instância usa as mesmas credenciais) e quebra o princípio da responsabilidade única (UserService também configura conexão).

Benefícios da DI:
- Testabilidade: dependências podem ser substituídas por mocks
- Flexibilidade: trocar implementações sem modificar o consumidor
- Manutenibilidade: responsabilidades claramente separadas

class UserService {
    private DatabaseInterface $db;

    public function __construct(DatabaseInterface $db) {
        $this->db = $db;
    }
}

2. Tipos de Injeção de Dependência

Injeção por Construtor

A mais comum e recomendada. Dependências são obrigatórias e imutáveis.

class OrderService {
    public function __construct(
        private PaymentGateway $gateway,
        private LoggerInterface $logger
    ) {}
}

Injeção por Setter

Útil para dependências opcionais ou que podem ser alteradas durante o ciclo de vida.

class MailService {
    private ?CacheInterface $cache = null;

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

Injeção por Interface

A classe implementa uma interface que recebe a dependência. Menos comum em PHP moderno.

interface CacheAwareInterface {
    public function setCache(CacheInterface $cache): void;
}

class ReportGenerator implements CacheAwareInterface {
    private CacheInterface $cache;

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

3. Implementando DI Manualmente em PHP

Vamos criar um exemplo prático:

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

class SmtpMailer implements MailerInterface {
    public function send(string $to, string $subject, string $body): bool {
        // Lógica de envio SMTP
        return true;
    }
}

class UserService {
    public function __construct(
        private UserRepository $repository,
        private MailerInterface $mailer
    ) {}

    public function register(string $email, string $password): User {
        $user = $this->repository->create($email, $password);
        $this->mailer->send($email, 'Bem-vindo!', 'Conta criada com sucesso.');
        return $user;
    }
}

// Uso manual
$mailer = new SmtpMailer();
$repository = new UserRepository($pdo);
$service = new UserService($repository, $mailer);

Factory Pattern para simplificar:

class ServiceFactory {
    public static function createUserService(): UserService {
        $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
        $repository = new UserRepository($pdo);
        $mailer = new SmtpMailer('smtp.example.com', 587);
        return new UserService($repository, $mailer);
    }
}

Limitação: conforme o projeto cresce, gerenciar manualmente todas as dependências se torna inviável.

4. Introdução aos Containers de DI

Um Container de Injeção de Dependência é um objeto que gerencia a criação e resolução de dependências automaticamente.

Como funciona:
1. Você registra serviços no container
2. Quando solicita um serviço, o container resolve todas as dependências recursivamente
3. Pode controlar ciclo de vida (singleton, nova instância, etc.)

Registro vs Autowiring:
- Registro manual: você define explicitamente como criar cada serviço
- Autowiring: o container usa reflexão para descobrir dependências automaticamente

5. Container de DI na Prática com PHP

Vamos construir um container simples:

class SimpleContainer {
    private array $bindings = [];
    private array $instances = [];

    public function set(string $id, callable $factory): void {
        $this->bindings[$id] = $factory;
    }

    public function singleton(string $id, callable $factory): void {
        $this->bindings[$id] = function () use ($id, $factory) {
            if (!isset($this->instances[$id])) {
                $this->instances[$id] = $factory($this);
            }
            return $this->instances[$id];
        };
    }

    public function get(string $id): mixed {
        if (isset($this->instances[$id])) {
            return $this->instances[$id];
        }

        if (isset($this->bindings[$id])) {
            return $this->bindings[$id]($this);
        }

        // Autowiring básico
        return $this->resolve($id);
    }

    private function resolve(string $class): object {
        $reflection = new ReflectionClass($class);
        $constructor = $reflection->getConstructor();

        if (!$constructor) {
            return $reflection->newInstance();
        }

        $parameters = $constructor->getParameters();
        $dependencies = [];

        foreach ($parameters as $param) {
            $type = $param->getType();
            if ($type && !$type->isBuiltin()) {
                $dependencies[] = $this->get($type->getName());
            } elseif ($param->isDefaultValueAvailable()) {
                $dependencies[] = $param->getDefaultValue();
            } else {
                throw new ContainerException("Cannot resolve {$param->getName()}");
            }
        }

        return $reflection->newInstanceArgs($dependencies);
    }
}

// Uso
$container = new SimpleContainer();
$container->singleton(PDO::class, fn() => new PDO('mysql:host=localhost;dbname=app', 'root', ''));
$container->set(MailerInterface::class, fn() => new SmtpMailer('smtp.example.com', 587));

$service = $container->get(UserService::class); // Resolve automaticamente!

6. Containers Populares no Ecossistema PHP

PHP-DI

Container standalone focado em autowiring:

use DI\ContainerBuilder;

$builder = new ContainerBuilder();
$builder->addDefinitions([
    MailerInterface::class => DI\autowire(SmtpMailer::class),
    PDO::class => DI\factory(function () {
        return new PDO('mysql:host=localhost;dbname=app', 'root', '');
    }),
]);

$container = $builder->build();
$service = $container->get(UserService::class);

Symfony DependencyInjection

Configuração via YAML:

# services.yaml
services:
    App\Service\UserService:
        arguments:
            - '@App\Repository\UserRepository'
            - '@App\Mailer\SmtpMailer'

    App\Mailer\SmtpMailer:
        arguments:
            $host: '%mailer.host%'
            $port: 587

Laravel Service Container

// Service Provider
class AppServiceProvider extends ServiceProvider {
    public function register(): void {
        $this->app->singleton(MailerInterface::class, SmtpMailer::class);
        $this->app->bind(UserRepository::class, function ($app) {
            return new UserRepository($app->make(PDO::class));
        });
    }
}

7. Boas Práticas e Armadilhas Comuns

Evitar Service Locator disfarçado

// RUIM - Service Locator
class UserController {
    public function register(Request $request): Response {
        $service = Container::get('user.service'); // Dependência oculta
    }
}

// BOM - DI real
class UserController {
    public function __construct(
        private UserService $userService
    ) {}

    public function register(Request $request): Response {
        return $this->userService->register($request->all());
    }
}

Ciclos de dependência

class A { public function __construct(B $b) {} }
class B { public function __construct(A $a) {} } // Ciclo!

// Solução: refatorar ou usar setter injection

Testes unitários

class UserServiceTest extends TestCase {
    public function testRegisterSendsEmail(): void {
        $mailer = $this->createMock(MailerInterface::class);
        $mailer->expects($this->once())
               ->method('send');

        $service = new UserService(
            $this->createMock(UserRepository::class),
            $mailer
        );

        $service->register('test@example.com', '123456');
    }
}

8. Caso Prático: Refatorando com DI e Container

Antes (código acoplado)

class UserController {
    public function create(): void {
        $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
        $repo = new UserRepository($pdo);
        $mailer = new SmtpMailer('smtp.example.com', 587);
        $service = new UserService($repo, $mailer);

        $user = $service->register($_POST['email'], $_POST['password']);
        echo "Usuário {$user->id} criado!";
    }
}

Depois (com DI e container)

class UserController {
    public function __construct(
        private UserService $userService
    ) {}

    public function create(Request $request): JsonResponse {
        $user = $this->userService->register(
            $request->get('email'),
            $request->get('password')
        );

        return new JsonResponse(['id' => $user->id]);
    }
}

// Configuração do container
$container = new SimpleContainer();
$container->singleton(PDO::class, fn() => new PDO(getenv('DATABASE_URL')));
$container->set(MailerInterface::class, fn() => new SmtpMailer(getenv('SMTP_HOST'), getenv('SMTP_PORT')));
$container->set(UserController::class, fn($c) => new UserController($c->get(UserService::class)));

$controller = $container->get(UserController::class);

A refatoração trouxe: testabilidade total, configuração centralizada, dependências explícitas e facilidade para trocar implementações (ex: trocar SmtpMailer por SendGridMailer sem alterar UserService).


Referências