Issues (23)

src/Base/Body.php (6 issues)

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
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
        if (!is_string($body)) {
0 ignored issues
show
The condition is_string($body) is always true.
Loading history...
110
            throw new NotMatchedException("Value '" . var_export($body, true) . "' in '$name' is not string. ", $this->structure);
111
        }
112
113
        return true;
114
    }
115
116
    private function checkPattern($name, $body, $pattern)
117
    {
118
        $pattern = '/' . rtrim(ltrim($pattern, '/'), '/') . '/';
119
        $isSuccess = (bool) preg_match($pattern, $body, $matches);
120
121
        if (!$isSuccess) {
122
            throw new NotMatchedException("Value '$body' in '$name' not matched in pattern. ", $this->structure);
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)
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'])) {
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)) {
0 ignored issues
show
The condition is_bool($body) is always false.
Loading history...
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)) {
0 ignored issues
show
The condition is_array($body) is always false.
Loading history...
292
            throw new InvalidRequestException(
293
                "I expected an array here, but I got an string. Maybe you did wrong request?",
294
                $body
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) {
0 ignored issues
show
$name is overwriting one of the parameters of this function.
Loading history...
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)) {
360
            return true;
361
        }
362
363
        if(!isset($schemaArray['$ref']) && isset($schemaArray['content'])) {
364
            $schemaArray = $schemaArray['content'][key($schemaArray['content'])]['schema'];
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)) {
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
        // Match any object
418
        if (count($schemaArray) === 1 && isset($schemaArray['type']) && $schemaArray['type'] === 'object') {
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
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