Completed
Push — release/0.10.0 ( f95fb1...d0e3a0 )
by Vladimir
02:29
created

Match::isEloCorrect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 0
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 BZIDs of players part of Team A who participated in the match, separated by commas
64
     * @var string
65
     */
66
    protected $team_a_players;
67
68
    /**
69
     * The BZIDs of players part of Team B who participated in the match, separated by commas
70
     * @var string
71
     */
72
    protected $team_b_players;
73
74
    /**
75
     * The ELO score of Team A after the match
76
     * @var int
77
     */
78
    protected $team_a_elo_new;
79
80
    /**
81
     * The ELO score of Team B after the match
82
     * @var int
83
     */
84
    protected $team_b_elo_new;
85
86
    /**
87
     * The map ID used in the match if the league supports more than one map
88
     * @var int
89
     */
90
    protected $map;
91
92
    /**
93
     * The type of match that occurred. Valid options: official, fm, special
94
     *
95
     * @var string
96
     */
97
    protected $match_type;
98
99
    /**
100
     * A JSON string of events that happened during a match, such as captures and substitutions
101
     * @var string
102
     */
103
    protected $match_details;
104
105
    /**
106
     * The ID of the server where this match took place
107
     * @var int
108
     */
109
    protected $server;
110
111
    /**
112
     * The server location of there the match took place
113
     * @var string
114
     */
115
    protected $server_address;
116
117
    /**
118
     * The file name of the replay file of the match
119
     * @var string
120
     */
121
    protected $replay_file;
122
123
    /**
124
     * The value of the ELO score difference
125
     * @var int
126
     */
127
    protected $elo_diff;
128
129
    /**
130
     * The value of the player Elo difference
131
     * @var int
132
     */
133
    protected $player_elo_diff;
134
135
    /**
136
     * @var array
137
     */
138
    protected $player_elo_changelog;
139
140
    /**
141
     * The timestamp representing when the match information was last updated
142
     * @var TimeDate
143
     */
144
    protected $updated;
145
146
    /**
147
     * The duration of the match in minutes
148
     * @var int
149
     */
150
    protected $duration;
151
152
    /**
153
     * The ID of the person (i.e. referee) who last updated the match information
154
     * @var string
155
     */
156
    protected $entered_by;
157
158
    /**
159
     * The status of the match. Can be 'entered', 'disabled', 'deleted' or 'reported'
160
     * @var string
161
     */
162
    protected $status;
163
164
    /**
165
     * The name of the database table used for queries
166
     */
167
    const TABLE = "matches";
168
169
    const CREATE_PERMISSION = Permission::ENTER_MATCH;
170
    const EDIT_PERMISSION = Permission::EDIT_MATCH;
171
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_MATCH;
172
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_MATCH;
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    protected function assignResult($match)
178
    {
179
        $this->team_a = $match['team_a'];
180
        $this->team_b = $match['team_b'];
181
        $this->team_a_color = $match['team_a_color'];
182
        $this->team_b_color = $match['team_b_color'];
183
        $this->team_a_points = $match['team_a_points'];
184
        $this->team_b_points = $match['team_b_points'];
185
        $this->team_a_players = $match['team_a_players'];
186
        $this->team_b_players = $match['team_b_players'];
187
        $this->team_a_elo_new = $match['team_a_elo_new'];
188
        $this->team_b_elo_new = $match['team_b_elo_new'];
189
        $this->map = $match['map'];
190
        $this->match_type = $match['match_type'];
191
        $this->match_details = $match['match_details'];
192
        $this->server_address = $match['server'];
193
        $this->replay_file = $match['replay_file'];
194
        $this->elo_diff = $match['elo_diff'];
195
        $this->player_elo_diff = $match['player_elo_diff'];
196
        $this->timestamp = TimeDate::fromMysql($match['timestamp']);
197
        $this->updated = TimeDate::fromMysql($match['updated']);
198
        $this->duration = $match['duration'];
199
        $this->entered_by = $match['entered_by'];
200
        $this->status = $match['status'];
201
202
        // For legacy support (e.g. Phinx migrations) we need to check if the key exists. Prior to migrations, it didn't
203
        // exist so Phinx migrations prior to 20170912201127_match_server_relationship will throw warnings.
204
        //
205
        // @todo Look for a better solution
206
        $this->server = isset($match['server_id']) ? $match['server_id'] : null;
207
    }
208
209
    /**
210
     * Get the name of the route that shows the object
211
     * @param  string $action The route's suffix
212
     * @return string
213
     */
214
    public static function getRouteName($action = 'show')
215
    {
216
        return "match_$action";
217
    }
218
219
    /**
220
     * Get a one word description of a match relative to a team (i.e. win, loss, or draw)
221
     *
222
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
223
     *
224
     * @return string Either "win", "loss", or "draw" relative to the team
225
     */
226
    public function getMatchDescription($teamID)
227
    {
228
        if ($this->getScore($teamID) > $this->getOpponentScore($teamID)) {
229
            return "win";
230
        } elseif ($this->getScore($teamID) < $this->getOpponentScore($teamID)) {
231
            return "loss";
232
        }
233
234
        return "tie";
235
    }
236
237
    /**
238
     * Get a one letter description of a match relative to a team (i.e. W, L, or T)
239
     *
240
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
241
     *
242
     * @return string Either "W", "L", or "T" relative to the team
243
     */
244
    public function getMatchLetter($teamID)
245
    {
246
        return strtoupper(substr($this->getMatchDescription($teamID), 0, 1));
247
    }
248
249
    /**
250
     * Get the score of a specific team
251
     *
252
     * @param int|string|TeamInterface $teamID The team we want the score for
253
     *
254
     * @return int The score that team received
255
     */
256 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...
257
    {
258
        if ($teamID instanceof TeamInterface) {
259
            // Oh no! The caller gave us a Team model instead of an ID!
260
            $teamID = $teamID->getId();
261
        } elseif (is_string($teamID)) {
262
            // Make sure we're comparing lowercase strings
263
            $teamID = strtolower($teamID);
264
        }
265
266
        if ($this->getTeamA()->getId() == $teamID) {
267
            return $this->getTeamAPoints();
268
        }
269
270
        return $this->getTeamBPoints();
271
    }
272
273
    /**
274
     * Get the score of the opponent relative to a team
275
     *
276
     * @param int|string|TeamInterface $teamID The opponent of the team we want the score for
277
     *
278
     * @return int The score of the opponent
279
     */
280
    public function getOpponentScore($teamID)
281
    {
282
        return $this->getScore($this->getOpponent($teamID));
283
    }
284
285
    /**
286
     * Get the opponent of a match relative to a team ID
287
     *
288
     * @param int|string|TeamInterface $teamID The team who is known in a match
289
     *
290
     * @return TeamInterface The opponent team
291
     */
292 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...
293
    {
294
        if ($teamID instanceof TeamInterface) {
295
            $teamID = $teamID->getId();
296
        } elseif (is_string($teamID)) {
297
            $teamID = strtolower($teamID);
298
        }
299
300
        if ($this->getTeamA()->getId() == $teamID) {
301
            return $this->getTeamB();
302
        }
303
304
        return $this->getTeamA();
305
    }
306
307
    /**
308
     * Get the timestamp of the last update of the match
309
     *
310
     * @return TimeDate The match's update timestamp
311
     */
312
    public function getUpdated()
313
    {
314
        return $this->updated->copy();
315
    }
316
317
    /**
318
     * Set the timestamp of the match
319
     *
320
     * @param  mixed $timestamp The match's new timestamp
321
     * @return $this
322
     */
323
    public function setTimestamp($timestamp)
324
    {
325
        $this->updateProperty($this->timestamp, "timestamp", TimeDate::from($timestamp));
326
327
        return $this;
328
    }
329
330
    /**
331
     * Get the first team involved in the match
332
     * @return TeamInterface Team A
333
     */
334
    public function getTeamA()
335
    {
336
        $team = Team::get($this->team_a);
337
338
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
339
            return $team;
340
        }
341
342
        return new ColorTeam($this->team_a_color);
343
    }
344
345
    /**
346
     * Get the second team involved in the match
347
     * @return TeamInterface Team B
348
     */
349
    public function getTeamB()
350
    {
351
        $team = Team::get($this->team_b);
352
353
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
354
            return $team;
355
        }
356
357
        return new ColorTeam($this->team_b_color);
358
    }
359
360
    /**
361
     * Get the color of Team A
362
     * @return string
363
     */
364
    public function getTeamAColor()
365
    {
366
        return $this->team_a_color;
367
    }
368
369
    /**
370
     * Get the color of Team B
371
     * @return string
372
     */
373
    public function getTeamBColor()
374
    {
375
        return $this->team_b_color;
376
    }
377
378
    /**
379
     * Get the list of players on Team A who participated in this match
380
     * @return Player[] Returns null if there were no players recorded for this match
381
     */
382
    public function getTeamAPlayers()
383
    {
384
        return $this->parsePlayers($this->team_a_players);
385
    }
386
387
    /**
388
     * Get the list of players on Team B who participated in this match
389
     * @return Player[] Returns null if there were no players recorded for this match
390
     */
391
    public function getTeamBPlayers()
392
    {
393
        return $this->parsePlayers($this->team_b_players);
394
    }
395
396
    /**
397
     * Get the list of players for a team in a match
398
     * @param  Team|int|null The team or team ID
399
     * @return Player[]|null Returns null if there were no players recorded for this match
400
     */
401
    public function getPlayers($team = null)
402
    {
403
        if ($team instanceof TeamInterface) {
404
            $team = $team->getId();
405
        }
406
407
        if ($this->getTeamA()->isValid() && $team === $this->getTeamA()->getId()) {
408
            return $this->getTeamAPlayers();
409
        } elseif ($this->getTeamB()->isValid() && $team === $this->getTeamB()->getId()) {
410
            return $this->getTeamBPlayers();
411
        }
412
413
        return $this->parsePlayers($this->team_a_players . "," . $this->team_b_players);
414
    }
415
416
    /**
417
     * Set the players of the match's teams
418
     *
419
     * @param int[] $teamAPlayers An array of player IDs
420
     * @param int[] $teamBPlayers An array of player IDs
421
     * @return self
422
     */
423
    public function setTeamPlayers($teamAPlayers = array(), $teamBPlayers = array())
424
    {
425
        $this->updateProperty($this->team_a_players, "team_a_players", implode(',', $teamAPlayers));
426
        $this->updateProperty($this->team_b_players, "team_b_players", implode(',', $teamBPlayers));
427
428
        return $this;
429
    }
430
431
    /**
432
     * Get an array of players based on a string representation
433
     * @param string $playerString
434
     * @return Player[] Returns null if there were no players recorded for this match
435
     */
436
    private function parsePlayers($playerString)
437
    {
438
        if ($playerString == null) {
439
            return [];
440
        }
441
442
        return Player::arrayIdToModel(explode(",", $playerString));
443
    }
444
445
    /**
446
     * Get the first team's points
447
     * @return int Team A's points
448
     */
449
    public function getTeamAPoints()
450
    {
451
        return $this->team_a_points;
452
    }
453
454
    /**
455
     * Get the second team's points
456
     * @return int Team B's points
457
     */
458
    public function getTeamBPoints()
459
    {
460
        return $this->team_b_points;
461
    }
462
463
    /**
464
     * Set the match team points
465
     *
466
     * @param  int $teamAPoints Team A's points
467
     * @param  int $teamBPoints Team B's points
468
     * @return self
469
     */
470
    public function setTeamPoints($teamAPoints, $teamBPoints)
471
    {
472
        $this->updateProperty($this->team_a_points, "team_a_points", $teamAPoints);
473
        $this->updateProperty($this->team_b_points, "team_b_points", $teamBPoints);
474
475
        return $this;
476
    }
477
478
    /**
479
     * Set the match team colors
480
     *
481
     * @param  ColorTeam|string $teamAColor The color of team A
482
     * @param  ColorTeam|string $teamBColor The color of team B
483
     * @return self
484
     */
485
    public function setTeamColors($teamAColor, $teamBColor)
486
    {
487
        if ($this->isOfficial()) {
488
            throw new \Exception("Cannot change team colors in an official match");
489
        }
490
491
        if ($teamAColor instanceof ColorTeam) {
492
            $teamAColor = $teamAColor->getId();
493
        }
494
        if ($teamBColor instanceof ColorTeam) {
495
            $teamBColor = $teamBColor->getId();
496
        }
497
498
        $this->updateProperty($this->team_a_color, "team_a_color", $teamAColor);
499
        $this->updateProperty($this->team_b_color, "team_b_color", $teamBColor);
500
    }
501
502
    /**
503
     * Get the ELO difference applied to each team's old ELO
504
     *
505
     * @param bool $absoluteValue Whether or not to get the absolute value of the Elo difference
506
     *
507
     * @return int The ELO difference
508
     */
509
    public function getEloDiff($absoluteValue = true)
510
    {
511
        return ($absoluteValue) ? abs($this->elo_diff) : $this->elo_diff;
512
    }
513
514
    /**
515
     * Get the Elo difference applied to players
516
     *
517
     * @param bool $absoluteValue Whether or not to get the absolute value of the Elo difference
518
     *
519
     * @return int The Elo difference for players
520
     */
521
    public function getPlayerEloDiff($absoluteValue = true)
522
    {
523
        return ($absoluteValue && $this->player_elo_diff !== null) ? abs($this->player_elo_diff) : $this->player_elo_diff;
524
    }
525
526
    /**
527
     * Get the changelog for the player Elos for this match and cache them
528
     */
529
    private function getPlayerEloChangelog()
530
    {
531
        if ($this->player_elo_changelog !== null) {
532
            return;
533
        }
534
535
        $results = $this->db->query('SELECT * FROM player_elo WHERE match_id = ?', $this->getId());
536
537
        foreach ($results as $result) {
538
            $this->player_elo_changelog[$result['user_id']] = [
539
                'before' => $result['elo_previous'],
540
                'after'  => $result['elo_new']
541
            ];
542
        }
543
    }
544
545
    /**
546
     * Get the Elo for the player before this match occurred
547
     *
548
     * @param Player $player
549
     *
550
     * @return null|int
551
     */
552 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...
553
    {
554
        $this->getPlayerEloChangelog();
555
556
        if (isset($this->player_elo_changelog[$player->getId()])) {
557
            return $this->player_elo_changelog[$player->getId()]['before'];
558
        }
559
560
        return null;
561
    }
562
563
    /**
564
     * Get the Elo for the player after this match occurred
565
     *
566
     * @param Player $player
567
     *
568
     * @return null|int
569
     */
570 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...
571
    {
572
        $this->getPlayerEloChangelog();
573
574
        if (isset($this->player_elo_changelog[$player->getId()])) {
575
            return $this->player_elo_changelog[$player->getId()]['after'];
576
        }
577
578
        return null;
579
    }
580
581
    /**
582
     * Set the Elo difference applied to players
583
     *
584
     * @param int $diff
585
     */
586
    public function setPlayerEloDiff($diff)
587
    {
588
        $this->updateProperty($this->player_elo_diff, 'player_elo_diff', $diff);
589
    }
590
591
    /**
592
     * Get the first team's new ELO
593
     * @return int Team A's new ELO
594
     */
595
    public function getTeamAEloNew()
596
    {
597
        return $this->team_a_elo_new;
598
    }
599
600
    /**
601
     * Get the second team's new ELO
602
     * @return int Team B's new ELO
603
     */
604
    public function getTeamBEloNew()
605
    {
606
        return $this->team_b_elo_new;
607
    }
608
609
    /**
610
     * Get the first team's old ELO
611
     * @return int
612
     */
613
    public function getTeamAEloOld()
614
    {
615
        return $this->team_a_elo_new - $this->elo_diff;
616
    }
617
618
    /**
619
     * Get the second team's old ELO
620
     * @return int
621
     */
622
    public function getTeamBEloOld()
623
    {
624
        return $this->team_b_elo_new + $this->elo_diff;
625
    }
626
627
    /**
628
     * Get the team's new ELO
629
     * @param  Team $team The team whose new ELO to return
630
     * @return int|null   The new ELO, or null if the team provided has not
631
     *                    participated in the match
632
     */
633
    public function getTeamEloNew(Team $team)
634
    {
635
        if ($team->getId() == $this->team_a) {
636
            return $this->getTeamAEloNew();
637
        } elseif ($team->getId() == $this->team_b) {
638
            return $this->getTeamBEloNew();
639
        }
640
641
        return null;
642
    }
643
644
    /**
645
     * Get the team's old ELO
646
     * @param  Team $team The team whose old ELO to return
647
     * @return int|null   The old ELO, or null if the team provided has not
648
     *                    participated in the match
649
     */
650
    public function getTeamEloOld(Team $team)
651
    {
652
        if ($team->getId() == $this->team_a) {
653
            return $this->getTeamAEloOld();
654
        } elseif ($team->getId() == $this->team_b) {
655
            return $this->getTeamBEloOld();
656
        }
657
658
        return null;
659
    }
660
661
    /**
662
     * Get the map where the match was played on
663
     * @return Map Returns an invalid map if no map was found
664
     */
665
    public function getMap()
666
    {
667
        return Map::get($this->map);
668
    }
669
670
    /**
671
     * Set the map where the match was played
672
     * @param  int $map The ID of the map
673
     * @return self
674
     */
675
    public function setMap($map)
676
    {
677
        $this->updateProperty($this->map, "map", $map, "s");
678
679
        return $this;
680
    }
681
682
    /**
683
     * Get the type of official match this is. Whether it has just traditional teams or has mixed teams.
684
     *
685
     * Possible official match types:
686
     *   - Team vs Team
687
     *   - Team vs Mixed
688
     *   - Mixed vs Mixed
689
     *
690
     * @see Match::TEAM_V_TEAM
691
     * @see Match::TEAM_V_MIXED
692
     * @see Match::MIXED_V_MIXED
693
     *
694
     * @return int
695
     */
696
    public function getTeamMatchType()
697
    {
698
        if ($this->getTeamA()->supportsMatchCount() && $this->getTeamB()->supportsMatchCount()) {
699
            return self::TEAM_V_TEAM;
700
        } elseif ($this->getTeamA()->supportsMatchCount() xor $this->getTeamB()->supportsMatchCount()) {
701
            return self::TEAM_V_MIXED;
702
        }
703
704
        return self::MIXED_V_MIXED;
705
    }
706
707
    /**
708
     * Get the match type
709
     *
710
     * @return string 'official', 'fm', or 'special'
711
     */
712
    public function getMatchType()
713
    {
714
        return $this->match_type;
715
    }
716
717
    /**
718
     * Set the match type
719
     *
720
     * @param  string $matchType A valid match type; official, fm, special
721
     *
722
     * @return static
723
     */
724
    public function setMatchType($matchType)
725
    {
726
        return $this->updateProperty($this->match_type, "match_type", $matchType, 's');
727
    }
728
729
    /**
730
     * Get a JSON decoded array of events that occurred during the match
731
     * @return mixed|null Returns null if there were no events recorded for the match
732
     */
733
    public function getMatchDetails()
734
    {
735
        return json_decode($this->match_details);
736
    }
737
738
    /**
739
     * Get the server this match took place on
740
     *
741
     * @return Server
742
     */
743
    public function getServer()
744
    {
745
        return Server::get($this->server);
746
    }
747
748
    /**
749
     * Set the server this match took place on
750
     *
751
     * @param  int $serverID
752
     *
753
     * @return $this
754
     */
755
    public function setServer($serverID = null)
756
    {
757
        $this->updateProperty($this->server, 'server_id', $serverID);
758
759
        return $this;
760
    }
761
762
    /**
763
     * Get the server address of the server where this match took place
764
     *
765
     * @deprecated 0.10.0 Use Match::getServer() instead. Using this function is reserved for migrations/legacy support.
766
     *
767
     * @see 20170912201127_match_server_relationship.php
768
     *
769
     * @return string|null Returns null if there was no server address recorded
770
     */
771
    public function getServerAddress()
772
    {
773
        return $this->server_address;
774
    }
775
776
    /**
777
     * Get the name of the replay file for this specific map
778
     * @param  int    $length The length of the replay file name; it will be truncated
779
     * @return string Returns null if there was no replay file name recorded
780
     */
781
    public function getReplayFileName($length = 0)
782
    {
783
        if ($length > 0) {
784
            return substr($this->replay_file, 0, $length);
785
        }
786
787
        return $this->replay_file;
788
    }
789
790
    /**
791
     * Get the match duration
792
     * @return int The duration of the match in minutes
793
     */
794
    public function getDuration()
795
    {
796
        return $this->duration;
797
    }
798
799
    /**
800
     * Set the match duration
801
     *
802
     * @param  int  $duration The new duration of the match in minutes
803
     * @return self
804
     */
805
    public function setDuration($duration)
806
    {
807
        return $this->updateProperty($this->duration, "duration", $duration);
808
    }
809
810
    /**
811
     * Get the user who entered the match
812
     * @return Player
813
     */
814
    public function getEnteredBy()
815
    {
816
        return Player::get($this->entered_by);
817
    }
818
819
    /**
820
     * Get the loser of the match
821
     *
822
     * @return TeamInterface The team that was the loser or the team with the lower elo if the match was a draw
823
     */
824
    public function getLoser()
825
    {
826
        // Get the winner of the match
827
        $winner = $this->getWinner();
828
829
        // Get the team that wasn't the winner... Duh
830
        return $this->getOpponent($winner);
831
    }
832
833
    /**
834
     * Get the winner of a match
835
     *
836
     * @return TeamInterface The team that was the victor or the team with the lower elo if the match was a draw
837
     */
838
    public function getWinner()
839
    {
840
        // "As Mother Teresa once said, it's not enough if you win. Others must lose."
841
        //   -Stephen Colbert
842
843
        // Get the team that had its Elo increased or the team whose players had their Elo increased
844
        if ($this->elo_diff > 0 || $this->player_elo_diff > 0) {
845
            return $this->getTeamA();
846
        } elseif ($this->elo_diff < 0 || $this->player_elo_diff < 0) {
847
            return $this->getTeamB();
848
        } elseif ($this->team_a_points > $this->team_b_points) {
849
            // In case we're dealing with a match such an FM that doesn't have an ELO difference
850
            return $this->getTeamA();
851
        } elseif ($this->team_a_points < $this->team_b_points) {
852
            return $this->getTeamB();
853
        }
854
855
        // If the scores are the same, return Team A because well, fuck you that's why
856
        return $this->getTeamA();
857
    }
858
859
    /**
860
     * Determine whether the match was a draw
861
     * @return bool True if the match ended without any winning teams
862
     */
863
    public function isDraw()
864
    {
865
        return $this->team_a_points == $this->team_b_points;
866
    }
867
868
    /**
869
     * Find out whether the match involves a team
870
     *
871
     * @param  TeamInterface $team The team to check
872
     * @return bool
873
     */
874
    public function involvesTeam($team)
875
    {
876
        return $team->getId() == $this->getTeamA()->getId() || $team->getId() == $this->getTeamB()->getId();
877
    }
878
879
    /**
880
     * Find out if the match is played between official teams
881
     */
882
    public function isOfficial()
883
    {
884
        return self::OFFICIAL === $this->getMatchType();
885
    }
886
887
    /**
888
     * Reset the ELOs of the teams participating in the match
889
     *
890
     * @return self
891
     */
892
    public function resetTeamElos()
893
    {
894
        if ($this->match_type === self::OFFICIAL) {
895
            $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...
896
            $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...
897
        }
898
899
        return $this;
900
    }
901
902
    /**
903
     * Calculate the match's contribution to the team activity
904
     *
905
     * @return float
906
     */
907
    public function getActivity()
908
    {
909
        $daysPassed = $this->getTimestamp()->diffInSeconds();
910
        $daysPassed = $daysPassed / TimeDate::SECONDS_PER_MINUTE / TimeDate::MINUTES_PER_HOUR / TimeDate::HOURS_PER_DAY;
911
912
        $activity = 0.0116687059537612 * (pow(45 - $daysPassed, (1 / 6)) + atan(31.0 - $daysPassed) / 2.0);
913
914
        if (is_nan($activity) || $activity < 0.0) {
915
            return 0.0;
916
        }
917
918
        return $activity;
919
    }
920
921
    /**
922
     * Calculate the Elo differences for players and teams for a given match.
923
     *
924
     * @param  Team $a
925
     * @param  Team $b
926
     * @param  int  $a_points
927
     * @param  int  $b_points
928
     * @param  int[]|Player[] $a_players
929
     * @param  int[]|Player[] $b_players
930
     * @param  int  $duration
931
     *
932
     * @throws InvalidArgumentException When a "Mixed" team is entered without a player roster
933
     *
934
     * @return array
935
     */
936
    private static function calculateElos($a, $b, $a_points, $b_points, $a_players, $b_players, $duration)
937
    {
938
        // Get the type of official match
939
        $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...
940
941
        if ($a->supportsMatchCount() && $b->supportsMatchCount()) {
942
            $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...
943
        } elseif ($a->supportsMatchCount() xor $b->supportsMatchCount()) {
944
            $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...
945
        }
946
947
        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...
948
            ((!$a->isValid() && empty($a_players)) || (!$b->isValid() && empty($b_players)))) {
949
            throw new InvalidArgumentException('A Mixed team must have a player roster to calculate the Elo for team Elo differences');
950
        }
951
952
        //
953
        // Handle Player Elo Diff Calculations
954
        //
955
956
        // By default, we won't have a player Elo difference since we won't force matches to have a roster
957
        $playerEloDiff = null;
958
959
        $a_players_elo = 1200;
960
        $b_players_elo = 1200;
961
962
        // Only bother to calculate a player Elo diff if we have players reported for both teams
963
        if (!empty($a_players) && !empty($b_players)) {
964
            $a_players_elo = self::getAveragePlayerElo($a_players);
965
            $b_players_elo = self::getAveragePlayerElo($b_players);
966
967
            $playerEloDiff = self::calculateEloDiff($a_players_elo, $b_players_elo, $a_points, $b_points, $duration);
968
        }
969
970
        //
971
        // Handle Team Elo Diff Calculations
972
        //
973
974
        // By default, we'll assume a Mixed vs Mixed official match where Elos do not change for teams
975
        $teamEloDiff = null;
976
977
        // Work with calculations for team Elos to handle the following situations:
978
        //   - Team vs Team  :: Use team Elos for calculations
979
        //   - Team vs Mixed :: Use team Elo and the player average Elo for the "Mixed" team
980
        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...
981
            $teamEloDiff = self::calculateEloDiff($a->getElo(), $b->getElo(), $a_points, $b_points, $duration);
982
        } 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...
983
            $a_team_elo = ($a->supportsMatchCount()) ? $a->getElo() : $a_players_elo;
984
            $b_team_elo = ($b->supportsMatchCount()) ? $b->getElo() : $b_players_elo;
985
986
            $teamEloDiff = self::calculateEloDiff($a_team_elo, $b_team_elo, $a_points, $b_points, $duration);
987
        }
988
989
        return [
990
            'match_type' => $matchType,
991
            'team_elo'   => $teamEloDiff,
992
            'player_elo' => $playerEloDiff
993
        ];
994
    }
995
996
    /**
997
     * Enter a new match to the database
998
     * @param  int             $a          Team A's ID
999
     * @param  int             $b          Team B's ID
1000
     * @param  int             $a_points   Team A's match points
1001
     * @param  int             $b_points   Team B's match points
1002
     * @param  int             $duration   The match duration in minutes
1003
     * @param  int|null        $entered_by The ID of the player reporting the match
1004
     * @param  string|DateTime $timestamp  When the match was played
1005
     * @param  int[]           $a_players  The IDs of the first team's players
1006
     * @param  int[]           $b_players  The IDs of the second team's players
1007
     * @param  string|null     $server     The address of the server where the match was played
1008
     * @param  string          $replayFile The name of the replay file of the match
1009
     * @param  int             $map        The ID of the map where the match was played, only for rotational leagues
1010
     * @param  string          $matchType  The type of match (e.g. official, fm, special)
1011
     * @param  string          $a_color    Team A's color
1012
     * @param  string          $b_color    Team b's color
1013
     *
1014
     * @throws InvalidArgumentException When a ColorTeam is selected for an official match and no players are defined
1015
     *                                  for that team
1016
     *
1017
     * @return Match           An object representing the match that was just entered
1018
     */
1019
    public static function enterMatch(
1020
        $a, $b, $a_points, $b_points, $duration, $entered_by, $timestamp = "now",
1021
        $a_players = array(), $b_players = array(), $server = null, $replayFile = null,
1022
        $map = null, $matchType = "official", $a_color = null, $b_color = null
1023
    ) {
1024
        $matchData = array(
1025
            'team_a_color'   => strtolower($a_color),
1026
            'team_b_color'   => strtolower($b_color),
1027
            'team_a_points'  => $a_points,
1028
            'team_b_points'  => $b_points,
1029
            'team_a_players' => implode(',', $a_players),
1030
            'team_b_players' => implode(',', $b_players),
1031
            'timestamp'      => TimeDate::from($timestamp)->toMysql(),
1032
            'duration'       => $duration,
1033
            'entered_by'     => $entered_by,
1034
            'server'         => $server,
1035
            'replay_file'    => $replayFile,
1036
            'map'            => $map,
1037
            'status'         => 'entered',
1038
            'match_type'     => $matchType
1039
        );
1040
1041
        // (P)layer Elo Diff and (T)eam Elo Diff; respectively
1042
        $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...
1043
1044
        if ($matchType === self::OFFICIAL) {
1045
            $team_a = Team::get($a);
1046
            $team_b = Team::get($b);
1047
1048
            $eloCalcs = self::calculateElos($team_a, $team_b, $a_points, $b_points, $a_players, $b_players, $duration);
1049
1050
            $matchData['elo_diff'] = $tEloDiff = $eloCalcs['team_elo'];
1051
            $matchData['player_elo_diff'] = $eloCalcs['player_elo'];
1052
1053
            // Update team ELOs
1054
            if ($team_a->isValid()) {
1055
                $team_a->adjustElo($tEloDiff);
1056
1057
                $matchData['team_a'] = $a;
1058
                $matchData['team_a_elo_new'] = $team_a->getElo();
1059
            }
1060
            if ($team_b->isValid()) {
1061
                $team_b->adjustElo(-$tEloDiff);
1062
1063
                $matchData['team_b'] = $b;
1064
                $matchData['team_b_elo_new'] = $team_b->getElo();
1065
            }
1066
        }
1067
1068
        $match = self::create($matchData, 'updated');
1069
        $match->updateMatchCount();
1070
        $match->updatePlayerElo();
1071
1072
        return $match;
1073
    }
1074
1075
    /**
1076
     * Calculate the ELO score difference
1077
     *
1078
     * Computes the ELO score difference on each team after a match, based on
1079
     * GU League's rules.
1080
     *
1081
     * @param  int $a_elo    Team A's current ELO score
1082
     * @param  int $b_elo    Team B's current ELO score
1083
     * @param  int $a_points Team A's match points
1084
     * @param  int $b_points Team B's match points
1085
     * @param  int $duration The match duration in minutes
1086
     * @return int The ELO score difference
1087
     */
1088
    public static function calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration)
1089
    {
1090
        $prob = 1.0 / (1 + pow(10, (($b_elo - $a_elo) / 400.0)));
1091
        if ($a_points > $b_points) {
1092
            $diff = 50 * (1 - $prob);
1093
        } elseif ($a_points == $b_points) {
1094
            $diff = 50 * (0.5 - $prob);
1095
        } else {
1096
            $diff = 50 * (0 - $prob);
1097
        }
1098
1099
        // Apply ELO modifiers from `config.yml`
1100
        $durations = Service::getParameter('bzion.league.duration');
1101
        $diff *= (isset($durations[$duration])) ? $durations[$duration] : 1;
1102
1103
        if (abs($diff) < 1 && $diff != 0) {
1104
            // ELOs such as 0.75 should round up to 1...
1105
            return ($diff > 0) ? 1 : -1;
1106
        }
1107
1108
        // ...everything else is rounded down (-3.7 becomes -3 and 48.1 becomes 48)
1109
        return intval($diff);
1110
    }
1111
1112
    /**
1113
     * Find if a match's stored ELO is correct
1114
     */
1115
    public function isEloCorrect()
1116
    {
1117
        return $this->elo_diff === $this->calculateEloDiff(
1118
            $this->getTeamAEloOld(),
1119
            $this->getTeamBEloOld(),
1120
            $this->getTeamAPoints(),
1121
            $this->getTeamBPoints(),
1122
            $this->getDuration()
1123
        );
1124
    }
1125
1126
    /**
1127
     * Remove Elo recordings for players participating in this match
1128
     */
1129
    public function resetPlayerElos()
1130
    {
1131
        $this->db->execute('DELETE FROM player_elo WHERE match_id = ?', [$this->getId()]);
1132
    }
1133
1134
    /**
1135
     * Recalculate the match's elo and adjust the team ELO values
1136
     */
1137
    public function recalculateElo()
1138
    {
1139
        if ($this->match_type !== self::OFFICIAL) {
1140
            return;
1141
        }
1142
1143
        $a = $this->getTeamA();
1144
        $b = $this->getTeamB();
1145
1146
        $this->resetPlayerElos();
1147
1148
        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...
1149
            $player->invalidateMatchFromCache($this);
1150
        }
1151
1152
        $eloCalcs = self::calculateElos(
1153
            $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...
1154
            $this->getTeamAPoints(), $this->getTeamBPoints(),
1155
            $this->getTeamAPlayers(), $this->getTeamBPlayers(),
1156
            $this->getDuration()
1157
        );
1158
1159
        $elo = $eloCalcs['team_elo'];
1160
1161
        $this->updateProperty($this->elo_diff, 'elo_diff', $elo);
1162
        $this->updateProperty($this->player_elo_diff, 'player_elo_diff', $eloCalcs['player_elo']);
1163
1164
        if ($a->supportsMatchCount()) {
1165
            $a->adjustElo($elo);
1166
            $this->updateProperty($this->team_a_elo_new, 'team_a_elo_new', $a->getElo());
1167
        }
1168
1169
        if ($b->supportsMatchCount()) {
1170
            $b->adjustElo(-$elo);
1171
            $this->updateProperty($this->team_b_elo_new, 'team_b_elo_new', $b->getElo());
1172
        }
1173
1174
        $this->updatePlayerElo();
1175
    }
1176
1177
    /**
1178
     * Get all the matches in the database
1179
     */
1180
    public static function getMatches()
1181
    {
1182
        return self::getQueryBuilder()->active()->getModels();
1183
    }
1184
1185
    /**
1186
     * Get a query builder for matches
1187
     * @return MatchQueryBuilder
1188
     */
1189
    public static function getQueryBuilder()
1190
    {
1191
        return new MatchQueryBuilder('Match', array(
1192
            'columns' => array(
1193
                'firstTeam'        => 'team_a',
1194
                'secondTeam'       => 'team_b',
1195
                'firstTeamPoints'  => 'team_a_points',
1196
                'secondTeamPoints' => 'team_b_points',
1197
                'time'             => 'timestamp',
1198
                'map'              => 'map',
1199
                'server'           => 'server_id',
1200
                'type'             => 'match_type',
1201
                'status'           => 'status'
1202
            ),
1203
        ));
1204
    }
1205
1206
    /**
1207
     * {@inheritdoc}
1208
     */
1209
    public function delete()
1210
    {
1211
        $this->updateMatchCount(true);
1212
1213
        parent::delete();
1214
    }
1215
1216
    /**
1217
     * {@inheritdoc}
1218
     */
1219
    public static function getActiveStatuses()
1220
    {
1221
        return array('entered');
1222
    }
1223
1224
    /**
1225
     * {@inheritdoc}
1226
     */
1227
    public function getName()
1228
    {
1229
        $description = '';
1230
1231
        switch ($this->getMatchType()) {
1232
            case self::OFFICIAL:
1233
                // Only show Elo diff if both teams are actual teams
1234
                if ($this->getTeamA()->supportsMatchCount() && $this->getTeamB()->supportsMatchCount()) {
1235
                    $description = "(+/- {$this->getEloDiff()})";
1236
                }
1237
                break;
1238
1239
            case self::FUN:
1240
                $description = 'Fun Match:';
1241
                break;
1242
1243
            case self::SPECIAL:
1244
                $description = 'Special Match:';
1245
                break;
1246
1247
            default:
1248
                break;
1249
        }
1250
1251
        return trim(sprintf('%s %s [%d] vs [%d] %s',
1252
            $description,
1253
            $this->getWinner()->getName(),
1254
            $this->getScore($this->getWinner()),
1255
            $this->getScore($this->getLoser()),
1256
            $this->getLoser()->getName()
1257
        ));
1258
    }
1259
1260
    /**
1261
     * Recalculates match history for all teams and matches
1262
     *
1263
     * Recalculation is done as follows:
1264
     * 1. A match is chosen as a starting point - it's stored old team ELOs are
1265
     *    considered correct
1266
     * 2. Team ELOs are reset to their values at the starting point
1267
     * 3. Each match that occurred since the first specified match has its ELO
1268
     *    recalculated based on the current team values, and the new match data
1269
     *    and team ELOs are stored in the database
1270
     *
1271
     * @param Match $match The first match
1272
     *
1273
     * @throws Exception
1274
     */
1275
    public static function recalculateMatchesSince(Match $match)
1276
    {
1277
        try {
1278
            // Commented out to prevent ridiculously large recalculations
1279
            //set_time_limit(0);
1280
1281
            $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...
1282
                ->where('status')->notEquals('deleted')
1283
                ->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...
1284
                ->where('time')->isAfter($match->getTimestamp(), $inclusive = true)
1285
                ->sortBy('time');
1286
1287
            /** @var Match[] $matches */
1288
            $matches = $query->getModels($fast = true);
1289
1290
            // Send the total count to client-side javascript
1291
            echo count($matches) . "\n";
1292
1293
            // Start a transaction so tables are locked and we don't stay with
1294
            // messed up data if something goes wrong
1295
            Database::getInstance()->startTransaction();
1296
1297
            $teamsReset = [];
1298
1299
            // Reset match teams, in case the selected match is deleted and does
1300
            // not show up in the list of matches to recalculate
1301
            if ($match->getTeamA()->supportsMatchCount()) {
1302
                $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...
1303
                $teamsReset[ $match->getTeamA()->getId() ] = true;
1304
            }
1305
            if ($match->getTeamB()->supportsMatchCount()) {
1306
                $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...
1307
                $teamsReset[ $match->getTeamB()->getId() ] = true;
1308
            }
1309
1310
            foreach ($matches as $i => &$match) {
1311
                // Reset teams' ELOs if they haven't been reset already
1312 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...
1313
                    $teamsReset[ $match->getTeamA()->getId() ] = true;
1314
                    $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...
1315
                }
1316 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...
1317
                    $teamsReset[ $match->getTeamB()->getId() ] = true;
1318
                    $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...
1319
                }
1320
1321
                $match->recalculateElo();
1322
1323
                // Send an update to the client-side javascript, so that a
1324
                // progress bar can be updated
1325
                echo "m";
1326
            }
1327
        } catch (Exception $e) {
1328
            Database::getInstance()->rollback();
1329
            Database::getInstance()->finishTransaction();
1330
            throw $e;
1331
        }
1332
1333
        Database::getInstance()->finishTransaction();
1334
1335
        echo "\n\nCalculation successful\n";
1336
    }
1337
1338
    /**
1339
     * Get the average ELO for an array of players
1340
     *
1341
     * @param int[]|Player[] $players
1342
     *
1343
     * @return float|int
1344
     */
1345
    private static function getAveragePlayerElo($players)
1346
    {
1347
        $getElo = function ($n) {
1348
            if ($n instanceof Player) {
1349
                return $n->getElo();
1350
            }
1351
1352
            return Player::get($n)->getElo();
1353
        };
1354
1355
        return array_sum(array_map($getElo, $players)) / count($players);
1356
    }
1357
1358
    /**
1359
     * Update the match count of the teams participating in the match
1360
     *
1361
     * @param bool $decrement Whether to decrement instead of incrementing the match count
1362
     */
1363
    private function updateMatchCount($decrement = false)
1364
    {
1365
        if ($this->match_type !== self::OFFICIAL) {
1366
            return;
1367
        }
1368
1369
        $diff = ($decrement) ? -1 : 1;
1370
1371
        if ($this->isDraw()) {
1372
            $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...
1373
            $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...
1374
        } else {
1375
            $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...
1376
            $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...
1377
        }
1378
    }
1379
1380
    /**
1381
     * Update the Elos for the participating players in a match
1382
     */
1383
    private function updatePlayerElo()
1384
    {
1385
        if ($this->match_type !== self::OFFICIAL || $this->getPlayerEloDiff() === null) {
1386
            return;
1387
        }
1388
1389
        $eloDiff = $this->getPlayerEloDiff(false);
1390
1391
        foreach ($this->getTeamAPlayers() as $player) {
1392
            $player->adjustElo($eloDiff, $this);
1393
            $player->setLastMatch($this->getId());
1394
        }
1395
1396
        foreach ($this->getTeamBPlayers() as $player) {
1397
            $player->adjustElo(-$eloDiff, $this);
1398
            $player->setLastMatch($this->getId());
1399
        }
1400
    }
1401
}
1402