-
-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
# 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
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
# 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]
# 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
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
];
}
}
$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"
// 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;
$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');
// Update all item quantities
$updatedItems = $order->items->map(function($item) {
return $item->with(['quantity' => $item->quantity + 1]);
});
$updatedOrder = $order->with(['items' => $updatedItems]);
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.']
]
*/
}
// 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.');
}
}
}
// 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));
}
}
// Only load what you need
$orderSummary = OrderDTO::from($orderData)->only([
'order_number',
'status',
'customer.name',
'total_amount'
]);
$order = OrderDTO::from($orderData);
$json = $order->toJson();
// Automatically includes all nested DTO data
$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(),
];
// 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(),
]
]);
}
// 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);
}
}
// Transform between different nested structures
$legacyOrderData = $legacyApiService->getOrder($orderId);
$modernOrderDto = OrderDTO::from($legacyOrderData);
// Convert to new API format
$newApiData = $modernOrderDto->transformTo(NewOrderFormatDTO::class);
Now that you understand nested DTOs:
- Collections - Working with arrays of DTOs
- Generated DTO Structure - Understanding generated code
- Example API Integration - Real-world nested DTO usage
- Field Types - All available field types for nesting
Nested DTOs provide type-safe composition of complex data structures, making your code more maintainable and less error-prone. π
Laravel Arc - Generate Type-Safe DTOs from YAML Definitions
π Home | π Get Started | π Examples | βοΈ Config
From YAML to Type-Safe Code - Made with β€οΈ for the Laravel community
π Home
- π Understanding YAML Structure
- π·οΈ Field Types
- π Field Transformers
- π Behavioral Traits
YAML β DTO β Type-Safe Code
Laravel Arc transforms your YAML definitions into powerful PHP DTOs with automatic validation, field transformers, and behavioral traits.
- π Get Started - Create your first DTO in 5 minutes
- π All Examples - Copy-paste ready examples
- β‘ Commands - CLI reference