O que é uma Action (handler) no Slim e qual deve ser o seu tamanho
No Slim Framework, uma Action (também chamada de handler ou controller) é a função/classe responsável por atender uma rota: ela recebe um Request, produz um Response e devolve esse response para o framework. Em uma arquitetura limpa, a Action não é o lugar para concentrar regras de negócio, acesso a banco, chamadas HTTP externas ou detalhes de infraestrutura.
Uma Action enxuta costuma ter três responsabilidades bem definidas:
- Parsear entrada: ler parâmetros de rota, query string, headers e body (JSON/form) e validar o básico (tipos, presença, formato).
- Delegar: chamar um caso de uso/serviço de aplicação com dados já organizados (DTOs simples).
- Formatar saída: transformar o resultado do caso de uso em JSON, status code e headers adequados.
O objetivo é reduzir acoplamento: se amanhã você trocar o banco, a fila, o client HTTP ou a forma de autenticação, a Action idealmente não muda (ou muda pouco), porque ela não conhece esses detalhes.
Anti-padrões comuns (e como evitá-los)
1) Regras de negócio dentro da Action
Evite condicionar fluxos complexos, cálculos e regras de domínio diretamente no handler. Isso dificulta testes e reaproveitamento. Em vez disso, mova para um caso de uso (serviço de aplicação) e teste o caso de uso isoladamente.
2) Infraestrutura dentro da Action
Evite instanciar diretamente dependências de infraestrutura no handler, como:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Conexão PDO/ORM
- Clients HTTP (Guzzle, cURL)
- SDKs de terceiros
- Leitura direta de variáveis de ambiente
A Action deve receber dependências já prontas via injeção (container) e conversar com interfaces/serviços de aplicação.
3) Retorno “manual” repetitivo
Montar JSON e headers repetidamente em cada endpoint gera duplicação e inconsistências. Crie um helper de resposta (por exemplo, JsonResponse) para padronizar.
Estrutura recomendada: Action + Use Case + DTO
Uma forma prática de manter handlers focados é separar em camadas:
- HTTP (Actions): adapta HTTP para a aplicação.
- Application (Use Cases): orquestra regras e dependências do domínio.
- Domain: entidades/serviços de domínio (quando aplicável).
- Infrastructure: implementação de repositórios, gateways, etc.
Neste capítulo, vamos focar no trio: Action (HTTP) → Use Case (aplicação) → DTO (comunicação interna).
Passo a passo: criando uma Action enxuta para “criar usuário”
Passo 1: Defina um DTO de entrada
DTOs (Data Transfer Objects) simples ajudam a evitar que o caso de uso dependa do Request. Eles também deixam explícito o contrato interno.
<?php
namespace App\Application\User\Create;
final class CreateUserInput
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $password
) {}
}
Passo 2: Defina um DTO de saída
O caso de uso retorna um DTO com dados relevantes para a camada HTTP. Evite retornar entidades inteiras se isso expõe detalhes desnecessários.
<?php
namespace App\Application\User\Create;
final class CreateUserOutput
{
public function __construct(
public readonly string $id,
public readonly string $name,
public readonly string $email
) {}
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}
Passo 3: Implemente o caso de uso (serviço de aplicação)
O caso de uso recebe o DTO de entrada, aplica validações de negócio e usa dependências (interfaces) para persistir ou consultar dados. Note que ele não conhece HTTP.
<?php
namespace App\Application\User\Create;
use App\Domain\User\UserRepository;
use App\Domain\User\PasswordHasher;
use App\Domain\User\User;
final class CreateUserUseCase
{
public function __construct(
private UserRepository $users,
private PasswordHasher $hasher
) {}
public function execute(CreateUserInput $input): CreateUserOutput
{
// Exemplo de regra: e-mail deve ser único
if ($this->users->existsByEmail($input->email)) {
throw new EmailAlreadyInUse($input->email);
}
$hashed = $this->hasher->hash($input->password);
$user = User::create(
name: $input->name,
email: $input->email,
passwordHash: $hashed
);
$this->users->save($user);
return new CreateUserOutput(
id: $user->id()->toString(),
name: $user->name(),
email: $user->email()
);
}
}
Perceba que UserRepository e PasswordHasher são abstrações. A implementação concreta (infra) fica fora do caso de uso.
Passo 4: Crie um helper de resposta JSON para padronizar retornos
Um helper reduz repetição e padroniza headers/status. Ele recebe o Response e devolve o mesmo objeto com body e headers ajustados.
<?php
namespace App\Http\Response;
use Psr\Http\Message\ResponseInterface as Response;
final class JsonResponse
{
public static function ok(Response $response, array $data): Response
{
return self::withJson($response, 200, $data);
}
public static function created(Response $response, array $data): Response
{
return self::withJson($response, 201, $data);
}
public static function error(Response $response, int $status, string $message, array $details = []): Response
{
return self::withJson($response, $status, [
'error' => $message,
'details' => $details,
]);
}
private static function withJson(Response $response, int $status, array $payload): Response
{
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$response->getBody()->write($json === false ? '{}' : $json);
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($status);
}
}
Passo 5: Implemente a Action focada (parseia → delega → formata)
A Action deve ser curta e previsível. Ela lê o body, valida o mínimo, cria o DTO, chama o caso de uso e retorna JSON.
<?php
namespace App\Http\Action\User;
use App\Application\User\Create\CreateUserInput;
use App\Application\User\Create\CreateUserUseCase;
use App\Application\User\Create\EmailAlreadyInUse;
use App\Http\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
final class CreateUserAction
{
public function __construct(
private CreateUserUseCase $useCase
) {}
public function __invoke(Request $request, Response $response): Response
{
$data = (array)($request->getParsedBody() ?? []);
$name = trim((string)($data['name'] ?? ''));
$email = trim((string)($data['email'] ?? ''));
$password = (string)($data['password'] ?? '');
$errors = [];
if ($name === '') $errors['name'][] = 'required';
if ($email === '') $errors['email'][] = 'required';
if ($password === '') $errors['password'][] = 'required';
if ($errors) {
return JsonResponse::error($response, 422, 'validation_error', $errors);
}
try {
$output = $this->useCase->execute(new CreateUserInput(
name: $name,
email: $email,
password: $password
));
return JsonResponse::created($response, $output->toArray());
} catch (EmailAlreadyInUse $e) {
return JsonResponse::error($response, 409, 'email_already_in_use', [
'email' => $email,
]);
}
}
}
Note o que a Action não faz: não abre transação, não usa PDO, não chama SDK, não decide como salvar. Ela apenas adapta HTTP para o caso de uso.
Padrões de retorno: consistência de status e payload
Defina um padrão para respostas e aplique em todas as Actions. Um exemplo simples:
| Cenário | Status | Payload |
|---|---|---|
| Sucesso (GET) | 200 | { ... } |
| Sucesso (POST create) | 201 | { id, ... } |
| Validação (entrada inválida) | 422 | { error: "validation_error", details: { campo: ["required"] } } |
| Conflito (regra de unicidade) | 409 | { error: "email_already_in_use", details: { email } } |
| Não encontrado | 404 | { error: "not_found" } |
Esse padrão facilita o consumo por front-end e integrações, e reduz decisões repetidas em cada endpoint.
DTOs simples: regras práticas para não virar “burocracia”
Quando usar DTO
- Quando o caso de uso precisa de vários campos e você quer um contrato explícito.
- Quando você quer evitar passar arrays soltos (frágeis) entre camadas.
- Quando o retorno do caso de uso não deve expor a entidade de domínio.
Quando evitar DTO
- Casos muito simples (1 ou 2 parâmetros) podem usar argumentos diretos, desde que não acoplem ao HTTP.
- Quando o DTO vira apenas um “espelho” de entidade sem propósito.
Checklist de DTO bom
- Imutável (propriedades
readonlyquando possível). - Sem dependência de Request/Response.
- Sem validação de infraestrutura (ex.: “campo existe no banco”).
- Com método
toArray()apenas quando fizer sentido para saída.
Como evitar acoplamento com infraestrutura dentro das Actions
1) Não injete repositórios concretos na Action
A Action deve depender do caso de uso, não do repositório. Assim, você evita que endpoints comecem a “pular” o caso de uso e falar direto com o banco.
Preferível: Action → UseCase → Repository (interface) → Repository (infra)
2) Não trate autenticação/autorização dentro da Action
Mesmo que a Action precise do usuário atual, evite ler e validar token manualmente nela. O ideal é receber o contexto já resolvido (por middleware) ou acessar um serviço de contexto (ex.: CurrentUser) que abstrai a origem.
<?php
namespace App\Security;
final class CurrentUser
{
public function __construct(
private ?string $id,
private array $roles = []
) {}
public function id(): ?string { return $this->id; }
public function roles(): array { return $this->roles; }
}
Na Action, você usa o CurrentUser como dependência, sem saber se veio de JWT, sessão, API key etc.
3) Não faça mapeamento complexo de erros na Action
Um padrão útil é: o caso de uso lança exceções de aplicação (ex.: EmailAlreadyInUse, UserNotFound) e a camada HTTP traduz para status/JSON. Para não repetir try/catch em todo handler, você pode centralizar isso em um middleware/handler de erros, mas mesmo quando fizer localmente, mantenha o mapeamento curto e previsível.
Exemplo adicional: Action de leitura (GET) com parâmetros de rota
Em endpoints de leitura, a Action normalmente pega o id da rota, chama um caso de uso e retorna 200 ou 404.
<?php
namespace App\Http\Action\User;
use App\Application\User\Get\GetUserUseCase;
use App\Application\User\Get\UserNotFound;
use App\Http\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
final class GetUserAction
{
public function __construct(
private GetUserUseCase $useCase
) {}
public function __invoke(Request $request, Response $response, array $args): Response
{
$id = (string)($args['id'] ?? '');
if ($id === '') {
return JsonResponse::error($response, 400, 'missing_id');
}
try {
$output = $this->useCase->execute($id);
return JsonResponse::ok($response, $output->toArray());
} catch (UserNotFound) {
return JsonResponse::error($response, 404, 'not_found', ['id' => $id]);
}
}
}
Guia rápido: tamanho e checklist de uma Action enxuta
- Entrada: extrair dados do request (body/query/args) e normalizar (trim, cast).
- Validação básica: required, formato simples; regras de negócio ficam no caso de uso.
- DTO: montar um objeto de entrada claro.
- Delegação: chamar
$useCase->execute(...). - Saída: usar
JsonResponsepara status e JSON padronizados. - Sem infraestrutura: nada de SQL, SDK, HTTP client, filesystem.
- Sem lógica complexa: no máximo mapeamento de exceções para HTTP.