RequestWithValidation   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 259
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 66
c 1
b 0
f 0
dl 0
loc 259
rs 9.6
wmc 35

23 Methods

Rating   Name   Duplication   Size   Complexity  
A valueIsArray() 0 4 2
A normalizeRequest() 0 5 1
A getErrors() 0 3 1
A addToErrors() 0 22 5
A validated() 0 11 3
A validate() 0 8 3
A getPreparedRequest() 0 7 1
A hasError() 0 3 1
A valueIs2DArray() 0 3 1
A addScalarParameterValueError() 0 3 1
A handleValidationProcess() 0 15 3
A getKeyOfFlatArrayValue() 0 3 1
A hasFormData() 0 3 1
A valueIsScalarType() 0 3 1
A getRequestData() 0 5 2
A addFlatArrayParameterValueError() 0 4 1
A valueIsFlatArray() 0 3 1
A add2DArrayParameterValueError() 0 4 1
A __construct() 0 15 1
A getValidatorInstance() 0 3 1
A filterRequest() 0 5 1
A getKeysOf2DArrayValue() 0 3 1
A addToValidated() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MojtabaGheytasi\RequestValidatorBundle\Request;
6
7
use MojtabaGheytasi\RequestValidatorBundle\Contract\FailedValidationInterface;
8
use Symfony\Component\HttpFoundation\Request;
9
use Symfony\Component\Validator\ConstraintViolation;
10
use Symfony\Component\Validator\ConstraintViolationListInterface;
11
use Symfony\Component\Validator\Validation;
12
use Symfony\Component\Validator\Validator\ValidatorInterface;
13
14
/**
15
 * @author Mojtaba Gheytasi <[email protected]>
16
 */
17
abstract class RequestWithValidation extends Request
18
{
19
    protected array $constraints = [];
20
21
    protected array $validated = [];
22
23
    protected array $errors = [];
24
25
    private ?FailedValidationInterface $failedValidation;
26
27
    protected ValidatorInterface $validator;
28
29
    /**
30
     * {@inheritDoc}
31
     */
32
    public function __construct(
33
        FailedValidationInterface $failedValidation = null,
34
        array $query = [],
35
        array $request = [],
36
        array $attributes = [],
37
        array $cookies = [],
38
        array $files = [],
39
        array $server = [],
40
        $content = null
41
    ) {
42
        parent::__construct($query, $request, $attributes, $cookies, $files, $server, $content);
43
44
        $this->failedValidation = $failedValidation;
45
46
        $this->handleValidationProcess();
47
    }
48
49
    /**
50
     * Get the validated data from the request.
51
     *
52
     * @return array|mixed
53
     */
54
    public function validated(string $key = null)
55
    {
56
        if (null === $key) {
57
            return $this->validated;
58
        }
59
60
        if (\array_key_exists($key, $this->validated)) {
61
            return $this->validated[$key];
62
        }
63
64
        throw new \InvalidArgumentException();
65
    }
66
67
    /**
68
     * Returns true if the request has validation errors, otherwise false.
69
     */
70
    public function hasError(): bool
71
    {
72
        return ! empty($this->errors);
73
    }
74
75
    /**
76
     * Return request validation errors.
77
     */
78
    public function getErrors(): array
79
    {
80
        return $this->errors;
81
    }
82
83
    private function handleValidationProcess()
84
    {
85
        $this->validator = $this->getValidatorInstance();
86
87
        $this->constraints = $this->constraints();
88
89
        $request = $this->getPreparedRequest();
90
91
        $this->validate($request);
92
93
        if (
94
            $this->hasError() &&
95
            $this->failedValidation instanceof FailedValidationInterface
96
        ) {
97
            $this->failedValidation->onFailedValidation($this->getErrors());
98
        }
99
    }
100
101
    /**
102
     * Returns normalized and filtered current request.
103
     */
104
    private function getPreparedRequest(): array
105
    {
106
        $request = $this->getRequestData();
107
108
        $normalizedRequest = $this->normalizeRequest($request, $this->constraints);
109
110
        return $this->filterRequest($normalizedRequest);
111
    }
112
113
    /**
114
     * Validate the given request with the constraints.
115
     *
116
     * @param $request
117
     */
118
    private function validate($request): void
119
    {
120
        foreach ($request as $key => $value) {
121
            $violations = $this->validator->validate($value, $this->constraints[$key]);
122
123
            0 === $violations->count() ?
124
                $this->addToValidated($key, $value) :
125
                $this->addToErrors($key, $violations);
126
        }
127
    }
128
129
    /**
130
     * Get the validator instance for the request.
131
     */
132
    private function getValidatorInstance(): ValidatorInterface
133
    {
134
        return Validation::createValidator();
135
    }
136
137
    /**
138
     * Ignore the fields that are not provided in constraints and return filtered request.
139
     *
140
     * @param $request
141
     */
142
    private function filterRequest($request): array
143
    {
144
        return array_filter($request, function ($parameter) {
145
            return \array_key_exists($parameter, $this->constraints);
146
        }, \ARRAY_FILTER_USE_KEY);
147
    }
148
149
    /**
150
     * This method will merge missing constraints (from request) with current
151
     * request for further validation. This way validation will be based
152
     * on constraints and all constraints will be considered.
153
     */
154
    private function normalizeRequest(array $request, array $constraints): array
155
    {
156
        $constraints = array_fill_keys(array_keys($constraints), null);
157
158
        return array_merge($constraints, $request);
159
    }
160
161
    private function addToErrors(string $field, ConstraintViolationListInterface $violations): void
162
    {
163
        foreach ($violations as $violation) {
164
            if ($this->valueIsScalarType($violation)) {
165
                $this->addScalarParameterValueError($field, $violation);
166
167
                continue;
168
            }
169
170
            if ($this->valueIsFlatArray($violation)) {
171
                $this->addFlatArrayParameterValueError($field, $violation);
172
173
                continue;
174
            }
175
176
            if ($this->valueIs2DArray($violation)) {
177
                $this->add2DArrayParameterValueError($field, $violation);
178
179
                continue;
180
            }
181
182
            throw new \InvalidArgumentException();
183
        }
184
    }
185
186
    private function addScalarParameterValueError(string $field, ConstraintViolation $violation)
187
    {
188
        $this->errors[$field][] = $violation->getMessage();
189
    }
190
191
    private function addFlatArrayParameterValueError(string $field, ConstraintViolation $violation)
192
    {
193
        $key = $this->getKeyOfFlatArrayValue($violation->getPropertyPath());
194
        $this->errors[$field][$key][] = $violation->getMessage();
195
    }
196
197
    private function add2DArrayParameterValueError(string $field, ConstraintViolation $violation)
198
    {
199
        [$parentKey, $key] = $this->getKeysOf2DArrayValue($violation->getPropertyPath());
200
        $this->errors[$field][$parentKey][$key][] = $violation->getMessage();
201
    }
202
203
    /**
204
     * @param $value
205
     */
206
    private function addToValidated(string $parameter, $value): void
207
    {
208
        $this->validated[$parameter] = $value;
209
    }
210
211
    private function getKeyOfFlatArrayValue(string $name): string
212
    {
213
        return substr($name, 1, -1);
214
    }
215
216
    private function getKeysOf2DArrayValue(string $name): array
217
    {
218
        return explode('][', substr($name, 1, -1));
219
    }
220
221
    /**
222
     * Returns true if provided value of parameter is an scalar, otherwise false.
223
     */
224
    private function valueIsScalarType(ConstraintViolation $violation): bool
225
    {
226
        return '' === $violation->getPropertyPath();
227
    }
228
229
    /**
230
     * Returns true if provided value of parameter is an array, otherwise false.
231
     */
232
    private function valueIsArray(ConstraintViolation $violation, int $dimension): bool
233
    {
234
        return '' !== $violation->getPropertyPath() &&
235
            $dimension === substr_count($violation->getPropertyPath(), '[');
236
    }
237
238
    /**
239
     * Returns true if provided value of parameter is a flat array, otherwise false.
240
     */
241
    private function valueIsFlatArray(ConstraintViolation $violation): bool
242
    {
243
        return $this->valueIsArray($violation, 1);
244
    }
245
246
    /**
247
     * Returns true if provided value of parameter is a two dimensional (2D) array, otherwise false.
248
     */
249
    private function valueIs2DArray(ConstraintViolation $violation): bool
250
    {
251
        return $this->valueIsArray($violation, 2);
252
    }
253
254
    /**
255
     * Indicates whether current request has a form.
256
     */
257
    private function hasFormData(): bool
258
    {
259
        return ! \in_array($this->getRealMethod(), [self::METHOD_GET, self::METHOD_HEAD]);
260
    }
261
262
    /**
263
     * Get data to be validated from the request.
264
     */
265
    private function getRequestData(): array
266
    {
267
        return $this->hasFormData() ?
268
            array_merge($this->request->all(), $this->query->all()) :
269
            $this->query->all();
270
    }
271
272
    /**
273
     * Get the validation constraints that apply to the request.
274
     */
275
    abstract protected function constraints(): array;
276
}
277