Skip to content

Nested DTOs

Jean-Marc Strauven edited this page Aug 6, 2025 · 1 revision

πŸ”— Nested DTOs

Laravel Arc supports complex data structures with nested DTOs, allowing you to build sophisticated data models with type-safe relationships and hierarchical data.

Understanding Nested DTOs

Nested DTOs let you compose complex data structures from simpler DTOs:

OrderDTO
β”œβ”€β”€ CustomerDTO (single nested DTO)
β”œβ”€β”€ OrderItemDTO[] (collection of DTOs)  
└── AddressDTO (single nested DTO)

This creates a type-safe, validated hierarchy of data objects.

Basic Nested DTO Example

Simple Profile Nesting

# resources/arc/user-with-profile.yaml
header:
  dto: UserDTO
  namespace: App\DTO
  traits: [HasTimestamps]

fields:
  name:
    type: string
    required: true
  email:
    type: email
    required: true
    
  # Nested DTO
  profile:
    type: dto
    dto: ProfileDTO
    required: false
# resources/arc/profile.yaml  
header:
  dto: ProfileDTO
  namespace: App\DTO

fields:
  bio:
    type: text
    max_length: 500
  avatar_url:
    type: url
    nullable: true
  birth_date:
    type: date
    nullable: true

Complex Nested Example

From examples/nested-order.yaml - a real e-commerce order structure:

# Comprehensive nested DTO example demonstrating complex relationships
header:
  dto: OrderDTO
  table: orders
  model: App\Models\Order
  namespace: App\DTO\Ecommerce
  traits:
    - HasTimestamps
    - HasUuid
    - HasSoftDeletes
    - HasVersioning
    - HasAuditing

fields:
  order_number:
    type: string
    required: true
    validation: [required, string, unique:orders, size:12]
  
  status:
    type: enum
    values: [pending, processing, shipped, delivered, cancelled, refunded]
    default: pending
    
  # Customer as nested DTO
  customer:
    type: dto
    dto: CustomerDTO
    required: true
    
  # Collection of order items
  items:
    type: array
    items:
      type: dto
      dto: OrderItemDTO
    min_items: 1
    
  # Billing address
  billing_address:
    type: dto
    dto: AddressDTO
    required: true
    
  # Optional shipping address
  shipping_address:
    type: dto
    dto: AddressDTO
    required: false
    
  # Payment information
  payment:
    type: dto
    dto: PaymentDTO
    required: true

  # Totals
  subtotal:
    type: decimal
    precision: 10
    scale: 2
    
  tax_amount:
    type: decimal
    precision: 10
    scale: 2
    
  total_amount:
    type: decimal
    precision: 10
    scale: 2

Supporting DTOs

CustomerDTO

# examples/nested-customer.yaml
header:
  dto: CustomerDTO
  namespace: App\DTO\Ecommerce
  traits: [HasTimestamps]

fields:
  customer_id:
    type: integer
    required: true
  name:
    type: string
    required: true
    validation: [required, string, max:255]
  email:
    type: email
    required: true
  phone:
    type: string
    nullable: true
    validation: [nullable, string, max:20]

AddressDTO

# examples/nested-address.yaml
header:
  dto: AddressDTO
  namespace: App\DTO\Ecommerce

fields:
  street:
    type: string
    required: true
    max_length: 255
  city:
    type: string
    required: true
    max_length: 100
  state:
    type: string
    required: true
    max_length: 100
  postal_code:
    type: string
    required: true
    pattern: "^[0-9]{5}(-[0-9]{4})?$"
  country:
    type: dto
    dto: CountryDTO
    required: true

Generated Nested DTO Structure

The nested order example generates this PHP structure:

<?php

declare(strict_types=1);

namespace App\DTO\Ecommerce;

use Illuminate\Support\Collection;

final readonly class OrderDTO
{
    public function __construct(
        public string $order_number,
        public string $status,
        public CustomerDTO $customer,           // Single nested DTO
        public Collection $items,               // Collection of OrderItemDTO
        public AddressDTO $billing_address,     // Single nested DTO
        public ?AddressDTO $shipping_address,   // Optional nested DTO
        public PaymentDTO $payment,             // Single nested DTO
        public float $subtotal,
        public float $tax_amount,
        public float $total_amount,
        // Trait fields...
        public string $uuid,
        public ?\DateTime $created_at = null,
        public ?\DateTime $updated_at = null,
    ) {}
    
    // Validation includes nested validation
    public static function validationRules(): array
    {
        return [
            'order_number' => ['required', 'string', 'unique:orders', 'size:12'],
            'status' => ['required', 'in:pending,processing,shipped,delivered,cancelled,refunded'],
            'customer' => ['required', 'array'],
            'customer.customer_id' => ['required', 'integer'],
            'customer.name' => ['required', 'string', 'max:255'],
            'customer.email' => ['required', 'email'],
            'items' => ['required', 'array', 'min:1'],
            'items.*.product_id' => ['required', 'integer'],
            'items.*.quantity' => ['required', 'integer', 'min:1'],
            'billing_address' => ['required', 'array'],
            'billing_address.street' => ['required', 'string', 'max:255'],
            // ... more nested validation
        ];
    }
}

Creating Nested DTOs

From Arrays

$orderData = [
    'order_number' => 'ORD-123456789',
    'status' => 'pending',
    'customer' => [
        'customer_id' => 1,
        'name' => 'John Doe',
        'email' => '[email protected]',
        'phone' => '+1-555-123-4567'
    ],
    'items' => [
        [
            'product_id' => 101,
            'product_name' => 'Laptop',
            'quantity' => 1,
            'unit_price' => 999.99
        ],
        [
            'product_id' => 102,
            'product_name' => 'Mouse',
            'quantity' => 2,
            'unit_price' => 29.99
        ]
    ],
    'billing_address' => [
        'street' => '123 Main St',
        'city' => 'Boston',
        'state' => 'MA',
        'postal_code' => '02101',
        'country' => [
            'code' => 'US',
            'name' => 'United States'
        ]
    ],
    'subtotal' => 1059.97,
    'tax_amount' => 84.80,
    'total_amount' => 1144.77
];

// Laravel Arc automatically creates nested DTOs
$order = OrderDTO::from($orderData);

// Access nested data with full type safety
echo $order->customer->name;                    // "John Doe"
echo $order->billing_address->city;             // "Boston"
echo $order->billing_address->country->name;    // "United States"
echo $order->items->first()->product_name;      // "Laptop"

From Eloquent Models

// Load order with relationships
$orderModel = Order::with([
    'customer', 
    'items.product', 
    'billingAddress.country',
    'shippingAddress.country',
    'payment'
])->find(1);

// Convert to nested DTO
$orderDto = OrderDTO::fromModel($orderModel);

// Type-safe access to all nested data
$customerName = $orderDto->customer->name;
$itemCount = $orderDto->items->count();
$shippingCountry = $orderDto->shipping_address?->country->name;

Working with Collections of Nested DTOs

Processing Order Items

$order = OrderDTO::from($orderData);

// Calculate totals using collection methods
$totalItems = $order->items->sum('quantity');
$heaviestItem = $order->items->max('weight');

// Filter and transform items
$expensiveItems = $order->items
    ->filter(fn($item) => $item->unit_price > 100)
    ->map(fn($item) => [
        'name' => $item->product_name,
        'total' => $item->quantity * $item->unit_price
    ]);

// Group items by category
$itemsByCategory = $order->items->groupBy('category');

Bulk Operations

// Update all item quantities
$updatedItems = $order->items->map(function($item) {
    return $item->with(['quantity' => $item->quantity + 1]);
});

$updatedOrder = $order->with(['items' => $updatedItems]);

Validation of Nested DTOs

Automatic Nested Validation

Laravel Arc automatically validates nested structures:

$invalidOrderData = [
    'order_number' => 'INVALID',  // Wrong format
    'customer' => [
        'email' => 'not-an-email'  // Invalid email
    ],
    'items' => [],  // Empty array (min:1 required)
    'billing_address' => [
        'postal_code' => 'INVALID'  // Wrong pattern
    ]
];

try {
    $order = OrderDTO::fromValidated($invalidOrderData);
} catch (ValidationException $e) {
    // Errors for all nested validation failures
    $errors = $e->errors();
    /*
    [
        'order_number' => ['The order number must be 12 characters.'],
        'customer.email' => ['The customer email must be a valid email address.'],
        'items' => ['The items field must have at least 1 items.'],
        'billing_address.postal_code' => ['The postal code format is invalid.']
    ]
    */
}

Custom Nested Validation

// Add custom validation for business rules
class OrderDTO extends BaseDTO
{
    public static function validationRules(): array
    {
        return array_merge(parent::validationRules(), [
            'items' => ['required', 'array', 'min:1', 'custom_order_items'],
            'total_amount' => ['required', 'numeric', 'custom_total_matches_items'],
        ]);
    }
    
    public function customOrderItemsValidation($attribute, $value, $fail)
    {
        $totalCalculated = collect($value)->sum(fn($item) => 
            $item['quantity'] * $item['unit_price']
        );
        
        if (abs($totalCalculated - $this->subtotal) > 0.01) {
            $fail('The order subtotal does not match item totals.');
        }
    }
}

Performance Considerations

Lazy Loading Nested DTOs

// For large nested structures, consider lazy loading
class OrderDTO extends BaseDTO
{
    private ?Collection $_items = null;
    
    public function getItems(): Collection
    {
        return $this->_items ??= collect($this->itemsData)
            ->map(fn($item) => OrderItemDTO::from($item));
    }
}

Selective Nesting

// Only load what you need
$orderSummary = OrderDTO::from($orderData)->only([
    'order_number',
    'status', 
    'customer.name',
    'total_amount'
]);

Export with Nested Data

JSON Export

$order = OrderDTO::from($orderData);

$json = $order->toJson();
// Automatically includes all nested DTO data

Custom Export Format

$orderSummary = [
    'order' => $order->only(['order_number', 'status', 'total_amount']),
    'customer' => $order->customer->only(['name', 'email']),
    'items_count' => $order->items->count(),
    'shipping_address' => $order->shipping_address?->toArray(),
];

Real-World Usage Patterns

API Responses

// API controller returning nested order data
public function show(Order $order): JsonResponse
{
    $orderDto = OrderDTO::fromModel($order);
    
    return response()->json([
        'order' => $orderDto->toArray(),
        'meta' => [
            'items_count' => $orderDto->items->count(),
            'has_shipping_address' => $orderDto->shipping_address !== null,
            'estimated_delivery' => $orderDto->calculateDeliveryDate(),
        ]
    ]);
}

Form Processing

// Handle complex form submissions
public function store(Request $request): JsonResponse
{
    try {
        // Validate entire nested structure
        $orderDto = OrderDTO::fromValidated($request->all());
        
        // Business logic with type-safe access
        $order = $this->orderService->createOrder($orderDto);
        
        return response()->json([
            'message' => 'Order created successfully',
            'order' => $orderDto->toArray()
        ], 201);
        
    } catch (ValidationException $e) {
        return response()->json([
            'message' => 'Validation failed',
            'errors' => $e->errors()
        ], 422);
    }
}

Data Transformation

// Transform between different nested structures
$legacyOrderData = $legacyApiService->getOrder($orderId);
$modernOrderDto = OrderDTO::from($legacyOrderData);

// Convert to new API format
$newApiData = $modernOrderDto->transformTo(NewOrderFormatDTO::class);

What's Next?

Now that you understand nested DTOs:


Nested DTOs provide type-safe composition of complex data structures, making your code more maintainable and less error-prone. πŸ”—

πŸš€ Laravel Arc Wiki

🏠 Home

πŸš€ Getting Started

πŸ“š Core Concepts

πŸ—οΈ Advanced Features

βš™οΈ Configuration & CLI

🌐 Real-World Examples


🎯 Key Concepts

YAML β†’ DTO β†’ Type-Safe Code

Laravel Arc transforms your YAML definitions into powerful PHP DTOs with automatic validation, field transformers, and behavioral traits.

πŸ”— Quick Links

Clone this wiki locally