Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.67% covered (warning)
66.67%
4 / 6
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ValidationException
66.67% covered (warning)
66.67%
4 / 6
60.00% covered (warning)
60.00%
3 / 5
5.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 forField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatusCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrors
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Exception;
6
7use DomainException;
8use 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 */
35final 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}