# FormRequests

**NUMBER:** ADR-REQ-001
**STATUS:** CANONICAL AUTHORITY
**SCOPE:** Every class extending `Illuminate\Foundation\Http\FormRequest` in this project, and all custom validation architectures.
**RULE:** This is the ONLY definitive source for FormRequest architecture, Custom Validation Rules, Domain Validators, and validation delegation. Any other document mentioning this topic is a REFERENCE, not a definition.
**VIOLATION:** Implementing validation patterns based on any other source constitutes an architectural breach.

> **What belongs in the DTO Constructor vs Business Validator:** `09_DTOS.md` → Input DTO Construction
> **How error messages are structured and translated:** `07_BOUNDARY_LAYERS.md` → Localization Confinement
> **How validation errors are formatted for the API response:** `02_ERROR_BOUNDARIES.md` → Mapping Layer
> **How to extract data in Controller:** `07_BOUNDARY_LAYERS.md` → Input Extraction Rules

---

## 1. Philosophy

**Network Gateway Guard.**

The FormRequest is the system's first line of defense. Its sole responsibility is to validate that incoming HTTP data meets structural requirements (types, lengths, formats) so the system can safely ingest it without choking. 

It understands data **shape**, not business **meaning**.

---

## 2. Absolute Prohibitions

```text
❌ PROHIBITED: Eloquent or Query Builder queries inside withValidator().
❌ PROHIBITED: Mathematical transformations that change a value's meaning (e.g., multiplying by 100 for currency conversion).
❌ PROHIBITED: Complex authorization logic (use Route Middleware instead).
❌ PROHIBITED: Cross-field business rules (e.g., verifying total = price × qty). This belongs to a Domain Validator called from the Action/Service.
❌ PROHIBITED: Manually throwing ValidationException in Service or Controller code. FormRequest is the sole driver of structural validation.
❌ PROHIBITED: Using $request->validated() in the Controller. Use only() or input().
```

---

## 3. Allowed Responsibilities

### 3.1 Structural Validation (`rules()`)

Validating types, lengths, formats, and standard framework rules.

```php
public function rules(): array
{
    return [
        'customer_id'  => ['required', 'integer', 'exists:clients,id'],
        'amount'       => ['required', 'string', 'regex:/^[0-9]+(\.[0-9]{1,2})?$/'],
        'installments' => ['required', 'integer', 'min:1', 'max:60'],
    ];
}
```

*Note: Using `exists` and `unique` is PERMITTED. These are standard framework rules that touch a single table and do not constitute architectural leakage.*

### 3.2 HTTP Sanitization (`prepareForValidation()`)

Clean the **form** of the input — never change its **mathematical meaning**.

```php
protected function prepareForValidation(): void
{
    // PERMITTED: Remove keyboard-added commas from a number string
    $this->merge([
        'amount' => str_replace(',', '', $this->amount),
    ]);

    // PROHIBITED: Mathematical conversion belongs in DTO Constructor
    // $this->merge(['amount' => (float) $this->amount * 100]); 
}
```

### 3.3 Simple Authorization (`authorize()`)

A single-line permission check that requires no complex queries or business logic evaluation.

```php
public function authorize(): bool
{
    return $this->user()->can('create', Contract::class);
}
```

### 3.4 Custom Error Messages (`messages()`)

Mapping validation rules to specific translation keys for the UI.

```php
public function messages(): array
{
    return [
        'amount.regex' => __('errors/contract.invalid_amount_format'),
        'phone.unique' => __('errors/client.phone_unique'),
    ];
}
```

---

## 4. The Delegation Map (CANONICAL)

Every validation rule in the system belongs to exactly one layer. This is the supreme reference for where validation logic MUST be written.

| Type of Validation | Example | Correct Layer | Why |
|:---|:---|:---|:---|
| Data shape (type, length, format) | `email`, `max:150` | FormRequest `rules()` | HTTP structural guard |
| Database uniqueness/existence | `unique:clients,phone` | FormRequest `rules()` | Standard framework capability |
| Custom reusable structural rule | `new UniqueExceptCurrentPhone($id)` | FormRequest `rules()` (via Shared Rule) | Reusable structural logic |
| Remove commas from number | `str_replace` | FormRequest `prepareForValidation()` | HTTP input sanitization |
| Convert dollars to cents (×100) | `$amount * 100` | **DTO Constructor** | Mathematical meaning change |
| Verify amount > 0 | `bccomp($amount, '0')` | **DTO Constructor** | Mathematical integrity guard |
| Verify LIFO sequence | Query + comparison | **Domain Validator** | Requires DB state + business rule |
| Verify contract math consistency | Complex algorithm | **Domain Validator** | Multi-field business invariant |
| Verify share balance sufficiency | Balance calculation | **Domain Validator** | Requires aggregated DB state |

---

## 5. `withValidator` Constraints

Using `withValidator()` is **SEMI-PROHIBITED**. It is allowed in exactly ONE narrow scenario.

### The ONLY Accepted Scenario
Conditional field rules that depend on other HTTP fields in the *same request* — with **NO database access** and **NO business logic**.

```php
// PERMITTED: Conditional field based on another HTTP field
public function withValidator(Validator $validator): void
{
    $validator->sometimes('reason', 'required|max:500', function ($input) {
        return $input->status === 'rejected';
    });
}
```

### The Prohibited Scenario
Any logic that queries the database or evaluates a business state.

```php
// PROHIBITED: Requires database query and business logic
public function withValidator(Validator $validator): void
{
    $validator->after(function ($validator) {
        $lastShare = Share::where('agent_id', $this->agent_id)->latest()->first();
        if ($lastShare->id !== $this->share_id) {
            $validator->errors()->add('share_id', 'LIFO violation');
        }
    });
}
```
*(The correct approach for the prohibited scenario is to throw a Domain Exception from inside a Domain Validator, which is called from the Action. See `02_ERROR_BOUNDARIES.md`).*

---

## 6. Validation Architecture (CANONICAL)

Custom validation is split into two distinct patterns based on reusability and complexity.

### 6.1 Custom Validation Rules (Shared, Reusable)

Used for structural logic that can be applied via standard `rules()` in any FormRequest.

**Location:** `App/Domains/Shared/Validation/Rules/`
**Naming:** Descriptive, flat structure (e.g., `UniqueExceptCurrentPhone`, `ValidEnumValue`).

**Construction:**
- Accept context via Constructor (e.g., current user ID, allowed statuses).
- MUST support the Laravel translation system natively via the `message()` method.
- MUST support Enum integration for rules validating multiple states in one go.

**Canonical Example (Enum-backed Rule):**
```php
namespace App\Domains\Shared\Validation\Rules;

use Illuminate\Contracts\Validation\Rule;
use App\Domains\Contract\Enums\ContractStatus;

class ValidContractStatusTransition implements Rule
{
    public function __construct(private ContractStatus $currentStatus) {}

    public function passes($attribute, $value): bool
    {
        $targetStatus = ContractStatus::from($value);
        return $this->currentStatus->canTransitionTo($targetStatus);
    }

    public function message(): string
    {
        return __('validation/contract.invalid_status_transition');
    }
}

// Usage in FormRequest
'status' => ['required', 'string', new ValidContractStatusTransition($contract->status)],
```

**Canonical Example (Exclusion Rule):**
```php
class UniqueExceptCurrentPhone implements Rule
{
    public function __construct(private int $clientId) {}

    public function passes($attribute, $value): bool
    {
        return !Client::where('phone', $value)
            ->where('id', '!=', $this->clientId)
            ->exists();
    }

    public function message(): string
    {
        return __('validation/client.phone_taken');
    }
}
```

*Note: The FormRequest `messages()` method can override the default message provided by the Rule if a specific context requires different wording.*

### 6.2 Domain Validators (Domain-Specific, Complex)

Used for business invariants that are too complex for a single Rule, require database aggregation, or are not reusable across domains.

**Location:** `App/Domains/{Domain}/Validation/`
**Structure:** One separate file per validation purpose (e.g., `ValidateShareWithdrawal.php`, `ValidateInvestorCreation.php`).
**Naming:** `{Purpose}Validation.php` or `Validate{Purpose}.php`.

**Rules:**
- Methods return `void`.
- Methods throw Domain Exceptions (never return booleans).
- Injected into Actions (or Services if no Action exists).

**Canonical Example:**
```php
namespace App\Domains\Shares\Validation;

use App\Exceptions\Business\InsufficientShareBalanceException;
use App\Exceptions\Business\ShareNotLatestException;

class ValidateShareWithdrawal
{
    public function __construct(
        private GetShareBalanceQuery $balanceQuery,
        private GetLatestShareLogQuery $latestLogQuery
    ) {}

    public function checkBalance(int $clientId, string $amount): void
    {
        $currentBalance = $this->balanceQuery->get($clientId);
        
        if (bccomp($amount, $currentBalance, 2) > 0) {
            throw new InsufficientShareBalanceException($amount, $currentBalance);
        }
    }

    public function checkIsLatestLog(int $logId, int $clientId): void
    {
        $latestLog = $this->latestLogQuery->get($clientId);
        
        if ($latestLog->id !== $logId) {
            throw new ShareNotLatestException($logId, $latestLog->id);
        }
    }
}
```

---

## 7. Update Requests & Sparse DTO Interaction

### 7.1 The `sometimes` Rule

In Update FormRequests, **NEVER** use `required`. Every field must use `sometimes` to ensure validation only runs if the field is present in the payload.

```php
public function rules(): array
{
    return [
        'name'        => ['sometimes', 'string', 'max:150'],
        'phone'       => ['sometimes', 'string', new UniqueExceptCurrentPhone($this->route('client'))],
        'description' => ['sometimes', 'nullable', 'string'],
    ];
}
```

```text
❌ PROHIBITED: Using 'required' in an Update FormRequest.
❌ PROHIBITED: Using 'required_without:field' in an Update FormRequest.
```

### 7.2 Feeding the Sparse DTO

The Controller uses `$request->only()` to extract exactly what was sent, passing it directly to the Sparse Update DTO. Because `only()` returns only sent fields, `array_key_exists` inside the DTO works perfectly to distinguish between "not sent" and "sent as null".

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

$this->updateClientService->update($client, $dto);
```

---

## 8. Decision Cheat Sheet

When writing validation logic:

1. Does this rule check data shape (type, length, format)?
   └─ YES → FormRequest rules().

2. Does it clean HTTP input appearance (remove commas, trim)?
   └─ YES → FormRequest prepareForValidation().

3. Is it a reusable structural check (e.g., unique phone excluding current user)?
   └─ YES → Create Custom Rule in `Shared/Validation/Rules/`.

4. Does it validate a state transition across multiple allowed values?
   └─ YES → Create Custom Rule in `Shared/Validation/Rules/` using Enums.

5. Does it transform a value's mathematical meaning?
   └─ YES → DTO Constructor.

6. Does it verify a complex business invariant (balance, LIFO sequence, math)?
   └─ YES → Domain Validator in `Domains/{Domain}/Validation/`.

7. Is this an Update request?
   └─ YES → Use `sometimes` for all fields. Controller uses `$request->only()`.

8. Am I throwing ValidationException manually in a Service?
   └─ YES → ERROR. Let FormRequest handle structural validation; use Domain Exceptions for business rules.