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
- PHP: Dependency Injection (Manual do PHP) — Explicação oficial sobre injeção de dependência no PHP
- PHP-DI Documentation — Documentação completa do container PHP-DI com exemplos práticos
- Symfony DependencyInjection Component — Guia oficial do componente de DI do Symfony
- Laravel Service Container — Documentação do container de serviços do Laravel
- Refactoring Guru: Dependency Injection — Explicação visual do padrão com exemplos em PHP
- PHP The Right Way: Dependency Injection — Seção sobre DI no guia de boas práticas PHP