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
stdClasscru 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.