|
| 1 | +# HOW_TO_USE — Validation Module |
| 2 | + |
| 3 | +This guide explains **how to use the Validation module** in controllers and |
| 4 | +application flow. |
| 5 | + |
| 6 | +It assumes: |
| 7 | +- The Validation module is available under the project namespace `App\Modules\Validation` |
| 8 | +- `respect/validation` is installed |
| 9 | +- PHP 8.2+ |
| 10 | +- PHPStan level max compatibility is required |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## 1️⃣ Basic Usage Pattern (API Controller) |
| 15 | + |
| 16 | +### Step 1 — Choose the Schema |
| 17 | +Each endpoint must have **one schema** representing its input. |
| 18 | + |
| 19 | +Example: |
| 20 | +- Login → `AuthLoginSchema` |
| 21 | +- Create Admin → `AdminCreateSchema` |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +### Step 2 — Validate the Input |
| 26 | + |
| 27 | +```php |
| 28 | +use App\Modules\Validation\Validator\RespectValidator; |
| 29 | +use app\Modules\Validation\Schemas\AuthLoginSchema; |
| 30 | +use app\Modules\Validation\ErrorMapper\SystemApiErrorMapper; |
| 31 | + |
| 32 | +/** @var array<string, mixed> $input */ |
| 33 | +$input = (array) $request->getParsedBody(); |
| 34 | + |
| 35 | +$validator = new RespectValidator(); |
| 36 | +$schema = new AuthLoginSchema(); |
| 37 | + |
| 38 | +$result = $validator->validate($schema, $input); |
| 39 | +``` |
| 40 | + |
| 41 | +📌 Notes: |
| 42 | + |
| 43 | +* Validation **never throws** for invalid input |
| 44 | +* All errors are structured and typed |
| 45 | +* HTTP status is always `400` for validation errors |
| 46 | + (by design — `422` is reserved for non-validation semantic failures) |
| 47 | +* ❌ Validation does **not** perform input sanitization (e.g., `trim`, `normalize`) |
| 48 | + – handle sanitization explicitly in the controller or input factory if needed |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +### Step 3 — Handle Validation Failure |
| 53 | + |
| 54 | +```php |
| 55 | +if (!$result->isValid()) { |
| 56 | + $errorMapper = new SystemApiErrorMapper(); |
| 57 | + $errorResponse = $errorMapper->mapValidationErrors( |
| 58 | + $result->getErrors() |
| 59 | + ); |
| 60 | + |
| 61 | + return $response |
| 62 | + ->withStatus($errorResponse->getStatus()) |
| 63 | + ->withJson($errorResponse->toArray()); |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +### Step 4 — Continue Normal Flow |
| 70 | + |
| 71 | +```php |
| 72 | +// Input is valid here |
| 73 | +// Call Service / Domain layer safely |
| 74 | +``` |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## 2️⃣ Adding a New Schema |
| 79 | + |
| 80 | +### Step 1 — Create Schema Class |
| 81 | + |
| 82 | +All schemas **must extend `AbstractSchema`**. |
| 83 | + |
| 84 | +```php |
| 85 | +use app\Modules\Validation\Schemas\AbstractSchema; |
| 86 | +use app\Modules\Validation\Rules\RequiredStringRule; |
| 87 | +use app\Modules\Validation\Enum\ValidationErrorCodeEnum; |
| 88 | + |
| 89 | +final class ExampleSchema extends AbstractSchema |
| 90 | +{ |
| 91 | + protected function rules(): array |
| 92 | + { |
| 93 | + return [ |
| 94 | + 'title' => [ |
| 95 | + RequiredStringRule::rule(3, 100), |
| 96 | + ValidationErrorCodeEnum::REQUIRED_FIELD, |
| 97 | + ], |
| 98 | + ]; |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +📌 Rules format: |
| 104 | + |
| 105 | +```php |
| 106 | +'field_name' => [Validatable, ValidationErrorCodeEnum] |
| 107 | +``` |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +## 3️⃣ Adding a New Rule |
| 112 | + |
| 113 | +Rules are **thin wrappers** around Respect validators. |
| 114 | + |
| 115 | +Example: |
| 116 | + |
| 117 | +```php |
| 118 | +use Respect\Validation\Validator as v; |
| 119 | +use Respect\Validation\Validatable; |
| 120 | + |
| 121 | +final class SlugRule |
| 122 | +{ |
| 123 | + /** |
| 124 | + * @return Validatable |
| 125 | + */ |
| 126 | + public static function rule() |
| 127 | + { |
| 128 | + return v::stringType()->regex('/^[a-z0-9-]+$/'); |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +Rules: |
| 134 | + |
| 135 | +* Must not know about Schemas |
| 136 | +* Must not throw custom exceptions |
| 137 | +* Must return `Validatable` (via docblock) |
| 138 | + |
| 139 | +--- |
| 140 | + |
| 141 | +## 4️⃣ Validation Error Codes (Enums) |
| 142 | + |
| 143 | +### Validation Errors |
| 144 | + |
| 145 | +All validation errors use: |
| 146 | + |
| 147 | +```php |
| 148 | +ValidationErrorCodeEnum |
| 149 | +``` |
| 150 | + |
| 151 | +Example: |
| 152 | + |
| 153 | +```php |
| 154 | +ValidationErrorCodeEnum::INVALID_EMAIL |
| 155 | +``` |
| 156 | + |
| 157 | +❌ Never use strings directly. |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +### Auth / Permission Errors |
| 162 | + |
| 163 | +Used by Guards (not Validation): |
| 164 | + |
| 165 | +```php |
| 166 | +AuthErrorCodeEnum |
| 167 | +``` |
| 168 | + |
| 169 | +Example: |
| 170 | + |
| 171 | +```php |
| 172 | +AuthErrorCodeEnum::STEP_UP_REQUIRED |
| 173 | +``` |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## 5️⃣ Error Mapping (System-Level) |
| 178 | + |
| 179 | +All errors are converted to API responses through: |
| 180 | + |
| 181 | +```php |
| 182 | +SystemApiErrorMapper |
| 183 | +``` |
| 184 | + |
| 185 | +### Validation Mapping |
| 186 | + |
| 187 | +```php |
| 188 | +$errorMapper->mapValidationErrors($errors); |
| 189 | +``` |
| 190 | + |
| 191 | +### Auth Mapping (used in exception handlers) |
| 192 | + |
| 193 | +```php |
| 194 | +$errorMapper->mapAuthError(AuthErrorCodeEnum::NOT_AUTHORIZED); |
| 195 | +``` |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +## 6️⃣ ApiErrorResponseDTO |
| 200 | + |
| 201 | +All error responses are returned as: |
| 202 | + |
| 203 | +```php |
| 204 | +ApiErrorResponseDTO |
| 205 | +``` |
| 206 | + |
| 207 | +### Accessors |
| 208 | + |
| 209 | +```php |
| 210 | +$errorResponse->getStatus(); // HTTP status code |
| 211 | +$errorResponse->toArray(); // API-safe payload |
| 212 | +``` |
| 213 | + |
| 214 | +Payload format: |
| 215 | + |
| 216 | +```json |
| 217 | +{ |
| 218 | + "code": "INPUT_INVALID", |
| 219 | + "errors": { |
| 220 | + "email": ["invalid_email"] |
| 221 | + } |
| 222 | +} |
| 223 | +``` |
| 224 | + |
| 225 | +--- |
| 226 | + |
| 227 | +## 7️⃣ What NOT To Do ❌ |
| 228 | + |
| 229 | +* ❌ Do not validate inside Domain services |
| 230 | +* ❌ Do not throw validation exceptions |
| 231 | +* ❌ Do not log validation errors |
| 232 | +* ❌ Do not return arrays from ErrorMappers |
| 233 | +* ❌ Do not use strings instead of Enums |
| 234 | +* ❌ Do not mix validation with authorization |
| 235 | + |
| 236 | +--- |
| 237 | + |
| 238 | +## 8️⃣ Common Mistakes |
| 239 | + |
| 240 | +| Mistake | Why It’s Wrong | |
| 241 | +|--------------------------|-----------------------------------| |
| 242 | +| Validating in Service | Breaks separation of concerns | |
| 243 | +| Using strings for errors | Breaks type-safety | |
| 244 | +| try/catch per field | Duplication (use AbstractSchema) | |
| 245 | +| HTTP logic in Schema | Schema must be framework-agnostic | |
| 246 | + |
| 247 | +--- |
| 248 | + |
| 249 | +## 9️⃣ Static Analysis Notes |
| 250 | + |
| 251 | +* Return types for Respect validators are declared via **docblocks** |
| 252 | +* This is intentional for PHPStan compatibility |
| 253 | +* Do not add strict return types to Rule methods |
| 254 | + |
| 255 | +--- |
| 256 | + |
| 257 | +## 🔒 Final Rule (LOCKED) |
| 258 | + |
| 259 | +> **Every request must be validated using a Schema. |
| 260 | +> Every validation error must be expressed as an Enum. |
| 261 | +> Every error response must be returned as a DTO.** |
| 262 | +
|
| 263 | +--- |
| 264 | + |
| 265 | +## ✅ Status |
| 266 | + |
| 267 | +* Usage pattern: **LOCKED** |
| 268 | +* API contract: **STABLE** |
| 269 | +* PHPStan: **PASS (level max)** |
| 270 | + |
| 271 | +--- |
0 commit comments