Controllers/Actions no Slim Framework: handlers enxutos e focados

Capítulo 4

Tempo estimado de leitura: 9 minutos

+ Exercício

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:

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

  • 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árioStatusPayload
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 encontrado404{ 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 readonly quando 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 JsonResponse para 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.

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

Qual abordagem melhor mantém uma Action (handler) no Slim enxuta e alinhada à arquitetura limpa?

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

Você errou! Tente novamente.

Uma Action enxuta adapta HTTP para a aplicação: parseia/valida o mínimo, cria DTOs, chama o caso de uso e retorna JSON/status de forma padronizada. Regras de negócio e detalhes de infraestrutura devem ficar fora para reduzir acoplamento e facilitar testes.

Próximo capitúlo

Middlewares no Slim Framework: pipeline, responsabilidades e reuso

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

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.