Passed
Pull Request — master (#67)
by Joao
07:03
created

Body::matchTypes()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 49
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 49
c 0
b 0
f 0
rs 9.2568
cc 5
nc 7
nop 3
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)) {
0 ignored issues
show
introduced by
The condition is_array($structure) is always true.
Loading history...
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. ( Ignorable by Annotation )

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

129
    protected function matchFile(/** @scrutinizer ignore-unused */ $name, $schemaArray, $body, $type)

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

129
    protected function matchFile($name, $schemaArray, /** @scrutinizer ignore-unused */ $body, $type)

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

129
    protected function matchFile($name, /** @scrutinizer ignore-unused */ $schemaArray, $body, $type)

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...
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
Comprehensibility Best Practice introduced by
The variable $schemaArray seems to never exist and therefore isset should always be false.
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)) {
0 ignored issues
show
introduced by
The condition is_bool($body) is always false.
Loading history...
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)) {
0 ignored issues
show
introduced by
The condition is_array($body) is always false.
Loading history...
300
            throw new InvalidRequestException(
301
                "I expected an array here, but I got an string. Maybe you did wrong request?",
302
                $body
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) {
0 ignored issues
show
introduced by
$name is overwriting one of the parameters of this function.
Loading history...
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 can also be of type array; however, parameter $body of ByJG\ApiTools\Base\Body::matchTypes() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

371
        if ($this->matchTypes($name, $schemaArray, /** @scrutinizer ignore-type */ $body)) {
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 can also be of type array; however, parameter $body of ByJG\ApiTools\Base\Body::matchObjectProperties() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

386
        if ($this->matchObjectProperties($name, $schemaArray, /** @scrutinizer ignore-type */ $body)) {
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)) {
0 ignored issues
show
introduced by
The condition is_null($body) is always false.
Loading history...
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