Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
90.00% covered (success)
90.00%
54 / 60
88.76% covered (warning)
88.76%
79 / 89
32.99% covered (danger)
32.99%
32 / 97
68.75% covered (warning)
68.75%
11 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Validator
90.00% covered (success)
90.00%
54 / 60
88.76% covered (warning)
88.76%
79 / 89
32.99% covered (danger)
32.99%
32 / 97
81.25% covered (warning)
81.25%
13 / 16
682.71
0.00% covered (danger)
0.00%
0 / 1
 getValidationErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 notEmpty
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
13 / 13
11.11% covered (danger)
11.11%
4 / 36
100.00% covered (success)
100.00%
1 / 1
41.41
 validate
50.00% covered (danger)
50.00%
1 / 2
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 requireNotNull
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 requireNotEmpty
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireValidEmail
100.00% covered (success)
100.00%
4 / 4
87.50% covered (warning)
87.50%
7 / 8
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
5.67
 requireValidUrl
83.33% covered (warning)
83.33%
10 / 12
80.00% covered (warning)
80.00%
16 / 20
20.00% covered (danger)
20.00%
4 / 20
0.00% covered (danger)
0.00%
0 / 1
32.09
 requireMinLength
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
7 / 7
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
5.02
 requireMaxLength
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
7 / 7
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
5.02
 requireMin
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
5 / 5
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 requireMinIfNotNull
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 requireMax
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
5 / 5
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 requireMaxIfNotNull
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 requireValidUuidV4
100.00% covered (success)
100.00%
3 / 3
83.33% covered (warning)
83.33%
5 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
2.50
 require
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Shared\Domain\Validation;
4
5use App\Shared\Domain\Exception\BaseErrorCode;
6use App\Shared\Domain\Exception\ErrorCode;
7use App\Shared\Domain\Exception\ValidationException;
8use App\Shared\Domain\Model\EntityId;
9
10/**
11 * @author Wilhelm Zwertvaegher
12 */
13
14/**
15 * @author Wilhelm Zwertvaegher
16 */
17
18class Validator
19{
20
21    public ValidationErrors $validationErrors;
22
23    /**
24     * @return ValidationErrors
25     */
26    public function getValidationErrors(): ValidationErrors
27    {
28        return $this->validationErrors;
29    }
30
31    /**
32     *
33     */
34    public function __construct()
35    {
36        $this->validationErrors = new ValidationErrors();
37    }
38
39    /**
40     * @param string $fieldName
41     * @param mixed $fieldValue
42     * @return bool
43     */
44    private function notEmpty(string $fieldName, mixed $fieldValue): bool
45    {
46        if ($fieldValue === null
47            || $fieldValue === ''
48            || $fieldValue instanceof EntityId && $fieldValue->value() === '    '
49            || is_array($fieldValue) && count($fieldValue) === 0
50        ) {
51            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::FIELD_CANNOT_BE_EMPTY, ['empty' => 'Field cannot be empty']));
52            return false;
53        }
54        return true;
55    }
56
57    /**
58     * @throws ValidationException
59     */
60    public function validate(): void
61    {
62        if ($this->validationErrors->hasErrors()) {
63            throw new ValidationException($this->validationErrors);
64        }
65    }
66
67    /**
68     * @param string $fieldName
69     * @param mixed $fieldValue
70     * @return $this
71     */
72    public function requireNotNull(string $fieldName, mixed $fieldValue): self
73    {
74        if (null === $fieldValue) {
75            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::FIELD_CANNOT_BE_NULL));
76        }
77        return $this;
78    }
79
80    /**
81     * @param string $fieldName
82     * @param mixed $fieldValue
83     * @return $this
84     */
85    public function requireNotEmpty(string $fieldName, mixed $fieldValue): self
86    {
87        $this->notEmpty($fieldName, $fieldValue);
88        return $this;
89    }
90
91    /**
92     * @param string $fieldName
93     * @param string $fieldValue
94     * @return $this
95     */
96    public function requireValidEmail(string $fieldName, string $fieldValue): self
97    {
98        $pattern = "/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/";
99        if ($this->notEmpty($fieldName, $fieldValue) && !preg_match($pattern, $fieldValue)) {
100            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::INVALID_EMAIL, ['invalid' => sprintf('%s is not a valid email', $fieldValue)]));
101        }
102        return $this;
103    }
104
105    /**
106     * @param string $fieldName
107     * @param string $fieldValue
108     * @return $this
109     */
110    public function requireValidUrl(string $fieldName, string $fieldValue): self
111    {
112        try {
113            $parts = parse_url($fieldValue);
114            if (!is_array($parts)) {
115                throw new \Exception('Malformed URL');
116            }
117
118            if (empty($parts['scheme']) || !str_starts_with($parts['scheme'], 'http')) {
119                throw new \Exception('URL must start with http(s).');
120            }
121
122            if (empty($parts['host'])) {
123                throw new \Exception('URL must contain a host.');
124            }
125
126            if (!str_contains($parts['host'], '.')) {
127                throw new \Exception('URL must contain a valid host.');
128            }
129
130        } catch (\Exception $e) {
131            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::INVALID_URL, ['invalid' => $e->getMessage()]));
132        }
133
134        return $this;
135    }
136
137    /**
138     * @param string $fieldName
139     * @param string $fieldValue
140     * @param int $minLength
141     * @return $this
142     */
143    public function requireMinLength(string $fieldName, string $fieldValue, int $minLength): self
144    {
145        if ($minLength < 1) {
146            throw new \InvalidArgumentException("minLength must be greater than 0");
147        }
148        if ($this->notEmpty($fieldName, $fieldValue) && strlen($fieldValue) < $minLength) {
149            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::FIELD_VALUE_TOO_SHORT));
150        }
151        return $this;
152    }
153
154    /**
155     * @param string $fieldName
156     * @param string $fieldValue
157     * @param int $maxLength
158     * @return $this
159     */
160    public function requireMaxLength(string $fieldName, string $fieldValue, int $maxLength): self
161    {
162        if ($maxLength < 1) {
163            throw new \InvalidArgumentException("maxLength must be greater than 0");
164        }
165        if ($this->notEmpty($fieldName, $fieldValue) && strlen($fieldValue) > $maxLength) {
166            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::FIELD_VALUE_TOO_LONG));
167        }
168        return $this;
169    }
170
171    /**
172     * @param string $fieldName
173     * @param int $fieldValue
174     * @param int $minValue
175     * @return $this
176     */
177    public function requireMin(string $fieldName, int $fieldValue, int $minValue): self
178    {
179        if ($this->notEmpty($fieldName, $fieldValue) && $fieldValue < $minValue) {
180            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::FIELD_VALUE_TOO_SMALL, ["min" => $minValue]));
181        }
182        return $this;
183    }
184
185    /**
186     * @param string $fieldName
187     * @param int $fieldValue
188     * @param int $minValue
189     * @return $this
190     */
191    public function requireMinIfNotNull(string $fieldName, int $fieldValue, int $minValue): self
192    {
193        if ($fieldValue < $minValue) {
194            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::FIELD_VALUE_TOO_SMALL, ["min" => $minValue]));
195        }
196        return $this;
197    }
198
199    /**
200     * @param string $fieldName
201     * @param int $fieldValue
202     * @param int $maxValue
203     * @return $this
204     */
205    public function requireMax(string $fieldName, int $fieldValue, int $maxValue): self
206    {
207        if ($this->notEmpty($fieldName, $fieldValue) && $fieldValue > $maxValue) {
208            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::FIELD_VALUE_TOO_BIG, ["max" => $maxValue]));
209        }
210        return $this;
211    }
212
213    /**
214     * @param string $fieldName
215     * @param int $fieldValue
216     * @param int $maxValue
217     * @return $this
218     */
219    public function requireMaxIfNotNull(string $fieldName, int $fieldValue, int $maxValue): self
220    {
221        if ($fieldValue > $maxValue) {
222            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::FIELD_VALUE_TOO_BIG, ["max" => $maxValue]));
223        }
224        return $this;
225    }
226
227    public function requireValidUuidV4(string $fieldName, string $fieldValue): self
228    {
229        if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $fieldValue)) {
230            $this->validationErrors->add(new ValidationError($fieldName, ErrorCode::INVALID_UUID));
231        }
232
233        return $this;
234    }
235
236    /**
237     * @param string $fieldName
238     * @param callable():bool $supplier
239     * @param BaseErrorCode $errorCode
240     * @return $this
241     */
242    public function require(string $fieldName, callable $supplier, BaseErrorCode $errorCode): self
243    {
244        if (true !== $supplier()) {
245            $this->validationErrors->add(new ValidationError($fieldName, $errorCode));
246        }
247        return $this;
248    }
249}