Passed
Pull Request — master (#62)
by Joao
02:16
created

Body::checkPattern()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
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)) {
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]) && !isset($schemaArray[self::SWAGGER_PROPERTIES])) {
280
            $schemaArray[self::SWAGGER_PROPERTIES] = [];
281
        }
282
283
        if (!isset($schemaArray[self::SWAGGER_PROPERTIES])) {
284
            return null;
285
        }
286
287
        if (!is_array($body)) {
288
            throw new InvalidRequestException(
289
                "I expected an array here, but I got an string. Maybe you did wrong request?",
290
                $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...
291
            );
292
        }
293
294
        if (!isset($schemaArray[self::SWAGGER_REQUIRED])) {
295
            $schemaArray[self::SWAGGER_REQUIRED] = [];
296
        }
297
        foreach ($schemaArray[self::SWAGGER_PROPERTIES] as $prop => $def) {
298
            $required = array_search($prop, $schemaArray[self::SWAGGER_REQUIRED]);
299
300
            if (!array_key_exists($prop, $body)) {
301
                if ($required !== false) {
302
                    throw new NotMatchedException("Required property '$prop' in '$name' not found in object");
303
                }
304
                unset($body[$prop]);
305
                continue;
306
            }
307
308
            $this->matchSchema($prop, $def, $body[$prop]);
309
            unset($schemaArray[self::SWAGGER_PROPERTIES][$prop]);
310
            if ($required !== false) {
311
                unset($schemaArray[self::SWAGGER_REQUIRED][$required]);
312
            }
313
            unset($body[$prop]);
314
        }
315
316
        if (count($schemaArray[self::SWAGGER_REQUIRED]) > 0) {
317
            throw new NotMatchedException(
318
                "The required property(ies) '"
319
                . implode(', ', $schemaArray[self::SWAGGER_REQUIRED])
320
                . "' does not exists in the body.",
321
                $this->structure
322
            );
323
        }
324
325
        if (count($body) > 0 && !isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES])) {
326
            throw new NotMatchedException(
327
                "The property(ies) '"
328
                . implode(', ', array_keys($body))
329
                . "' has not defined in '$name'",
330
                $body
331
            );
332
        }
333
334
        foreach ($body as $name => $prop) {
335
            $def = $schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES];
336
            $this->matchSchema($name, $def, $prop);
337
        }
338
        return true;
339
    }
340
341
    /**
342
     * @param string $name
343
     * @param array $schemaArray
344
     * @param array $body
345
     * @return bool
346
     * @throws DefinitionNotFoundException
347
     * @throws InvalidDefinitionException
348
     * @throws GenericSwaggerException
349
     * @throws InvalidRequestException
350
     * @throws NotMatchedException
351
     */
352
    protected function matchSchema($name, $schemaArray, $body)
353
    {
354
        // Match Single Types
355
        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...
356
            return true;
357
        }
358
359
        if(!isset($schemaArray['$ref']) && isset($schemaArray['content'])) {
360
            $schemaArray['$ref'] = $schemaArray['content'][key($schemaArray['content'])]['schema']['$ref'];
361
        }
362
363
        // Get References and try to match it again
364
        if (isset($schemaArray['$ref'])) {
365
            $defintion = $this->schema->getDefinition($schemaArray['$ref']);
366
            return $this->matchSchema($schemaArray['$ref'], $defintion, $body);
367
        }
368
369
        // Match object properties
370
        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...
371
            return true;
372
        }
373
374
        if (isset($schemaArray['allOf'])) {
375
            $mergedSchema = array_merge_recursive(...$schemaArray['allOf']);
376
            return $this->matchSchema($name, $mergedSchema, $body);
377
        }
378
379
        if (isset($schemaArray['oneOf'])) {
380
            $matched = false;
381
            $catchedException = null;
382
            foreach ($schemaArray['oneOf'] as $scheme) {
383
                try {
384
                    $matched = $matched || $this->matchSchema($name, $scheme, $body);
385
                } catch (NotMatchedException $exception) {
386
                    $catchedException = $exception;
387
                }
388
            }
389
            if ($catchedException !== null && $matched === false) {
390
                throw $catchedException;
391
            }
392
393
            return $matched;
394
        }
395
396
        /**
397
         * OpenApi 2.0 does not describe ANY object value
398
         * But there is hack that makes ANY object possible, described in link below
399
         * To make that hack works, we need such condition
400
         * @link https://stackoverflow.com/questions/32841298/swagger-2-0-what-schema-to-accept-any-complex-json-value
401
         */
402
        if ($schemaArray === []) {
403
            return true;
404
        }
405
406
        throw new GenericSwaggerException("Not all cases are defined. Please open an issue about this. Schema: $name");
407
    }
408
409
    /**
410
     * @param string $name
411
     * @param string $body
412
     * @param string $type
413
     * @param bool $nullable
414
     * @return bool
415
     * @throws NotMatchedException
416
     */
417
    protected function matchNull($name, $body, $type, $nullable)
418
    {
419
        if (!is_null($body)) {
420
            return null;
421
        }
422
423
        if (!$nullable) {
424
            throw new NotMatchedException(
425
                "Value of property '$name' is null, but should be of type '$type'",
426
                $this->structure
427
            );
428
        }
429
430
        return true;
431
    }
432
}
433