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:
- Suporte type-safety completo com PHP 8.3+
- Facilite validação automática
- Permita transformação de/para arrays, JSON, Eloquent models
- Seja bem mantida e testada
- Tenha bom desempenho
Alternativas Consideradas
Opção 1: Classes PHP Nativas
Implementação:
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:
use Spatie\LaravelData\Data;
class PaymentData extends Data
{
public function __construct(
public string $id,
public float $amount,
public string $currency,
) {}
}Uso:
// 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
- Produtividade: Reduz drasticamente boilerplate code
- Consistência: API única em todos os módulos
- Type-Safety: PHP 8.3+ com validação em runtime
- Manutenibilidade: Spatie é referência em qualidade de código Laravel
- Performance: Otimizado internamente com cache
- 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
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
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
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
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)
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:
FilamentCore\Contracts\{Module}\Data\{Name}DataExemplos:
FilamentCore\Contracts\Invoices\Data\InvoiceDataFilamentCore\Contracts\People\Data\PersonDataFilamentCore\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:
// ✅ 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:
readonly class PaymentData extends Data { }Documentação
Sempre adicione docblock para collections:
/**
* @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:
src/
├── Invoices/Data/
│ ├── InvoiceData.php
│ ├── InvoiceItemData.php
│ └── TaxesData.php
├── People/Data/
│ ├── PersonData.php
│ ├── ContactData.php
│ └── DocumentData.php
└── Communications/Data/
├── NotificationData.php
└── MessageTemplateData.phpMódulos Implementadores
Usam os Data objects dos contracts:
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:
$invoice = [
'number' => 'INV-001',
'total' => 100.50,
];Depois:
$invoice = InvoiceData::from([
'number' => 'INV-001',
'total' => 100.50,
]);Recursos
Revisões Futuras
- Q1 2025: Avaliar uso de
DataResourcepara APIs - Q2 2025: Considerar
DataPipelinepara transformações complexas - Q3 2025: Review de performance em produção