Test Setup Failed
Pull Request — master (#67)
by
unknown
02:32
created

Body   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 439
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 71
lcom 1
cbo 6
dl 0
loc 439
rs 2.7199
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
match() 0 1 ?
A __construct() 0 10 2
A getInstance() 0 12 3
A matchString() 0 16 5
A checkPattern() 0 8 2
A matchFile() 0 8 2
A matchNumber() 0 16 6
A matchBool() 0 12 4
A matchArray() 0 14 4
B matchTypes() 0 50 5
F matchObjectProperties() 0 75 17
D matchSchema() 0 66 18
A matchNull() 0 15 3

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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\OpenApi\OpenApiResponseBody;
11
use ByJG\ApiTools\OpenApi\OpenApiSchema;
12
use ByJG\ApiTools\Swagger\SwaggerResponseBody;
13
use ByJG\ApiTools\Swagger\SwaggerSchema;
14
use InvalidArgumentException;
15
16
abstract class Body
17
{
18
    const SWAGGER_PROPERTIES="properties";
19
    const SWAGGER_ADDITIONAL_PROPERTIES="additionalProperties";
20
    const SWAGGER_REQUIRED="required";
21
22
    /**
23
     * @var Schema
24
     */
25
    protected $schema;
26
27
    /**
28
     * @var array
29
     */
30
    protected $structure;
31
32
    /**
33
     * @var string
34
     */
35
    protected $name;
36
37
    /**
38
     * OpenApi 2.0 does not describe null values, so this flag defines,
39
     * if match is ok when one of property, which has type, is null
40
     *
41
     * @var bool
42
     */
43
    protected $allowNullValues;
44
45
    /**
46
     * Body constructor.
47
     *
48
     * @param Schema $schema
49
     * @param string $name
50
     * @param array $structure
51
     * @param bool $allowNullValues
52
     */
53
    public function __construct(Schema $schema, $name, $structure, $allowNullValues = false)
54
    {
55
        $this->schema = $schema;
56
        $this->name = $name;
57
        if (!is_array($structure)) {
58
            throw new InvalidArgumentException('I expected the structure to be an array');
59
        }
60
        $this->structure = $structure;
61
        $this->allowNullValues = $allowNullValues;
62
    }
63
64
    /**
65
     * @param Schema $schema
66
     * @param string $name
67
     * @param array $structure
68
     * @param bool $allowNullValues
69
     * @return OpenApiResponseBody|SwaggerResponseBody
70
     * @throws GenericSwaggerException
71
     */
72
    public static function getInstance(Schema $schema, $name, $structure, $allowNullValues = false)
73
    {
74
        if ($schema instanceof SwaggerSchema) {
75
            return new SwaggerResponseBody($schema, $name, $structure, $allowNullValues);
76
        }
77
78
        if ($schema instanceof OpenApiSchema) {
79
            return new OpenApiResponseBody($schema, $name, $structure, $allowNullValues);
80
        }
81
82
        throw new GenericSwaggerException("Cannot get instance SwaggerBody or SchemaBody from " . get_class($schema));
83
    }
84
85
    abstract public function match($body);
86
87
    /**
88
     * @param string $name
89
     * @param array $schemaArray
90
     * @param string $body
91
     * @param string $type
92
     * @return bool
93
     * @throws NotMatchedException
94
     */
95
    protected function matchString($name, $schemaArray, $body, $type)
96
    {
97
        if ($type !== 'string') {
98
            return null;
99
        }
100
101
        if (isset($schemaArray['enum']) && !in_array($body, $schemaArray['enum'])) {
102
            throw new NotMatchedException("Value '$body' in '$name' not matched in ENUM. ", $this->structure);
103
        }
104
105
        if (isset($schemaArray['pattern'])) {
106
            $this->checkPattern($name, $body, $schemaArray['pattern']);
107
        }
108
109
        return true;
110
    }
111
112
    private function checkPattern($name, $body, $pattern)
113
    {
114
        $isSuccess = (bool) preg_match($pattern, $body, $matches);
115
116
        if (!$isSuccess) {
117
            throw new NotMatchedException("Value '$body' in '$name' not matched in pattern. ", $this->structure);
118
        }
119
    }
120
121
122
    /**
123
     * @param string $name
124
     * @param array $schemaArray
125
     * @param string $body
126
     * @param string $type
127
     * @return bool
128
     */
129
    protected function matchFile($name, $schemaArray, $body, $type)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from 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.

This check looks from 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.

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

Loading history...
130
    {
131
        if ($type !== 'file') {
132
            return null;
133
        }
134
135
        return true;
136
    }
137
138
    /**
139
     * @param string $name
140
     * @param string $body
141
     * @param string $type
142
     * @return bool
143
     * @throws NotMatchedException
144
     */
145
    protected function matchNumber($name, $body, $type)
146
    {
147
        if ($type !== 'integer' && $type !== 'float' && $type !== 'number') {
148
            return null;
149
        }
150
151
        if (!is_numeric($body)) {
152
            throw new NotMatchedException("Expected '$name' to be numeric, but found '$body'. ", $this->structure);
153
        }
154
155
        if (isset($schemaArray['pattern'])) {
0 ignored issues
show
Bug introduced by
The variable $schemaArray seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
156
            $this->checkPattern($name, $body, $schemaArray['pattern']);
157
        }
158
159
        return true;
160
    }
161
162
    /**
163
     * @param string $name
164
     * @param string $body
165
     * @param string $type
166
     * @return bool
167
     * @throws NotMatchedException
168
     */
169
    protected function matchBool($name, $body, $type)
170
    {
171
        if ($type !== 'bool' && $type !== 'boolean') {
172
            return null;
173
        }
174
175
        if (!is_bool($body)) {
176
            throw new NotMatchedException("Expected '$name' to be boolean, but found '$body'. ", $this->structure);
177
        }
178
179
        return true;
180
    }
181
182
    /**
183
     * @param string $name
184
     * @param array $schemaArray
185
     * @param string $body
186
     * @param string $type
187
     * @return bool
188
     * @throws DefinitionNotFoundException
189
     * @throws GenericSwaggerException
190
     * @throws InvalidDefinitionException
191
     * @throws InvalidRequestException
192
     * @throws NotMatchedException
193
     */
194
    protected function matchArray($name, $schemaArray, $body, $type)
195
    {
196
        if ($type !== 'array') {
197
            return null;
198
        }
199
200
        foreach ((array)$body as $item) {
201
            if (!isset($schemaArray['items'])) {  // If there is no type , there is no test.
202
                continue;
203
            }
204
            $this->matchSchema($name, $schemaArray['items'], $item);
205
        }
206
        return true;
207
    }
208
209
    /**
210
     * @param string $name
211
     * @param array $schemaArray
212
     * @param string $body
213
     * @return mixed|null
214
     */
215
    protected function matchTypes($name, $schemaArray, $body)
216
    {
217
        if (!isset($schemaArray['type'])) {
218
            return null;
219
        }
220
221
        $type = $schemaArray['type'];
222
        $nullable = isset($schemaArray['nullable']) ? (bool)$schemaArray['nullable'] : $this->schema->isAllowNullValues();
223
224
        $validators = [
225
            function () use ($name, $body, $type, $nullable)
226
            {
227
                return $this->matchNull($name, $body, $type, $nullable);
228
            },
229
230
            function () use ($name, $schemaArray, $body, $type)
231
            {
232
                return $this->matchString($name, $schemaArray, $body, $type);
233
            },
234
235
            function () use ($name, $body, $type)
236
            {
237
                return $this->matchNumber($name, $body, $type);
238
            },
239
240
            function () use ($name, $body, $type)
241
            {
242
                return $this->matchBool($name, $body, $type);
243
            },
244
245
            function () use ($name, $schemaArray, $body, $type)
246
            {
247
                return $this->matchArray($name, $schemaArray, $body, $type);
248
            },
249
250
            function () use ($name, $schemaArray, $body, $type)
251
            {
252
                return $this->matchFile($name, $schemaArray, $body, $type);
253
            },
254
        ];
255
256
        foreach ($validators as $validator) {
257
            $result = $validator();
258
            if (!is_null($result)) {
259
                return $result;
260
            }
261
        }
262
263
        return null;
264
    }
265
266
    /**
267
     * @param string $name
268
     * @param array $schemaArray
269
     * @param string $body
270
     * @return bool|null
271
     * @throws DefinitionNotFoundException
272
     * @throws GenericSwaggerException
273
     * @throws InvalidDefinitionException
274
     * @throws InvalidRequestException
275
     * @throws NotMatchedException
276
     */
277
    public function matchObjectProperties($name, $schemaArray, $body)
278
    {
279
        if (!isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES])) {
280
            $schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES] = true;
281
        }
282
283
        if (is_bool($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES])) {
284
            if ($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES]) {
285
                $schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES] = [];
286
            } else {
287
                unset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES]);
288
            }
289
        }
290
291
        if (isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES]) && !isset($schemaArray[self::SWAGGER_PROPERTIES])) {
292
            $schemaArray[self::SWAGGER_PROPERTIES] = [];
293
        }
294
295
        if (!isset($schemaArray[self::SWAGGER_PROPERTIES])) {
296
            return null;
297
        }
298
299
        if (!is_array($body)) {
300
            throw new InvalidRequestException(
301
                "I expected an array here, but I got an string. Maybe you did wrong request?",
302
                $body
0 ignored issues
show
Documentation introduced by
$body is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
303
            );
304
        }
305
306
        if (!isset($schemaArray[self::SWAGGER_REQUIRED])) {
307
            $schemaArray[self::SWAGGER_REQUIRED] = [];
308
        }
309
        foreach ($schemaArray[self::SWAGGER_PROPERTIES] as $prop => $def) {
310
            $required = array_search($prop, $schemaArray[self::SWAGGER_REQUIRED]);
311
312
            if (!array_key_exists($prop, $body)) {
313
                if ($required !== false) {
314
                    throw new NotMatchedException("Required property '$prop' in '$name' not found in object");
315
                }
316
                unset($body[$prop]);
317
                continue;
318
            }
319
320
            $this->matchSchema($prop, $def, $body[$prop]);
321
            unset($schemaArray[self::SWAGGER_PROPERTIES][$prop]);
322
            if ($required !== false) {
323
                unset($schemaArray[self::SWAGGER_REQUIRED][$required]);
324
            }
325
            unset($body[$prop]);
326
        }
327
328
        if (count($schemaArray[self::SWAGGER_REQUIRED]) > 0) {
329
            throw new NotMatchedException(
330
                "The required property(ies) '"
331
                . implode(', ', $schemaArray[self::SWAGGER_REQUIRED])
332
                . "' does not exists in the body.",
333
                $this->structure
334
            );
335
        }
336
337
        if (count($body) > 0 && !isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES])) {
338
            throw new NotMatchedException(
339
                "The property(ies) '"
340
                . implode(', ', array_keys($body))
341
                . "' has not defined in '$name'",
342
                $body
343
            );
344
        }
345
346
        foreach ($body as $name => $prop) {
347
            $def = $schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES];
348
            $this->matchSchema($name, $def, $prop);
349
        }
350
        return true;
351
    }
352
353
    /**
354
     * @param string $name
355
     * @param array $schemaArray
356
     * @param array|string $body
357
     * @return bool
358
     * @throws DefinitionNotFoundException
359
     * @throws InvalidDefinitionException
360
     * @throws GenericSwaggerException
361
     * @throws InvalidRequestException
362
     * @throws NotMatchedException
363
     */
364
    protected function matchSchema($name, $schemaArray, $body)
365
    {
366
        if ($schemaArray === []) {
367
            return true;
368
        }
369
370
        // Match Single Types
371
        if ($this->matchTypes($name, $schemaArray, $body)) {
0 ignored issues
show
Bug introduced by
It seems like $body defined by parameter $body on line 364 can also be of type array; however, ByJG\ApiTools\Base\Body::matchTypes() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
372
            return true;
373
        }
374
375
        if(!isset($schemaArray['$ref']) && isset($schemaArray['content'])) {
376
            $schemaArray['$ref'] = $schemaArray['content'][key($schemaArray['content'])]['schema']['$ref'];
377
        }
378
379
        // Get References and try to match it again
380
        if (isset($schemaArray['$ref']) && !is_array($schemaArray['$ref'])) {
381
            $definition = $this->schema->getDefinition($schemaArray['$ref']);
382
            return $this->matchSchema($schemaArray['$ref'], $definition, $body);
383
        }
384
385
        // Match object properties
386
        if ($this->matchObjectProperties($name, $schemaArray, $body)) {
0 ignored issues
show
Bug introduced by
It seems like $body defined by parameter $body on line 364 can also be of type array; however, ByJG\ApiTools\Base\Body::matchObjectProperties() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
387
            return true;
388
        }
389
390
        if (isset($schemaArray['allOf'])) {
391
            $allOfSchemas = $schemaArray['allOf'];
392
            foreach ($allOfSchemas as $schema) {
393
                if (!$this->matchSchema($name, $schema, $body)) {
394
                    return false;
395
                }
396
            }
397
398
            return true;
399
        }
400
401
        if (isset($schemaArray['oneOf'])) {
402
            $matched = false;
403
            $catchedException = null;
404
            foreach ($schemaArray['oneOf'] as $schema) {
405
                try {
406
                    $matched = $matched || $this->matchSchema($name, $schema, $body);
407
                } catch (NotMatchedException $exception) {
408
                    $catchedException = $exception;
409
                }
410
            }
411
            if ($catchedException !== null && $matched === false) {
412
                throw $catchedException;
413
            }
414
415
            return $matched;
416
        }
417
418
        /**
419
         * OpenApi 2.0 does not describe ANY object value
420
         * But there is hack that makes ANY object possible, described in link below
421
         * To make that hack works, we need such condition
422
         * @link https://stackoverflow.com/questions/32841298/swagger-2-0-what-schema-to-accept-any-complex-json-value
423
         */
424
        if ($schemaArray === []) {
425
            return true;
426
        }
427
428
        throw new GenericSwaggerException("Not all cases are defined. Please open an issue about this. Schema: $name");
429
    }
430
431
    /**
432
     * @param string $name
433
     * @param string $body
434
     * @param string $type
435
     * @param bool $nullable
436
     * @return bool
437
     * @throws NotMatchedException
438
     */
439
    protected function matchNull($name, $body, $type, $nullable)
440
    {
441
        if (!is_null($body)) {
442
            return null;
443
        }
444
445
        if (!$nullable) {
446
            throw new NotMatchedException(
447
                "Value of property '$name' is null, but should be of type '$type'",
448
                $this->structure
449
            );
450
        }
451
452
        return true;
453
    }
454
}
455