# Data Transfer Objects (DTOs)

**NUMBER:** ADR-DTO-001
**STATUS:** CANONICAL AUTHORITY
**SCOPE:** Every object that carries data across layer boundaries (Controller → Action, Query → Resource, between internal steps) in this project.
**RULE:** This is the ONLY definitive source for DTO architecture, taxonomy, and construction patterns. Any other document mentioning this topic is a REFERENCE, not a definition. This explicitly supersedes ADR-028 and ADR-047.
**VIOLATION:** Implementing DTO patterns based on any other source constitutes an architectural breach.

> **When to use a Scope or Query Class instead of a DTO for reading:** `04_DATABASE_COMMUNICATION.md` → The Read Path
> **What belongs in FormRequest vs what belongs in the DTO Constructor:** `08_FORM_REQUESTS.md` → The Delegation Map
> **How to extract data from FormRequest:** `07_BOUNDARY_LAYERS.md` → Input Extraction Rules

---

## 1. Philosophy

**Strict Contracts & Impenetrable Boundaries.**

The DTO is a gatekeeper. Its job is to group scattered primitives, enforce type safety, and prevent technical details (like database column names) from leaking across layers. A correctly built DTO makes it impossible to pass data in the wrong shape: it either constructs successfully, or it throws an exception before any business logic executes.

---

## 2. Absolute Prohibitions

```text
❌ PROHIBITED: Importing Request or Resource classes.
❌ PROHIBITED: Importing Eloquent Models as Properties (Exception: passing an ID as int or string is permitted).
❌ PROHIBITED: Dependency Injection in the constructor (no __construct(Service $s)).
❌ PROHIBITED: Business workflow logic (e.g., checking customer balance).
❌ PROHIBITED: Using a fromRequest(Request $r) method. The Controller must unpack the request.
❌ PROHIBITED: Providing a public toArray() method for API Resource consumption. The Resource reads properties directly.
```

---

## 3. DTO Taxonomy

There is no "one-size-fits-all" DTO. To prevent architectural ambiguity, DTOs are strictly categorized into three types.

### 3.1 Input DTO
Carries data **INTO** the logic layer (Controller/Job → Action/Service).
- **State:** `readonly class`.
- **Purpose:** Data grouping, structural mapping via `toModelArray()`, and data integrity guarding in the Constructor.

### 3.2 Output DTO (Read Model)
Carries aggregated data **OUT OF** a Query Class.
- **Purpose:** Acts as a Read Model.
- **When to use:** **ONLY** when the query result is aggregated from multiple tables and does not map to a single Eloquent Model. If the Query returns a standard Model or Collection, an Output DTO is NOT used.

### 3.3 Context DTO
Carries composed state between private methods **WITHIN** a single Action or Service.
- **Purpose:** Reduces parameter count in internal method signatures.
- **Boundary:** Never crosses service boundaries.

---

## 4. The DTO Threshold Rule (CANONICAL)

This rule overrides all previous decisions (including ADR-028) regarding when to introduce a DTO for write operations.

### Mandatory (NO EXCEPTIONS)
- Create/Update operations with **> 3 fields**.
- Create/Update operations requiring **field name transformation** (e.g., `isSigned` → `status`).
- Create/Update operations requiring **type transformation** (e.g., `bool` → `string`).
- **All Update operations** (to safely handle null semantics and partial updates).

### Optional (Developer Judgment)
- Create operations with **≤ 3 fields** AND **1:1 mapping** to database columns AND **no transformations**.
- *In doubt: USE A DTO.*

### Prohibited
- Asking "should I use a DTO?" — If you have to ask, use it.

---

## 5. Input DTO — Creation Path

### 5.1 Construction & Guarding

The constructor validates data integrity — NOT HTTP validity. It throws if the data is mathematically or logically invalid (e.g., negative amounts, out-of-bounds integers).

```php
readonly class CreateContractDTO
{
    public function __construct(
        public readonly int $customerId,
        public readonly string $purchaseAmount,
        public readonly int $months,
        public readonly bool $isSigned,
    ) {
        // Data Integrity Guarding (PERMITTED here)
        if (bccomp($this->purchaseAmount, '0', 2) <= 0) {
            throw new \InvalidArgumentException('Purchase amount must be greater than zero.');
        }
        if ($this->months < 1 || $this->months > 60) {
            throw new \InvalidArgumentException('Months must be between 1 and 60.');
        }
    }
}
```

### 5.2 The `fromArray()` Pattern (CANONICAL)

This is the default and preferred method for instantiating DTOs from array data (like `$request->only()`).

**Rule:** Use the PHP 8 spread operator to pass the array directly to the constructor. This preserves strict typing and constructor guarding (like the `bccomp` check above).

```php
public static function fromArray(array $data): self
{
    return new self(...$data);
}
```

**Controller Usage:**
```php
$dto = CreateContractDTO::fromArray($request->only([
    'customer_id', 'purchase_amount', 'months', 'is_signed'
]));
```

**Fallback Rule:** Only use explicit key mapping (`return new self(customerId: (int)$data['customer_id'], ...)` ) if the magic spread operator is impossible due to complex type casting that cannot be done in the constructor.

### 5.3 Structural Mapping (`toModelArray()`)

Converts business concepts to database schema physics. 

**PERMITTED:** Changing key names, converting types (bool to string), adding computed timestamps.
**PROHIBITED:** Database queries, Service calls, complex business logic.

```php
public function toModelArray(): array
{
    return [
        'customer_id' => $this->customerId,
        'status'     => $this->isSigned ? ContractStatus::Active->value : ContractStatus::Draft->value,
        'signed_at'  => $this->isSigned ? now() : null,
        'start_date' => $this->startDate,
    ];
}
```

**Edge Case Limit:** If `toModelArray()` requires complex logic or multiple database queries to prepare the data, STOP. Move that logic to the Service. The DTO provides raw data; the Service prepares it for Eloquent.

---

## 6. Input DTO — The Sparse Update Pattern

This is the canonical solution for the hardest API edge case: **Distinguishing between "User did not send the field" (no change) and "User sent the field as null" (intentional deletion).**

Using standard nullable properties fails because `array_filter` removes nulls, preventing intentional null updates.

### 6.1 The Canonical Implementation

The DTO does NOT have pre-declared properties. It stores exactly what was sent.

```php
readonly class UpdateCustomerDTO
{
    private function __construct(
        private array $changedFields
    ) {}

    public static function fromValidatedRequest(array $validatedFields): self
    {
        return new self($validatedFields);
    }

    /** Checks if the user actually sent the field (even if the value is null) */
    public function has(string $field): bool
    {
        return array_key_exists($field, $this->changedFields);
    }

    /** Retrieves the value */
    public function get(string $field, mixed $default = null): mixed
    {
        return $this->changedFields[$field] ?? $default;
    }

    /** Returns only what was sent for the database update */
    public function toUpdateArray(): array
    {
        return $this->changedFields;
    }
}
```

### 6.2 Interaction with `$request->only()` (CANONICAL)

Because `$request->only(['name', 'phone'])` returns an array of **only** the fields present in the HTTP payload, passing it directly to the Sparse DTO perfectly preserves the "not sent" vs "sent as null" semantics.

```php
// In Controller
$dto = UpdateCustomerDTO::fromValidatedRequest(
    $request->only(['name', 'phone', 'description'])
);

// In Service: Optional cleanup before saving
public function update(Client $client, UpdateCustomerDTO $dto): Client
{
    $data = $dto->toUpdateArray();

    // Example: Convert empty strings to null
    if ($dto->has('description') && $dto->get('description') === '') {
        $data['description'] = null;
    }

    $client->update($data);
    return $client->fresh();
}
```

---

## 7. Output DTO (Read Model)

Used exclusively when a Query Class returns data that does not map to a single Eloquent Model (e.g., financial summaries aggregating multiple tables).

**Rules:**
1. Usually a standard `class` (not necessarily `readonly` if built dynamically).
2. Contains public properties for the aggregated data.
3. Uses the standard `fromArray()` magic pattern.
4. The API Resource reads these properties directly.

```php
class CustomerFinancialSummaryDTO
{
    public function __construct(
        public readonly string $totalDebt,
        public readonly string $totalCollected,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(...$data);
    }
}

// The Resource reads it blindly
class FinancialSummaryResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'debt' => $this->resource->totalDebt,
            'collected' => $this->resource->totalCollected,
        ];
    }
}
```

---

## 8. Edge Cases

### Handling Dates
**Rule:** The DTO receives dates as `string` (e.g., `'Y-m-d'`). 
**Reason:** The DTO must not depend on the `Carbon` library. Let Eloquent Model Casts handle the string-to-Carbon conversion at the database boundary.

### Handling Collections (Arrays of Objects)
If a Request contains an array of objects, do not pass a raw array. Map it into an array of child DTOs.

```php
readonly class BulkUpdateInstallmentsDTO
{
    /**
     * @param array<UpdateInstallmentDTO> $installments
     */
    public function __construct(
        public readonly array $installments,
    ) {}

    public static function fromArray(array $data): self
    {
        $dtos = array_map(
            fn(array $item) => UpdateInstallmentDTO::fromArray($item),
            $data['installments']
        );

        return new self(installments: $dtos);
    }
}
```

## 10. Financial Precision Rule

System-wide rule for handling monetary values to prevent precision loss.

- **Mandatory:** All calculations involving monetary values MUST use the `Money` Value Object from `App\Domains\Shared\ValueObjects\Money`.
- **Prohibited:** Using raw `float`, `double`, or bare `string` with arithmetic operators (like `+`, `-`, `*`) for financial calculations.
- **Input Handling:** When receiving money from HTTP requests or reading from DB (which return `string`), pass it directly to `Money::of($amount)`.
- **Output Handling:** When returning money to the API, use `$money->formatted()` inside API Resources only.

❌ PROHIBITED:
```php
$profit = ($purchaseAmount * $profitPercentage) / 100; // Float math!
$total = $initialPayment + $remainingAmount; // String concatenation math!
```

✅ MANDATORY:
```php
$profit = Money::of($purchaseAmount)->multiply(Money::of($profitPercentage))->divide(Money::of('100'));
```

---

## 9. Decision Cheat Sheet

When transferring data between layers:

1. Is the data coming OUT of a complex aggregation Query (not a single Model)?
   └─ YES → Create an Output DTO (Read Model) using `fromArray()`.

2. Is the data a temporary state between private methods inside ONE Action/Service?
   └─ YES → Create a Context DTO.

3. Is the data going INTO a Service to CREATE a record?
   ├─ Does it need structural mapping (key names/types) to the DB?
   │  └─ YES → Create an Input DTO with `toModelArray()`. Build via `fromArray($request->only(...))`.
   └─ Is it ≤3 fields, 1:1 mapping, no transformations?
      └─ YES → DTO is Optional (use `$request->input()` directly if preferred).

4. Is the data going INTO a Service to UPDATE a record?
   └─ YES → ALWAYS use the Sparse Update DTO pattern.
   └─ Build via `UpdateDTO::fromValidatedRequest($request->only(...))`.

5. Am I writing a fromRequest(Request $r) method?
   └─ YES → ERROR. Use `fromArray()` and let the Controller unpack the Request via `only()`.

6. Am I writing a public toArray() method on the DTO?
   └─ YES → ERROR. The Resource reads public properties directly.

7. Is my toModelArray() running database queries?
   └─ YES → ERROR. Move that logic to the Service.