DTOs (Data Transfer Objects)
What Is It?
DTOs are simple objects that carry data between layers. They define a clear contract for what data is expected and provide type safety for transferred data.
Why?
Passing raw arrays between methods loses type information, makes refactoring dangerous, and hides what data a method actually needs. DTOs make data structures explicit and validated.
When to Use
When passing complex data between services, when you want type safety for transferred data, or when the data structure is used across multiple methods.
When Not to Use
Simple cases where Eloquent models or collections are sufficient. When the DTO would just mirror the model without adding value.
Example
Example
<?php
// DTO class
readonly class CreateOrderDTO
{
public function __construct(
public int $userId,
public array $items,
public string $paymentMethod,
public ?string $couponCode = null,
public ?string $shippingAddress = null,
) {}
public static function fromRequest(Request $request): self
{
return new self(
userId: $request->user()->id,
items: $request->input('items'),
paymentMethod: $request->input('payment_method'),
couponCode: $request->input('coupon_code'),
shippingAddress: $request->input('shipping_address'),
);
}
}
// Service using DTO
class OrderService
{
public function createOrder(CreateOrderDTO $dto): Order
{
// $dto has typed properties - IDE autocomplete works
$total = collect($dto->items)->sum(fn ($item) => $item['price'] * $item['quantity']);
if ($dto->couponCode) {
$total = $this->applyCoupon($total, $dto->couponCode);
}
return Order::create([
'user_id' => $dto->userId,
'total' => $total,
'payment_method' => $dto->paymentMethod,
]);
}
}
// Controller
public function store(Request $request, OrderService $service)
{
$dto = CreateOrderDTO::fromRequest($request);
$order = $service->createOrder($dto);
return redirect()->route('orders.show', $order);
}
Pros
- Type safety and IDE autocomplete
- Explicit data contracts
- Immutable by default (readonly)
- Easy to validate data structure
- Self-documenting code
Cons
- Additional classes to maintain
- Can duplicate model properties
- May add unnecessary abstraction for simple data
- Team needs to understand when to use them