OSS-ADR: Spatie Laravel Data
Exploring the PHP Reflection API and DataClass caching mechanism behind Spatie's powerful Laravel Data package.
1. Context & Decision
The Problem: In modern PHP applications, developers constantly pass array payloads around between controllers, actions, and jobs. This results in "array blindness" — you lose track of what keys exist, what types they are, and you lose IDE autocomplete. While standard FormRequests handle validation, they don't solve the type-safety problem deeper in your domain logic. Standard raw DTOs (Data Transfer Objects) solve the type issue but require a lot of boilerplate for mapping, validation, and JSON serialization.
The Alternatives:
- FormRequest + passing arrays: Built into Laravel, but lacks type safety.
- Manually written DTOs: Type-safe, but painful to serialize and map.
- Packages like cweagans/composer-patches or specialized typed mappers.
The Decision:
We chose spatie/laravel-data because it provides a unified, powerful approach to rich domain models. It acts as a FormRequest, a DTO, and an API Resource all at the same time. The robust type safety it brings to the entire application lifecycle drastically reduces runtime errors and improves Developer Experience (DX).
2. The Integration
The Quick Win: Creating a Data object is as simple as defining a class with typed properties:
use Spatie\LaravelData\Data;
class UserData extends Data
{
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
}
Practical Usage: You can use this class everywhere. It acts as a Form Request in your controller, ensuring the incoming request matches the properties:
public function store(UserData $userData)
{
// $userData is already validated and instantiated!
$user = User::create($userData->toArray());
// It also acts as an API Resource to return a response
return UserData::from($user);
}
3. Under the Hood
Architecture & Design Patterns:
The package is fundamentally built around the PHP Reflection API and a Pipeline pattern.
When you interact with a Data object, Spatie uses reflection (ReflectionClass, ReflectionProperty, ReflectionMethod) to inspect the types, constructor parameters, and attributes (PHP 8 attributes).
Because Reflection is notoriously slow in PHP, Spatie employs a robust caching mechanism. They parse a class once using their DataClassFactory and store the result in a DataClass container.
Code Masterclass: The DataClassFactory
If you look inside DataClassFactory.php, you see how elegantly they extract knowledge about your class without forcing you to write boilerplate:
public function build(ReflectionClass $reflectionClass): DataClass
{
$attributes = DataAttributesCollectionFactory::buildFromReflectionClass($reflectionClass);
$methods = collect($reflectionClass->getMethods());
$constructorReflectionMethod = $methods->first(fn (ReflectionMethod $method) => $method->isConstructor());
$properties = $this->resolveProperties(
$reflectionClass,
$constructorReflectionMethod,
NameMappersResolver::create()->execute($attributes),
// ...
);
// ...
}
They use NameMappersResolver to allow you to map snake_case from the database/request to camelCase in your class properties automatically. They also heavily check interface implementations (like $reflectionClass->implementsInterface(ResponsableData::class)) to conditionally enable functionality, which is a textbook example of coding to interfaces.
4. Consequences & Trade-offs
The Good: - End-to-End Type Safety: From HTTP request to database and back to JSON response. - Massive DX Improvement: IDE autocompletion everywhere. You never have to guess what's in a payload again. - Single Responsibility: Combines validation, mapping, and transformation in one clean class.
The Gotchas: - Learning Curve: It replaces fundamental Laravel concepts (FormRequests and Resources), which can confuse juniors used to standard Laravel documentation. - Reflection Performance Overhead: Even with caching, resolving nested Data objects requires a lot of processing. If you return thousands of deeply nested Data objects in a single API response without care, you will notice a performance hit compared to standard Eloquent serialization.