# Request Validation

This guide explains how to implement Form Request validation classes following our established patterns.

## Overview

Form Requests provide:

1. **Validation rules** for incoming data
2. **Authorization checks** for access control
3. **Data preparation** before validation
4. **Custom error messages** with translations

## Standard Request Template

```php
<?php

namespace App\Domains\YourDomain\YourModule\Http\Requests\V1;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StoreYourEntityRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true; // Or check specific permission
    }

    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'code' => [
                'nullable',
                'string',
                'max:50',
                Rule::unique('your_entities', 'code')
                    ->where('company_id', company()->id),
            ],
            'description' => 'nullable|string|max:1000',
            'status' => 'nullable|string|in:draft,pending,active',
            'amount' => 'nullable|numeric|min:0',
            'quantity' => 'nullable|integer|min:1',
            'date' => 'nullable|date|date_format:Y-m-d',
            'is_active' => 'nullable|boolean',
            'category_id' => 'nullable|integer|exists:categories,id',

            // Nested array validation
            'items' => 'nullable|array',
            'items.*.name' => 'required_with:items|string|max:255',
            'items.*.quantity' => 'required_with:items|integer|min:1',
            'items.*.price' => 'required_with:items|numeric|min:0',

            // File uploads
            'temporary_folders' => 'nullable|array',
            'temporary_folders.*' => 'string',
        ];
    }

    /**
     * Get custom error messages.
     */
    public function messages(): array
    {
        return [
            'name.required' => __('The name field is required'),
            'code.unique' => __('This code already exists'),
            'amount.min' => __('Amount must be at least :min'),
            'items.*.name.required_with' => __('Item name is required'),
        ];
    }

    /**
     * Get custom attribute names.
     */
    public function attributes(): array
    {
        return [
            'name' => __('Name'),
            'code' => __('Code'),
            'category_id' => __('Category'),
            'items.*.name' => __('Item name'),
        ];
    }
}
```

## Update Request Template

```php
<?php

namespace App\Domains\YourDomain\YourModule\Http\Requests\V1;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateYourEntityRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        // Get the entity ID from route
        $entityId = $this->route('yourEntity')?->id;

        return [
            'name' => 'sometimes|required|string|max:255',
            'code' => [
                'sometimes',
                'nullable',
                'string',
                'max:50',
                Rule::unique('your_entities', 'code')
                    ->where('company_id', company()->id)
                    ->ignore($entityId),
            ],
            'description' => 'nullable|string|max:1000',
            'status' => 'nullable|string|in:draft,pending,active',
            'amount' => 'nullable|numeric|min:0',
            'is_active' => 'nullable|boolean',
            'category_id' => 'nullable|integer|exists:categories,id',
        ];
    }
}
```

## Validation Rules Reference

### String Validation

```php
'name' => 'required|string|max:255',
'code' => 'nullable|string|min:3|max:50',
'email' => 'required|email|max:255',
'phone' => 'nullable|string|regex:/^[0-9+\-\s]+$/',
'url' => 'nullable|url',
'ip' => 'nullable|ip',
'uuid' => 'nullable|uuid',
```

### Numeric Validation

```php
'amount' => 'required|numeric|min:0|max:999999.99',
'quantity' => 'required|integer|min:1|max:1000',
'percentage' => 'nullable|numeric|between:0,100',
'price' => 'required|decimal:0,2|min:0',
```

### Date Validation

```php
'date' => 'required|date|date_format:Y-m-d',
'start_date' => 'required|date|after_or_equal:today',
'end_date' => 'required|date|after:start_date',
'birth_date' => 'required|date|before:today',
'datetime' => 'required|date_format:Y-m-d H:i:s',
```

### Boolean Validation

```php
'is_active' => 'nullable|boolean',
'agree_terms' => 'required|accepted',
```

### Array Validation

```php
// Simple array
'tags' => 'nullable|array',
'tags.*' => 'string|max:50',

// Array with specific items
'items' => 'required|array|min:1|max:50',
'items.*.name' => 'required|string|max:255',
'items.*.quantity' => 'required|integer|min:1',
'items.*.price' => 'required|numeric|min:0',

// Associative array
'settings' => 'nullable|array',
'settings.key1' => 'nullable|string',
'settings.key2' => 'nullable|boolean',
```

### File Validation

```php
// Single file
'document' => 'required|file|mimes:pdf,doc,docx|max:10240',
'image' => 'required|image|mimes:jpeg,png,webp|max:5120',

// Multiple files
'attachments' => 'nullable|array|max:5',
'attachments.*' => 'file|mimes:pdf,doc,docx,jpg,png|max:10240',
```

### Existence Validation

```php
// Check exists in table
'category_id' => 'required|exists:categories,id',

// With additional conditions
'category_id' => [
    'required',
    Rule::exists('categories', 'id')
        ->where('company_id', company()->id)
        ->where('is_active', true),
],

// Check unique with company scope
'code' => [
    'required',
    Rule::unique('your_entities', 'code')
        ->where('company_id', company()->id),
],

// Check unique ignoring current record
'code' => [
    'required',
    Rule::unique('your_entities', 'code')
        ->where('company_id', company()->id)
        ->ignore($this->route('yourEntity')?->id),
],
```

### Enum Validation

```php
use App\Domains\YourDomain\YourModule\Enums\YourEntityStatus;

'status' => [
    'required',
    Rule::enum(YourEntityStatus::class),
],

// Or simple in validation
'status' => 'required|in:draft,pending,active,completed',
```

### Conditional Validation

```php
'status' => 'required|in:draft,pending,active',
'rejection_reason' => 'required_if:status,rejected|string|max:500',
'approved_by' => 'required_if:status,approved|exists:users,id',

// Using when
'discount' => [
    'nullable',
    'numeric',
    Rule::when($this->discount_type === 'percentage', ['between:0,100']),
    Rule::when($this->discount_type === 'fixed', ['min:0']),
],
```

## Authorization

### Basic Authorization

```php
public function authorize(): bool
{
    return true; // Allow all authenticated users
}
```

### Permission-Based Authorization

```php
public function authorize(): bool
{
    return $this->user()->can('your-entities.create');
}
```

### Ownership Authorization

```php
public function authorize(): bool
{
    $entity = $this->route('yourEntity');

    // Allow if user owns the entity
    return $entity && $entity->created_by === $this->user()->id;
}
```

### Combined Authorization

```php
public function authorize(): bool
{
    $user = $this->user();
    $entity = $this->route('yourEntity');

    // Admin can do anything
    if ($user->hasRole('admin')) {
        return true;
    }

    // Owner can edit their own
    if ($entity && $entity->created_by === $user->id) {
        return true;
    }

    return false;
}
```

## Data Preparation

### prepareForValidation()

Transform data before validation:

```php
protected function prepareForValidation(): void
{
    // Set default values
    $this->merge([
        'is_active' => $this->is_active ?? true,
        'status' => $this->status ?? 'draft',
    ]);

    // Transform data
    if ($this->has('phone')) {
        $this->merge([
            'phone' => preg_replace('/[^0-9+]/', '', $this->phone),
        ]);
    }

    // Compute values
    if ($this->has('items')) {
        $total = collect($this->items)->sum(function ($item) {
            return ($item['quantity'] ?? 0) * ($item['price'] ?? 0);
        });
        $this->merge(['total' => $total]);
    }
}
```

### passedValidation()

Process data after validation:

```php
protected function passedValidation(): void
{
    // Add computed fields to validated data
    $this->merge([
        'slug' => \Str::slug($this->name),
        'processed_at' => now(),
    ]);
}
```

## Custom Validation Rules

### Inline Closure Rule

```php
public function rules(): array
{
    return [
        'code' => [
            'required',
            'string',
            function ($attribute, $value, $fail) {
                if (!preg_match('/^[A-Z]{3}-[0-9]{4}$/', $value)) {
                    $fail(__('Code must be in format XXX-0000'));
                }
            },
        ],
    ];
}
```

### Using withValidator()

```php
public function withValidator($validator): void
{
    $validator->after(function ($validator) {
        // Cross-field validation
        if ($this->end_date && $this->start_date) {
            if ($this->end_date < $this->start_date) {
                $validator->errors()->add(
                    'end_date',
                    __('End date must be after start date')
                );
            }
        }

        // Business rule validation
        if ($this->amount > 10000 && !$this->approval_required) {
            $validator->errors()->add(
                'approval_required',
                __('Approval is required for amounts over 10,000')
            );
        }
    });
}
```

### Custom Rule Class

**Location:** `app/Domains/YourDomain/YourModule/Rules/ValidCode.php`

```php
<?php

namespace App\Domains\YourDomain\YourModule\Rules;

use Illuminate\Contracts\Validation\Rule;

class ValidCode implements Rule
{
    public function __construct(
        private ?int $ignoreId = null
    ) {}

    public function passes($attribute, $value): bool
    {
        // Check format
        if (!preg_match('/^[A-Z]{3}-[0-9]{4}$/', $value)) {
            return false;
        }

        // Check uniqueness
        $query = YourEntity::where('code', $value)
            ->where('company_id', company()->id);

        if ($this->ignoreId) {
            $query->where('id', '!=', $this->ignoreId);
        }

        return !$query->exists();
    }

    public function message(): string
    {
        return __('Invalid code format or code already exists');
    }
}

// Usage in Request
'code' => ['required', new ValidCode($this->route('yourEntity')?->id)],
```

## Filter Request

For index/list endpoints with filters:

```php
<?php

namespace App\Domains\YourDomain\YourModule\Http\Requests\V1;

use Illuminate\Foundation\Http\FormRequest;

class FilterYourEntityRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            // Search
            'search' => 'nullable|string|max:255',

            // Filters
            'status' => 'nullable|string|in:draft,pending,active,completed',
            'category_id' => 'nullable|integer|exists:categories,id',
            'is_active' => 'nullable|boolean',

            // Multi-value filters
            'statuses' => 'nullable|array',
            'statuses.*' => 'string|in:draft,pending,active,completed',

            // Date range
            'date_from' => 'nullable|date|date_format:Y-m-d',
            'date_to' => 'nullable|date|date_format:Y-m-d|after_or_equal:date_from',

            // Range filters
            'amount_min' => 'nullable|numeric|min:0',
            'amount_max' => 'nullable|numeric|min:0|gte:amount_min',

            // Sorting
            'sort_by' => 'nullable|string|in:name,created_at,amount,status',
            'sort_order' => 'nullable|string|in:asc,desc',

            // Pagination
            'per_page' => 'nullable|integer|min:1|max:100',
            'page' => 'nullable|integer|min:1',
        ];
    }
}
```

## Multilingual Messages

```php
public function messages(): array
{
    return [
        // English messages with Arabic translations
        'name.required' => __('validation.required', ['attribute' => __('Name')]),
        'code.unique' => __('This :attribute already exists', ['attribute' => __('code')]),

        // Direct translations
        'amount.min' => app()->getLocale() === 'ar'
            ? 'يجب أن يكون المبلغ على الأقل :min'
            : 'Amount must be at least :min',
    ];
}
```

## Best Practices

### DO:

1. **Create separate requests** for Store and Update
2. **Use Rule class** for complex unique constraints
3. **Scope unique checks** to company_id
4. **Provide translated messages**
5. **Validate nested arrays** properly
6. **Use conditional validation** when needed

### DON'T:

1. **Don't validate in controllers** - Use FormRequest
2. **Don't skip authorization** - Always implement authorize()
3. **Don't hardcode error messages** - Use translations
4. **Don't forget exists checks** - Validate foreign keys
5. **Don't allow any fields** - Be explicit about allowed fields

## Common Validation Patterns

| Scenario | Rules |
|----------|-------|
| Required string | `'field' => 'required\|string\|max:255'` |
| Optional number | `'field' => 'nullable\|numeric\|min:0'` |
| Date range | `'end' => 'required\|date\|after:start'` |
| Foreign key | `'id' => 'required\|exists:table,id'` |
| Unique per company | `Rule::unique('table', 'col')->where('company_id', company()->id)` |
| File upload | `'file' => 'required\|file\|mimes:pdf\|max:10240'` |
| Array of items | `'items.*.name' => 'required\|string'` |
