Passed
Pull Request — master (#66)
by Vitor
04:11
created

Body::matchArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

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...
360
            return true;
361
        }
362
363
        if(!isset($schemaArray['$ref']) && isset($schemaArray['content'])) {
364
            $schemaArray['$ref'] = $schemaArray['content'][key($schemaArray['content'])]['schema']['$ref'];
365
        }
366
367
        // Get References and try to match it again
368
        if (isset($schemaArray['$ref']) && !is_array($schemaArray['$ref'])) {
369
            $defintion = $this->schema->getDefinition($schemaArray['$ref']);
370
            return $this->matchSchema($schemaArray['$ref'], $defintion, $body);
371
        }
372
373
        // Match object properties
374
        if ($this->matchObjectProperties($name, $schemaArray, $body)) {
0 ignored issues
show
Documentation introduced by
$body is of type array, but the function expects a string.

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