Completed
Push — master ( dca578...2fbbce )
by Tomáš
09:39
created

ImportValidator::isObject()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 1
nc 4
nop 1
dl 0
loc 2
ccs 2
cts 2
cp 1
crap 4
rs 10
c 1
b 0
f 0
1
<?php
2
3
4
namespace TournamentGenerator\Import;
5
6
use TournamentGenerator\Constants;
7
use TournamentGenerator\Preset\DoubleElimination;
8
use TournamentGenerator\Preset\R2G;
9
use TournamentGenerator\Preset\SingleElimination;
10
11
/**
12
 * Validator for import data
13
 *
14
 * Validates if the input data is a valid import object.
15
 *
16
 * @package TournamentGenerator\Import
17
 * @author  Tomáš Vojík <[email protected]>
18
 * @since   0.5
19
 */
20
class ImportValidator
21
{
22
23
	/**
24
	 * @var array Expected import data structure description
25
	 */
26
	public const STRUCTURE = [
27
		'tournament'   => [
28
			'type'       => 'object',
29
			'parameters' => [
30
				'type'       => [
31
					'default' => 'general',
32
					'type'    => 'string',
33
					'values'  => ['general', SingleElimination::class, DoubleElimination::class, R2G::class],
34
				],
35
				'name'       => [
36
					'default' => '',
37
					'type'    => 'string',
38
				],
39
				'skip'       => [
40
					'default' => false,
41
					'type'    => 'bool',
42
				],
43
				'timing'     => [
44
					'default'    => null,
45
					'type'       => 'object',
46
					'parameters' => [
47
						'play'         => [
48
							'default' => 0,
49
							'type'    => 'int',
50
						],
51
						'gameWait'     => [
52
							'default' => 0,
53
							'type'    => 'int',
54
						],
55
						'categoryWait' => [
56
							'default' => 0,
57
							'type'    => 'int',
58
						],
59
						'roundWait'    => [
60
							'default' => 0,
61
							'type'    => 'int',
62
						],
63
						'expectedTime' => [
64
							'default' => 0,
65
							'type'    => 'int',
66
						],
67
					]
68
				],
69
				'categories' => [
70
					'default'   => [],
71
					'type'      => 'array',
72
					'subtype'   => 'id',
73
					'reference' => 'categories',
74
				],
75
				'rounds'     => [
76
					'default'   => [],
77
					'type'      => 'array',
78
					'subtype'   => 'id',
79
					'reference' => 'rounds',
80
				],
81
				'groups'     => [
82
					'default'   => [],
83
					'type'      => 'array',
84
					'subtype'   => 'id',
85
					'reference' => 'groups',
86
				],
87
				'teams'      => [
88
					'default'   => [],
89
					'type'      => 'array',
90
					'subtype'   => 'id',
91
					'reference' => 'teams',
92
				],
93
				'games'      => [
94
					'default'   => [],
95
					'type'      => 'array',
96
					'subtype'   => 'int',
97
					'reference' => 'games',
98
				],
99
			],
100
		],
101
		'categories'   => [
102
			'type'       => 'array',
103
			'subtype'    => 'object',
104
			'parameters' => [
105
				'id'     => [
106
					'default' => null,
107
					'type'    => 'id',
108
				],
109
				'name'   => [
110
					'default' => '',
111
					'type'    => 'string',
112
				],
113
				'skip'   => [
114
					'default' => false,
115
					'type'    => 'bool',
116
				],
117
				'rounds' => [
118
					'default'   => [],
119
					'type'      => 'array',
120
					'subtype'   => 'id',
121
					'reference' => 'rounds',
122
				],
123
				'groups' => [
124
					'default'   => [],
125
					'type'      => 'array',
126
					'subtype'   => 'id',
127
					'reference' => 'groups',
128
				],
129
				'teams'  => [
130
					'default'   => [],
131
					'type'      => 'array',
132
					'subtype'   => 'id',
133
					'reference' => 'teams',
134
				],
135
				'games'  => [
136
					'default'   => [],
137
					'type'      => 'array',
138
					'subtype'   => 'int',
139
					'reference' => 'games',
140
				],
141
			],
142
		],
143
		'rounds'       => [
144
			'type'       => 'array',
145
			'subtype'    => 'object',
146
			'parameters' => [
147
				'id'     => [
148
					'default' => null,
149
					'type'    => 'id',
150
				],
151
				'name'   => [
152
					'default' => '',
153
					'type'    => 'string',
154
				],
155
				'skip'   => [
156
					'default' => false,
157
					'type'    => 'bool',
158
				],
159
				'played' => [
160
					'default' => false,
161
					'type'    => 'bool',
162
				],
163
				'groups' => [
164
					'default'   => [],
165
					'type'      => 'array',
166
					'subtype'   => 'id',
167
					'reference' => 'groups',
168
				],
169
				'teams'  => [
170
					'default'   => [],
171
					'type'      => 'array',
172
					'subtype'   => 'id',
173
					'reference' => 'teams',
174
				],
175
				'games'  => [
176
					'default'   => [],
177
					'type'      => 'array',
178
					'subtype'   => 'int',
179
					'reference' => 'games',
180
				],
181
			],
182
		],
183
		'groups'       => [
184
			'type'       => 'array',
185
			'subtype'    => 'object',
186
			'parameters' => [
187
				'id'      => [
188
					'default' => null,
189
					'type'    => 'id',
190
				],
191
				'name'    => [
192
					'default' => '',
193
					'type'    => 'string',
194
				],
195
				'type'    => [
196
					'default' => Constants::ROUND_ROBIN,
197
					'type'    => 'string',
198
					'values'  => Constants::GroupTypes,
199
				],
200
				'skip'    => [
201
					'default' => false,
202
					'type'    => 'bool',
203
				],
204
				'points'  => [
205
					'default'    => null,
206
					'type'       => 'object',
207
					'parameters' => [
208
						'win'         => [
209
							'default' => 3,
210
							'type'    => 'int',
211
						],
212
						'loss'        => [
213
							'default' => 0,
214
							'type'    => 'int',
215
						],
216
						'draw'        => [
217
							'default' => 1,
218
							'type'    => 'int',
219
						],
220
						'second'      => [
221
							'default' => 2,
222
							'type'    => 'int',
223
						],
224
						'third'       => [
225
							'default' => 3,
226
							'type'    => 'int',
227
						],
228
						'progression' => [
229
							'default' => 50,
230
							'type'    => 'int',
231
						],
232
					]
233
				],
234
				'played'  => [
235
					'default' => false,
236
					'type'    => 'bool',
237
				],
238
				'inGame'  => [
239
					'default' => 2,
240
					'type'    => 'int',
241
					'values'  => [2, 3, 4],
242
				],
243
				'maxSize' => [
244
					'default' => 4,
245
					'type'    => 'int',
246
				],
247
				'teams'   => [
248
					'default'   => [],
249
					'type'      => 'array',
250
					'subtype'   => 'id',
251
					'reference' => 'teams',
252
				],
253
				'games'   => [
254
					'default'   => [],
255
					'type'      => 'array',
256
					'subtype'   => 'int',
257
					'reference' => 'games',
258
				],
259
			],
260
		],
261
		'progressions' => [
262
			'type'       => 'array',
263
			'subtype'    => 'object',
264
			'parameters' => [
265
				'from'       => [
266
					'type'      => 'id',
267
					'reference' => 'groups',
268
				],
269
				'to'         => [
270
					'type'      => 'id',
271
					'reference' => 'groups',
272
				],
273
				'offset'     => [
274
					'type'    => 'int',
275
					'default' => 0,
276
				],
277
				'length'     => [
278
					'type'    => 'int',
279
					'default' => null,
280
				],
281
				'filters'    => [
282
					'type'       => 'array',
283
					'subtype'    => 'object',
284
					'default'    => [],
285
					'parameters' => [
286
						'what'   => [
287
							'type'    => 'string',
288
							'default' => 'points',
289
							'values'  => ['points', 'score', 'wins', 'draws', 'losses', 'second', 'third', 'team', 'not-progressed', 'progressed'],
290
						],
291
						'how'    => [
292
							'type'    => 'string',
293
							'default' => '>',
294
							'values'  => ['>', '<', '>=', '<=', '=', '!='],
295
						],
296
						'val'    => [
297
							'default' => 0,
298
						],
299
						'groups' => [
300
							'type'      => 'array',
301
							'subtype'   => 'id',
302
							'reference' => 'groups',
303
						],
304
					],
305
				],
306
				'progressed' => [
307
					'type'    => 'bool',
308
					'default' => false,
309
				],
310
			],
311
		],
312
		'teams'        => [
313
			'type'       => 'array',
314
			'subtype'    => 'object',
315
			'parameters' => [
316
				'id'     => [
317
					'type'    => 'id',
318
					'default' => null,
319
				],
320
				'name'   => [
321
					'type'    => 'string',
322
					'default' => '',
323
				],
324
				'scores' => [
325
					'type'         => 'array',
326
					'subtype'      => 'object',
327
					'keyReference' => 'groups',
328
					'parameters'   => [
329
						'points' => [
330
							'type'    => 'int',
331
							'default' => 0,
332
						],
333
						'score'  => [
334
							'type'    => 'int',
335
							'default' => 0,
336
						],
337
						'wins'   => [
338
							'type'    => 'int',
339
							'default' => 0,
340
						],
341
						'draws'  => [
342
							'type'    => 'int',
343
							'default' => 0,
344
						],
345
						'losses' => [
346
							'type'    => 'int',
347
							'default' => 0,
348
						],
349
						'second' => [
350
							'type'    => 'int',
351
							'default' => 0,
352
						],
353
						'third'  => [
354
							'type'    => 'int',
355
							'default' => 0,
356
						],
357
					],
358
				],
359
			],
360
		],
361
		'games'        => [
362
			'type'       => 'array',
363
			'subtype'    => 'object',
364
			'parameters' => [
365
				'id'     => [
366
					'type'    => 'int',
367
					'default' => null,
368
				],
369
				'teams'  => [
370
					'type'      => 'array',
371
					'subtype'   => 'id',
372
					'reference' => 'teams',
373
				],
374
				'scores' => [
375
					'type'         => 'array',
376
					'subtype'      => 'object',
377
					'keyReference' => 'teams',
378
					'parameters'   => [
379
						'score'  => [
380
							'type' => 'int',
381
						],
382
						'points' => [
383
							'type' => 'int',
384
						],
385
						'type'   => [
386
							'type'   => 'string',
387
							'values' => ['win', 'loss', 'draw', 'second', 'third'],
388
						],
389
					],
390
				],
391
			],
392
		],
393
	];
394
395
	protected static array $data;
396
397
	/**
398
	 * Validates if the data is correct
399
	 *
400
	 * Checks the data
401
	 *
402
	 * @param array $data         Data to check - can be modified (type casted from array to object)
403
	 * @param bool  $throwOnError If true, throw a InvalidImportDataException
404
	 *
405
	 * @return bool
406
	 * @throws InvalidImportDataException
407
	 */
408 20
	public static function validate(array &$data, bool $throwOnError = false) : bool {
409
410
		// Check for empty
411 20
		if (empty($data)) {
412 2
			if ($throwOnError) {
413 1
				throw new InvalidImportDataException('Import data is empty.');
414
			}
415 1
			return false;
416
		}
417
418 18
		self::$data = $data;
419
420
		try {
421 18
			foreach ($data as $key => &$value) {
422 18
				if (!isset(self::STRUCTURE[$key])) {
423 2
					throw new InvalidImportDataException('Unknown data key: '.$key);
424
				}
425 16
				self::validateParams($value, [$key], self::STRUCTURE[$key]);
426
			}
427 12
		} catch (InvalidImportDataException $e) {
428 12
			if ($throwOnError) {
429 6
				throw $e;
430
			}
431 6
			return false;
432
		}
433
434 6
		return true;
435
	}
436
437
	/**
438
	 * @param       $data
439
	 * @param array $keys
440
	 * @param array $setting
441
	 *
442
	 * @throws InvalidImportDataException
443
	 */
444 16
	public static function validateParams(&$data, array $keys, array $setting) : void {
445 16
		$primitive = false;
446
		// Check type
447 16
		if (isset($setting['type'])) {
448 16
			switch ($setting['type']) {
449 16
				case 'array':
450 10
					if (!is_array($data)) {
451 2
						throw new InvalidImportDataException('Invalid data type for: '.implode('->', $keys).'. Expected array.');
452
					}
453
					// Validate subtypes
454 8
					if (isset($setting['subtype'])) {
455 8
						foreach ($data as $key => $var) {
456 8
							self::validateType($var, array_merge($keys, [$key]), $setting['subtype']);
457
						}
458
					}
459 8
					if (isset($setting['keyReference'])) {
460 2
						foreach ($data as $key => $val) {
461 2
							self::validateReference($key, $setting['keyReference']);
462
						}
463
					}
464 8
					if (isset($setting['reference'])) {
465 8
						foreach ($data as $val) {
466 8
							self::validateReference($val, $setting['reference']);
467
						}
468
					}
469 8
					break;
470 14
				case 'object':
471 14
					if (!self::isObject($data)) {
472 2
						throw new InvalidImportDataException('Invalid data type for: '.implode('->', $keys).'. Expected object.');
473
					}
474 12
					break;
475
				default:
476 12
					$primitive = true;
477 12
					if (!array_key_exists('default', $setting) || !is_null($data)) {
478 12
						if (!is_array($setting['type'])) {
479 12
							$setting['type'] = [$setting['type']];
480
						}
481 12
						self::validateType($data, $keys, ...$setting['type']);
482
					}
483 12
					break;
484
			}
485
		}
486
487 12
		if (!$primitive) {
488 12
			if (isset($setting['parameters'])) {
489 12
				if ($setting['type'] === 'object') {
490 12
					foreach (((array) $data) as $key => $value) {
491 12
						self::validateParams($value, array_merge($keys, [$key]), $setting['parameters'][$key]);
492
					}
493
				}
494
				else {
495 6
					foreach (((array) $data) as $object) {
496 6
						foreach (((array) $object) as $key => $value) {
497 6
							self::validateParams($value, array_merge($keys, [$key]), $setting['parameters'][$key]);
498
						}
499
					}
500
				}
501
			}
502 8
			return;
503
		}
504 12
		if (isset($setting['values']) && !in_array($data, $setting['values'], true)) {
505 2
			throw new InvalidImportDataException('Invalid value for: '.implode('->', $keys).'. Expected values: '.implode(', ', $setting['values']).'.');
506
		}
507 10
		if (isset($setting['reference'])) {
508 6
			self::validateReference($data, $setting['reference']);
509
		}
510 10
	}
511
512
	/**
513
	 * Check type of a variable
514
	 *
515
	 * @param        $var
516
	 * @param array  $keys
517
	 * @param string ...$types Expected type
518
	 *
519
	 * @throws InvalidImportDataException
520
	 */
521 31
	public static function validateType($var, array $keys, string ...$types) : void {
522 31
		foreach ($types as $type) {
523 31
			switch ($type) {
524 31
				case 'array':
525 2
					if (is_array($var)) {
526 1
						return;
527
					}
528 1
					break;
529 29
				case 'object':
530 11
					if (self::isObject($var)) {
531 8
						return;
532
					}
533 3
					break;
534 24
				case 'int':
535 13
					if (is_int($var)) {
536 10
						return;
537
					}
538 3
					break;
539 22
				case 'string':
540 15
					if (is_string($var)) {
541 14
						return;
542
					}
543 1
					break;
544 17
				case 'id':
545 11
					if (is_int($var) || is_string($var)) {
546 10
						return;
547
					}
548 1
					break;
549 14
				case 'bool':
550 14
					if (is_bool($var)) {
551 12
						return;
552
					}
553 2
					break;
554
			}
555
		}
556 10
		throw new InvalidImportDataException('Invalid data type for: '.implode('->', $keys).'. Expected '.implode('|', $types).'.');
557
	}
558
559
	/**
560
	 * Checks if a variable is object or associative array
561
	 *
562
	 * @param $data
563
	 *
564
	 * @return bool
565
	 */
566 19
	public static function isObject($data) : bool {
567 19
		return is_object($data) || (is_array($data) && !empty($data) && array_keys($data) !== range(0, count($data) - 1));
568
	}
569
570
	/**
571
	 * Check if object of id exists in export data
572
	 *
573
	 * @param        $id
574
	 * @param string $key
575
	 *
576
	 * @throws InvalidImportDataException
577
	 */
578 8
	public static function validateReference($id, string $key) : void {
579 8
		if (isset(self::$data[$key])) {
580 8
			$ids = array_map(static function($object) {
581 8
				return ((object) $object)->id;
582 8
			}, self::$data[$key]);
583 8
			if (in_array($id, $ids, false)) {
584 8
				return;
585
			}
586 2
			throw new InvalidImportDataException('Invalid reference of '.$key.' on id: '.$id);
587
		}
588 4
	}
589
590
}