Completed
Pull Request — master (#56)
by
unknown
08:38
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_REQUIRED="required";
20
21
    /**
22
     * @var Schema
23
     */
24
    protected $schema;
25
26
    protected $structure;
27
28
    protected $name;
29
30
    /**
31
     * OpenApi 2.0 does not describe null values, so this flag defines,
32
     * if match is ok when one of property, which has type, is null
33
     *
34
     * @var bool
35
     */
36
    protected $allowNullValues;
37
38
    /**
39
     * Body constructor.
40
     *
41
     * @param Schema $schema
42
     * @param string $name
43
     * @param array $structure
44
     * @param bool $allowNullValues
45
     */
46
    public function __construct(Schema $schema, $name, $structure, $allowNullValues = false)
47
    {
48
        $this->schema = $schema;
49
        $this->name = $name;
50
        if (!is_array($structure)) {
51
            throw new InvalidArgumentException('I expected the structure to be an array');
52
        }
53
        $this->structure = $structure;
54
        $this->allowNullValues = $allowNullValues;
55
    }
56
57
    /**
58
     * @param Schema $schema
59
     * @param $name
60
     * @param $structure
61
     * @param bool $allowNullValues
62
     * @return OpenApiResponseBody|SwaggerResponseBody
63
     * @throws GenericSwaggerException
64
     */
65
    public static function getInstance(Schema $schema, $name, $structure, $allowNullValues = false)
66
    {
67
        if ($schema instanceof SwaggerSchema) {
68
            return new SwaggerResponseBody($schema, $name, $structure, $allowNullValues);
69
        }
70
71
        if ($schema instanceof OpenApiSchema) {
72
            return new OpenApiResponseBody($schema, $name, $structure, $allowNullValues);
73
        }
74
75
        throw new GenericSwaggerException("Cannot get instance SwaggerBody or SchemaBody from " . get_class($schema));
76
    }
77
78
    abstract public function match($body);
79
80
    /**
81
     * @param $name
82
     * @param $schema
83
     * @param $body
84
     * @param $type
85
     * @return bool
86
     * @throws NotMatchedException
87
     */
88
    protected function matchString($name, $schema, $body, $type)
89
    {
90
        if ($type !== 'string') {
91
            return null;
92
        }
93
94
        if (isset($schema['enum']) && !in_array($body, $schema['enum'])) {
95
            throw new NotMatchedException("Value '$body' in '$name' not matched in ENUM. ", $this->structure);
96
        }
97
98
        return true;
99
    }
100
101
    /**
102
     * @param $name
103
     * @param $body
104
     * @param $type
105
     * @return bool
106
     * @throws NotMatchedException
107
     */
108
    protected function matchNumber($name, $body, $type)
109
    {
110
        if ($type !== 'integer' && $type !== 'float' && $type !== 'number') {
111
            return null;
112
        }
113
114
        if (!is_numeric($body)) {
115
            throw new NotMatchedException("Expected '$name' to be numeric, but found '$body'. ", $this->structure);
116
        }
117
118
        return true;
119
    }
120
121
    /**
122
     * @param $name
123
     * @param $body
124
     * @param $type
125
     * @return bool
126
     * @throws NotMatchedException
127
     */
128
    protected function matchBool($name, $body, $type)
129
    {
130
        if ($type !== 'bool' && $type !== 'boolean') {
131
            return null;
132
        }
133
134
        if (!is_bool($body)) {
135
            throw new NotMatchedException("Expected '$name' to be boolean, but found '$body'. ", $this->structure);
136
        }
137
138
        return true;
139
    }
140
141
    /**
142
     * @param $name
143
     * @param $schema
144
     * @param $body
145
     * @param $type
146
     * @return bool
147
     * @throws DefinitionNotFoundException
148
     * @throws GenericSwaggerException
149
     * @throws InvalidDefinitionException
150
     * @throws InvalidRequestException
151
     * @throws NotMatchedException
152
     */
153
    protected function matchArray($name, $schema, $body, $type)
154
    {
155
        if ($type !== 'array') {
156
            return null;
157
        }
158
159
        foreach ((array)$body as $item) {
160
            if (!isset($schema['items'])) {  // If there is no type , there is no test.
161
                continue;
162
            }
163
            $this->matchSchema($name, $schema['items'], $item);
164
        }
165
        return true;
166
    }
167
168
    /**
169
     * Checks if the value is valid date.
170
     *
171
     * @param $name
172
     * @param $schema
173
     * @param $body
174
     * @param $type
175
     *
176
     * @return bool|null
177
     * @throws NotMatchedException
178
     */
179
    protected function matchDate($name, $schema, $body, $type)
0 ignored issues
show
Unused Code introduced by
The parameter $schema 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...
180
    {
181
        if ($type !== 'date') {
182
            return null;
183
        }
184
185
        if (!(bool)strtotime($body)) {
186
            throw new NotMatchedException("Expected '$name' to be date, but found '$body'. ", $this->structure);
187
        }
188
189
        return true;
190
    }
191
192
    protected function matchTypes($name, $schema, $body)
193
    {
194
        if (!isset($schema['type'])) {
195
            return null;
196
        }
197
198
        $type = $schema['type'];
199
        $nullable = isset($schema['nullable']) ? (bool)$schema['nullable'] : $this->schema->isAllowNullValues();
200
201
        $validators = [
202
            function () use ($name, $body, $type, $nullable)
203
            {
204
                return $this->matchNull($name, $body, $type, $nullable);
205
            },
206
207
            function () use ($name, $schema, $body, $type)
208
            {
209
                return $this->matchString($name, $schema, $body, $type);
210
            },
211
212
            function () use ($name, $body, $type)
213
            {
214
                return $this->matchNumber($name, $body, $type);
215
            },
216
217
            function () use ($name, $body, $type)
218
            {
219
                return $this->matchBool($name, $body, $type);
220
            },
221
222
            function () use ($name, $schema, $body, $type)
223
            {
224
                return $this->matchArray($name, $schema, $body, $type);
225
            },
226
227
            function () use ($name, $schema, $body, $type)
228
            {
229
                return $this->matchDate($name, $schema, $body, $type);
230
            }
231
        ];
232
233
        foreach ($validators as $validator) {
234
            $result = $validator();
235
            if (!is_null($result)) {
236
                return $result;
237
            }
238
        }
239
240
        return null;
241
    }
242
243
    /**
244
     * @param $name
245
     * @param $schema
246
     * @param $body
247
     * @return bool|null
248
     * @throws DefinitionNotFoundException
249
     * @throws GenericSwaggerException
250
     * @throws InvalidDefinitionException
251
     * @throws InvalidRequestException
252
     * @throws NotMatchedException
253
     */
254
    public function matchObjectProperties($name, $schema, $body)
255
    {
256
        if (!isset($schema[self::SWAGGER_PROPERTIES])) {
257
            return null;
258
        }
259
260
        if (!is_array($body)) {
261
            throw new InvalidRequestException(
262
                "I expected an array here, but I got an string. Maybe you did wrong request?",
263
                $body
264
            );
265
        }
266
267
        if (!isset($schema[self::SWAGGER_REQUIRED])) {
268
            $schema[self::SWAGGER_REQUIRED] = [];
269
        }
270
        foreach ($schema[self::SWAGGER_PROPERTIES] as $prop => $def) {
271
            $required = array_search($prop, $schema[self::SWAGGER_REQUIRED]);
272
273
            if (!array_key_exists($prop, $body)) {
274
                if ($required !== false) {
275
                    throw new NotMatchedException("Required property '$prop' in '$name' not found in object");
276
                }
277
                unset($body[$prop]);
278
                continue;
279
            }
280
281
            $this->matchSchema($prop, $def, $body[$prop]);
282
            unset($schema[self::SWAGGER_PROPERTIES][$prop]);
283
            if ($required !== false) {
284
                unset($schema[self::SWAGGER_REQUIRED][$required]);
285
            }
286
            unset($body[$prop]);
287
        }
288
289
        if (count($schema[self::SWAGGER_REQUIRED]) > 0) {
290
            throw new NotMatchedException(
291
                "The required property(ies) '"
292
                . implode(', ', $schema[self::SWAGGER_REQUIRED])
293
                . "' does not exists in the body.",
294
                $this->structure
295
            );
296
        }
297
298
        if (count($body) > 0) {
299
            throw new NotMatchedException(
300
                "The property(ies) '"
301
                . implode(', ', array_keys($body))
302
                . "' has not defined in '$name'",
303
                $body
304
            );
305
        }
306
        return true;
307
    }
308
309
    /**
310
     * @param string $name
311
     * @param $schema
312
     * @param array $body
313
     * @return bool
314
     * @throws DefinitionNotFoundException
315
     * @throws InvalidDefinitionException
316
     * @throws GenericSwaggerException
317
     * @throws InvalidRequestException
318
     * @throws NotMatchedException
319
     */
320
    protected function matchSchema($name, $schema, $body)
321
    {
322
        // Match Single Types
323
        if ($this->matchTypes($name, $schema, $body)) {
324
            return true;
325
        }
326
327
        if(!isset($schema['$ref']) && isset($schema['content'])) {
328
            $schema['$ref'] = $schema['content'][key($schema['content'])]['schema']['$ref'];
329
        }
330
331
        // Get References and try to match it again
332
        if (isset($schema['$ref'])) {
333
            $defintion = $this->schema->getDefinition($schema['$ref']);
334
            return $this->matchSchema($schema['$ref'], $defintion, $body);
335
        }
336
337
        // Match object properties
338
        if ($this->matchObjectProperties($name, $schema, $body)) {
339
            return true;
340
        }
341
342
        /**
343
         * OpenApi 2.0 does not describe ANY object value
344
         * But there is hack that makes ANY object possible, described in link below
345
         * To make that hack works, we need such condition
346
         * @link https://stackoverflow.com/questions/32841298/swagger-2-0-what-schema-to-accept-any-complex-json-value
347
         */
348
        if ($schema === []) {
349
            return true;
350
        }
351
352
        throw new GenericSwaggerException("Not all cases are defined. Please open an issue about this. Schema: $name");
353
    }
354
355
    /**
356
     * @param $name
357
     * @param $body
358
     * @param $type
359
     * @param $nullable
360
     * @return bool
361
     * @throws NotMatchedException
362
     */
363
    protected function matchNull($name, $body, $type, $nullable)
364
    {
365
        if (!is_null($body)) {
366
            return null;
367
        }
368
369
        if (!$nullable) {
370
            throw new NotMatchedException(
371
                "Value of property '$name' is null, but should be of type '$type'",
372
                $this->structure
373
            );
374
        }
375
376
        return true;
377
    }
378
}
379