Completed
Push — feature/player-elo ( 127bff...a3fab4 )
by Vladimir
07:14
created

Match::getTeamBEloNew()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
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
    use Timestamp;
21
22
    /**
23
     * The ID of the first team of the match
24
     * @var int
25
     */
26
    protected $team_a;
27
28
    /**
29
     * The ID of the second team of the match
30
     * @var int
31
     */
32
    protected $team_b;
33
34
    /**
35
     * The color of the first team
36
     * @var string
37
     */
38
    protected $team_a_color;
39
40
    /**
41
     * The color of the second team
42
     * @var string
43
     */
44
    protected $team_b_color;
45
46
    /**
47
     * The match points (usually the number of flag captures) Team A scored
48
     * @var int
49
     */
50
    protected $team_a_points;
51
52
    /**
53
     * The match points Team B scored
54
     * @var int
55
     */
56
    protected $team_b_points;
57
58
    /**
59
     * The BZIDs of players part of Team A who participated in the match, separated by commas
60
     * @var string
61
     */
62
    protected $team_a_players;
63
64
    /**
65
     * The BZIDs of players part of Team B who participated in the match, separated by commas
66
     * @var string
67
     */
68
    protected $team_b_players;
69
70
    /**
71
     * The ELO score of Team A after the match
72
     * @var int
73
     */
74
    protected $team_a_elo_new;
75
76
    /**
77
     * The ELO score of Team B after the match
78
     * @var int
79
     */
80
    protected $team_b_elo_new;
81
82
    /**
83
     * The map ID used in the match if the league supports more than one map
84
     * @var int
85
     */
86
    protected $map;
87
88
    /**
89
     * The type of match that occurred. Valid options: official, fm, special
90
     *
91
     * @var string
92
     */
93
    protected $match_type;
94
95
    /**
96
     * A JSON string of events that happened during a match, such as captures and substitutions
97
     * @var string
98
     */
99
    protected $match_details;
100
101
    /**
102
     * The server location of there the match took place
103
     * @var string
104
     */
105
    protected $server;
106
107
    /**
108
     * The file name of the replay file of the match
109
     * @var string
110
     */
111
    protected $replay_file;
112
113
    /**
114
     * The absolute value of the ELO score difference
115
     * @var int
116
     */
117
    protected $elo_diff;
118
119
    /**
120
     * The timestamp representing when the match information was last updated
121
     * @var TimeDate
122
     */
123
    protected $updated;
124
125
    /**
126
     * The duration of the match in minutes
127
     * @var int
128
     */
129
    protected $duration;
130
131
    /**
132
     * The ID of the person (i.e. referee) who last updated the match information
133
     * @var string
134
     */
135
    protected $entered_by;
136
137
    /**
138
     * The status of the match. Can be 'entered', 'disabled', 'deleted' or 'reported'
139
     * @var string
140
     */
141
    protected $status;
142
143
    /**
144
     * The name of the database table used for queries
145
     */
146
    const TABLE = "matches";
147
148
    const CREATE_PERMISSION = Permission::ENTER_MATCH;
149
    const EDIT_PERMISSION = Permission::EDIT_MATCH;
150
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_MATCH;
151
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_MATCH;
152
153
    /**
154
     * {@inheritdoc}
155
     */
156 14
    protected function assignResult($match)
157
    {
158 14
        $this->team_a = $match['team_a'];
159 14
        $this->team_b = $match['team_b'];
160 14
        $this->team_a_color = $match['team_a_color'];
161 14
        $this->team_b_color = $match['team_b_color'];
162 14
        $this->team_a_points = $match['team_a_points'];
163 14
        $this->team_b_points = $match['team_b_points'];
164 14
        $this->team_a_players = $match['team_a_players'];
165 14
        $this->team_b_players = $match['team_b_players'];
166 14
        $this->team_a_elo_new = $match['team_a_elo_new'];
167 14
        $this->team_b_elo_new = $match['team_b_elo_new'];
168 14
        $this->map = $match['map'];
169 14
        $this->match_type = $match['match_type'];
170 14
        $this->match_details = $match['match_details'];
171 14
        $this->server = $match['server'];
172 14
        $this->replay_file = $match['replay_file'];
173 14
        $this->elo_diff = $match['elo_diff'];
174 14
        $this->timestamp = TimeDate::fromMysql($match['timestamp']);
175 14
        $this->updated = TimeDate::fromMysql($match['updated']);
176 14
        $this->duration = $match['duration'];
177 14
        $this->entered_by = $match['entered_by'];
178 14
        $this->status = $match['status'];
179 14
    }
180
181
    /**
182
     * Get the name of the route that shows the object
183
     * @param  string $action The route's suffix
184
     * @return string
185
     */
186 1
    public static function getRouteName($action = 'show')
187
    {
188 1
        return "match_$action";
189
    }
190
191
    /**
192
     * Get a one word description of a match relative to a team (i.e. win, loss, or draw)
193
     *
194
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
195
     *
196
     * @return string Either "win", "loss", or "draw" relative to the team
197
     */
198 1
    public function getMatchDescription($teamID)
199
    {
200 1
        if ($this->getScore($teamID) > $this->getOpponentScore($teamID)) {
201 1
            return "win";
202 1
        } elseif ($this->getScore($teamID) < $this->getOpponentScore($teamID)) {
203 1
            return "loss";
204
        }
205
206 1
        return "tie";
207
    }
208
209
    /**
210
     * Get a one letter description of a match relative to a team (i.e. W, L, or T)
211
     *
212
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
213
     *
214
     * @return string Either "W", "L", or "T" relative to the team
215
     */
216 1
    public function getMatchLetter($teamID)
217
    {
218 1
        return strtoupper(substr($this->getMatchDescription($teamID), 0, 1));
219
    }
220
221
    /**
222
     * Get the score of a specific team
223
     *
224
     * @param int|string|TeamInterface $teamID The team we want the score for
225
     *
226
     * @return int The score that team received
227
     */
228 3 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...
229
    {
230 3
        if ($teamID instanceof TeamInterface) {
231
            // Oh no! The caller gave us a Team model instead of an ID!
232 3
            $teamID = $teamID->getId();
233 2
        } elseif (is_string($teamID)) {
234
            // Make sure we're comparing lowercase strings
235
            $teamID = strtolower($teamID);
236
        }
237
238 3
        if ($this->getTeamA()->getId() == $teamID) {
239 3
            return $this->getTeamAPoints();
240
        }
241
242 3
        return $this->getTeamBPoints();
243
    }
244
245
    /**
246
     * Get the score of the opponent relative to a team
247
     *
248
     * @param int|string|TeamInterface $teamID The opponent of the team we want the score for
249
     *
250
     * @return int The score of the opponent
251
     */
252 2
    public function getOpponentScore($teamID)
253
    {
254 2
        return $this->getScore($this->getOpponent($teamID));
255
    }
256
257
    /**
258
     * Get the opponent of a match relative to a team ID
259
     *
260
     * @param int|string|TeamInterface $teamID The team who is known in a match
261
     *
262
     * @return TeamInterface The opponent team
263
     */
264 12 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...
265
    {
266 12
        if ($teamID instanceof TeamInterface) {
267 12
            $teamID = $teamID->getId();
268 2
        } elseif (is_string($teamID)) {
269
            $teamID = strtolower($teamID);
270
        }
271
272 12
        if ($this->getTeamA()->getId() == $teamID) {
273 9
            return $this->getTeamB();
274
        }
275
276 5
        return $this->getTeamA();
277
    }
278
279
    /**
280
     * Get the timestamp of the last update of the match
281
     *
282
     * @return TimeDate The match's update timestamp
283
     */
284 1
    public function getUpdated()
285
    {
286 1
        return $this->updated->copy();
287
    }
288
289
    /**
290
     * Set the timestamp of the match
291
     *
292
     * @param  mixed $timestamp The match's new timestamp
293
     * @return $this
294
     */
295
    public function setTimestamp($timestamp)
296
    {
297
        $this->updateProperty($this->timestamp, "timestamp", TimeDate::from($timestamp));
298
299
        return $this;
300
    }
301
302
    /**
303
     * Get the first team involved in the match
304
     * @return TeamInterface Team A
305
     */
306 14
    public function getTeamA()
307
    {
308 14
        $team = Team::get($this->team_a);
309
310 14
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
311 12
            return $team;
312
        }
313
314 3
        return new ColorTeam($this->team_a_color);
315
    }
316
317
    /**
318
     * Get the second team involved in the match
319
     * @return TeamInterface Team B
320
     */
321 13
    public function getTeamB()
322
    {
323 13
        $team = Team::get($this->team_b);
324
325 13
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
326 11
            return $team;
327
        }
328
329 3
        return new ColorTeam($this->team_b_color);
330
    }
331
332
    /**
333
     * Get the color of Team A
334
     * @return string
335
     */
336
    public function getTeamAColor()
337
    {
338
        return $this->team_a_color;
339
    }
340
341
    /**
342
     * Get the color of Team B
343
     * @return string
344
     */
345
    public function getTeamBColor()
346
    {
347
        return $this->team_b_color;
348
    }
349
350
    /**
351
     * Get the list of players on Team A who participated in this match
352
     * @return Player[]|null Returns null if there were no players recorded for this match
353
     */
354 2
    public function getTeamAPlayers()
355
    {
356 2
        return $this->parsePlayers($this->team_a_players);
357
    }
358
359
    /**
360
     * Get the list of players on Team B who participated in this match
361
     * @return Player[]|null Returns null if there were no players recorded for this match
362
     */
363 1
    public function getTeamBPlayers()
364
    {
365 1
        return $this->parsePlayers($this->team_b_players);
366
    }
367
368
    /**
369
     * Get the list of players for a team in a match
370
     * @param  Team|int|null The team or team ID
371
     * @return Player[]|null Returns null if there were no players recorded for this match
372
     */
373 14
    public function getPlayers($team = null)
374
    {
375 14
        if ($team instanceof TeamInterface) {
376 1
            $team = $team->getId();
377
        }
378
379 14
        if ($this->getTeamA()->isValid() && $team == $this->getTeamA()->getId()) {
380 2
            return $this->getTeamAPlayers();
381 13
        } elseif ($this->getTeamB()->isValid() && $team == $this->getTeamB()->getId()) {
382 1
            return $this->getTeamBPlayers();
383
        }
384
385 13
        return $this->parsePlayers($this->team_a_players . "," . $this->team_b_players);
386
    }
387
388
    /**
389
     * Set the players of the match's teams
390
     *
391
     * @param int[] $teamAPlayers An array of player IDs
392
     * @param int[] $teamBPlayers An array of player IDs
393
     * @return self
394
     */
395
    public function setTeamPlayers($teamAPlayers = array(), $teamBPlayers = array())
396
    {
397
        $this->updateProperty($this->team_a_players, "team_a_players", implode(',', $teamAPlayers));
398
        $this->updateProperty($this->team_b_players, "team_b_players", implode(',', $teamBPlayers));
399
400
        return $this;
401
    }
402
403
    /**
404
     * Get an array of players based on a string representation
405
     * @param string $playerString
406
     * @return Player[]|null Returns null if there were no players recorded for this match
407
     */
408 14
    private function parsePlayers($playerString)
409
    {
410 14
        if ($playerString == null) {
411 1
            return null;
412
        }
413
414 14
        return Player::arrayIdToModel(explode(",", $playerString));
415
    }
416
417
    /**
418
     * Get the first team's points
419
     * @return int Team A's points
420
     */
421 5
    public function getTeamAPoints()
422
    {
423 5
        return $this->team_a_points;
424
    }
425
426
    /**
427
     * Get the second team's points
428
     * @return int Team B's points
429
     */
430 5
    public function getTeamBPoints()
431
    {
432 5
        return $this->team_b_points;
433
    }
434
435
    /**
436
     * Set the match team points
437
     *
438
     * @param  int $teamAPoints Team A's points
439
     * @param  int $teamBPoints Team B's points
440
     * @return self
441
     */
442
    public function setTeamPoints($teamAPoints, $teamBPoints)
443
    {
444
        $this->updateProperty($this->team_a_points, "team_a_points", $teamAPoints);
445
        $this->updateProperty($this->team_b_points, "team_b_points", $teamBPoints);
446
447
        return $this;
448
    }
449
450
    /**
451
     * Set the match team colors
452
     *
453
     * @param  ColorTeam|string $teamAColor The color of team A
454
     * @param  ColorTeam|string $teamBColor The color of team B
455
     * @return self
456
     */
457
    public function setTeamColors($teamAColor, $teamBColor)
458
    {
459
        if ($this->isOfficial()) {
460
            throw new \Exception("Cannot change team colors in an official match");
461
        }
462
463
        if ($teamAColor instanceof ColorTeam) {
464
            $teamAColor = $teamAColor->getId();
465
        }
466
        if ($teamBColor instanceof ColorTeam) {
467
            $teamBColor = $teamBColor->getId();
468
        }
469
470
        $this->updateProperty($this->team_a_color, "team_a_color", $teamAColor);
471
        $this->updateProperty($this->team_b_color, "team_b_color", $teamBColor);
472
    }
473
474
    /**
475
     * Get the ELO difference applied to each team's old ELO
476
     * @return int The ELO difference
477
     */
478 8
    public function getEloDiff()
479
    {
480 8
        return abs($this->elo_diff);
481
    }
482
483
    /**
484
     * Get the first team's new ELO
485
     * @return int Team A's new ELO
486
     */
487 7
    public function getTeamAEloNew()
488
    {
489 7
        return $this->team_a_elo_new;
490
    }
491
492
    /**
493
     * Get the second team's new ELO
494
     * @return int Team B's new ELO
495
     */
496 7
    public function getTeamBEloNew()
497
    {
498 7
        return $this->team_b_elo_new;
499
    }
500
501
    /**
502
     * Get the first team's old ELO
503
     * @return int
504
     */
505 6
    public function getTeamAEloOld()
506
    {
507 6
        return $this->team_a_elo_new - $this->elo_diff;
508
    }
509
510
    /**
511
     * Get the second team's old ELO
512
     * @return int
513
     */
514 6
    public function getTeamBEloOld()
515
    {
516 6
        return $this->team_b_elo_new + $this->elo_diff;
517
    }
518
519
    /**
520
     * Get the team's new ELO
521
     * @param  Team $team The team whose new ELO to return
522
     * @return int|null   The new ELO, or null if the team provided has not
523
     *                    participated in the match
524
     */
525 1
    public function getTeamEloNew(Team $team)
526
    {
527 1
        if ($team->getId() == $this->team_a) {
528 1
            return $this->getTeamAEloNew();
529 1
        } elseif ($team->getId() == $this->team_b) {
530 1
            return $this->getTeamBEloNew();
531
        }
532
533
        return null;
534
    }
535
536
    /**
537
     * Get the team's old ELO
538
     * @param  Team $team The team whose old ELO to return
539
     * @return int|null   The old ELO, or null if the team provided has not
540
     *                    participated in the match
541
     */
542 1
    public function getTeamEloOld(Team $team)
543
    {
544 1
        if ($team->getId() == $this->team_a) {
545 1
            return $this->getTeamAEloOld();
546 1
        } elseif ($team->getId() == $this->team_b) {
547 1
            return $this->getTeamBEloOld();
548
        }
549
550
        return null;
551
    }
552
553
    /**
554
     * Get the map where the match was played on
555
     * @return Map Returns an invalid map if no map was found
556
     */
557 1
    public function getMap()
558
    {
559 1
        return Map::get($this->map);
560
    }
561
562
    /**
563
     * Set the map where the match was played
564
     * @param  int $map The ID of the map
565
     * @return self
566
     */
567
    public function setMap($map)
568
    {
569
        $this->updateProperty($this->map, "map", $map, "s");
570
    }
571
572
    /**
573
     * Get the match type
574
     *
575
     * @return string 'official', 'fm', or 'special'
576
     */
577 14
    public function getMatchType()
578
    {
579 14
        return $this->match_type;
580
    }
581
582
    /**
583
     * Set the match type
584
     *
585
     * @param  string $matchType A valid match type; official, fm, special
586
     *
587
     * @return static
588
     */
589
    public function setMatchType($matchType)
590
    {
591
        return $this->updateProperty($this->match_type, "match_type", $matchType, 's');
592
    }
593
594
    /**
595
     * Get a JSON decoded array of events that occurred during the match
596
     * @return mixed|null Returns null if there were no events recorded for the match
597
     */
598
    public function getMatchDetails()
599
    {
600
        return json_decode($this->match_details);
601
    }
602
603
    /**
604
     * Get the server address of the server where this match took place
605
     * @return string|null Returns null if there was no server address recorded
606
     */
607 1
    public function getServerAddress()
608
    {
609 1
        return $this->server;
610
    }
611
612
    /**
613
     * Set the server address of the server where this match took place
614
     *
615
     * @param  string|null $server The server hostname
616
     * @param  int|null    $port   The server port
0 ignored issues
show
Bug introduced by
There is no parameter named $port. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
617
     * @return self
618
     */
619
    public function setServerAddress($server = null)
620
    {
621
        $this->updateProperty($this->server, "server", $server);
622
623
        return $this;
624
    }
625
626
    /**
627
     * Get the name of the replay file for this specific map
628
     * @param  int    $length The length of the replay file name; it will be truncated
629
     * @return string Returns null if there was no replay file name recorded
630
     */
631 1
    public function getReplayFileName($length = 0)
632
    {
633 1
        if ($length > 0) {
634
            return substr($this->replay_file, 0, $length);
635
        }
636
637 1
        return $this->replay_file;
638
    }
639
640
    /**
641
     * Get the match duration
642
     * @return int The duration of the match in minutes
643
     */
644 3
    public function getDuration()
645
    {
646 3
        return $this->duration;
647
    }
648
649
    /**
650
     * Set the match duration
651
     *
652
     * @param  int  $duration The new duration of the match in minutes
653
     * @return self
654
     */
655
    public function setDuration($duration)
656
    {
657
        return $this->updateProperty($this->duration, "duration", $duration);
658
    }
659
660
    /**
661
     * Get the user who entered the match
662
     * @return Player
663
     */
664 2
    public function getEnteredBy()
665
    {
666 2
        return Player::get($this->entered_by);
667
    }
668
669
    /**
670
     * Get the loser of the match
671
     *
672
     * @return TeamInterface The team that was the loser or the team with the lower elo if the match was a draw
673
     */
674 12
    public function getLoser()
675
    {
676
        // Get the winner of the match
677 12
        $winner = $this->getWinner();
678
679
        // Get the team that wasn't the winner... Duh
680 12
        return $this->getOpponent($winner);
681
    }
682
683
    /**
684
     * Get the winner of a match
685
     *
686
     * @return TeamInterface The team that was the victor or the team with the lower elo if the match was a draw
687
     */
688 12
    public function getWinner()
689
    {
690
        // Get the team that had its ELO increased
691 12
        if ($this->elo_diff > 0) {
692 9
            return $this->getTeamA();
693 4
        } elseif ($this->elo_diff < 0) {
694 3
            return $this->getTeamB();
695 1
        } elseif ($this->team_a_points > $this->team_b_points) {
696
            // In case we're dealing with a match such an FM that doesn't have an ELO difference
697 1
            return $this->getTeamA();
698
        } elseif ($this->team_a_points < $this->team_b_points) {
699
            return $this->getTeamB();
700
        }
701
702
        // If the scores are the same, return Team A because well, fuck you that's why
703
        return $this->getTeamA();
704
    }
705
706
    /**
707
     * Determine whether the match was a draw
708
     * @return bool True if the match ended without any winning teams
709
     */
710 13
    public function isDraw()
711
    {
712 13
        return $this->team_a_points == $this->team_b_points;
713
    }
714
715
    /**
716
     * Find out whether the match involves a team
717
     *
718
     * @param  TeamInterface $team The team to check
719
     * @return bool
720
     */
721 1
    public function involvesTeam($team)
722
    {
723 1
        return $team->getId() == $this->getTeamA()->getId() || $team->getId() == $this->getTeamB()->getId();
724
    }
725
726
    /**
727
     * Find out if the match is played between official teams
728
     */
729 14
    public function isOfficial()
730
    {
731 14
        return self::OFFICIAL === $this->getMatchType();
732
    }
733
734
    /**
735
     * Reset the ELOs of the teams participating in the match
736
     *
737
     * @return self
738
     */
739
    public function resetELOs()
740
    {
741
        if ($this->match_type === self::OFFICIAL) {
742
            $this->getTeamA()->changeELO(-$this->elo_diff);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeELO() 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...
743
            $this->getTeamB()->changeELO(+$this->elo_diff);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeELO() 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...
744
        }
745
746
        return $this;
747
    }
748
749
    /**
750
     * Calculate the match's contribution to the team activity
751
     *
752
     * @return float
753
     */
754 1
    public function getActivity()
755
    {
756 1
        $daysPassed = $this->getTimestamp()->diffInSeconds();
757 1
        $daysPassed = $daysPassed / TimeDate::SECONDS_PER_MINUTE / TimeDate::MINUTES_PER_HOUR / TimeDate::HOURS_PER_DAY;
758
759 1
        $activity = 0.0116687059537612 * (pow(45 - $daysPassed, (1 / 6)) + atan(31.0 - $daysPassed) / 2.0);
760
761 1
        if (is_nan($activity) || $activity < 0.0) {
762
            return 0.0;
763
        }
764
765 1
        return $activity;
766
    }
767
768
    /**
769
     * Enter a new match to the database
770
     * @param  int             $a          Team A's ID
771
     * @param  int             $b          Team B's ID
772
     * @param  int             $a_points   Team A's match points
773
     * @param  int             $b_points   Team B's match points
774
     * @param  int             $duration   The match duration in minutes
775
     * @param  int|null        $entered_by The ID of the player reporting the match
776
     * @param  string|DateTime $timestamp  When the match was played
777
     * @param  int[]           $a_players  The IDs of the first team's players
778
     * @param  int[]           $b_players  The IDs of the second team's players
779
     * @param  string|null     $server     The address of the server where the match was played
780
     * @param  int|null        $port       The port of the server where the match was played
0 ignored issues
show
Bug introduced by
There is no parameter named $port. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
781
     * @param  string          $replayFile The name of the replay file of the match
782
     * @param  int             $map        The ID of the map where the match was played, only for rotational leagues
783
     * @param  string          $matchType  The type of match (e.g. official, fm, special)
784
     * @param  string          $a_color    Team A's color
785
     * @param  string          $b_color    Team b's color
786
     * @return Match           An object representing the match that was just entered
787
     */
788 14
    public static function enterMatch(
789
        $a, $b, $a_points, $b_points, $duration, $entered_by, $timestamp = "now",
790
        $a_players = array(), $b_players = array(), $server = null, $replayFile = null,
791
        $map = null, $matchType = "official", $a_color = null, $b_color = null
792
    ) {
793
        $matchData = array(
794 14
            'team_a_color'   => strtolower($a_color),
795 14
            'team_b_color'   => strtolower($b_color),
796 14
            'team_a_points'  => $a_points,
797 14
            'team_b_points'  => $b_points,
798 14
            'team_a_players' => implode(',', $a_players),
799 14
            'team_b_players' => implode(',', $b_players),
800 14
            'timestamp'      => TimeDate::from($timestamp)->toMysql(),
801 14
            'duration'       => $duration,
802 14
            'entered_by'     => $entered_by,
803 14
            'server'         => $server,
804 14
            'replay_file'    => $replayFile,
805 14
            'map'            => $map,
806 14
            'status'         => 'entered',
807 14
            'match_type'     => $matchType
808
        );
809
810 14
        $playerEloDiff = null;
811
812 14
        if ($matchType === self::OFFICIAL) {
813 13
            $team_a = Team::get($a);
814 13
            $team_b = Team::get($b);
815
816 13
            $a_players_elo = null;
817 13
            $b_players_elo = null;
818
819
            // Only bother if we have players reported for both teams
820 13
            if (!empty($a_players) && !empty($b_players)) {
821 4
                $a_players_elo = self::getAveragePlayerElo($a_players);
822 4
                $b_players_elo = self::getAveragePlayerElo($b_players);
823
824 4
                $playerEloDiff = self::calculateEloDiff($a_players_elo, $b_players_elo, $a_points, $b_points, $duration);
825
            }
826
827
            // Get team ELOs, if not default to the average ELO of the players on the respective team
828 13
            $a_team_elo = ($team_a->isValid()) ? $team_a->getElo() : $a_players_elo;
829 13
            $b_team_elo = ($team_b->isValid()) ? $team_b->getElo() : $b_players_elo;
830
831 13
            if ($a_team_elo === null || $b_team_elo === null) {
832
                throw new Exception('An ELO for each team must be calculated somehow.');
833
            }
834
835 13
            $teamEloDiff = self::calculateEloDiff($a_team_elo, $b_team_elo, $a_points, $b_points, $duration);
836
837 13
            $matchData['elo_diff'] = $teamEloDiff;
838
839
            // Update team ELOs
840 13
            if ($team_a->isValid()) {
841 12
                $team_a->changeElo($teamEloDiff);
842
843 12
                $matchData['team_a'] = $a;
844 12
                $matchData['team_a_elo_new'] = $team_a->getElo();
845
            }
846 13
            if ($team_b->isValid()) {
847 11
                $team_b->changeElo(-$teamEloDiff);
848
849 11
                $matchData['team_b'] = $b;
850 11
                $matchData['team_b_elo_new'] = $team_b->getElo();
851
            }
852
        }
853
854 14
        $match = self::create($matchData, 'updated');
855 14
        $match->updateMatchCount();
856
857 14
        $players = $match->getPlayers();
858
859 14
        $db = Database::getInstance();
860 14
        $db->startTransaction();
861
862
        /** @var Player $player */
863 14
        foreach ($players as $player) {
0 ignored issues
show
Bug introduced by
The expression $players of type null|array<integer,object<Model>> 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...
864 14
            $diff = $playerEloDiff;
865
866 14
            if ($playerEloDiff !== null && !in_array($player->getId(), $a_players)) {
867 4
                $diff = -$playerEloDiff;
868
            }
869
870 14
            $player->setMatchParticipation($match, $diff);
871
        }
872
873 14
        $db->finishTransaction();
874
875 14
        return $match;
876
    }
877
878
    /**
879
     * Calculate the ELO score difference
880
     *
881
     * Computes the ELO score difference on each team after a match, based on
882
     * GU League's rules.
883
     *
884
     * @param  int $a_elo    Team A's current ELO score
885
     * @param  int $b_elo    Team B's current ELO score
886
     * @param  int $a_points Team A's match points
887
     * @param  int $b_points Team B's match points
888
     * @param  int $duration The match duration in minutes
889
     * @return int The ELO score difference
890
     */
891 13
    public static function calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration)
892
    {
893 13
        $prob = 1.0 / (1 + pow(10, (($b_elo - $a_elo) / 400.0)));
894 13
        if ($a_points > $b_points) {
895 8
            $diff = 50 * (1 - $prob);
896 6
        } elseif ($a_points == $b_points) {
897 4
            $diff = 50 * (0.5 - $prob);
898
        } else {
899 2
            $diff = 50 * (0 - $prob);
900
        }
901
902
        // Apply ELO modifiers from `config.yml`
903 13
        $durations = Service::getParameter('bzion.league.duration');
904 13
        $diff *= (isset($durations[$duration])) ? $durations[$duration] : 1;
905
906 13
        if (abs($diff) < 1 && $diff != 0) {
907
            // ELOs such as 0.75 should round up to 1...
908 2
            return ($diff > 0) ? 1 : -1;
909
        }
910
911
        // ...everything else is rounded down (-3.7 becomes -3 and 48.1 becomes 48)
912 11
        return intval($diff);
913
    }
914
915
    /**
916
     * Find if a match's stored ELO is correct
917
     */
918
    public function isEloCorrect()
919
    {
920
        return $this->elo_diff === $this->calculateEloDiff(
921
            $this->getTeamAEloOld(),
922
            $this->getTeamBEloOld(),
923
            $this->getTeamAPoints(),
924
            $this->getTeamBPoints(),
925
            $this->getDuration()
926
        );
927
    }
928
929
    /**
930
     * Recalculate the match's elo and adjust the team ELO values
931
     */
932
    public function recalculateElo()
933
    {
934
        if ($this->match_type !== self::OFFICIAL) {
935
            return;
936
        }
937
938
        $a = $this->getTeamA();
939
        $b = $this->getTeamB();
940
941
        $elo = $this->calculateEloDiff(
942
            $a->getElo(),
943
            $b->getElo(),
944
            $this->getTeamAPoints(),
945
            $this->getTeamBPoints(),
946
            $this->getDuration()
947
        );
948
949
        $this->updateProperty($this->elo_diff, "elo_diff", $elo);
950
951
        $a->changeElo($elo);
952
        $b->changeElo(-$elo);
953
954
        $this->updateProperty($this->team_a_elo_new, "team_a_elo_new", $a->getElo());
955
        $this->updateProperty($this->team_b_elo_new, "team_b_elo_new", $b->getElo());
956
    }
957
958
    /**
959
     * Get all the matches in the database
960
     */
961 1
    public static function getMatches()
962
    {
963 1
        return self::getQueryBuilder()->active()->getModels();
964
    }
965
966
    /**
967
     * Get a query builder for matches
968
     * @return MatchQueryBuilder
969
     */
970 4
    public static function getQueryBuilder()
971
    {
972 4
        return new MatchQueryBuilder('Match', array(
973 4
            'columns' => array(
974
                'firstTeam'        => 'team_a',
975
                'secondTeam'       => 'team_b',
976
                'firstTeamPoints'  => 'team_a_points',
977
                'secondTeamPoints' => 'team_b_points',
978
                'time'             => 'timestamp',
979
                'map'              => 'map',
980
                'type'             => 'match_type',
981
                'status'           => 'status'
982
            ),
983
        ));
984
    }
985
986
    /**
987
     * {@inheritdoc}
988
     */
989
    public function delete()
990
    {
991
        $this->updateMatchCount(true);
992
993
        return parent::delete();
994
    }
995
996
    /**
997
     * {@inheritdoc}
998
     */
999 3
    public static function getActiveStatuses()
1000
    {
1001 3
        return array('entered');
1002
    }
1003
1004
    /**
1005
     * {@inheritdoc}
1006
     */
1007 2
    public function getName()
1008
    {
1009 2
        switch ($this->getMatchType()) {
1010 2
            case self::OFFICIAL:
1011 2
                $description = "(+/- " . $this->getEloDiff() . ")";
1012 2
                break;
1013 1
            case self::FUN:
1014 1
                $description = "Fun Match:";
1015 1
                break;
1016
            case self::SPECIAL:
1017
                $description = "Special Match:";
1018
                break;
1019
            default:
1020
                $description = "";
1021
        }
1022
1023 2
        return sprintf("%s %s [%d] vs [%d] %s",
1024 2
            $description,
1025 2
            $this->getWinner()->getName(),
1026 2
            $this->getScore($this->getWinner()),
1027 2
            $this->getScore($this->getLoser()),
1028 2
            $this->getLoser()->getName()
1029
        );
1030
    }
1031
1032
    /**
1033
     * Get the average ELO for an array of players
1034
     *
1035
     * @param int[] $players
1036
     *
1037
     * @return float|int
1038
     */
1039
    private static function getAveragePlayerElo($players)
1040
    {
1041 4
        $getElo = function ($n) {
1042 4
            return Player::get($n)->getElo();
1043 4
        };
1044
1045 4
        return array_sum(array_map($getElo, $players)) / count($players);
1046
    }
1047
1048
    /**
1049
     * Update the match count of the teams participating in the match
1050
     *
1051
     * @param bool $decrement Whether to decrement instead of incrementing the match count
1052
     */
1053 14
    private function updateMatchCount($decrement = false)
1054
    {
1055 14
        if ($this->match_type !== self::OFFICIAL) {
1056 2
            return;
1057
        }
1058
1059 13
        $diff = ($decrement) ? -1 : 1;
1060
1061 13
        if ($this->isDraw()) {
1062 4
            $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...
1063 4
            $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...
1064
        } else {
1065 10
            $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...
1066 10
            $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...
1067
        }
1068 13
    }
1069
}
1070