ImportValidator::validate()   B
last analyzed

Complexity

Conditions 7
Paths 14

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 16
nc 14
nop 2
dl 0
loc 28
ccs 16
cts 16
cp 1
crap 7
rs 8.8333
c 2
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 57
	public static function validate(array &$data, bool $throwOnError = false) : bool {
409
410
		// Check for empty
411 57
		if (empty($data)) {
412 2
			if ($throwOnError) {
413 1
				throw new InvalidImportDataException('Import data is empty.');
414
			}
415 1
			return false;
416
		}
417
418 55
		self::$data = $data;
419
420
		try {
421 55
			foreach ($data as $key => &$value) {
422 55
				if (!isset(self::STRUCTURE[$key])) {
423 2
					throw new InvalidImportDataException('Unknown data key: '.$key);
424
				}
425 53
				self::validateParams($value, [$key], self::STRUCTURE[$key]);
426
			}
427 43
			self::validateGroupParents($data);
428 18
		} catch (InvalidImportDataException $e) {
429 18
			if ($throwOnError) {
430 6
				throw $e;
431
			}
432 12
			return false;
433
		}
434
435 37
		return true;
436
	}
437
438
	/**
439
	 * @param       $data
440
	 * @param array $keys
441
	 * @param array $setting
442
	 *
443
	 * @throws InvalidImportDataException
444
	 */
445 53
	public static function validateParams(&$data, array $keys, array $setting) : void {
446 53
		$primitive = false;
447
		// Check type
448 53
		if (isset($setting['type'])) {
449 53
			switch ($setting['type']) {
450 53
				case 'array':
451 41
					if (!is_array($data)) {
452 2
						throw new InvalidImportDataException('Invalid data type for: '.implode('->', $keys).'. Expected array.');
453
					}
454
					// Validate subtypes
455 39
					if (isset($setting['subtype'])) {
456 39
						foreach ($data as $key => $var) {
457 39
							self::validateType($var, array_merge($keys, [$key]), $setting['subtype']);
458
						}
459
					}
460 39
					if (isset($setting['keyReference'])) {
461 5
						foreach ($data as $key => $val) {
462 5
							self::validateReference($key, $setting['keyReference']);
463
						}
464
					}
465 39
					if (isset($setting['reference'])) {
466 24
						foreach ($data as $val) {
467 24
							self::validateReference($val, $setting['reference']);
468
						}
469
					}
470 39
					break;
471 51
				case 'object':
472 40
					if (!self::isObject($data)) {
473 2
						throw new InvalidImportDataException('Invalid data type for: '.implode('->', $keys).'. Expected object.');
474
					}
475 38
					break;
476
				default:
477 49
					$primitive = true;
478 49
					if (!array_key_exists('default', $setting) || !is_null($data)) {
479 49
						if (!is_array($setting['type'])) {
480 49
							$setting['type'] = [$setting['type']];
481
						}
482 49
						self::validateType($data, $keys, ...$setting['type']);
483
					}
484 49
					break;
485
			}
486
		}
487
488 49
		if (!$primitive) {
489 49
			if (isset($setting['parameters'])) {
490 49
				if ($setting['type'] === 'object') {
491 38
					foreach (((array) $data) as $key => $value) {
492 38
						self::validateParams($value, array_merge($keys, [$key]), $setting['parameters'][$key]);
493
					}
494
				}
495
				else {
496 37
					foreach (((array) $data) as $object) {
497 37
						foreach (((array) $object) as $key => $value) {
498 37
							self::validateParams($value, array_merge($keys, [$key]), $setting['parameters'][$key]);
499
						}
500
					}
501
				}
502
			}
503 45
			return;
504
		}
505 49
		if (isset($setting['values']) && !in_array($data, $setting['values'], true)) {
506 2
			throw new InvalidImportDataException('Invalid value for: '.implode('->', $keys).'. Expected values: '.implode(', ', $setting['values']).'.');
507
		}
508 47
		if (isset($setting['reference'])) {
509 10
			self::validateReference($data, $setting['reference']);
510
		}
511 47
	}
512
513
	/**
514
	 * Check type of a variable
515
	 *
516
	 * @param        $var
517
	 * @param array  $keys
518
	 * @param string ...$types Expected type
519
	 *
520
	 * @throws InvalidImportDataException
521
	 */
522 68
	public static function validateType($var, array $keys, string ...$types) : void {
523 68
		foreach ($types as $type) {
524 68
			switch ($type) {
525 68
				case 'array':
526 2
					if (is_array($var)) {
527 1
						return;
528
					}
529 1
					break;
530 66
				case 'object':
531 42
					if (self::isObject($var)) {
532 39
						return;
533
					}
534 3
					break;
535 61
				case 'int':
536 35
					if (is_int($var)) {
537 32
						return;
538
					}
539 3
					break;
540 59
				case 'string':
541 52
					if (is_string($var)) {
542 51
						return;
543
					}
544 1
					break;
545 49
				case 'id':
546 39
					if (is_int($var) || is_string($var)) {
547 38
						return;
548
					}
549 1
					break;
550 41
				case 'bool':
551 41
					if (is_bool($var)) {
552 39
						return;
553
					}
554 2
					break;
555
			}
556
		}
557 10
		throw new InvalidImportDataException('Invalid data type for: '.implode('->', $keys).'. Expected '.implode('|', $types).'.');
558
	}
559
560
	/**
561
	 * Checks if a variable is object or associative array
562
	 *
563
	 * @param $data
564
	 *
565
	 * @return bool
566
	 */
567 56
	public static function isObject($data) : bool {
568 56
		return is_object($data) || (is_array($data) && !empty($data) && array_keys($data) !== range(0, count($data) - 1));
569
	}
570
571
	/**
572
	 * Check if object of id exists in export data
573
	 *
574
	 * @param        $id
575
	 * @param string $key
576
	 *
577
	 * @throws InvalidImportDataException
578
	 */
579 24
	public static function validateReference($id, string $key) : void {
580 24
		if (isset(self::$data[$key])) {
581 24
			$ids = array_map(static function($object) {
582 24
				return ((object) $object)->id;
583 24
			}, self::$data[$key]);
584 24
			if (in_array($id, $ids, false)) {
585 24
				return;
586
			}
587 2
			throw new InvalidImportDataException('Invalid reference of '.$key.' on id: '.$id);
588
		}
589 4
	}
590
591
	/**
592
	 * Validate that the import does not contain groups without a parent round
593
	 *
594
	 * @param array $data
595
	 *
596
	 * @return void
597
	 * @throws InvalidImportDataException
598
	 */
599 46
	public static function validateGroupParents(array $data) : void {
600 46
		if (empty($data['groups'])) {
601 20
			return;
602
		}
603 26
		$groups = [];
604 26
		foreach ($data['groups'] as $group) {
605 26
			$groups[] = is_array($group) ? $group['id'] : $group->id;
606
		}
607
608 26
		$rounds = $data['rounds'] ?? [];
609 26
		foreach ($rounds as $round) {
610 19
			foreach (is_array($round) ? $round['groups'] : $round->groups as $groupId) {
611 19
				unset($groups[array_search($groupId, $groups, true)]);
612
			}
613
		}
614
615 26
		if (!empty($groups)) {
616 8
			throw new InvalidImportDataException('Some groups are missing a parent round: '.implode(', ', $groups));
617
		}
618 18
	}
619
620
}