Passed
Pull Request — master (#60)
by
unknown
01:58
created

Body::matchSchema()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 8.4426
c 0
b 0
f 0
cc 7
nc 9
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
        return true;
106
    }
107
108
    /**
109
     * @param string $name
110
     * @param array $schemaArray
111
     * @param string $body
112
     * @param string $type
113
     * @return bool
114
     */
115
    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...
116
    {
117
        if ($type !== 'file') {
118
            return null;
119
        }
120
121
        return true;
122
    }
123
124
    /**
125
     * @param string $name
126
     * @param string $body
127
     * @param string $type
128
     * @return bool
129
     * @throws NotMatchedException
130
     */
131
    protected function matchNumber($name, $body, $type)
132
    {
133
        if ($type !== 'integer' && $type !== 'float' && $type !== 'number') {
134
            return null;
135
        }
136
137
        if (!is_numeric($body)) {
138
            throw new NotMatchedException("Expected '$name' to be numeric, but found '$body'. ", $this->structure);
139
        }
140
141
        return true;
142
    }
143
144
    /**
145
     * @param string $name
146
     * @param string $body
147
     * @param string $type
148
     * @return bool
149
     * @throws NotMatchedException
150
     */
151
    protected function matchBool($name, $body, $type)
152
    {
153
        if ($type !== 'bool' && $type !== 'boolean') {
154
            return null;
155
        }
156
157
        if (!is_bool($body)) {
158
            throw new NotMatchedException("Expected '$name' to be boolean, but found '$body'. ", $this->structure);
159
        }
160
161
        return true;
162
    }
163
164
    /**
165
     * @param string $name
166
     * @param array $schemaArray
167
     * @param string $body
168
     * @param string $type
169
     * @return bool
170
     * @throws DefinitionNotFoundException
171
     * @throws GenericSwaggerException
172
     * @throws InvalidDefinitionException
173
     * @throws InvalidRequestException
174
     * @throws NotMatchedException
175
     */
176
    protected function matchArray($name, $schemaArray, $body, $type)
177
    {
178
        if ($type !== 'array') {
179
            return null;
180
        }
181
182
        foreach ((array)$body as $item) {
183
            if (!isset($schemaArray['items'])) {  // If there is no type , there is no test.
184
                continue;
185
            }
186
            $this->matchSchema($name, $schemaArray['items'], $item);
187
        }
188
        return true;
189
    }
190
191
    /**
192
     * @param string $name
193
     * @param array $schemaArray
194
     * @param string $body
195
     * @return mixed|null
196
     */
197
    protected function matchTypes($name, $schemaArray, $body)
198
    {
199
        if (!isset($schemaArray['type'])) {
200
            return null;
201
        }
202
203
        $type = $schemaArray['type'];
204
        $nullable = isset($schemaArray['nullable']) ? (bool)$schemaArray['nullable'] : $this->schema->isAllowNullValues();
205
206
        $validators = [
207
            function () use ($name, $body, $type, $nullable)
208
            {
209
                return $this->matchNull($name, $body, $type, $nullable);
210
            },
211
212
            function () use ($name, $schemaArray, $body, $type)
213
            {
214
                return $this->matchString($name, $schemaArray, $body, $type);
215
            },
216
217
            function () use ($name, $body, $type)
218
            {
219
                return $this->matchNumber($name, $body, $type);
220
            },
221
222
            function () use ($name, $body, $type)
223
            {
224
                return $this->matchBool($name, $body, $type);
225
            },
226
227
            function () use ($name, $schemaArray, $body, $type)
228
            {
229
                return $this->matchArray($name, $schemaArray, $body, $type);
230
            },
231
232
            function () use ($name, $schemaArray, $body, $type)
233
            {
234
                return $this->matchFile($name, $schemaArray, $body, $type);
235
            },
236
        ];
237
238
        foreach ($validators as $validator) {
239
            $result = $validator();
240
            if (!is_null($result)) {
241
                return $result;
242
            }
243
        }
244
245
        return null;
246
    }
247
248
    /**
249
     * @param string $name
250
     * @param array $schemaArray
251
     * @param string $body
252
     * @return bool|null
253
     * @throws DefinitionNotFoundException
254
     * @throws GenericSwaggerException
255
     * @throws InvalidDefinitionException
256
     * @throws InvalidRequestException
257
     * @throws NotMatchedException
258
     */
259
    public function matchObjectProperties($name, $schemaArray, $body)
260
    {
261
        if (isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES]) && !isset($schemaArray[self::SWAGGER_PROPERTIES])) {
262
            $schemaArray[self::SWAGGER_PROPERTIES] = [];
263
        }
264
265
        if (!isset($schemaArray[self::SWAGGER_PROPERTIES])) {
266
            return null;
267
        }
268
269
        if (!is_array($body)) {
270
            throw new InvalidRequestException(
271
                "I expected an array here, but I got an string. Maybe you did wrong request?",
272
                $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...
273
            );
274
        }
275
276
        if (!isset($schemaArray[self::SWAGGER_REQUIRED])) {
277
            $schemaArray[self::SWAGGER_REQUIRED] = [];
278
        }
279
        foreach ($schemaArray[self::SWAGGER_PROPERTIES] as $prop => $def) {
280
            $required = array_search($prop, $schemaArray[self::SWAGGER_REQUIRED]);
281
282
            if (!array_key_exists($prop, $body)) {
283
                if ($required !== false) {
284
                    throw new NotMatchedException("Required property '$prop' in '$name' not found in object");
285
                }
286
                unset($body[$prop]);
287
                continue;
288
            }
289
290
            $this->matchSchema($prop, $def, $body[$prop]);
291
            unset($schemaArray[self::SWAGGER_PROPERTIES][$prop]);
292
            if ($required !== false) {
293
                unset($schemaArray[self::SWAGGER_REQUIRED][$required]);
294
            }
295
            unset($body[$prop]);
296
        }
297
298
        if (count($schemaArray[self::SWAGGER_REQUIRED]) > 0) {
299
            throw new NotMatchedException(
300
                "The required property(ies) '"
301
                . implode(', ', $schemaArray[self::SWAGGER_REQUIRED])
302
                . "' does not exists in the body.",
303
                $this->structure
304
            );
305
        }
306
307
        if (count($body) > 0 && !isset($schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES])) {
308
            throw new NotMatchedException(
309
                "The property(ies) '"
310
                . implode(', ', array_keys($body))
311
                . "' has not defined in '$name'",
312
                $body
313
            );
314
        }
315
316
        foreach ($body as $name => $prop) {
317
            $def = $schemaArray[self::SWAGGER_ADDITIONAL_PROPERTIES];
318
            $this->matchSchema($name, $def, $prop);
319
        }
320
        return true;
321
    }
322
323
    /**
324
     * @param string $name
325
     * @param array $schemaArray
326
     * @param array $body
327
     * @return bool
328
     * @throws DefinitionNotFoundException
329
     * @throws InvalidDefinitionException
330
     * @throws GenericSwaggerException
331
     * @throws InvalidRequestException
332
     * @throws NotMatchedException
333
     */
334
    protected function matchSchema($name, $schemaArray, $body)
335
    {
336
        // Match Single Types
337
        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...
338
            return true;
339
        }
340
341
        if(!isset($schemaArray['$ref']) && isset($schemaArray['content'])) {
342
            $schemaArray['$ref'] = $schemaArray['content'][key($schemaArray['content'])]['schema']['$ref'];
343
        }
344
345
        // Get References and try to match it again
346
        if (isset($schemaArray['$ref'])) {
347
            $defintion = $this->schema->getDefinition($schemaArray['$ref']);
348
            return $this->matchSchema($schemaArray['$ref'], $defintion, $body);
349
        }
350
351
        // Match object properties
352
        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...
353
            return true;
354
        }
355
356
        /**
357
         * OpenApi 2.0 does not describe ANY object value
358
         * But there is hack that makes ANY object possible, described in link below
359
         * To make that hack works, we need such condition
360
         * @link https://stackoverflow.com/questions/32841298/swagger-2-0-what-schema-to-accept-any-complex-json-value
361
         */
362
        if ($schemaArray === []) {
363
            return true;
364
        }
365
366
        throw new GenericSwaggerException("Not all cases are defined. Please open an issue about this. Schema: $name");
367
    }
368
369
    /**
370
     * @param string $name
371
     * @param string $body
372
     * @param string $type
373
     * @param bool $nullable
374
     * @return bool
375
     * @throws NotMatchedException
376
     */
377
    protected function matchNull($name, $body, $type, $nullable)
378
    {
379
        if (!is_null($body)) {
380
            return null;
381
        }
382
383
        if (!$nullable) {
384
            throw new NotMatchedException(
385
                "Value of property '$name' is null, but should be of type '$type'",
386
                $this->structure
387
            );
388
        }
389
390
        return true;
391
    }
392
}
393