Injeção de dependências no Slim Framework: container, factories e acoplamento mínimo

Capítulo 8

Tempo estimado de leitura: 9 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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-di

2) 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 settings e 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/header

Padrõ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)); }, ];
NecessidadeComo resolver com DI
Trocar implementação (ex.: repo em memória no teste)Depender de interface e registrar outra factory
Evitar dependências escondidasInjetar por construtor, não acessar container dentro da classe
Configurar recursos externosFactories na infra + settings como dados
Testes rápidosInstanciar classes diretamente com mocks/stubs

Agora responda o exercício sobre o conteúdo:

Qual abordagem melhor reduz acoplamento e facilita testes ao usar injeção de dependências no Slim, evitando o anti-padrão Service Locator?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A injeção por construtor torna dependências explícitas, reduz o acoplamento e simplifica testes com mocks/stubs. O container deve ficar responsável pela construção via factories (infraestrutura), sem ser acessado de dentro das classes (evitando Service Locator).

Próximo capitúlo

Configuração por ambiente no Back-end com Slim Framework: dev, teste e produção

Arrow Right Icon
Capa do Ebook gratuito Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa
50%

Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.