O que é injeção de dependências (DI) e por que ela reduz acoplamento
Injeção de dependências é uma técnica em que um objeto recebe (por construtor, método ou propriedade) tudo o que precisa para trabalhar, em vez de criar essas dependências internamente. Na prática, isso reduz acoplamento porque a classe deixa de conhecer detalhes de construção (DSN, credenciais, implementação concreta de logger, etc.) e passa a depender de contratos (interfaces) e valores já prontos.
No Slim, DI costuma ser aplicada com um container PSR-11 que sabe como construir objetos (infraestrutura) e as Actions/Handlers apenas usam esses objetos (aplicação). O objetivo é: separar construção do uso.
DI vs Service Locator (o anti-padrão comum)
Um erro frequente é transformar o container em um “super objeto global” acessado de dentro das classes (Service Locator). Isso esconde dependências e dificulta testes.
// Evite: dependências escondidas (Service Locator) dentro da Action class UserListAction { public function __invoke($request, $response, $args) { $repo = $this->container->get(UserRepository::class); // ... } }Prefira declarar dependências explicitamente no construtor:
// Prefira: dependências explícitas final class UserListAction { public function __construct(private UserRepository $repo) {} public function __invoke($request, $response, $args) { $users = $this->repo->all(); // ... } }Container PSR-11 no Slim: estrutura recomendada
O Slim 4 trabalha bem com containers PSR-11. Uma abordagem comum é usar PHP-DI (ou outro container PSR-11) e registrar tudo via factories (funções/closures que constroem objetos). A ideia é centralizar a montagem em arquivos de infraestrutura.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Organização de pastas (exemplo)
config/(configurações e definições)config/settings.php(valores puros: arrays)config/dependencies.php(registro de factories no container)public/index.php(bootstrap: cria container, app e registra rotas/middlewares)src/(código da aplicação: Actions, Services, Repositories, Domain)
Passo a passo: montar um container PSR-11 e registrar factories
1) Instalar um container PSR-11 (ex.: PHP-DI)
composer require php-di/php-di2) Criar settings (configuração como dados, sem objetos)
Configuração deve ser simples e serializável (array), para facilitar troca por variáveis de ambiente e testes.
// config/settings.php return [ 'app' => [ 'env' => 'dev', ], 'logger' => [ 'name' => 'app', 'path' => __DIR__ . '/../var/log/app.log', 'level' => 'debug', ], 'db' => [ 'dsn' => 'mysql:host=localhost;dbname=app;charset=utf8mb4', 'user' => 'root', 'pass' => 'root', 'options' => [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ], ], ];3) Registrar dependências com factories
Factories são responsáveis por construir objetos e resolver dependências. Elas vivem na camada de infraestrutura (bootstrap/config).
// config/dependencies.php use Psr\Container\ContainerInterface; return [ 'settings' => function (): array { return require __DIR__ . '/settings.php'; }, PDO::class => function (ContainerInterface $c): PDO { $db = $c->get('settings')['db']; return new PDO($db['dsn'], $db['user'], $db['pass'], $db['options']); }, ];Note que o container injeta ContainerInterface na factory, mas isso não deve “vazar” para dentro das classes de domínio/aplicação.
4) Bootstrap do container e do App
// public/index.php use DI\ContainerBuilder; use Slim\Factory\AppFactory; require __DIR__ . '/../vendor/autoload.php'; $builder = new ContainerBuilder(); $definitions = require __DIR__ . '/../config/dependencies.php'; $builder->addDefinitions($definitions); $container = $builder->build(); AppFactory::setContainer($container); $app = AppFactory::create(); // rotas/middlewares em arquivos próprios (não detalhar aqui) $app->run();Exemplos de registro: logger, configuração, banco, repositórios e serviços
Logger (ex.: Monolog) via factory
composer require monolog/monolog// config/dependencies.php use Monolog\Logger; use Monolog\Handler\StreamHandler; use Psr\Log\LoggerInterface; use Psr\Container\ContainerInterface; return [ // ... LoggerInterface::class => function (ContainerInterface $c): LoggerInterface { $cfg = $c->get('settings')['logger']; $logger = new Logger($cfg['name']); $level = Logger::toMonologLevel($cfg['level']); $logger->pushHandler(new StreamHandler($cfg['path'], $level)); return $logger; }, ];Na aplicação, dependa de Psr\Log\LoggerInterface, não de Monolog\Logger.
Conexão com banco (PDO) e boas práticas de escopo
Para PDO, normalmente faz sentido um singleton no container (uma instância compartilhada). A maioria dos containers já compartilha instâncias por padrão quando definidas dessa forma. Se você precisar de múltiplas conexões (leitura/escrita), registre chaves diferentes.
// config/dependencies.php return [ 'db.read' => function (ContainerInterface $c): PDO { $db = $c->get('settings')['db_read']; return new PDO($db['dsn'], $db['user'], $db['pass'], $db['options']); }, 'db.write' => function (ContainerInterface $c): PDO { $db = $c->get('settings')['db_write']; return new PDO($db['dsn'], $db['user'], $db['pass'], $db['options']); }, ];Repository: contrato + implementação
Repositórios devem expor uma interface (contrato) para reduzir acoplamento e facilitar mocks em testes.
// src/Domain/User/UserRepository.php namespace App\Domain\User; interface UserRepository { public function all(): array; }// src/Infra/Persistence/PdoUserRepository.php namespace App\Infra\Persistence; use App\Domain\User\UserRepository; use PDO; final class PdoUserRepository implements UserRepository { public function __construct(private PDO $pdo) {} public function all(): array { $stmt = $this->pdo->query('SELECT id, name, email FROM users'); return $stmt->fetchAll(); } }Registro no container:
// config/dependencies.php use App\Domain\User\UserRepository; use App\Infra\Persistence\PdoUserRepository; return [ UserRepository::class => function (ContainerInterface $c): UserRepository { return new PdoUserRepository($c->get(PDO::class)); }, ];Service (caso de uso) consumindo repositório e logger
// src/Application/User/ListUsersService.php namespace App\Application\User; use App\Domain\User\UserRepository; use Psr\Log\LoggerInterface; final class ListUsersService { public function __construct( private UserRepository $repo, private LoggerInterface $logger ) {} public function execute(): array { $this->logger->info('Listing users'); return $this->repo->all(); } }Registro no container:
// config/dependencies.php use App\Application\User\ListUsersService; return [ ListUsersService::class => function (ContainerInterface $c): ListUsersService { return new ListUsersService( $c->get(App\Domain\User\UserRepository::class), $c->get(Psr\Log\LoggerInterface::class) ); }, ];Padrões para passar dependências às Actions no Slim
No Slim, Actions/Handlers podem ser resolvidos pelo container. O padrão recomendado é: Action invokable com dependências no construtor, e o container cria a Action via factory.
Action invokable com dependências explícitas
// src/Http/Action/User/ListUsersAction.php namespace App\Http\Action\User; use App\Application\User\ListUsersService; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; final class ListUsersAction { public function __construct(private ListUsersService $service) {} public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $data = $this->service->execute(); $payload = json_encode(['data' => $data], JSON_UNESCAPED_UNICODE); $response->getBody()->write($payload); return $response->withHeader('Content-Type', 'application/json'); } }Factory da Action (quando não há autowiring ou quando quer controle)
// config/dependencies.php use App\Http\Action\User\ListUsersAction; use Psr\Container\ContainerInterface; return [ ListUsersAction::class => function (ContainerInterface $c): ListUsersAction { return new ListUsersAction($c->get(App\Application\User\ListUsersService::class)); }, ];Na definição de rota, referencie a classe:
// em rotas $app->get('/users', App\Http\Action\User\ListUsersAction::class);Quando usar autowiring vs factories explícitas
- Factories explícitas: quando você precisa de parâmetros escalares (ex.:
baseUrl), múltiplas instâncias (read/write), decorators, ou quer tornar a composição totalmente visível. - Autowiring: quando as dependências são apenas classes/interfaces já registradas e você quer reduzir boilerplate. Mesmo com autowiring, mantenha
settingse recursos externos (PDO, clients HTTP) registrados explicitamente.
Evitando acoplamento: regras práticas
- Classes de aplicação/domínio não devem conhecer o container.
- Dependa de interfaces (ex.:
UserRepository,LoggerInterface). - Configuração (arrays) fica fora das classes; classes recebem valores prontos (ou objetos de configuração dedicados).
- Infraestrutura (PDO, Monolog, clients) é montada em factories.
- Actions devem ser finas: orquestram chamada de serviço e formatação de resposta, sem construir dependências.
Testando componentes com mocks (sem container)
Quando dependências são injetadas por construtor, testar fica direto: você instancia a classe com doubles (mocks/stubs) sem precisar subir Slim ou container.
Exemplo: teste do service com mocks (PHPUnit)
composer require --dev phpunit/phpunit// tests/Application/User/ListUsersServiceTest.php use PHPUnit\Framework\TestCase; use App\Application\User\ListUsersService; use App\Domain\User\UserRepository; use Psr\Log\LoggerInterface; final class ListUsersServiceTest extends TestCase { public function testExecuteReturnsUsers(): void { $repo = $this->createMock(UserRepository::class); $logger = $this->createMock(LoggerInterface::class); $repo->method('all')->willReturn([ ['id' => 1, 'name' => 'Ana'], ]); $logger->expects($this->once())->method('info'); $service = new ListUsersService($repo, $logger); $result = $service->execute(); $this->assertCount(1, $result); $this->assertSame('Ana', $result[0]['name']); } }Exemplo: teste da Action com service stub
Para Actions, você pode mockar o service e usar uma implementação de Response/Request (por exemplo, uma factory PSR-17) ou objetos de teste. O ponto principal é: a Action não acessa container, então pode ser instanciada diretamente.
// pseudoexemplo: Action test (foco na ideia) $service = $this->createMock(ListUsersService::class); $service->method('execute')->willReturn([['id' => 1]]); $action = new ListUsersAction($service); // crie $request e $response via factories PSR-17 usadas no projeto // chame $action($request, $response) e valide body/headerPadrões úteis de registro no container (receitas rápidas)
Registrar valores escalares com objeto de configuração
Em vez de espalhar $c->get('settings') por factories, você pode criar um objeto de configuração tipado.
// src/Infra/Config/DbConfig.php namespace App\Infra\Config; final class DbConfig { public function __construct( public readonly string $dsn, public readonly string $user, public readonly string $pass, public readonly array $options, ) {} }// config/dependencies.php use App\Infra\Config\DbConfig; return [ DbConfig::class => function (ContainerInterface $c): DbConfig { $db = $c->get('settings')['db']; return new DbConfig($db['dsn'], $db['user'], $db['pass'], $db['options']); }, PDO::class => function (ContainerInterface $c): PDO { $cfg = $c->get(App\Infra\Config\DbConfig::class); return new PDO($cfg->dsn, $cfg->user, $cfg->pass, $cfg->options); }, ];Decorator (ex.: repository com cache) sem mudar quem consome
Você pode trocar a implementação registrada para a interface sem alterar Actions/Services.
// config/dependencies.php use App\Domain\User\UserRepository; use App\Infra\Persistence\PdoUserRepository; use App\Infra\Persistence\CachedUserRepository; return [ UserRepository::class => function (ContainerInterface $c): UserRepository { $pdoRepo = new PdoUserRepository($c->get(PDO::class)); return new CachedUserRepository($pdoRepo, $c->get(Psr\SimpleCache\CacheInterface::class)); }, ];| Necessidade | Como resolver com DI |
|---|---|
| Trocar implementação (ex.: repo em memória no teste) | Depender de interface e registrar outra factory |
| Evitar dependências escondidas | Injetar por construtor, não acessar container dentro da classe |
| Configurar recursos externos | Factories na infra + settings como dados |
| Testes rápidos | Instanciar classes diretamente com mocks/stubs |