Body   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 67
eloc 148
c 6
b 0
f 0
dl 0
loc 421
rs 3.04

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A matchBool() 0 11 4
A checkPattern() 0 7 2
C matchObjectProperties() 0 62 14
A matchNumber() 0 15 6
A matchArray() 0 13 4
A matchTypes() 0 49 5
A matchString() 0 19 6
D matchSchema() 0 67 20
A matchNull() 0 14 3
A matchFile() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like Body often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Body, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace ByJG\ApiTools\Base;
4
5
use ByJG\ApiTools\Exception\DefinitionNotFoundException;
6
use ByJG\ApiTools\Exception\GenericSwaggerException;
7
use ByJG\ApiTools\Exception\InvalidDefinitionException;
8
use ByJG\ApiTools\Exception\InvalidRequestException;
9
use ByJG\ApiTools\Exception\NotMatchedException;
10
use ByJG\ApiTools\Exception\RequiredArgumentNotFound;
11
12
abstract class Body
13
{
14
    const SWAGGER_PROPERTIES="properties";
15
    const SWAGGER_ADDITIONAL_PROPERTIES="additionalProperties";
16
    const SWAGGER_REQUIRED="required";
17
18
    /**
19
     * @var Schema
20
     */
21
    protected Schema $schema;
22
23
    /**
24
     * @var array
25
     */
26
    protected array $structure;
27
28
    /**
29
     * @var string
30
     */
31
    protected string $name;
32
33
    /**
34
     * OpenApi 2.0 does not describe null values, so this flag defines,
35
     * if match is ok when one of property, which has type, is null
36
     *
37
     * @var bool
38
     */
39
    protected bool $allowNullValues;
40
41
    /**
42
     * Body constructor.
43
     *
44
     * @param Schema $schema
45
     * @param string $name
46
     * @param array $structure
47
     * @param bool $allowNullValues
48
     */
49
    public function __construct(Schema $schema, string $name, array $structure, bool $allowNullValues = false)
50
    {
51
        $this->schema = $schema;
52
        $this->name = $name;
53
        $this->structure = $structure;
54
        $this->allowNullValues = $allowNullValues;
55
    }
56
57
    /**
58
     * @param mixed $body
59
     * @throws DefinitionNotFoundException
60
     * @throws GenericSwaggerException
61
     * @throws InvalidDefinitionException
62
     * @throws InvalidRequestException
63
     * @throws NotMatchedException
64
     * @throws RequiredArgumentNotFound
65
     * @return bool
66
     */
67
    abstract public function match(mixed $body): bool;
68
69
    /**
70
     * @param string $name
71
     * @param array $schemaArray
72
     * @param mixed $body
73
     * @param mixed $type
74
     * @return ?bool
75
     * @throws NotMatchedException
76
     */
77
    protected function matchString(string $name, array $schemaArray, mixed $body, mixed $type): ?bool
78
    {
79
        if ($type !== 'string') {
80
            return null;
81
        }
82
83
        if (isset($schemaArray['enum']) && !in_array($body, $schemaArray['enum'])) {
84
            throw new NotMatchedException("Value '$body' in '$name' not matched in ENUM. ", $this->structure);
85
        }
86
87
        if (isset($schemaArray['pattern'])) {
88
            $this->checkPattern($name, $body, $schemaArray['pattern']);
89
        }
90
91
        if (!is_string($body)) {
92
            throw new NotMatchedException("Value '" . var_export($body, true) . "' in '$name' is not string. ", $this->structure);
93
        }
94
95
        return true;
96
    }
97
98
    /**
99
     * @throws NotMatchedException
100
     */
101
    private function checkPattern(string $name, mixed $body, string $pattern): void
102
    {
103
        $pattern = '/' . rtrim(ltrim($pattern, '/'), '/') . '/';
104
        $isSuccess = (bool) preg_match($pattern, $body);
105
106
        if (!$isSuccess) {
107
            throw new NotMatchedException("Value '$body' in '$name' not matched in pattern. ", $this->structure);
108
        }
109
    }
110
111
    /**
112
     * @param string $name
113
     * @param array $schemaArray
114
     * @param mixed $body
115
     * @param mixed $type
116
     * @return bool|null
117
     */
118
    protected function matchFile(string $name, array $schemaArray, mixed $body, mixed $type): ?bool
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

118
    protected function matchFile(/** @scrutinizer ignore-unused */ string $name, array $schemaArray, mixed $body, mixed $type): ?bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $body is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

118
    protected function matchFile(string $name, array $schemaArray, /** @scrutinizer ignore-unused */ mixed $body, mixed $type): ?bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $schemaArray is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

118
    protected function matchFile(string $name, /** @scrutinizer ignore-unused */ array $schemaArray, mixed $body, mixed $type): ?bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
119
    {
120
        if ($type !== 'file') {
121
            return null;
122
        }
123
124
        return true;
125
    }
126
127
    /**
128
     * @param string $name
129
     * @param array $schemaArray
130
     * @param mixed $body
131
     * @param mixed $type
132
     * @return ?bool
133
     * @throws NotMatchedException
134
     */
135
    protected function matchNumber(string $name, array $schemaArray, mixed $body, mixed $type): ?bool
136
    {
137
        if ($type !== 'integer' && $type !== 'float' && $type !== 'number') {
138
            return null;
139
        }
140
141
        if (!is_numeric($body)) {
142
            throw new NotMatchedException("Expected '$name' to be numeric, but found '$body'. ", $this->structure);
143
        }
144
145
        if (isset($schemaArray['pattern'])) {
146
            $this->checkPattern($name, $body, $schemaArray['pattern']);
147
        }
148
149
        return true;
150
    }
151
152
    /**
153
     * @param string $name
154
     * @param mixed $body
155
     * @param mixed $type
156
     * @return ?bool
157
     * @throws NotMatchedException
158
     */
159
    protected function matchBool(string $name, mixed $body, mixed $type): ?bool
160
    {
161
        if ($type !== 'bool' && $type !== 'boolean') {
162
            return null;
163
        }
164
165
        if (!is_bool($body)) {
166
            throw new NotMatchedException("Expected '$name' to be boolean, but found '$body'. ", $this->structure);
167
        }
168
169
        return true;
170
    }
171
172
    /**
173
     * @param string $name
174
     * @param array $schemaArray
175
     * @param mixed $body
176
     * @param mixed $type
177
     * @return ?bool
178
     * @throws DefinitionNotFoundException
179
     * @throws GenericSwaggerException
180
     * @throws InvalidDefinitionException
181
     * @throws InvalidRequestException
182
     * @throws NotMatchedException
183
     */
184
    protected function matchArray(string $name, array $schemaArray, mixed $body, mixed $type): ?bool
185
    {
186
        if ($type !== 'array') {
187
            return null;
188
        }
189
190
        foreach ((array)$body as $item) {
191
            if (!isset($schemaArray['items'])) {  // If there is no type , there is no test.
192
                continue;
193
            }
194
            $this->matchSchema($name, $schemaArray['items'], $item);
195
        }
196
        return true;
197
    }
198
199
    /**
200
     * @param string $name
201
     * @param mixed $schemaArray
202
     * @param mixed $body
203
     * @return ?bool
204
     */
205
    protected function matchTypes(string $name, mixed $schemaArray, mixed $body): ?bool
206
    {
207
        if (!isset($schemaArray['type'])) {
208
            return null;
209
        }
210
211
        $type = $schemaArray['type'];
212
        $nullable = isset($schemaArray['nullable']) ? (bool)$schemaArray['nullable'] : $this->schema->isAllowNullValues();
213
214
        $validators = [
215
            function () use ($name, $body, $type, $nullable)
216
            {
217
                return $this->matchNull($name, $body, $type, $nullable);
218
            },
219
220
            function () use ($name, $schemaArray, $body, $type)
221
            {
222
                return $this->matchString($name, $schemaArray, $body, $type);
223
            },
224
225
            function () use ($name, $schemaArray, $body, $type)
226
            {
227
                return $this->matchNumber($name, $schemaArray, $body, $type);
228
            },
229
230
            function () use ($name, $body, $type)
231
            {
232
                return $this->matchBool($name, $body, $type);
233
            },
234
235
            function () use ($name, $schemaArray, $body, $type)
236
            {
237
                return $this->matchArray($name, $schemaArray, $body, $type);
238
            },
239
240
            function () use ($name, $schemaArray, $body, $type)
241
            {
242
                return $this->matchFile($name, $schemaArray, $body, $type);
243
            },
244
        ];
245
246
        foreach ($validators as $validator) {
247
            $result = $validator();
248
            if (!is_null($result)) {
249
                return $result;
250
            }
251
        }
252
253
        return null;
254
    }
255
256
    /**
257
     * @param string $name
258
     * @param array $schemaArray
259
     * @param mixed $body
260
     * @return bool|null
261
     * @throws DefinitionNotFoundException
262
     * @throws GenericSwaggerException
263
     * @throws InvalidDefinitionException
264
     * @throws InvalidRequestException
265
     * @throws NotMatchedException
266
     */
267
    public function matchObjectProperties(string $name, mixed $schemaArray, mixed $body): ?bool
268
    {
269
        if (isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES]) && !isset($schemaArray[self::SWAGGER_PROPERTIES])) {
270
            $schemaArray[self::SWAGGER_PROPERTIES] = [];
271
        }
272
273
        if (!isset($schemaArray[self::SWAGGER_PROPERTIES])) {
274
            return null;
275
        }
276
277
        if (!is_array($body)) {
278
            throw new InvalidRequestException(
279
                "I expected an array here, but I got an string. Maybe you did wrong request?",
280
                $body
281
            );
282
        }
283
284
        if (!isset($schemaArray[self::SWAGGER_REQUIRED])) {
285
            $schemaArray[self::SWAGGER_REQUIRED] = [];
286
        }
287
        foreach ($schemaArray[self::SWAGGER_PROPERTIES] as $prop => $def) {
288
            $required = array_search($prop, $schemaArray[self::SWAGGER_REQUIRED]);
289
290
            if (!array_key_exists($prop, $body)) {
291
                if ($required !== false) {
292
                    throw new NotMatchedException("Required property '$prop' in '$name' not found in object");
293
                }
294
                unset($body[$prop]);
295
                continue;
296
            }
297
298
            $this->matchSchema($prop, $def, $body[$prop]);
299
            unset($schemaArray[self::SWAGGER_PROPERTIES][$prop]);
300
            if ($required !== false) {
301
                unset($schemaArray[self::SWAGGER_REQUIRED][$required]);
302
            }
303
            unset($body[$prop]);
304
        }
305
306
        if (count($schemaArray[self::SWAGGER_REQUIRED]) > 0) {
307
            throw new NotMatchedException(
308
                "The required property(ies) '"
309
                . implode(', ', $schemaArray[self::SWAGGER_REQUIRED])
310
                . "' does not exists in the body.",
311
                $this->structure
312
            );
313
        }
314
315
        if (count($body) > 0 && !isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES])) {
316
            throw new NotMatchedException(
317
                "The property(ies) '"
318
                . implode(', ', array_keys($body))
319
                . "' has not defined in '$name'",
320
                $body
321
            );
322
        }
323
324
        foreach ($body as $name => $prop) {
325
            $def = $schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES];
326
            $this->matchSchema($name, $def, $prop);
327
        }
328
        return true;
329
    }
330
331
    /**
332
     * @param string $name
333
     * @param array $schemaArray
334
     * @param array $body
335
     * @return ?bool
336
     * @throws DefinitionNotFoundException
337
     * @throws InvalidDefinitionException
338
     * @throws GenericSwaggerException
339
     * @throws InvalidRequestException
340
     * @throws NotMatchedException
341
     */
342
    protected function matchSchema(string $name, mixed $schemaArray, mixed $body): ?bool
343
    {
344
        // Match Single Types
345
        if ($this->matchTypes($name, $schemaArray, $body)) {
346
            return true;
347
        }
348
349
        if(!isset($schemaArray['$ref']) && isset($schemaArray['content'])) {
350
            $schemaArray = $schemaArray['content'][key($schemaArray['content'])]['schema'];
351
        }
352
353
        // Get References and try to match it again
354
        if (isset($schemaArray['$ref']) && !is_array($schemaArray['$ref'])) {
355
            $definition = $this->schema->getDefinition($schemaArray['$ref']);
356
            return $this->matchSchema($schemaArray['$ref'], $definition, $body);
357
        }
358
359
        // Match object properties
360
        if ($this->matchObjectProperties($name, $schemaArray, $body)) {
361
            return true;
362
        }
363
364
        if (isset($schemaArray['allOf'])) {
365
            $allOfSchemas = $schemaArray['allOf'];
366
            foreach ($allOfSchemas as &$schema) {
367
                if (isset($schema['$ref'])) {
368
                    $schema = $this->schema->getDefinition($schema['$ref']);
369
                }
370
            }
371
            unset($schema);
372
            $mergedSchema = array_merge_recursive(...$allOfSchemas);
373
            return $this->matchSchema($name, $mergedSchema, $body);
374
        }
375
376
        if (isset($schemaArray['oneOf'])) {
377
            $matched = false;
378
            $catchException = null;
379
            foreach ($schemaArray['oneOf'] as $schema) {
380
                try {
381
                    $matched = $matched || $this->matchSchema($name, $schema, $body);
382
                } catch (NotMatchedException $exception) {
383
                    $catchException = $exception;
384
                }
385
            }
386
            if ($catchException !== null && $matched === false) {
387
                throw $catchException;
388
            }
389
390
            return $matched;
391
        }
392
393
        /**
394
         * OpenApi 2.0 does not describe ANY object value
395
         * But there is hack that makes ANY object possible, described in link below
396
         * To make that hack works, we need such condition
397
         * @link https://stackoverflow.com/questions/32841298/swagger-2-0-what-schema-to-accept-any-complex-json-value
398
         */
399
        if ($schemaArray === []) {
400
            return true;
401
        }
402
403
        // Match any object
404
        if (count($schemaArray) === 1 && isset($schemaArray['type']) && $schemaArray['type'] === 'object') {
405
            return true;
406
        }
407
408
        throw new GenericSwaggerException("Not all cases are defined. Please open an issue about this. Schema: $name");
409
    }
410
411
    /**
412
     * @param string $name
413
     * @param mixed $body
414
     * @param mixed $type
415
     * @param bool $nullable
416
     * @return ?bool
417
     * @throws NotMatchedException
418
     */
419
    protected function matchNull(string $name, mixed $body, mixed $type, bool $nullable): ?bool
420
    {
421
        if (!is_null($body)) {
422
            return null;
423
        }
424
425
        if (!$nullable) {
426
            throw new NotMatchedException(
427
                "Value of property '$name' is null, but should be of type '$type'",
428
                $this->structure
429
            );
430
        }
431
432
        return true;
433
    }
434
}
435