Pular para conteúdo

DTOs e Data Mappers

O MIDDAG usa DTOs para transportar dados de entrada e mappers para traduzir entre storage físico e domínio.

Esse desenho evita que arrays soltos, stdClass de banco e detalhes de tabela contaminem services, controllers e facades.

Papel de cada componente

DTO

DTO é estrutura de transporte.

Use DTO para:

  • capturar dados de formulário, API ou comando;
  • deixar explícitos os campos aceitos para escrita;
  • separar dados brutos do objeto de domínio final.

Mapper

Mapper é tradutor.

Use mapper para:

  • converter record do banco em objeto de domínio;
  • separar colunas estruturais de metadata;
  • serializar e desserializar valores persistidos;
  • apoiar hidratação polimórfica orientada por TYPE.

Repository

Repository coordena escrita, leitura e transação usando DTO e mapper, sem vazar isso para fora.

Fluxo conceitual

flowchart TB
    Input[Input / API / Form] --> DTO
    DTO --> Repository
    Repository --> Mapper
    Mapper --> Storage[(XMLDB / Moodle DB)]
    Storage --> Mapper
    Mapper --> Domain[Domain Object]

Exemplo educacional de DTO

<?php

final class item_write_dto
{
    public function __construct(
        public readonly string $type,
        public readonly string $fullname,
        public readonly ?int $contextid = null,
        public readonly array $metadata = [],
    ) {}
}

Esse DTO não tem regra de negócio. Ele só transporta dados.

Exemplo educacional de mapper

<?php

final class item_mapper
{
    public function domain_to_db(object $entity): \stdClass
    {
        $record = new \stdClass();
        $record->type = 'company';
        $record->fullname = 'Empresa X';

        return $record;
    }

    public function db_to_domain(\stdClass $record, array $metadata): object
    {
        return new \stdClass();
    }
}

O mapper é interno ao boundary de persistência. Ele não deve ser usado manualmente em controller ou facade.

Como cada camada deve aplicar

Framework

<?php

namespace local_middag\framework\application\service\item;

use local_middag\framework\contract\repository\item_repository_interface;

final class item_write_service
{
    public function __construct(
        private item_repository_interface $item_repository,
    ) {}

    public function create(item_write_dto $dto): object
    {
        return $this->item_repository->create($dto);
    }
}

Extension do ecossistema

<?php

namespace local_middag\extensions\ecommerce\service;

use local_middag\framework\contract\repository\item_repository_interface;

final class store_write_service
{
    public function __construct(
        private item_repository_interface $item_repository,
    ) {}

    public function create_store(item_write_dto $dto): object
    {
        return $this->item_repository->create($dto);
    }
}

Plugin terceiro

<?php

use local_middag\framework\contract\repository\item_repository_interface;
use local_middag\middag;

middag::init();

$repository = middag::get(item_repository_interface::class);
$item = $repository->create(
    new item_write_dto(
        type: 'partner_order',
        fullname: 'Pedido 1001',
        metadata: ['external_id' => 'abc-1001'],
    )
);

Polimorfismo por TYPE

O mapper e o repository precisam respeitar o fato de que item é tipado.

<?php

final class company_item
{
    public const TYPE = 'company';
}

final class store_item
{
    public const TYPE = 'store';
}

O mesmo storage físico pode hidratar objetos de domínio diferentes, desde que o tipo lógico esteja registrado e o mapper saiba reconstruí-los corretamente.

O que não fazer

  • não passar stdClass cru do banco para services;
  • não usar array solto em escrita quando o fluxo exigir contrato claro;
  • não chamar mapper manualmente em controller, facade ou código externo;
  • não tratar metadata como desculpa para abandonar modelagem estrutural recorrente.