romanzipp/laravel-dto
A lightweight base class for building Data Transfer Objects in Laravel. Define typed properties, cast nested DTOs and collections, and hydrate from arrays or requests, giving your app predictable, self-validating data objects instead of loose arrays.
composer require romanzipp/laravel-dto. The package is small and Laravel auto-discovery means there is typically nothing to publish or configure. Before committing, check the composer.json constraints (php, illuminate/*) against your app’s versions — a low-traffic package can lag behind current Laravel releases.vendor/romanzipp/laravel-dto/src. The package is a thin reflection-based hydrator; the source is the real documentation and you can read it in ten minutes. That is where you confirm the exact factory names (fromArray, fromRequest, or similar), casting rules, and missing-key behavior.$request->validated() arrays from controllers into actions/services. Define an intent-specific DTO and hand that around instead.namespace App\Dtos;
use RomanZipp\LaravelDto\Dto;
class CreateUserDto extends Dto
{
public string $name;
public string $email;
public ?int $age = null;
}
public function store(StoreUserRequest $request, CreateUserAction $action)
{
return $action->execute(
CreateUserDto::fromArray($request->validated())
);
}
app/Dtos and make them intent-specific (CreateUserDto, UpdateUserDto, UserFilterDto) rather than one-per-model. An UpdateUserDto with all-nullable properties is what makes PATCH semantics explicit.Controller → DTO → Action. The highest-value pattern: controller validates and hydrates, the action’s execute(CreateUserDto $dto) signature is self-documenting, and the DTO becomes the seam you can test without HTTP.
Validation stays in FormRequests. Let the DTO own shape and types only. StoreUserRequest::validated() produces a clean array; the DTO turns that into typed properties. Resist the urge to also put rules in the DTO — two validation layers drift.
Nested DTOs. Type a property as another DTO class (public AddressDto $address;) and check whether the hydrator recurses. If it does, you get free composition; if it doesn’t, write an explicit static factory (CreateUserDto::fromPayload()) that maps child arrays manually. Don’t assume recursion works — test it on day one.
DTO → Eloquent. User::create($dto->toArray()) works well for creates but requires $fillable/$guarded to line up. For updates, filter nulls first so a partially-populated DTO doesn’t blank columns:
$user->update(array_filter($dto->toArray(), fn ($v) => $v !== null));
DTO → response. Return $dto->toArray() from an API Resource or response()->json($dto) if the base class is JsonSerializable. This gives you a stable response shape decoupled from the model.
Jobs and events. Passing a DTO as a job constructor argument is convenient and plain PHP objects serialize into the queue payload fine. Verify a round-trip with dispatchSync in a test before relying on it; if the base class keeps data in non-public properties, the serialized form may not survive unserialize the way you expect.
Testing. Because DTOs are plain objects, unit-test actions by constructing CreateUserDto::fromArray([...]) directly — no request, no HTTP kernel, no database.
Own base class. Create App\Dtos\Dto extends RomanZipp\LaravelDto\Dto and put shared helpers (only(), except(), toModel()) there. When the hydrator’s behavior doesn’t fit, you change one file — or swap packages — instead of touching fifty DTOs.
Collections. collect($rows)->map(fn ($r) => UserDto::fromArray($r)) for list endpoints; keep it in a small UserDto::collect() helper so the mapping logic isn’t duplicated.
"not-an-email". Always validate upstream.(int) "12abc" is 12, (bool) "false" is true, (int) "" is 0. If the hydrator casts naively, normalise in prepareForValidation() on the FormRequest.Typed property ... must not be accessed before initialization on read. Give every optional field a default (= null) so “absent” and “explicitly null” are at least distinguishable from a crash.$request->all() — always validated().toArray() semantics. Confirm how it handles nested DTOs, private/protected properties, and nulls. This is the single behavior most likely to differ from your assumptions.MapFrom-style name mapping, lazy properties, or rich casting, spatie/laravel-data covers the same ground with far more community support — evaluate it before standardising on this one.App\Dtos\Dto rather than patching vendor code; that keeps upgrades (or a fork) trivial.How can I help you explore Laravel packages today?