Completed
Push — master ( 7aab3a...2b8a1f )
by Vladimir
18:21 queued 01:15
created

Match::getPlayerIpAddress()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * This file contains functionality relating to the official matches played in the league
4
 *
5
 * @package    BZiON\Models
6
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
7
 */
8
use BZIon\Model\Column\Timestamp;
9
10
/**
11
 * A match played between two teams
12
 * @package    BZiON\Models
13
 */
14
class Match extends UrlModel implements NamedModel
15
{
16
    const OFFICIAL = "official";
17
    const SPECIAL  = "special";
18
    const FUN      = "fm";
19
20
    const TEAM_V_TEAM   = 0;
21
    const TEAM_V_MIXED  = 1;
22
    const MIXED_V_MIXED = 2;
23
24
    use Timestamp;
25
26
    /**
27
     * The ID of the first team of the match
28
     * @var int
29
     */
30
    protected $team_a;
31
32
    /**
33
     * The ID of the second team of the match
34
     * @var int
35
     */
36
    protected $team_b;
37
38
    /**
39
     * The color of the first team
40
     * @var string
41
     */
42
    protected $team_a_color;
43
44
    /**
45
     * The color of the second team
46
     * @var string
47
     */
48
    protected $team_b_color;
49
50
    /**
51
     * The match points (usually the number of flag captures) Team A scored
52
     * @var int
53
     */
54
    protected $team_a_points;
55
56
    /**
57
     * The match points Team B scored
58
     * @var int
59
     */
60
    protected $team_b_points;
61
62
    /**
63
     * The callsigns players used during this match.
64
     * @var string[]
65
     */
66
    protected $player_callsigns;
67
68
    /**
69
     * An associative array of IP addresses used by players during this match.
70
     * @var string[]
71
     */
72
    protected $player_ip_addresses;
73
74
    /**
75
     * The players who participated in Team A during this match.
76
     * @var Player[]
77
     */
78
    protected $team_a_players;
79
80
    /**
81
     * The players who participated in Team B during this match.
82
     * @var Player[]
83
     */
84
    protected $team_b_players;
85
86
    /**
87
     * The ELO score of Team A after the match
88
     * @var int
89
     */
90
    protected $team_a_elo_new;
91
92
    /**
93
     * The ELO score of Team B after the match
94
     * @var int
95
     */
96
    protected $team_b_elo_new;
97
98
    /**
99
     * The map ID used in the match if the league supports more than one map
100
     * @var int
101
     */
102
    protected $map;
103
104
    /**
105
     * The type of match that occurred. Valid options: official, fm, special
106
     *
107
     * @var string
108
     */
109
    protected $match_type;
110
111
    /**
112
     * A JSON string of events that happened during a match, such as captures and substitutions
113
     * @var string
114
     */
115
    protected $match_details;
116
117
    /**
118
     * The ID of the server where this match took place
119
     * @var int
120
     */
121
    protected $server;
122
123
    /**
124
     * The server location of there the match took place
125
     * @var string
126
     */
127
    protected $server_address;
128
129
    /**
130
     * The file name of the replay file of the match
131
     * @var string
132
     */
133
    protected $replay_file;
134
135
    /**
136
     * The value of the ELO score difference
137
     * @var int
138
     */
139
    protected $elo_diff;
140
141
    /**
142
     * The value of the player Elo difference
143
     * @var int
144
     */
145
    protected $player_elo_diff;
146
147
    /**
148
     * @var array
149
     */
150
    protected $player_elo_changelog;
151
152
    /**
153
     * The timestamp representing when the match information was last updated
154
     * @var TimeDate
155
     */
156
    protected $updated;
157
158
    /**
159
     * The duration of the match in minutes
160
     * @var int
161
     */
162
    protected $duration;
163
164
    /**
165
     * The ID of the person (i.e. referee) who last updated the match information
166
     * @var string
167
     */
168
    protected $entered_by;
169
170
    const DEFAULT_STATUS = 'entered';
171 42
172
    /**
173 42
     * The name of the database table used for queries
174 42
     */
175 42
    const TABLE = "matches";
176 42
177 42
    const CREATE_PERMISSION = Permission::ENTER_MATCH;
178 42
    const EDIT_PERMISSION = Permission::EDIT_MATCH;
179 42
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_MATCH;
180 42
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_MATCH;
181 42
182 42
    /**
183 42
     * {@inheritdoc}
184 42
     */
185 42
    protected function assignResult($match)
186 42
    {
187 42
        $this->team_a = $match['team_a'];
188 42
        $this->team_b = $match['team_b'];
189 42
        $this->team_a_color = $match['team_a_color'];
190 42
        $this->team_b_color = $match['team_b_color'];
191 42
        $this->team_a_points = $match['team_a_points'];
192 42
        $this->team_b_points = $match['team_b_points'];
193 42
        $this->team_a_elo_new = $match['team_a_elo_new'];
194 42
        $this->team_b_elo_new = $match['team_b_elo_new'];
195 42
        $this->map = $match['map'];
196
        $this->match_type = $match['match_type'];
197
        $this->match_details = $match['match_details'];
198
        $this->server_address = $match['server'];
199
        $this->replay_file = $match['replay_file'];
200
        $this->elo_diff = $match['elo_diff'];
201
        $this->player_elo_diff = $match['player_elo_diff'];
202 1
        $this->timestamp = TimeDate::fromMysql($match['timestamp']);
203
        $this->updated = TimeDate::fromMysql($match['updated']);
204 1
        $this->duration = $match['duration'];
205
        $this->entered_by = $match['entered_by'];
206
        $this->status = $match['status'];
207
208
        // For legacy support (e.g. Phinx migrations) we need to check if the key exists. Prior to migrations, it didn't
209
        // exist so Phinx migrations prior to 20170912201127_match_server_relationship will throw warnings.
210
        //
211
        // @todo Look for a better solution
212
        $this->server = isset($match['server_id']) ? $match['server_id'] : null;
213
    }
214 1
215
    /**
216 1
     * Get the name of the route that shows the object
217 1
     * @param  string $action The route's suffix
218 1
     * @return string
219 1
     */
220
    public static function getRouteName($action = 'show')
221
    {
222 1
        return "match_$action";
223
    }
224
225
    /**
226
     * Get a one word description of a match relative to a team (i.e. win, loss, or draw)
227
     *
228
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
229
     *
230
     * @return string Either "win", "loss", or "draw" relative to the team
231
     */
232 1
    public function getMatchDescription($teamID)
233
    {
234 1
        if ($this->getScore($teamID) > $this->getOpponentScore($teamID)) {
235
            return "win";
236
        } elseif ($this->getScore($teamID) < $this->getOpponentScore($teamID)) {
237
            return "loss";
238
        }
239
240
        return "tie";
241
    }
242
243
    /**
244 23
     * Get a one letter description of a match relative to a team (i.e. W, L, or T)
245
     *
246 23
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
247
     *
248 23
     * @return string Either "W", "L", or "T" relative to the team
249 2
     */
250
    public function getMatchLetter($teamID)
251
    {
252
        return strtoupper(substr($this->getMatchDescription($teamID), 0, 1));
253
    }
254 23
255 23
    /**
256
     * Get the score of a specific team
257
     *
258 23
     * @param int|string|TeamInterface $teamID The team we want the score for
259
     *
260
     * @return int The score that team received
261
     */
262 View Code Duplication
    public function getScore($teamID)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263
    {
264
        if ($teamID instanceof TeamInterface) {
265
            // Oh no! The caller gave us a Team model instead of an ID!
266
            $teamID = $teamID->getId();
267
        } elseif (is_string($teamID)) {
268 2
            // Make sure we're comparing lowercase strings
269
            $teamID = strtolower($teamID);
270 2
        }
271
272
        if ($this->getTeamA()->getId() == $teamID) {
273
            return $this->getTeamAPoints();
274
        }
275
276
        return $this->getTeamBPoints();
277
    }
278
279
    /**
280 36
     * Get the score of the opponent relative to a team
281
     *
282 36
     * @param int|string|TeamInterface $teamID The opponent of the team we want the score for
283 36
     *
284 2
     * @return int The score of the opponent
285
     */
286
    public function getOpponentScore($teamID)
287
    {
288 36
        return $this->getScore($this->getOpponent($teamID));
289 24
    }
290
291
    /**
292 14
     * Get the opponent of a match relative to a team ID
293
     *
294
     * @param int|string|TeamInterface $teamID The team who is known in a match
295
     *
296
     * @return TeamInterface The opponent team
297
     */
298 View Code Duplication
    public function getOpponent($teamID)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
    {
300 1
        if ($teamID instanceof TeamInterface) {
301
            $teamID = $teamID->getId();
302 1
        } elseif (is_string($teamID)) {
303
            $teamID = strtolower($teamID);
304
        }
305
306
        if ($this->getTeamA()->getId() == $teamID) {
307
            return $this->getTeamB();
308
        }
309
310
        return $this->getTeamA();
311
    }
312
313
    /**
314
     * Get the timestamp of the last update of the match
315
     *
316
     * @return TimeDate The match's update timestamp
317
     */
318
    public function getUpdated()
319
    {
320
        return $this->updated->copy();
321
    }
322 41
323
    /**
324 41
     * Set the timestamp of the match
325
     *
326 41
     * @param  mixed $timestamp The match's new timestamp
327 29
     * @return $this
328
     */
329
    public function setTimestamp($timestamp)
330 13
    {
331
        $this->updateProperty($this->timestamp, "timestamp", TimeDate::from($timestamp));
332
333
        return $this;
334
    }
335
336
    /**
337 41
     * Get the first team involved in the match
338
     * @return TeamInterface Team A
339 41
     */
340
    public function getTeamA()
341 41
    {
342 27
        $team = Team::get($this->team_a);
343
344
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
345 15
            return $team;
346
        }
347
348
        return new ColorTeam($this->team_a_color);
349
    }
350
351
    /**
352
     * Get the second team involved in the match
353
     * @return TeamInterface Team B
354
     */
355
    public function getTeamB()
356
    {
357
        $team = Team::get($this->team_b);
358
359
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
360
            return $team;
361
        }
362
363
        return new ColorTeam($this->team_b_color);
364
    }
365
366
    /**
367
     * Get the color of Team A
368
     * @return string
369
     */
370 39
    public function getTeamAColor()
371
    {
372 39
        return $this->team_a_color;
373
    }
374
375
    /**
376
     * Get the color of Team B
377
     * @return string
378
     */
379 39
    public function getTeamBColor()
380
    {
381 39
        return $this->team_b_color;
382
    }
383
384
    /**
385
     * Get the IP address a player used during this match.
386
     *
387
     * @param Player|int $id
388
     *
389 4
     * @return string|null
390
     */
391 4
    public function getPlayerIpAddress($id)
392 1
    {
393
        $this->lazyLoadMatchParticipants();
394
395 4
        if ($id instanceof Player) {
396 1
            $id = $id->getId();
397 4
        }
398 1
399
        return __::get($this->player_ip_addresses, $id, null);
400
    }
401 3
402
    /**
403
     * Get the callsign a player used during this match.
404
     *
405
     * @param Player|int $id
406
     *
407
     * @return string|null
408
     */
409
    public function getPlayerCallsign($id)
410
    {
411
        $this->lazyLoadMatchParticipants();
412
413
        if ($id instanceof Player) {
414
            $id = $id->getId();
415
        }
416
417
        return __::get($this->player_callsigns, $id, null);
418
    }
419
420
    /**
421
     * Get the list of players on Team A who participated in this match
422
     * @return Player[] Returns null if there were no players recorded for this match
423
     */
424 39
    public function getTeamAPlayers()
425
    {
426 39
        $this->lazyLoadMatchParticipants();
427 12
428
        return $this->team_a_players;
429
    }
430 30
431
    /**
432
     * Get the list of players on Team B who participated in this match
433
     * @return Player[] Returns null if there were no players recorded for this match
434
     */
435
    public function getTeamBPlayers()
436
    {
437 28
        $this->lazyLoadMatchParticipants();
438
439 28
        return $this->team_b_players;
440
    }
441
442
    /**
443
     * Get the list of players for a team in a match
444
     * @param  Team|int|null The team or team ID
445
     * @return Player[]|null Returns null if there were no players recorded for this match
446 28
     */
447
    public function getPlayers($team = null)
448 28
    {
449
        if ($team instanceof TeamInterface) {
450
            $team = $team->getId();
451
        }
452
453
        if ($this->getTeamA()->isValid() && $team === $this->getTeamA()->getId()) {
454
            return $this->getTeamAPlayers();
455
        } elseif ($this->getTeamB()->isValid() && $team === $this->getTeamB()->getId()) {
456
            return $this->getTeamBPlayers();
457
        }
458 1
459
        return array_merge($this->getTeamAPlayers(), $this->getTeamBPlayers());
460 1
    }
461 1
462
    /**
463 1
     * Set the players of the match's teams.
464
     *
465
     * @param int[]    $a_players   An array of player IDs on Team A
466
     * @param int[]    $b_players   An array of player IDs on Team B
467
     * @param string[] $a_ips       The IPs used by players on Team A. The order MUST match the order of $teamAPlayers
468
     * @param string[] $b_ips       The IPs used by players on Team B. The order MUST match the order of $teamBPlayers
469
     * @param string[] $a_callsigns The callsigns used by players on Team A. The order MUST match the order of $teamAPlayers
470
     * @param string[] $b_callsigns The callsigns used by players on Team B. The order MUST match the order of $teamBPlayers
471
     *
472
     * @return self
473
     */
474
    public function setTeamPlayers($a_players = [], $b_players = [], $a_ips = [], $b_ips = [], $a_callsigns = [], $b_callsigns = [])
475
    {
476
        $this->db->execute('DELETE FROM match_participation WHERE match_id = ?', [
477
            $this->getId(),
478
        ]);
479
480
        $matchParticipation = [];
481
482
        $this->matchParticipationEntryBuilder(
483
            $matchParticipation,
484
            ($this->getTeamA() instanceof Team) ? $this->team_a : null,
485
            0,
486
            $a_players,
487
            $a_ips,
488
            $a_callsigns
489
        );
490
        $this->matchParticipationEntryBuilder(
491
            $matchParticipation,
492
            ($this->getTeamB() instanceof Team) ? $this->team_b : null,
493
            1,
494
            $b_players,
495
            $b_ips,
496
            $b_callsigns
497 30
        );
498
499 30
        $this->db->insertBatch('match_participation', $matchParticipation);
500
501
        return $this;
502
    }
503
504
    /**
505
     * Load player participation in this match from its respective tables.
506
     */
507
    private function lazyLoadMatchParticipants()
508
    {
509 39
        if ($this->team_a_players !== null || $this->team_b_players !== null) {
510
            return;
511 39
        }
512
513
        $participation = $this->db->query('SELECT * FROM match_participation WHERE match_id = ?', [
514
            $this->getId(),
515
        ]);
516
517 1
        $loyalty = __::groupBy($participation, 'team_loyalty');
518
519 1
        $this->team_a_players = Player::arrayIdToModel(array_column(__::get($loyalty, 0, []), 'user_id'));
0 ignored issues
show
Documentation Bug introduced by
It seems like \Player::arrayIdToModel(..., array()), 'user_id')) of type array<integer,object<Model>> is incompatible with the declared type array<integer,object<Player>> of property $team_a_players.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
520 1
        $this->team_b_players = Player::arrayIdToModel(array_column(__::get($loyalty, 1, []), 'user_id'));
0 ignored issues
show
Documentation Bug introduced by
It seems like \Player::arrayIdToModel(..., array()), 'user_id')) of type array<integer,object<Model>> is incompatible with the declared type array<integer,object<Player>> of property $team_b_players.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
521
522
        $this->player_callsigns = array_column($participation, 'callsign', 'user_id');
523 1
        $this->player_ip_addresses = array_column($participation, 'ip_address', 'user_id');
524
    }
525 1
526 1
    /**
527 1
     * Get the first team's points
528 1
     * @return int Team A's points
529
     */
530
    public function getTeamAPoints()
531 1
    {
532
        return $this->team_a_points;
533
    }
534
535
    /**
536
     * Get the second team's points
537
     * @return int Team B's points
538
     */
539
    public function getTeamBPoints()
540 1
    {
541
        return $this->team_b_points;
542 1
    }
543
544 1
    /**
545 1
     * Set the match team points
546
     *
547
     * @param  int $teamAPoints Team A's points
548 1
     * @param  int $teamBPoints Team B's points
549
     * @return self
550
     */
551
    public function setTeamPoints($teamAPoints, $teamBPoints)
552
    {
553
        $this->updateProperty($this->team_a_points, "team_a_points", $teamAPoints);
554
        $this->updateProperty($this->team_b_points, "team_b_points", $teamBPoints);
555
556
        return $this;
557
    }
558 1
559
    /**
560 1
     * Set the match team colors
561
     *
562 1
     * @param  ColorTeam|string $teamAColor The color of team A
563 1
     * @param  ColorTeam|string $teamBColor The color of team B
564
     * @return self
565
     */
566 1
    public function setTeamColors($teamAColor, $teamBColor)
567
    {
568
        if ($this->isOfficial()) {
569
            throw new \Exception("Cannot change team colors in an official match");
570
        }
571
572
        if ($teamAColor instanceof ColorTeam) {
573
            $teamAColor = $teamAColor->getId();
574
        }
575
        if ($teamBColor instanceof ColorTeam) {
576
            $teamBColor = $teamBColor->getId();
577
        }
578
579
        $this->updateProperty($this->team_a_color, "team_a_color", $teamAColor);
580
        $this->updateProperty($this->team_b_color, "team_b_color", $teamBColor);
581
    }
582
583 7
    /**
584
     * Get the ELO difference applied to each team's old ELO
585 7
     *
586
     * @param bool $absoluteValue Whether or not to get the absolute value of the Elo difference
587
     *
588
     * @return int The ELO difference
589
     */
590
    public function getEloDiff($absoluteValue = true)
591
    {
592 7
        return ($absoluteValue) ? abs($this->elo_diff) : $this->elo_diff;
593
    }
594 7
595
    /**
596
     * Get the Elo difference applied to players
597
     *
598
     * @param bool $absoluteValue Whether or not to get the absolute value of the Elo difference
599
     *
600
     * @return int The Elo difference for players
601 7
     */
602
    public function getPlayerEloDiff($absoluteValue = true)
603 7
    {
604
        return ($absoluteValue && $this->player_elo_diff !== null) ? abs($this->player_elo_diff) : $this->player_elo_diff;
605
    }
606
607
    /**
608
     * Get the changelog for the player Elos for this match and cache them
609
     */
610 7
    private function getPlayerEloChangelog()
611
    {
612 7
        if ($this->player_elo_changelog !== null) {
613
            return;
614
        }
615
616
        $results = $this->db->query('SELECT * FROM player_elo WHERE match_id = ?', $this->getId());
617
618
        foreach ($results as $result) {
619
            $this->player_elo_changelog[$result['user_id']] = [
620
                'before' => $result['elo_previous'],
621 1
                'after'  => $result['elo_new']
622
            ];
623 1
        }
624 1
    }
625 1
626 1
    /**
627
     * Get the Elo for the player before this match occurred
628
     *
629
     * @param Player $player
630
     *
631
     * @return null|int
632
     */
633 View Code Duplication
    public function getPlayerEloBefore(Player $player)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
634
    {
635
        $this->getPlayerEloChangelog();
636
637
        if (isset($this->player_elo_changelog[$player->getId()])) {
638 1
            return $this->player_elo_changelog[$player->getId()]['before'];
639
        }
640 1
641 1
        return null;
642 1
    }
643 1
644
    /**
645
     * Get the Elo for the player after this match occurred
646
     *
647
     * @param Player $player
648
     *
649
     * @return null|int
650
     */
651 View Code Duplication
    public function getPlayerEloAfter(Player $player)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
652
    {
653 1
        $this->getPlayerEloChangelog();
654
655 1
        if (isset($this->player_elo_changelog[$player->getId()])) {
656
            return $this->player_elo_changelog[$player->getId()]['after'];
657
        }
658
659
        return null;
660
    }
661
662
    /**
663
     * Set the Elo difference applied to players
664
     *
665
     * @param int $diff
666
     */
667
    public function setPlayerEloDiff($diff)
668
    {
669
        $this->updateProperty($this->player_elo_diff, 'player_elo_diff', $diff);
670
    }
671
672
    /**
673
     * Get the first team's new ELO
674
     * @return int Team A's new ELO
675
     */
676
    public function getTeamAEloNew()
677
    {
678
        return $this->team_a_elo_new;
679
    }
680
681
    /**
682 23
     * Get the second team's new ELO
683
     * @return int Team B's new ELO
684 23
     */
685 9
    public function getTeamBEloNew()
686 14
    {
687 9
        return $this->team_b_elo_new;
688
    }
689
690 5
    /**
691
     * Get the first team's old ELO
692
     * @return int
693
     */
694
    public function getTeamAEloOld()
695
    {
696
        return $this->team_a_elo_new - $this->elo_diff;
697
    }
698 23
699
    /**
700 23
     * Get the second team's old ELO
701
     * @return int
702
     */
703
    public function getTeamBEloOld()
704
    {
705
        return $this->team_b_elo_new + $this->elo_diff;
706
    }
707
708
    /**
709
     * Get the team's new ELO
710
     * @param  Team $team The team whose new ELO to return
711
     * @return int|null   The new ELO, or null if the team provided has not
712
     *                    participated in the match
713
     */
714
    public function getTeamEloNew(Team $team)
715
    {
716
        if ($team->getId() == $this->team_a) {
717
            return $this->getTeamAEloNew();
718
        } elseif ($team->getId() == $this->team_b) {
719
            return $this->getTeamBEloNew();
720
        }
721
722
        return null;
723
    }
724
725
    /**
726
     * Get the team's old ELO
727
     * @param  Team $team The team whose old ELO to return
728 1
     * @return int|null   The old ELO, or null if the team provided has not
729
     *                    participated in the match
730 1
     */
731
    public function getTeamEloOld(Team $team)
732
    {
733
        if ($team->getId() == $this->team_a) {
734
            return $this->getTeamAEloOld();
735
        } elseif ($team->getId() == $this->team_b) {
736
            return $this->getTeamBEloOld();
737
        }
738
739
        return null;
740
    }
741
742
    /**
743
     * Get the map where the match was played on
744
     * @return Map Returns an invalid map if no map was found
745
     */
746
    public function getMap()
747
    {
748
        return Map::get($this->map);
749
    }
750
751
    /**
752 1
     * Set the map where the match was played
753
     * @param  int $map The ID of the map
754 1
     * @return self
755
     */
756
    public function setMap($map)
757
    {
758 1
        $this->updateProperty($this->map, "map", $map, "s");
759
760
        return $this;
761
    }
762
763
    /**
764
     * Get the type of official match this is. Whether it has just traditional teams or has mixed teams.
765 6
     *
766
     * Possible official match types:
767 6
     *   - Team vs Team
768
     *   - Team vs Mixed
769
     *   - Mixed vs Mixed
770
     *
771
     * @see Match::TEAM_V_TEAM
772
     * @see Match::TEAM_V_MIXED
773
     * @see Match::MIXED_V_MIXED
774
     *
775
     * @return int
776
     */
777
    public function getTeamMatchType()
778
    {
779
        if ($this->getTeamA()->supportsMatchCount() && $this->getTeamB()->supportsMatchCount()) {
780
            return self::TEAM_V_TEAM;
781
        } elseif ($this->getTeamA()->supportsMatchCount() xor $this->getTeamB()->supportsMatchCount()) {
782
            return self::TEAM_V_MIXED;
783
        }
784
785 2
        return self::MIXED_V_MIXED;
786
    }
787 2
788
    /**
789
     * Get the match type
790
     *
791
     * @return string 'official', 'fm', or 'special'
792
     */
793
    public function getMatchType()
794
    {
795 36
        return $this->match_type;
796
    }
797
798 36
    /**
799
     * Set the match type
800
     *
801 36
     * @param  string $matchType A valid match type; official, fm, special
802
     *
803
     * @return static
804
     */
805
    public function setMatchType($matchType)
806
    {
807
        return $this->updateProperty($this->match_type, "match_type", $matchType, 's');
808
    }
809 36
810
    /**
811
     * Get a JSON decoded array of events that occurred during the match
812
     * @return mixed|null Returns null if there were no events recorded for the match
813
     */
814
    public function getMatchDetails()
815 36
    {
816 22
        return json_decode($this->match_details);
817 15
    }
818 12
819 3
    /**
820
     * Get the server this match took place on
821 2
     *
822 1
     * @return Server
823
     */
824
    public function getServer()
825
    {
826
        return Server::get($this->server);
827 1
    }
828
829
    /**
830
     * Set the server this match took place on
831
     *
832
     * @param  int $serverID
833
     *
834 39
     * @return $this
835
     */
836 39
    public function setServer($serverID = null)
837
    {
838
        $this->updateProperty($this->server, 'server_id', $serverID);
839
840
        return $this;
841
    }
842
843
    /**
844
     * Get the server address of the server where this match took place
845 1
     *
846
     * @deprecated 0.10.0 Use Match::getServer() instead. Using this function is reserved for migrations/legacy support.
847 1
     *
848
     * @see 20170912201127_match_server_relationship.php
849
     *
850
     * @return string|null Returns null if there was no server address recorded
851
     */
852
    public function getServerAddress()
853 22
    {
854
        return $this->server_address;
855 22
    }
856
857
    /**
858
     * Get the name of the replay file for this specific map
859
     * @param  int    $length The length of the replay file name; it will be truncated
860
     * @return string Returns null if there was no replay file name recorded
861
     */
862
    public function getReplayFileName($length = 0)
863
    {
864
        if ($length > 0) {
865
            return substr($this->replay_file, 0, $length);
866
        }
867
868
        return $this->replay_file;
869
    }
870
871
    /**
872
     * Get the match duration
873
     * @return int The duration of the match in minutes
874
     */
875
    public function getDuration()
876
    {
877
        return $this->duration;
878 1
    }
879
880 1
    /**
881 1
     * Set the match duration
882
     *
883 1
     * @param  int  $duration The new duration of the match in minutes
884
     * @return self
885 1
     */
886
    public function setDuration($duration)
887
    {
888
        return $this->updateProperty($this->duration, "duration", $duration);
889 1
    }
890
891
    /**
892
     * Get the user who entered the match
893
     * @return Player
894
     */
895
    public function getEnteredBy()
896
    {
897
        return Player::get($this->entered_by);
898
    }
899
900
    /**
901
     * Get the loser of the match
902
     *
903
     * @return TeamInterface The team that was the loser or the team with the lower elo if the match was a draw
904
     */
905
    public function getLoser()
906
    {
907 42
        // Get the winner of the match
908
        $winner = $this->getWinner();
909
910 42
        // Get the team that wasn't the winner... Duh
911
        return $this->getOpponent($winner);
912 42
    }
913 23
914 20
    /**
915 14
     * Get the winner of a match
916
     *
917
     * @return TeamInterface The team that was the victor or the team with the lower elo if the match was a draw
918 42
     */
919 42
    public function getWinner()
920 3
    {
921
        // "As Mother Teresa once said, it's not enough if you win. Others must lose."
922
        //   -Stephen Colbert
923
924
        // Get the team that had its Elo increased or the team whose players had their Elo increased
925
        if ($this->elo_diff > 0 || $this->player_elo_diff > 0) {
926
            return $this->getTeamA();
927
        } elseif ($this->elo_diff < 0 || $this->player_elo_diff < 0) {
928 39
            return $this->getTeamB();
929
        } elseif ($this->team_a_points > $this->team_b_points) {
930 39
            // In case we're dealing with a match such an FM that doesn't have an ELO difference
931 39
            return $this->getTeamA();
932
        } elseif ($this->team_a_points < $this->team_b_points) {
933
            return $this->getTeamB();
934 39
        }
935 28
936 28
        // If the scores are the same, return Team A because well, fuck you that's why
937
        return $this->getTeamA();
938 28
    }
939
940
    /**
941
     * Determine whether the match was a draw
942
     * @return bool True if the match ended without any winning teams
943
     */
944
    public function isDraw()
945
    {
946 39
        return $this->team_a_points == $this->team_b_points;
947
    }
948
949
    /**
950
     * Find out whether the match involves a team
951 39
     *
952 23
     * @param  TeamInterface $team The team to check
953 17
     * @return bool
954 11
     */
955 11
    public function involvesTeam($team)
956
    {
957 11
        return $team->getId() == $this->getTeamA()->getId() || $team->getId() == $this->getTeamB()->getId();
958
    }
959
960
    /**
961 39
     * Find out if the match is played between official teams
962 39
     */
963 39
    public function isOfficial()
964
    {
965
        return self::OFFICIAL === $this->getMatchType();
966
    }
967
968
    /**
969
     * Reset the ELOs of the teams participating in the match
970
     *
971
     * @return self
972
     */
973
    public function resetTeamElos()
974
    {
975
        if ($this->match_type === self::OFFICIAL) {
976
            $this->getTeamA()->supportsMatchCount() && $this->getTeamA()->changeELO(-$this->elo_diff);
0 ignored issues
show
Bug introduced by
The method changeELO() does not seem to exist on object<TeamInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
977
            $this->getTeamB()->supportsMatchCount() && $this->getTeamB()->changeELO(+$this->elo_diff);
0 ignored issues
show
Bug introduced by
The method changeELO() does not seem to exist on object<TeamInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
978
        }
979
980
        return $this;
981
    }
982
983
    /**
984
     * Calculate the match's contribution to the team activity
985
     *
986
     * @return float
987
     */
988
    public function getActivity()
989
    {
990 45
        $daysPassed = $this->getTimestamp()->diffInSeconds();
991
        $daysPassed = $daysPassed / TimeDate::SECONDS_PER_MINUTE / TimeDate::MINUTES_PER_HOUR / TimeDate::HOURS_PER_DAY;
992
993
        $activity = 0.0116687059537612 * (pow(45 - $daysPassed, (1 / 6)) + atan(31.0 - $daysPassed) / 2.0);
994
995
        if (is_nan($activity) || $activity < 0.0) {
996 45
            return 0.0;
997 45
        }
998 45
999 45
        return $activity;
1000 45
    }
1001 45
1002 45
    /**
1003 45
     * Calculate the Elo differences for players and teams for a given match.
1004 45
     *
1005 45
     * @param  Team $a
1006 45
     * @param  Team $b
1007 45
     * @param  int  $a_points
1008 45
     * @param  int  $b_points
1009 45
     * @param  int[]|Player[] $a_players
1010
     * @param  int[]|Player[] $b_players
1011
     * @param  int  $duration
1012
     *
1013 45
     * @throws InvalidArgumentException When a "Mixed" team is entered without a player roster
1014
     *
1015 45
     * @return array
1016 42
     */
1017 42
    private static function calculateElos($a, $b, $a_points, $b_points, $a_players, $b_players, $duration)
1018
    {
1019 42
        // Get the type of official match
1020
        $matchType = Match::MIXED_V_MIXED;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1021 39
1022 39
        if ($a->supportsMatchCount() && $b->supportsMatchCount()) {
1023
            $matchType = Match::TEAM_V_TEAM;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1024
        } elseif ($a->supportsMatchCount() xor $b->supportsMatchCount()) {
1025 39
            $matchType = Match::TEAM_V_MIXED;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1026 29
        }
1027
1028 29
        if ($matchType == Match::TEAM_V_MIXED &&
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1029 29
            ((!$a->isValid() && empty($a_players)) || (!$b->isValid() && empty($b_players)))) {
1030
            throw new InvalidArgumentException('A Mixed team must have a player roster to calculate the Elo for team Elo differences');
1031 39
        }
1032 27
1033
        //
1034 27
        // Handle Player Elo Diff Calculations
1035 27
        //
1036
1037
        // By default, we won't have a player Elo difference since we won't force matches to have a roster
1038
        $playerEloDiff = null;
1039 42
1040 42
        $a_players_elo = 1200;
1041 42
        $b_players_elo = 1200;
1042
1043 42
        // Only bother to calculate a player Elo diff if we have players reported for both teams
1044
        if (!empty($a_players) && !empty($b_players)) {
1045
            $a_players_elo = self::getAveragePlayerElo($a_players);
1046
            $b_players_elo = self::getAveragePlayerElo($b_players);
1047
1048
            $playerEloDiff = self::calculateEloDiff($a_players_elo, $b_players_elo, $a_points, $b_points, $duration);
1049
        }
1050
1051
        //
1052
        // Handle Team Elo Diff Calculations
1053
        //
1054
1055
        // By default, we'll assume a Mixed vs Mixed official match where Elos do not change for teams
1056
        $teamEloDiff = null;
1057
1058
        // Work with calculations for team Elos to handle the following situations:
1059 39
        //   - Team vs Team  :: Use team Elos for calculations
1060
        //   - Team vs Mixed :: Use team Elo and the player average Elo for the "Mixed" team
1061 39
        if ($matchType == Match::TEAM_V_TEAM) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1062 39
            $teamEloDiff = self::calculateEloDiff($a->getElo(), $b->getElo(), $a_points, $b_points, $duration);
1063 14
        } elseif ($matchType == Match::TEAM_V_MIXED) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1064 27
            $a_team_elo = ($a->supportsMatchCount()) ? $a->getElo() : $a_players_elo;
1065 19
            $b_team_elo = ($b->supportsMatchCount()) ? $b->getElo() : $b_players_elo;
1066
1067 8
            $teamEloDiff = self::calculateEloDiff($a_team_elo, $b_team_elo, $a_points, $b_points, $duration);
1068
        }
1069
1070
        return [
1071 39
            'match_type' => $matchType,
1072 39
            'team_elo'   => $teamEloDiff,
1073
            'player_elo' => $playerEloDiff
1074 39
        ];
1075
    }
1076 2
1077
    /**
1078
     * Enter a new match to the database
1079
     * @param  int             $a          Team A's ID
1080 37
     * @param  int             $b          Team B's ID
1081
     * @param  int             $a_points   Team A's match points
1082
     * @param  int             $b_points   Team B's match points
1083
     * @param  int             $duration   The match duration in minutes
1084
     * @param  int|null        $entered_by The ID of the player reporting the match
1085
     * @param  string|DateTime $timestamp  When the match was played
1086
     * @param  int[]           $a_players  The IDs of the first team's players
1087
     * @param  int[]           $b_players  The IDs of the second team's players
1088
     * @param  string|null     $server     The address of the server where the match was played
1089
     * @param  string          $replayFile The name of the replay file of the match
1090
     * @param  int             $map        The ID of the map where the match was played, only for rotational leagues
1091
     * @param  string          $matchType  The type of match (e.g. official, fm, special)
1092
     * @param  string          $a_color    Team A's color
1093
     * @param  string          $b_color    Team b's color
1094
     * @param  string[]        $a_ipAddresses The IP addresses of players on Team A. The order of this array should
1095
     *                                        match the order of $a_players
1096
     * @param  string[]        $b_ipAddresses The IP addresses of players on Team B. The order of this array should
1097
     *                                        match the order of $b_players
1098
     * @param  string[]        $a_callsigns   The callsigns of players on Team A. The order of this array should match
1099
     *                                        the order of $a_players
1100 3
     * @param  string[]        $b_callsigns   The callsigns of players on Team B. The order of this array should match
1101
     *                                        the order of $b_players
1102 3
     *
1103
     * @throws \Exception               When no testing environment has been configured for the database.
1104
     * @throws InvalidArgumentException When a ColorTeam is selected for an official match and no players are defined
1105
     *                                  for that team
1106 3
     *
1107 3
     * @return Match           An object representing the match that was just entered
1108
     */
1109 3
    public static function enterMatch(
1110
        $a, $b, $a_points, $b_points, $duration, $entered_by, $timestamp = "now",
1111 3
        $a_players = array(), $b_players = array(), $server = null, $replayFile = null,
1112 3
        $map = null, $matchType = "official", $a_color = null, $b_color = null,
1113
        $a_ipAddresses = array(), $b_ipAddresses = array(), $a_callsigns = array(), $b_callsigns = array()
1114
    ) {
1115 3
        $matchData = array(
1116 3
            'team_a_color'   => strtolower($a_color),
1117 3
            'team_b_color'   => strtolower($b_color),
1118 3
            'team_a_points'  => $a_points,
1119 3
            'team_b_points'  => $b_points,
1120
            'timestamp'      => TimeDate::from($timestamp)->toMysql(),
1121
            'duration'       => $duration,
1122 3
            'entered_by'     => $entered_by,
1123
            'server'         => $server,
1124 3
            'replay_file'    => $replayFile,
1125 3
            'map'            => $map,
1126
            'status'         => 'entered',
1127 3
            'match_type'     => $matchType
1128 3
        );
1129 3
1130
        // (P)layer Elo Diff and (T)eam Elo Diff; respectively
1131
        $tEloDiff = null;
0 ignored issues
show
Unused Code introduced by
$tEloDiff is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1132 3
1133 3
        if ($matchType === self::OFFICIAL) {
1134 3
            $team_a = Team::get($a);
1135
            $team_b = Team::get($b);
1136
1137 3
            $eloCalcs = self::calculateElos($team_a, $team_b, $a_points, $b_points, $a_players, $b_players, $duration);
1138 3
1139
            $matchData['elo_diff'] = $tEloDiff = $eloCalcs['team_elo'];
1140
            $matchData['player_elo_diff'] = $eloCalcs['player_elo'];
1141
1142
            // Update team ELOs
1143 1
            if ($team_a->isValid()) {
1144
                $team_a->adjustElo($tEloDiff);
1145 1
1146
                $matchData['team_a'] = $a;
1147
                $matchData['team_a_elo_new'] = $team_a->getElo();
1148
            }
1149
            if ($team_b->isValid()) {
1150
                $team_b->adjustElo(-$tEloDiff);
1151
1152 26
                $matchData['team_b'] = $b;
1153
                $matchData['team_b_elo_new'] = $team_b->getElo();
1154 26
            }
1155 26
        }
1156
1157
        $match = self::create($matchData, 'updated');
1158
        $match->setTeamPlayers(
1159
            $a_players, $b_players,
1160
            $a_ipAddresses, $b_ipAddresses,
1161
            $a_callsigns, $b_callsigns
1162
        );
1163
        $match->updateMatchCount();
1164
        $match->updatePlayerElo();
1165
1166
        return $match;
1167
    }
1168
1169
    /**
1170
     * Calculate the ELO score difference
1171 1
     *
1172
     * Computes the ELO score difference on each team after a match, based on
1173 1
     * GU League's rules.
1174
     *
1175 1
     * @param  int $a_elo    Team A's current ELO score
1176 1
     * @param  int $b_elo    Team B's current ELO score
1177
     * @param  int $a_points Team A's match points
1178
     * @param  int $b_points Team B's match points
1179
     * @param  int $duration The match duration in minutes
1180
     * @return int The ELO score difference
1181 3
     */
1182
    public static function calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration)
1183 3
    {
1184
        $prob = 1.0 / (1 + pow(10, (($b_elo - $a_elo) / 400.0)));
1185
        if ($a_points > $b_points) {
1186
            $diff = 50 * (1 - $prob);
1187
        } elseif ($a_points == $b_points) {
1188
            $diff = 50 * (0.5 - $prob);
1189 22
        } else {
1190
            $diff = 50 * (0 - $prob);
1191 22
        }
1192
1193 22
        // Apply ELO modifiers from `config.yml`
1194 22
        $durations = Service::getParameter('bzion.league.duration');
1195
        $diff *= (isset($durations[$duration])) ? $durations[$duration] : 1;
1196 21
1197 9
        if (abs($diff) < 1 && $diff != 0) {
1198
            // ELOs such as 0.75 should round up to 1...
1199 21
            return ($diff > 0) ? 1 : -1;
1200
        }
1201 2
1202 2
        // ...everything else is rounded down (-3.7 becomes -3 and 48.1 becomes 48)
1203 2
        return intval($diff);
1204
    }
1205
1206
    /**
1207
     * Find if a match's stored ELO is correct
1208
     */
1209
    public function isEloCorrect()
1210
    {
1211
        return $this->elo_diff === $this->calculateEloDiff(
1212
            $this->getTeamAEloOld(),
1213 22
            $this->getTeamBEloOld(),
1214 22
            $this->getTeamAPoints(),
1215 22
            $this->getTeamBPoints(),
1216 22
            $this->getDuration()
1217 22
        );
1218 22
    }
1219
1220
    /**
1221
     * Remove Elo recordings for players participating in this match
1222
     */
1223
    public function resetPlayerElos()
1224
    {
1225
        $this->db->execute('DELETE FROM player_elo WHERE match_id = ?', [$this->getId()]);
1226
    }
1227
1228
    /**
1229
     * Recalculate the match's elo and adjust the team ELO values
1230
     */
1231
    public function recalculateElo()
1232
    {
1233
        if ($this->match_type !== self::OFFICIAL) {
1234
            return;
1235
        }
1236
1237 1
        $a = $this->getTeamA();
1238
        $b = $this->getTeamB();
1239
1240
        $this->resetPlayerElos();
1241
1242
        foreach ($this->getPlayers() as $player) {
0 ignored issues
show
Bug introduced by
The expression $this->getPlayers() of type array<integer,object<Player>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1243 1
            $player->invalidateMatchFromCache($this);
1244 1
        }
1245 1
1246 1
        $eloCalcs = self::calculateElos(
1247 1
            $a, $b,
0 ignored issues
show
Compatibility introduced by
$a of type object<TeamInterface> is not a sub-type of object<Team>. It seems like you assume a concrete implementation of the interface TeamInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
Compatibility introduced by
$b of type object<TeamInterface> is not a sub-type of object<Team>. It seems like you assume a concrete implementation of the interface TeamInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1248
            $this->getTeamAPoints(), $this->getTeamBPoints(),
1249
            $this->getTeamAPlayers(), $this->getTeamBPlayers(),
1250 1
            $this->getDuration()
1251
        );
1252
1253 1
        $elo = $eloCalcs['team_elo'];
1254
1255
        $this->updateProperty($this->elo_diff, 'elo_diff', $elo);
1256
        $this->updateProperty($this->player_elo_diff, 'player_elo_diff', $eloCalcs['player_elo']);
1257 1
1258
        if ($a->supportsMatchCount()) {
1259 1
            $a->adjustElo($elo);
1260
            $this->updateProperty($this->team_a_elo_new, 'team_a_elo_new', $a->getElo());
1261
        }
1262
1263 1
        if ($b->supportsMatchCount()) {
1264 1
            $b->adjustElo(-$elo);
1265 1
            $this->updateProperty($this->team_b_elo_new, 'team_b_elo_new', $b->getElo());
1266
        }
1267 1
1268 1
        $this->updatePlayerElo();
1269 1
    }
1270
1271
    /**
1272 1
     * Get all the matches in the database
1273
     */
1274 1
    public static function getMatches()
1275
    {
1276
        return self::getQueryBuilder()->active()->getModels();
1277
    }
1278 1
1279
    /**
1280
     * Get a query builder for matches
1281
     * @return MatchQueryBuilder
1282
     */
1283 1
    public static function getQueryBuilder()
1284
    {
1285
        return new MatchQueryBuilder('Match', array(
1286
            'columns' => array(
1287 1
                'firstTeam'        => 'team_a',
1288
                'secondTeam'       => 'team_b',
1289
                'firstTeamPoints'  => 'team_a_points',
1290
                'secondTeamPoints' => 'team_b_points',
1291
                'time'             => 'timestamp',
1292
                'map'              => 'map',
1293
                'server'           => 'server_id',
1294
                'type'             => 'match_type',
1295 1
                'status'           => 'status'
1296
            ),
1297 1
        ));
1298 1
    }
1299
1300
    /**
1301
     * {@inheritdoc}
1302
     */
1303
    public function delete()
1304
    {
1305
        $this->updateMatchCount(true);
1306
1307
        parent::delete();
1308
    }
1309 28
1310 28
    /**
1311 2
     * {@inheritdoc}
1312
     */
1313
    public function getName()
1314 28
    {
1315 28
        $description = '';
1316
1317 28
        switch ($this->getMatchType()) {
1318
            case self::OFFICIAL:
1319
                // Only show Elo diff if both teams are actual teams
1320
                if ($this->getTeamA()->supportsMatchCount() && $this->getTeamB()->supportsMatchCount()) {
1321
                    $description = "(+/- {$this->getEloDiff()})";
1322
                }
1323
                break;
1324
1325 42
            case self::FUN:
1326
                $description = 'Fun Match:';
1327 42
                break;
1328 4
1329
            case self::SPECIAL:
1330
                $description = 'Special Match:';
1331 39
                break;
1332
1333 39
            default:
1334 19
                break;
1335 19
        }
1336
1337 21
        return trim(sprintf('%s %s [%d] vs [%d] %s',
1338 21
            $description,
1339
            $this->getWinner()->getName(),
1340 39
            $this->getScore($this->getWinner()),
1341
            $this->getScore($this->getLoser()),
1342
            $this->getLoser()->getName()
1343
        ));
1344
    }
1345 42
1346
    /**
1347 42
     * Recalculates match history for all teams and matches
1348 4
     *
1349
     * Recalculation is done as follows:
1350
     * 1. A match is chosen as a starting point - it's stored old team ELOs are
1351 39
     *    considered correct
1352
     * 2. Team ELOs are reset to their values at the starting point
1353 39
     * 3. Each match that occurred since the first specified match has its ELO
1354 28
     *    recalculated based on the current team values, and the new match data
1355 28
     *    and team ELOs are stored in the database
1356
     *
1357
     * @param Match $match The first match
1358 39
     *
1359 29
     * @throws Exception
1360 29
     */
1361
    public static function recalculateMatchesSince(Match $match)
1362 39
    {
1363
        try {
1364
            // Commented out to prevent ridiculously large recalculations
1365
            //set_time_limit(0);
1366
1367
            $query = Match::getQueryBuilder()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1368
                ->where('status')->notEquals('deleted')
1369
                ->where('type')->equals(Match::OFFICIAL)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1370
                ->where('time')->isAfter($match->getTimestamp(), $inclusive = true)
1371
                ->sortBy('time');
1372
1373
            /** @var Match[] $matches */
1374
            $matches = $query->getModels($fast = true);
1375
1376
            // Send the total count to client-side javascript
1377
            echo count($matches) . "\n";
1378
1379
            // Start a transaction so tables are locked and we don't stay with
1380
            // messed up data if something goes wrong
1381
            Database::getInstance()->startTransaction();
1382
1383
            $teamsReset = [];
1384
1385
            // Reset match teams, in case the selected match is deleted and does
1386
            // not show up in the list of matches to recalculate
1387
            if ($match->getTeamA()->supportsMatchCount()) {
1388
                $match->getTeamA()->setElo($match->getTeamAEloOld());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method setElo() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1389
                $teamsReset[ $match->getTeamA()->getId() ] = true;
1390
            }
1391
            if ($match->getTeamB()->supportsMatchCount()) {
1392
                $match->getTeamB()->setElo($match->getTeamBEloOld());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method setElo() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1393
                $teamsReset[ $match->getTeamB()->getId() ] = true;
1394
            }
1395
1396
            foreach ($matches as $i => &$match) {
1397
                // Reset teams' ELOs if they haven't been reset already
1398 View Code Duplication
                if ($match->getTeamA()->supportsMatchCount() && !isset($teamsReset[ $match->getTeamA()->getId() ])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1399
                    $teamsReset[ $match->getTeamA()->getId() ] = true;
1400
                    $match->getTeamA()->setElo($match->getTeamAEloOld());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method setElo() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1401
                }
1402 View Code Duplication
                if ($match->getTeamB()->supportsMatchCount() && !isset($teamsReset[ $match->getTeamB()->getId() ])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1403
                    $teamsReset[ $match->getTeamB()->getId() ] = true;
1404
                    $match->getTeamB()->setElo($match->getTeamBEloOld());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method setElo() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1405
                }
1406
1407
                $match->recalculateElo();
1408
1409
                // Send an update to the client-side javascript, so that a
1410
                // progress bar can be updated
1411
                echo "m";
1412
            }
1413
        } catch (Exception $e) {
1414
            Database::getInstance()->rollback();
1415
            Database::getInstance()->finishTransaction();
1416
            throw $e;
1417
        }
1418
1419
        Database::getInstance()->finishTransaction();
1420
1421
        echo "\n\nCalculation successful\n";
1422
    }
1423
1424
    /**
1425
     * Get the average ELO for an array of players
1426
     *
1427
     * @param int[]|Player[] $players
1428
     *
1429
     * @return float|int
1430
     */
1431
    private static function getAveragePlayerElo($players)
1432
    {
1433
        $getElo = function ($n) {
1434
            if ($n instanceof Player) {
1435
                return $n->getElo();
1436
            }
1437
1438
            return Player::get($n)->getElo();
1439
        };
1440
1441
        return array_sum(array_map($getElo, $players)) / count($players);
1442
    }
1443
1444
    /**
1445
     * Update the match count of the teams participating in the match
1446
     *
1447
     * @param bool $decrement Whether to decrement instead of incrementing the match count
1448
     */
1449
    private function updateMatchCount($decrement = false)
1450
    {
1451
        if ($this->match_type !== self::OFFICIAL) {
1452
            return;
1453
        }
1454
1455
        $diff = ($decrement) ? -1 : 1;
1456
1457
        if ($this->isDraw()) {
1458
            $this->getTeamA()->supportsMatchCount() && $this->getTeamA()->changeMatchCount($diff, 'draw');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeMatchCount() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1459
            $this->getTeamB()->supportsMatchCount() && $this->getTeamB()->changeMatchCount($diff, 'draw');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeMatchCount() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1460
        } else {
1461
            $this->getWinner()->supportsMatchCount() && $this->getWinner()->changeMatchCount($diff, 'win');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeMatchCount() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1462
            $this->getLoser()->supportsMatchCount()  && $this->getLoser()->changeMatchCount($diff, 'loss');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeMatchCount() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1463
        }
1464
    }
1465
1466
    /**
1467
     * Update the Elos for the participating players in a match
1468
     */
1469
    private function updatePlayerElo()
1470
    {
1471
        if ($this->match_type !== self::OFFICIAL || $this->getPlayerEloDiff() === null) {
1472
            return;
1473
        }
1474
1475
        $eloDiff = $this->getPlayerEloDiff(false);
1476
1477
        foreach ($this->getTeamAPlayers() as $player) {
1478
            $player->adjustElo($eloDiff, $this);
1479
            $player->setLastMatch($this->getId());
1480
        }
1481
1482
        foreach ($this->getTeamBPlayers() as $player) {
1483
            $player->adjustElo(-$eloDiff, $this);
1484
            $player->setLastMatch($this->getId());
1485
        }
1486
    }
1487
1488
    /**
1489
     * Build an array of match participation records for a given match.
1490
     *
1491
     * @param array    $storage     The referenced array that'll be storing all of the created references.
1492
     * @param int|null $teamID      The ID of the Team this player played for, or NULL if players didn't play for a team
1493
     * @param int      $teamLoyalty Representation for team color: 0 for "Team A" or 1 for "Team B"
1494
     * @param int[]    $playerIDs   The BZiON player IDs that played for this team
1495
     * @param string[] $ipAddresses The IP addresses for the recorded players, the order must match the order of $playerIDs
1496
     * @param string[] $callsigns   The callsigns for the recorded players, the order must match the order of $playerIDs
1497
     */
1498
    private function matchParticipationEntryBuilder(array &$storage, $teamID, $teamLoyalty, array $playerIDs, array $ipAddresses = [], array $callsigns = [])
1499
    {
1500
        foreach ($playerIDs as $index => $playerID) {
1501
            if (empty($playerID)) {
1502
                continue;
1503
            }
1504
1505
            $workspace = [
1506
                'match_id' => $this->getId(),
1507
                'user_id' => $playerID,
1508
                'team_id' => $teamID,
1509
                'callsign' => __::get($callsigns, $index, null),
1510
                'ip_address' => __::get($ipAddresses, $index, null),
1511
                'team_loyalty' => $teamLoyalty,
1512
            ];
1513
1514
            $storage[] = $workspace;
1515
        }
1516
    }
1517
}
1518