Skip to content

ADR-007: Spatie Laravel Data para Objetos de Dados

Status: Aceito Data: 2025-01-03 Decisores: Aron Cardoso

Contexto

Data objects (objetos de dados) são fundamentais na arquitetura do Filament Core para:

  • Transferir dados entre módulos de forma type-safe
  • Garantir imutabilidade de dados sensíveis (valores monetários, IDs, timestamps)
  • Validar dados antes de processamento
  • Serializar/deserializar para APIs e eventos

Nota sobre terminologia: Evitamos usar o termo "DTO" (Data Transfer Object) por ser muito técnico. Preferimos simplesmente "Data" ou "Data object", que é mais simples e direto.

Precisamos de uma solução robusta que:

  1. Suporte type-safety completo com PHP 8.3+
  2. Facilite validação automática
  3. Permita transformação de/para arrays, JSON, Eloquent models
  4. Seja bem mantida e testada
  5. Tenha bom desempenho

Alternativas Consideradas

Opção 1: Classes PHP Nativas

Implementação:

php
readonly class PaymentData
{
    public function __construct(
        public string $id,
        public float $amount,
        public string $currency,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            id: $data['id'],
            amount: $data['amount'],
            currency: $data['currency'],
        );
    }

    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'amount' => $this->amount,
            'currency' => $this->currency,
        ];
    }
}

Prós:

  • Sem dependências externas
  • Total controle sobre implementação
  • Leve e rápido

Contras:

  • Muito boilerplate (fromArray, toArray, validação manual)
  • Sem validação automática
  • Sem transformações complexas
  • Cada DTO precisa reimplementar lógica comum
  • Difícil manter consistência

Opção 2: Symfony Serializer

Prós:

  • Maduro e bem testado
  • Suporte a múltiplos formatos (JSON, XML, YAML)
  • Extensível via normalizers

Contras:

  • Dependência pesada (todo symfony/serializer)
  • Configuração complexa
  • Não focado em Laravel
  • Curva de aprendizado alta
  • Pouca integração com Eloquent

Opção 3: Spatie Laravel Data ⭐ (ESCOLHIDA)

Implementação:

php
use Spatie\LaravelData\Data;

class PaymentData extends Data
{
    public function __construct(
        public string $id,
        public float $amount,
        public string $currency,
    ) {}
}

Uso:

php
// From array
$payment = PaymentData::from([
    'id' => 'pay_123',
    'amount' => 100.50,
    'currency' => 'BRL',
]);

// From Eloquent
$payment = PaymentData::from($model);

// From request
$payment = PaymentData::from($request);

// To array
$array = $payment->toArray();

// To JSON
$json = $payment->toJson();

// Com validação
class PaymentData extends Data
{
    public function __construct(
        public string $id,
        #[Min(0)]
        public float $amount,
        #[In(['BRL', 'USD', 'EUR'])]
        public string $currency,
    ) {}
}

Prós:

  • ✅ Desenvolvido especificamente para Laravel
  • ✅ Minimal boilerplate (só constructor)
  • ✅ Validação via attributes do Laravel
  • ✅ Transformações automáticas (array, JSON, Eloquent)
  • ✅ Suporte a nested data objects
  • ✅ Casts personalizados
  • ✅ Lazy properties
  • ✅ Bem mantido pela Spatie (referência em Laravel)
  • ✅ Excelente documentação
  • ✅ Performance otimizada

Contras:

  • Dependência externa (aceitável pela qualidade)
  • Precisa aprender API da biblioteca

Decisão

Adotamos Spatie Laravel Data como biblioteca oficial para todos os Data objects no ecossistema Filament Core.

Todos os pacotes devem usar Spatie\LaravelData\Data como classe base para objetos de dados.

Justificativa

  1. Produtividade: Reduz drasticamente boilerplate code
  2. Consistência: API única em todos os módulos
  3. Type-Safety: PHP 8.3+ com validação em runtime
  4. Manutenibilidade: Spatie é referência em qualidade de código Laravel
  5. Performance: Otimizado internamente com cache
  6. Integração: Funciona perfeitamente com Eloquent, APIs, Events

Consequências

Positivas

  • ✅ DTOs muito mais simples de criar e manter
  • ✅ Validação declarativa via attributes
  • ✅ Transformações automáticas reduzem erros
  • ✅ Nested data objects facilitam estruturas complexas
  • ✅ Lazy properties melhoram performance
  • ✅ Documentação excelente da Spatie

Negativas

  • ⚠️ Dependência externa (mitigado pela qualidade da Spatie)
  • ⚠️ Desenvolvedores precisam aprender API do Laravel Data
  • ⚠️ Abstração adicional (mas vale a pena)

Padrões de Implementação

DTO Básico

php
namespace FilamentCore\Contracts\Invoices\Data;

use Spatie\LaravelData\Data;

class InvoiceData extends Data
{
    public function __construct(
        public string $number,
        public string $personId,
        public float $subtotal,
        public float $total,
        public string $status,
    ) {}
}

DTO com Validação

php
namespace FilamentCore\Contracts\Invoices\Data;

use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\In;

class InvoiceData extends Data
{
    public function __construct(
        public string $number,
        public string $personId,
        #[Min(0)]
        public float $subtotal,
        #[Min(0)]
        public float $total,
        #[In(['draft', 'pending', 'paid', 'cancelled'])]
        public string $status,
    ) {}
}

DTO com Nested Objects

php
namespace FilamentCore\Contracts\Invoices\Data;

use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection;

class InvoiceData extends Data
{
    public function __construct(
        public string $number,
        public PersonData $person,
        /** @var DataCollection<InvoiceItemData> */
        public DataCollection $items,
        public TaxesData $taxes,
    ) {}
}

DTO com Casts

php
namespace FilamentCore\Contracts\Invoices\Data;

use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Casts\DateTimeInterfaceCast;
use Carbon\CarbonImmutable;

class InvoiceData extends Data
{
    public function __construct(
        public string $number,
        #[WithCast(DateTimeInterfaceCast::class, format: 'Y-m-d')]
        public CarbonImmutable $issueDate,
        #[WithCast(DateTimeInterfaceCast::class, format: 'Y-m-d')]
        public CarbonImmutable $dueDate,
    ) {}
}

DTO Readonly (Imutável)

php
namespace FilamentCore\Contracts\Payments\Data;

use Spatie\LaravelData\Data;

readonly class PaymentData extends Data
{
    public function __construct(
        public string $id,
        public float $amount,
        public string $currency,
        public string $gatewayReference,
    ) {}
}

Convenções

Namespace

Todos os Data objects devem estar em Data namespace:

text
FilamentCore\Contracts\{Module}\Data\{Name}Data

Exemplos:

  • FilamentCore\Contracts\Invoices\Data\InvoiceData
  • FilamentCore\Contracts\People\Data\PersonData
  • FilamentCore\Contracts\Communications\Data\NotificationData

Nomenclatura

IMPORTANTE: Usamos "Data" ao invés de "DTO" (Data Transfer Object).

Razão: "Data" é mais simples, direto e alinhado com a biblioteca Spatie Laravel Data. O termo "DTO" é técnico demais e adiciona complexidade desnecessária.

Regras:

  • Sempre sufixo Data: InvoiceData, PersonData, PaymentData
  • PascalCase
  • Nome descritivo: Representa claramente o que contém
  • Nunca usar sufixo DTO: InvoiceDTO (errado)
  • Nunca usar sufixo Object: InvoiceObject (errado)

Exemplos corretos:

php
// ✅ Correto
class InvoiceData extends Data { }
class PersonData extends Data { }
class PaymentData extends Data { }
class NotificationData extends Data { }

// ❌ Errado
class InvoiceDTO extends Data { }
class PersonDataObject extends Data { }
class PaymentDataTransferObject extends Data { }

Na comunicação e documentação:

  • ✅ "Data object"
  • ✅ "Data class"
  • ✅ "Objeto de dados"
  • ❌ "DTO"
  • ❌ "Data Transfer Object"

Imutabilidade

Use readonly quando o Data object representa dados que não devem mudar:

php
readonly class PaymentData extends Data { }

Documentação

Sempre adicione docblock para collections:

php
/**
 * @property DataCollection<InvoiceItemData> $items
 */
class InvoiceData extends Data
{
    public function __construct(
        public string $number,
        public DataCollection $items,
    ) {}
}

Implementação nos Módulos

filament-core-contracts

Define todos os Data objects usados pelos contratos:

text
src/
├── Invoices/Data/
│   ├── InvoiceData.php
│   ├── InvoiceItemData.php
│   └── TaxesData.php
├── People/Data/
│   ├── PersonData.php
│   ├── ContactData.php
│   └── DocumentData.php
└── Communications/Data/
    ├── NotificationData.php
    └── MessageTemplateData.php

Módulos Implementadores

Usam os Data objects dos contracts:

php
namespace FilamentCore\Invoices\Services;

use FilamentCore\Contracts\Invoices\Data\InvoiceData;

class InvoiceService
{
    public function create(InvoiceData $data): Invoice
    {
        return Invoice::create($data->toArray());
    }
}

Migração

Código existente que usa arrays ou classes personalizadas deve migrar gradualmente:

Antes:

php
$invoice = [
    'number' => 'INV-001',
    'total' => 100.50,
];

Depois:

php
$invoice = InvoiceData::from([
    'number' => 'INV-001',
    'total' => 100.50,
]);

Recursos

Revisões Futuras

  • Q1 2025: Avaliar uso de DataResource para APIs
  • Q2 2025: Considerar DataPipeline para transformações complexas
  • Q3 2025: Review de performance em produção

Documentação privada do ecossistema Filament Core.