Skip to content

Política de Camada de Domínio ​

O NENE2 separa a infraestrutura do framework (runtime HTTP, DI, config, adaptadores de banco de dados) da lógica de aplicação (casos de uso, repositórios, regras de domínio). Este documento define as convenções para a camada de aplicação que fica entre os handlers HTTP e os adaptadores de banco de dados.

Posição ​

A camada de domínio é o conjunto de casos de uso e interfaces de repositório que expressam o que a aplicação faz, independentemente de como os requests chegam ou como os dados são armazenados.

text
HTTP handler (fino)
  → UseCase (lógica de aplicação, invariantes de negócio)
    → RepositoryInterface (contrato de acesso a dados)
      → PdoRepositoryAdapter (detalhe de persistência)

A infraestrutura do framework vive em src/. Casos de uso e interfaces de repositório específicos da aplicação devem viver em um namespace que o projeto cliente controla. O NENE2 fornece convenções e exemplos de trabalho mínimos; ele não força um namespace no código de aplicação.

Convenção UseCase ​

Um caso de uso expressa uma operação de aplicação. Ele recebe um DTO de entrada readonly, aplica invariantes de negócio e retorna uma saída tipada.

Formato da Interface ​

php
interface CreateItemUseCaseInterface
{
    public function execute(CreateItemInput $input): CreateItemOutput;
}

Regras:

  • Um método por interface de caso de uso, sempre nomeado execute.
  • Entrada e saída são DTOs readonly tipados, nunca arrays brutos ou objetos PSR-7.
  • A interface vive ao lado ou acima de seus adaptadores, não dentro de um diretório do framework.
  • Casos de uso podem lançar exceções específicas de domínio para violações de invariantes que chamadores devem tratar.
  • Casos de uso não conhecem HTTP, sessões, templates ou filas.
  • Casos de uso não chamam o container PSR-11 diretamente.

DTO de Entrada ​

php
final readonly class CreateItemInput
{
    public function __construct(
        public string $name,
        public int    $year,
    ) {
    }
}
  • readonly e final por padrão.
  • O construtor recebe valores já validados; validação de formato acontece no handler antes de chamar o caso de uso.
  • Invariantes de negócio (unicidade, regras de estado) são verificados dentro do caso de uso, não aqui.

DTO de Saída ​

php
final readonly class CreateItemOutput
{
    public function __construct(
        public int    $id,
        public string $name,
        public int    $year,
    ) {
    }
}
  • Carregue apenas o que os chamadores precisam.
  • Retorne uma saída tipada mesmo para operações com efeitos colaterais.

Implementação ​

php
final class CreateItemUseCase implements CreateItemUseCaseInterface
{
    public function __construct(
        private readonly ItemRepositoryInterface $items,
    ) {
    }

    public function execute(CreateItemInput $input): CreateItemOutput
    {
        if ($this->items->existsByName($input->name)) {
            throw new ItemAlreadyExistsException($input->name);
        }

        $id = $this->items->save(new Item(name: $input->name, year: $input->year));

        return new CreateItemOutput(id: $id, name: $input->name, year: $input->year);
    }
}
  • Apenas injeção por construtor.
  • Sem chamadas new para dependências que precisam ser testáveis.
  • Transações de banco de dados pertencem ao adaptador ou a um serviço gerenciador de transações.

Convenção de Interface de Repositório ​

Uma interface de repositório descreve um contrato de acesso a dados para um agregado ou conceito de domínio. Adaptadores a implementam.

Formato da Interface ​

php
interface ItemRepositoryInterface
{
    public function findById(int $id): ?Item;
    public function existsByName(string $name): bool;
    public function save(Item $item): int;
}

Regras:

  • Métodos usam termos de domínio, não verbos SQL. findById, não selectById.
  • Tipos de retorno usam objetos de domínio ou primitivos, não linhas PDO ou arrays brutos.
  • Retorno nullable (?Item) em vez de lançar exceção quando a ausência é um caso válido.
  • Interfaces vivem no namespace de aplicação, não em src/Database/.

Objeto de Domínio ​

php
final readonly class Item
{
    public function __construct(
        public string $name,
        public int    $year,
        public ?int   $id = null,
    ) {
    }
}
  • id é nullable antes da persistência.
  • Mantenha objetos de domínio livres de anotações ORM ou acoplamento com banco de dados.

Adaptador PDO ​

php
final class PdoItemRepository implements ItemRepositoryInterface
{
    public function __construct(
        private readonly DatabaseQueryExecutorInterface $query,
    ) {
    }

    public function findById(int $id): ?Item
    {
        $row = $this->query->fetchOne('SELECT id, name, year FROM items WHERE id = ?', [$id]);

        return $row !== null
            ? new Item(name: $row['name'], year: (int) $row['year'], id: (int) $row['id'])
            : null;
    }

    public function existsByName(string $name): bool
    {
        return $this->query->fetchOne('SELECT 1 FROM items WHERE name = ?', [$name]) !== null;
    }

    public function save(Item $item): int
    {
        return $this->query->insert('INSERT INTO items (name, year) VALUES (?, ?)', [$item->name, $item->year]);
    }
}
  • Use DatabaseQueryExecutorInterface de src/Database/, não PDO bruto.
  • Todo o SQL fica dentro do adaptador.
  • Faça cast dos valores de linhas do banco de dados para valores PHP tipados na saída.
  • Prefixo do nome da classe adaptadora: Pdo (ex. PdoItemRepository).

Fronteira do Handler (Controller) ​

Handlers permanecem finos. Seu trabalho é mapear o request HTTP em uma entrada de caso de uso, chamar o caso de uso e retornar uma resposta.

php
final class CreateItemHandler
{
    public function __construct(
        private readonly CreateItemUseCaseInterface $useCase,
        private readonly JsonResponseFactory        $response,
    ) {
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $body = (array) json_decode((string) $request->getBody(), associative: true);

        $input = new CreateItemInput(
            name: (string) ($body['name'] ?? ''),
            year: (int) ($body['year'] ?? 0),
        );

        $output = $this->useCase->execute($input);

        return $this->response->ok(['id' => $output->id, 'name' => $output->name, 'year' => $output->year]);
    }
}

Regras:

  • Handlers não contêm lógica de negócio.
  • Validação de formato e construção do DTO acontecem aqui; invariantes de negócio ficam no caso de uso.
  • Handlers não chamam repositórios diretamente.
  • Handlers recebem o caso de uso por injeção de construtor, tipado à interface.

Layout do Código ​

src/
  Item/
    CreateItemInput.php
    CreateItemOutput.php
    CreateItemUseCaseInterface.php
    CreateItemUseCase.php
    Item.php
    ItemRepositoryInterface.php
    ItemAlreadyExistsException.php
    PdoItemRepository.php
    CreateItemHandler.php

Agrupe por conceito de domínio, não por tipo de camada.

Wiring do Container PSR-11 ​

php
final class ItemServiceProvider implements ServiceProviderInterface
{
    public function register(ContainerBuilder $builder): void
    {
        $builder->bind(ItemRepositoryInterface::class, static function (ContainerInterface $c): ItemRepositoryInterface {
            return new PdoItemRepository($c->get(DatabaseQueryExecutorInterface::class));
        });

        $builder->bind(CreateItemUseCaseInterface::class, static function (ContainerInterface $c): CreateItemUseCaseInterface {
            return new CreateItemUseCase($c->get(ItemRepositoryInterface::class));
        });

        $builder->bind(CreateItemHandler::class, static function (ContainerInterface $c): CreateItemHandler {
            return new CreateItemHandler(
                $c->get(CreateItemUseCaseInterface::class),
                $c->get(JsonResponseFactory::class),
            );
        });
    }
}

Testes ​

Testes Unitários de Casos de Uso ​

php
final class CreateItemUseCaseTest extends TestCase
{
    public function test_throws_when_item_name_already_exists(): void
    {
        $items = new InMemoryItemRepository([new Item(name: 'duplicate', year: 2026, id: 1)]);
        $useCase = new CreateItemUseCase($items);

        $this->expectException(ItemAlreadyExistsException::class);

        $useCase->execute(new CreateItemInput(name: 'duplicate', year: 2026));
    }
}

Testes de Integração de Adaptadores de Repositório ​

bash
docker compose run --rm app composer test:database
docker compose up -d mysql
docker compose run --rm app composer test:database:mysql

Tratamento de Erros ​

  • Lance exceções de domínio nomeadas para violações de invariantes de negócio.
  • Mapeie exceções de domínio para Problem Details na fronteira de erro HTTP.
  • Não exponha erros SQL, stack traces ou identificadores internos em respostas de erro.

Não-objetivos ​

  • Active record ou modelos estilo Eloquent.
  • Geração automática de código a partir de OpenAPI ou schemas de banco de dados.
  • CQRS, event sourcing ou padrões saga na primeira passagem.
  • Injeção de dependência por reflexão ou anotação.
  • Chamadas de service locator dentro de casos de uso ou objetos de domínio.

Documentação Relacionada ​

  • Padrões de codificação: docs/development/coding-standards.md
  • Estratégia de teste de banco de dados: docs/development/test-database-strategy.md
  • Workflow de scaffold de endpoint: docs/development/endpoint-scaffold.md
  • Guia de início de projeto cliente: docs/development/client-project-start.md

Publicado sob a licença MIT.