Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
66.67% |
4 / 6 |
|
60.00% |
3 / 5 |
CRAP | |
0.00% |
0 / 1 |
| ValidationException | |
66.67% |
4 / 6 |
|
60.00% |
3 / 5 |
5.93 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| forField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getStatusCode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getErrors | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace App\Domain\Exception; |
| 6 | |
| 7 | use DomainException; |
| 8 | use Throwable; |
| 9 | |
| 10 | /** |
| 11 | * Thrown when the request parsed correctly, but the *values* themselves |
| 12 | * violate semantic or business rules. No retry of the same request will |
| 13 | * succeed — the data itself is wrong and the user must change it. |
| 14 | * |
| 15 | * Use this for both field-level validation and broader business-rule |
| 16 | * violations on values: |
| 17 | * |
| 18 | * - "Amount must be greater than zero" |
| 19 | * - "Term must be one of: 6, 12, 24, 36, 48, 60" |
| 20 | * - "Insufficient available balance for this transaction" |
| 21 | * - "Loan amount exceeds 80% LTV" |
| 22 | * - "Email address is malformed" |
| 23 | * |
| 24 | * For state conflicts (where the same request body might succeed later), |
| 25 | * use {@see ConflictException} instead. For request structure errors |
| 26 | * (missing fields, wrong types), use {@see BadRequestException}. |
| 27 | * |
| 28 | * Field-level errors can be attached for the frontend to highlight the |
| 29 | * offending fields. Use the static {@see ValidationException::forField()} |
| 30 | * helper for the common single-field case, or pass an errors array to |
| 31 | * the constructor for multi-field validation results. |
| 32 | * |
| 33 | * Maps to HTTP 422 Unprocessable Entity. |
| 34 | */ |
| 35 | final class ValidationException extends DomainException implements HttpStatusException |
| 36 | { |
| 37 | /** |
| 38 | * @var array<string, string[]> Field name → list of error messages |
| 39 | */ |
| 40 | private array $errors; |
| 41 | |
| 42 | /** |
| 43 | * @param string $message Top-level error message |
| 44 | * @param array<string, string[]> $errors Field-level errors keyed by field name |
| 45 | * @param ?Throwable $previous |
| 46 | */ |
| 47 | public function __construct( |
| 48 | string $message = 'Validation failed', |
| 49 | array $errors = [], |
| 50 | ?Throwable $previous = null, |
| 51 | ) { |
| 52 | parent::__construct($message, 0, $previous); |
| 53 | $this->errors = $errors; |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Convenience constructor for the common single-field case. |
| 58 | * @param string $field |
| 59 | * @param string $message |
| 60 | */ |
| 61 | public static function forField(string $field, string $message): self |
| 62 | { |
| 63 | return new self($message, [$field => [$message]]); |
| 64 | } |
| 65 | |
| 66 | public function getStatusCode(): int |
| 67 | { |
| 68 | return 422; |
| 69 | } |
| 70 | |
| 71 | public function getTitle(): string |
| 72 | { |
| 73 | return 'Validation Error'; |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * @return array<string, string[]> |
| 78 | */ |
| 79 | public function getErrors(): array |
| 80 | { |
| 81 | return $this->errors; |
| 82 | } |
| 83 | } |