Skip to content
lpachecob edited this page Mar 24, 2026 · 1 revision

πŸ“¦ Data Transfer Objects (DTOs)

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.


🎯 Why Use DTOs?

  1. Type Safety: Define exactly what data an Action expects
  2. Immutability: DTOs are read-only after creation
  3. Validation Bridge: Acts as a clean interface between HTTP and Domain
  4. IDE Support: Full autocomplete and type hints
  5. Testing: Easy to mock and test in isolation

πŸ“ Location

Application/
└── Users/
    β”œβ”€β”€ Actions/          # Your Actions
    └── DTOs/             # Your DTOs

πŸ—οΈ Basic DTO Pattern

The DTO Class

// 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,
        ];
    }
}

Using in an Action

// 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,
        ];
    }
}

πŸ”„ Complex DTOs

Nested DTOs

// 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;
    }
}

πŸ”’ DTO with Transformation

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,
        ];
    }
}

βœ… DTO Best Practices

1. Make Properties Read-Only

// βœ… Good - immutable
public function __construct(
    public readonly string $name,
    public readonly string $email,
) {}

// ❌ Bad - mutable
public function __construct(
    public string $name,
    public string $email,
) {}

2. Use Named Arguments (PHP 8+)

// βœ… 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']);

3. Provide Factory Methods

// βœ… 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): self

4. Validate in Factory

public 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'),
    );
}

πŸ“‹ DTO Quick Reference

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

Minimal DTO Example

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];
    }
}

πŸ”— Related Topics


Next Step: Action-Definition 🧬

Clone this wiki locally