RequestWithValidation::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 8
dl 0
loc 15
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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