-
Notifications
You must be signed in to change notification settings - Fork 0
DTOs
lpachecob edited this page Mar 24, 2026
·
1 revision
DTOs are simple data containers that wrap validated request data. In BOUNDLY, they are the recommended way to pass data between your Actions and the Domain layer.
- Type Safety: Define exactly what data an Action expects
- Immutability: DTOs are read-only after creation
- Validation Bridge: Acts as a clean interface between HTTP and Domain
- IDE Support: Full autocomplete and type hints
- Testing: Easy to mock and test in isolation
Application/
βββ Users/
βββ Actions/ # Your Actions
βββ DTOs/ # Your DTOs
// Application/Users/DTOs/UserDTO.php
namespace Application\Users\DTOs;
class UserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly ?string $phone = null,
public readonly ?string $address = null,
) {}
/**
* Create DTO from validated request data
*/
public static function fromRequest(array $data): self
{
return new self(
name: $data['name'],
email: $data['email'],
phone: $data['phone'] ?? null,
address: $data['address'] ?? null,
);
}
/**
* Convert to array for persistence
*/
public function toArray(): array
{
return [
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
'address' => $this->address,
];
}
}// Application/Users/Actions/CreateUser.php
namespace Application\Users\Actions;
use Application\Users\DTOs\UserDTO;
use Infrastructure\FrameworkCore\Attributes\UseCase\Action;
use Infrastructure\FrameworkCore\Database\DynamicRepository;
use Illuminate\Http\Request;
#[Action(resource: 'users', method: 'POST')]
class CreateUser
{
public function __construct(
protected DynamicRepository $repository
) {}
public function execute(Request $request): array
{
// 1. Validate in Action (or use EntityValidator)
$data = $request->validate([
'name' => 'required|string|max:150',
'email' => 'required|email|unique:users,email',
'phone' => 'nullable|string',
'address' => 'nullable|string',
]);
// 2. Wrap in DTO
$dto = UserDTO::fromRequest($data);
// 3. Use in domain/repository
$user = $this->repository->insert('users', $dto->toArray());
return [
'status' => 'success',
'data' => $user,
];
}
}// Application/Orders/DTOs/OrderDTO.php
namespace Application\Orders\DTOs;
class OrderDTO
{
public function __construct(
public readonly string $customerName,
public readonly string $customerEmail,
public readonly array $items, // Array of OrderItemDTO
public readonly ?string $notes = null,
) {}
public static function fromRequest(array $data): self
{
$items = array_map(
fn($item) => OrderItemDTO::fromArray($item),
$data['items'] ?? []
);
return new self(
customerName: $data['customer_name'],
customerEmail: $data['customer_email'],
items: $items,
notes: $data['notes'] ?? null,
);
}
public function getTotal(): float
{
return array_reduce(
$this->items,
fn($total, OrderItemDTO $item) => $total + $item->getSubtotal(),
0.0
);
}
}
// Application/Orders/DTOs/OrderItemDTO.php
namespace Application\Orders\DTOs;
class OrderItemDTO
{
public function __construct(
public readonly int $productId,
public readonly int $quantity,
public readonly float $unitPrice,
) {}
public static function fromArray(array $data): self
{
return new self(
productId: $data['product_id'],
quantity: $data['quantity'],
unitPrice: (float) $data['unit_price'],
);
}
public function getSubtotal(): float
{
return $this->quantity * $this->unitPrice;
}
}Use DTOs to transform and normalize data:
// Application/Products/DTOs/CreateProductDTO.php
namespace Application\Products\DTOs;
class CreateProductDTO
{
public function __construct(
public readonly string $title,
public readonly float $priceInCents,
public readonly int $categoryId,
public readonly array $tagIds = [],
) {}
public static function fromRequest(array $data): self
{
// Transform: convert decimal string to cents
$priceInCents = (int) (floatval($data['price']) * 100);
// Transform: convert comma-separated tags to array
$tagIds = [];
if (!empty($data['tags'])) {
$tagIds = array_map('intval', explode(',', $data['tags']));
}
return new self(
title: trim($data['title']),
priceInCents: $priceInCents,
categoryId: (int) $data['category_id'],
tagIds: $tagIds,
);
}
public function toArray(): array
{
return [
'title' => $this->title,
'price_cents' => $this->priceInCents,
'category_id' => $this->categoryId,
];
}
}// β
Good - immutable
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
// β Bad - mutable
public function __construct(
public string $name,
public string $email,
) {}// β
Good - clear what each value means
return new self(
name: $data['name'],
email: $data['email'],
);
// β Bad - positional, error-prone
return new self($data['name'], $data['email']);// β
Good - explicit creation
public static function fromRequest(array $data): self
public static function fromJson(string $json): self
public static function fromModel(Model $model): self
// β
Good - multiple factory methods
public static function create(array $data): self
public static function update(int $id, array $data): selfpublic static function fromRequest(array $data): self
{
// Let PHP validate at construction time
return new self(
name: $data['name'] ?? throw new \InvalidArgumentException('Name required'),
email: filter_var($data['email'], FILTER_VALIDATE_EMAIL)
?: throw new \InvalidArgumentException('Invalid email'),
);
}| Method | Purpose |
|---|---|
__construct() |
Define expected properties with types |
fromRequest() |
Create from HTTP request data |
toArray() |
Convert back to array for persistence |
toJson() |
Convert to JSON string |
class UserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
public static function fromRequest(array $data): self
{
return new self(name: $data['name'], email: $data['email']);
}
public function toArray(): array
{
return ['name' => $this->name, 'email' => $this->email];
}
}- Action-Definition - How to create Actions that use DTOs
- Entity-Definition - How entities work
- Query-Engine - Querying data
Next Step: Action-Definition π§¬