Passed
Pull Request — master (#71)
by Vitor
05:47 queued 03:34
created

Body::matchNull()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 14
c 0
b 0
f 0
rs 10
cc 3
nc 3
nop 4
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
        $pattern = $this->preparePattern($pattern);
115
        $isSuccess = (bool) preg_match($pattern, $body, $matches);
116
117
        if (!$isSuccess) {
118
            throw new NotMatchedException("Value '$body' in '$name' not matched in pattern. ", $this->structure);
119
        }
120
    }
121
122
    private function preparePattern($pattern)
123
    {
124
        if ($pattern[0] !== '/' && substr($pattern, -1) !== '/') {
125
            $pattern = '/' . $pattern . '/';
126
        }
127
        return $pattern;
128
    }
129
130
131
    /**
132
     * @param string $name
133
     * @param array $schemaArray
134
     * @param string $body
135
     * @param string $type
136
     * @return bool
137
     */
138
    protected function matchFile($name, $schemaArray, $body, $type)
0 ignored issues
show
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

138
    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...
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

138
    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 $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

138
    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...
139
    {
140
        if ($type !== 'file') {
141
            return null;
142
        }
143
144
        return true;
145
    }
146
147
    /**
148
     * @param string $name
149
     * @param string $body
150
     * @param string $type
151
     * @return bool
152
     * @throws NotMatchedException
153
     */
154
    protected function matchNumber($name, $body, $type)
155
    {
156
        if ($type !== 'integer' && $type !== 'float' && $type !== 'number') {
157
            return null;
158
        }
159
160
        if (!is_numeric($body)) {
161
            throw new NotMatchedException("Expected '$name' to be numeric, but found '$body'. ", $this->structure);
162
        }
163
164
        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...
165
            $this->checkPattern($name, $body, $schemaArray['pattern']);
166
        }
167
168
        return true;
169
    }
170
171
    /**
172
     * @param string $name
173
     * @param string $body
174
     * @param string $type
175
     * @return bool
176
     * @throws NotMatchedException
177
     */
178
    protected function matchBool($name, $body, $type)
179
    {
180
        if ($type !== 'bool' && $type !== 'boolean') {
181
            return null;
182
        }
183
184
        if (!is_bool($body)) {
0 ignored issues
show
introduced by
The condition is_bool($body) is always false.
Loading history...
185
            throw new NotMatchedException("Expected '$name' to be boolean, but found '$body'. ", $this->structure);
186
        }
187
188
        return true;
189
    }
190
191
    /**
192
     * @param string $name
193
     * @param array $schemaArray
194
     * @param string $body
195
     * @param string $type
196
     * @return bool
197
     * @throws DefinitionNotFoundException
198
     * @throws GenericSwaggerException
199
     * @throws InvalidDefinitionException
200
     * @throws InvalidRequestException
201
     * @throws NotMatchedException
202
     */
203
    protected function matchArray($name, $schemaArray, $body, $type)
204
    {
205
        if ($type !== 'array') {
206
            return null;
207
        }
208
209
        foreach ((array)$body as $item) {
210
            if (!isset($schemaArray['items'])) {  // If there is no type , there is no test.
211
                continue;
212
            }
213
            $this->matchSchema($name, $schemaArray['items'], $item);
214
        }
215
        return true;
216
    }
217
218
    /**
219
     * @param string $name
220
     * @param array $schemaArray
221
     * @param string $body
222
     * @return mixed|null
223
     */
224
    protected function matchTypes($name, $schemaArray, $body)
225
    {
226
        if (!isset($schemaArray['type'])) {
227
            return null;
228
        }
229
230
        $type = $schemaArray['type'];
231
        $nullable = isset($schemaArray['nullable']) ? (bool)$schemaArray['nullable'] : $this->schema->isAllowNullValues();
232
233
        $validators = [
234
            function () use ($name, $body, $type, $nullable)
235
            {
236
                return $this->matchNull($name, $body, $type, $nullable);
237
            },
238
239
            function () use ($name, $schemaArray, $body, $type)
240
            {
241
                return $this->matchString($name, $schemaArray, $body, $type);
242
            },
243
244
            function () use ($name, $body, $type)
245
            {
246
                return $this->matchNumber($name, $body, $type);
247
            },
248
249
            function () use ($name, $body, $type)
250
            {
251
                return $this->matchBool($name, $body, $type);
252
            },
253
254
            function () use ($name, $schemaArray, $body, $type)
255
            {
256
                return $this->matchArray($name, $schemaArray, $body, $type);
257
            },
258
259
            function () use ($name, $schemaArray, $body, $type)
260
            {
261
                return $this->matchFile($name, $schemaArray, $body, $type);
262
            },
263
        ];
264
265
        foreach ($validators as $validator) {
266
            $result = $validator();
267
            if (!is_null($result)) {
268
                return $result;
269
            }
270
        }
271
272
        return null;
273
    }
274
275
    /**
276
     * @param string $name
277
     * @param array $schemaArray
278
     * @param string $body
279
     * @return bool|null
280
     * @throws DefinitionNotFoundException
281
     * @throws GenericSwaggerException
282
     * @throws InvalidDefinitionException
283
     * @throws InvalidRequestException
284
     * @throws NotMatchedException
285
     */
286
    public function matchObjectProperties($name, $schemaArray, $body)
287
    {
288
        if (isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES]) && !isset($schemaArray[self::SWAGGER_PROPERTIES])) {
289
            $schemaArray[self::SWAGGER_PROPERTIES] = [];
290
        }
291
292
        if (!isset($schemaArray[self::SWAGGER_PROPERTIES])) {
293
            return null;
294
        }
295
296
        if (!is_array($body)) {
0 ignored issues
show
introduced by
The condition is_array($body) is always false.
Loading history...
297
            throw new InvalidRequestException(
298
                "I expected an array here, but I got an string. Maybe you did wrong request?",
299
                $body
300
            );
301
        }
302
303
        if (!isset($schemaArray[self::SWAGGER_REQUIRED])) {
304
            $schemaArray[self::SWAGGER_REQUIRED] = [];
305
        }
306
        foreach ($schemaArray[self::SWAGGER_PROPERTIES] as $prop => $def) {
307
            $required = array_search($prop, $schemaArray[self::SWAGGER_REQUIRED]);
308
309
            if (!array_key_exists($prop, $body)) {
310
                if ($required !== false) {
311
                    throw new NotMatchedException("Required property '$prop' in '$name' not found in object");
312
                }
313
                unset($body[$prop]);
314
                continue;
315
            }
316
317
            $this->matchSchema($prop, $def, $body[$prop]);
318
            unset($schemaArray[self::SWAGGER_PROPERTIES][$prop]);
319
            if ($required !== false) {
320
                unset($schemaArray[self::SWAGGER_REQUIRED][$required]);
321
            }
322
            unset($body[$prop]);
323
        }
324
325
        if (count($schemaArray[self::SWAGGER_REQUIRED]) > 0) {
326
            throw new NotMatchedException(
327
                "The required property(ies) '"
328
                . implode(', ', $schemaArray[self::SWAGGER_REQUIRED])
329
                . "' does not exists in the body.",
330
                $this->structure
331
            );
332
        }
333
334
        if (count($body) > 0 && !isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES])) {
335
            throw new NotMatchedException(
336
                "The property(ies) '"
337
                . implode(', ', array_keys($body))
338
                . "' has not defined in '$name'",
339
                $body
340
            );
341
        }
342
343
        foreach ($body as $name => $prop) {
0 ignored issues
show
introduced by
$name is overwriting one of the parameters of this function.
Loading history...
344
            $def = $schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES];
345
            $this->matchSchema($name, $def, $prop);
346
        }
347
        return true;
348
    }
349
350
    /**
351
     * @param string $name
352
     * @param array $schemaArray
353
     * @param array $body
354
     * @return bool
355
     * @throws DefinitionNotFoundException
356
     * @throws InvalidDefinitionException
357
     * @throws GenericSwaggerException
358
     * @throws InvalidRequestException
359
     * @throws NotMatchedException
360
     */
361
    protected function matchSchema($name, $schemaArray, $body)
362
    {
363
        // Match Single Types
364
        if ($this->matchTypes($name, $schemaArray, $body)) {
0 ignored issues
show
Bug introduced by
$body of type array is incompatible with the type string expected by parameter $body of ByJG\ApiTools\Base\Body::matchTypes(). ( Ignorable by Annotation )

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

364
        if ($this->matchTypes($name, $schemaArray, /** @scrutinizer ignore-type */ $body)) {
Loading history...
365
            return true;
366
        }
367
368
        if(!isset($schemaArray['$ref']) && isset($schemaArray['content'])) {
369
            $schemaArray['$ref'] = $schemaArray['content'][key($schemaArray['content'])]['schema']['$ref'];
370
        }
371
372
        // Get References and try to match it again
373
        if (isset($schemaArray['$ref']) && !is_array($schemaArray['$ref'])) {
374
            $defintion = $this->schema->getDefinition($schemaArray['$ref']);
375
            return $this->matchSchema($schemaArray['$ref'], $defintion, $body);
376
        }
377
378
        // Match object properties
379
        if ($this->matchObjectProperties($name, $schemaArray, $body)) {
0 ignored issues
show
Bug introduced by
$body of type array is incompatible with the type string expected by parameter $body of ByJG\ApiTools\Base\Body::matchObjectProperties(). ( Ignorable by Annotation )

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

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