Completed
Push — feature/player-elo-v3 ( 8c49df...69db9f )
by Vladimir
03:30
created

Match   D

Complexity

Total Complexity 121

Size/Duplication

Total Lines 1057
Duplicated Lines 2.93 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 121
lcom 1
cbo 11
dl 31
loc 1057
rs 4
c 0
b 0
f 0

58 Methods

Rating   Name   Duplication   Size   Complexity  
B assignResult() 0 24 1
A getRouteName() 0 4 1
A getMatchDescription() 0 10 3
A getMatchLetter() 0 4 1
A getScore() 16 16 4
A getOpponentScore() 0 4 1
A getOpponent() 14 14 4
A getUpdated() 0 4 1
A setTimestamp() 0 6 1
A getTeamA() 0 10 3
A getTeamB() 0 10 3
A getTeamAColor() 0 4 1
A getTeamBColor() 0 4 1
A getTeamAPlayers() 0 4 1
A getTeamBPlayers() 0 4 1
B getPlayers() 0 14 6
A setTeamPlayers() 0 7 1
A parsePlayers() 0 8 2
A getTeamAPoints() 0 4 1
A getTeamBPoints() 0 4 1
A setTeamPoints() 0 7 1
A setTeamColors() 0 16 4
A getEloDiff() 0 4 1
A getTeamAEloNew() 0 4 1
A getTeamBEloNew() 0 4 1
A getTeamAEloOld() 0 4 1
A getTeamBEloOld() 0 4 1
A getTeamEloNew() 0 10 3
A getTeamEloOld() 0 10 3
A getMap() 0 4 1
A setMap() 0 4 1
A getMatchType() 0 4 1
A setMatchType() 0 4 1
A getMatchDetails() 0 4 1
A getServerAddress() 0 4 1
A setServerAddress() 0 6 1
A getReplayFileName() 0 8 2
A getDuration() 0 4 1
A setDuration() 0 4 1
A getEnteredBy() 0 4 1
A getLoser() 0 8 1
B getWinner() 0 17 5
A isDraw() 0 4 1
A involvesTeam() 0 4 2
A isOfficial() 0 4 1
A resetELOs() 0 9 2
A getActivity() 0 13 3
D enterMatch() 0 90 13
C calculateEloDiff() 0 23 7
A isEloCorrect() 0 10 1
B recalculateElo() 0 25 2
A getMatches() 0 4 1
A getQueryBuilder() 0 15 1
A delete() 0 6 1
A getActiveStatuses() 0 4 1
B getName() 0 24 4
A getAveragePlayerElo() 0 8 1
B updateMatchCount() 0 16 8

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Match often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Match, and based on these observations, apply Extract Interface, too.

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
    protected function assignResult($match)
157
    {
158
        $this->team_a = $match['team_a'];
159
        $this->team_b = $match['team_b'];
160
        $this->team_a_color = $match['team_a_color'];
161
        $this->team_b_color = $match['team_b_color'];
162
        $this->team_a_points = $match['team_a_points'];
163
        $this->team_b_points = $match['team_b_points'];
164
        $this->team_a_players = $match['team_a_players'];
165
        $this->team_b_players = $match['team_b_players'];
166
        $this->team_a_elo_new = $match['team_a_elo_new'];
167
        $this->team_b_elo_new = $match['team_b_elo_new'];
168
        $this->map = $match['map'];
169
        $this->match_type = $match['match_type'];
170
        $this->match_details = $match['match_details'];
171
        $this->server = $match['server'];
172
        $this->replay_file = $match['replay_file'];
173
        $this->elo_diff = $match['elo_diff'];
174
        $this->timestamp = TimeDate::fromMysql($match['timestamp']);
175
        $this->updated = TimeDate::fromMysql($match['updated']);
176
        $this->duration = $match['duration'];
177
        $this->entered_by = $match['entered_by'];
178
        $this->status = $match['status'];
179
    }
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
    public static function getRouteName($action = 'show')
187
    {
188
        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
    public function getMatchDescription($teamID)
199
    {
200
        if ($this->getScore($teamID) > $this->getOpponentScore($teamID)) {
201
            return "win";
202
        } elseif ($this->getScore($teamID) < $this->getOpponentScore($teamID)) {
203
            return "loss";
204
        }
205
206
        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
    public function getMatchLetter($teamID)
217
    {
218
        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 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
        if ($teamID instanceof TeamInterface) {
231
            // Oh no! The caller gave us a Team model instead of an ID!
232
            $teamID = $teamID->getId();
233
        } elseif (is_string($teamID)) {
234
            // Make sure we're comparing lowercase strings
235
            $teamID = strtolower($teamID);
236
        }
237
238
        if ($this->getTeamA()->getId() == $teamID) {
239
            return $this->getTeamAPoints();
240
        }
241
242
        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
    public function getOpponentScore($teamID)
253
    {
254
        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 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
        if ($teamID instanceof TeamInterface) {
267
            $teamID = $teamID->getId();
268
        } elseif (is_string($teamID)) {
269
            $teamID = strtolower($teamID);
270
        }
271
272
        if ($this->getTeamA()->getId() == $teamID) {
273
            return $this->getTeamB();
274
        }
275
276
        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
    public function getUpdated()
285
    {
286
        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
    public function getTeamA()
307
    {
308
        $team = Team::get($this->team_a);
309
310
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
311
            return $team;
312
        }
313
314
        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
    public function getTeamB()
322
    {
323
        $team = Team::get($this->team_b);
324
325
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
326
            return $team;
327
        }
328
329
        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
    public function getTeamAPlayers()
355
    {
356
        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
    public function getTeamBPlayers()
364
    {
365
        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
    public function getPlayers($team = null)
374
    {
375
        if ($team instanceof TeamInterface) {
376
            $team = $team->getId();
377
        }
378
379
        if ($this->getTeamA()->isValid() && $team == $this->getTeamA()->getId()) {
380
            return $this->getTeamAPlayers();
381
        } elseif ($this->getTeamB()->isValid() && $team == $this->getTeamB()->getId()) {
382
            return $this->getTeamBPlayers();
383
        }
384
385
        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
    private function parsePlayers($playerString)
409
    {
410
        if ($playerString == null) {
411
            return null;
412
        }
413
414
        return Player::arrayIdToModel(explode(",", $playerString));
415
    }
416
417
    /**
418
     * Get the first team's points
419
     * @return int Team A's points
420
     */
421
    public function getTeamAPoints()
422
    {
423
        return $this->team_a_points;
424
    }
425
426
    /**
427
     * Get the second team's points
428
     * @return int Team B's points
429
     */
430
    public function getTeamBPoints()
431
    {
432
        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
    public function getEloDiff()
479
    {
480
        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
    public function getTeamAEloNew()
488
    {
489
        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
    public function getTeamBEloNew()
497
    {
498
        return $this->team_b_elo_new;
499
    }
500
501
    /**
502
     * Get the first team's old ELO
503
     * @return int
504
     */
505
    public function getTeamAEloOld()
506
    {
507
        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
    public function getTeamBEloOld()
515
    {
516
        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
    public function getTeamEloNew(Team $team)
526
    {
527
        if ($team->getId() == $this->team_a) {
528
            return $this->getTeamAEloNew();
529
        } elseif ($team->getId() == $this->team_b) {
530
            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
    public function getTeamEloOld(Team $team)
543
    {
544
        if ($team->getId() == $this->team_a) {
545
            return $this->getTeamAEloOld();
546
        } elseif ($team->getId() == $this->team_b) {
547
            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
    public function getMap()
558
    {
559
        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
    public function getMatchType()
578
    {
579
        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
    public function getServerAddress()
608
    {
609
        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
    public function getReplayFileName($length = 0)
632
    {
633
        if ($length > 0) {
634
            return substr($this->replay_file, 0, $length);
635
        }
636
637
        return $this->replay_file;
638
    }
639
640
    /**
641
     * Get the match duration
642
     * @return int The duration of the match in minutes
643
     */
644
    public function getDuration()
645
    {
646
        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
    public function getEnteredBy()
665
    {
666
        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
    public function getLoser()
675
    {
676
        // Get the winner of the match
677
        $winner = $this->getWinner();
678
679
        // Get the team that wasn't the winner... Duh
680
        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
    public function getWinner()
689
    {
690
        // Get the team that had its ELO increased
691
        if ($this->elo_diff > 0) {
692
            return $this->getTeamA();
693
        } elseif ($this->elo_diff < 0) {
694
            return $this->getTeamB();
695
        } 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
            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
    public function isDraw()
711
    {
712
        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
    public function involvesTeam($team)
722
    {
723
        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
    public function isOfficial()
730
    {
731
        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
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...
743
            $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...
744
        }
745
746
        return $this;
747
    }
748
749
    /**
750
     * Calculate the match's contribution to the team activity
751
     *
752
     * @return float
753
     */
754
    public function getActivity()
755
    {
756
        $daysPassed = $this->getTimestamp()->diffInSeconds();
757
        $daysPassed = $daysPassed / TimeDate::SECONDS_PER_MINUTE / TimeDate::MINUTES_PER_HOUR / TimeDate::HOURS_PER_DAY;
758
759
        $activity = 0.0116687059537612 * (pow(45 - $daysPassed, (1 / 6)) + atan(31.0 - $daysPassed) / 2.0);
760
761
        if (is_nan($activity) || $activity < 0.0) {
762
            return 0.0;
763
        }
764
765
        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
    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
            'team_a_color'   => strtolower($a_color),
795
            'team_b_color'   => strtolower($b_color),
796
            'team_a_points'  => $a_points,
797
            'team_b_points'  => $b_points,
798
            'team_a_players' => implode(',', $a_players),
799
            'team_b_players' => implode(',', $b_players),
800
            'timestamp'      => TimeDate::from($timestamp)->toMysql(),
801
            'duration'       => $duration,
802
            'entered_by'     => $entered_by,
803
            'server'         => $server,
804
            'replay_file'    => $replayFile,
805
            'map'            => $map,
806
            'status'         => 'entered',
807
            'match_type'     => $matchType
808
        );
809
810
        $playerEloDiff = null;
811
812
        if ($matchType === self::OFFICIAL) {
813
            $team_a = Team::get($a);
814
            $team_b = Team::get($b);
815
816
            $a_players_elo = null;
817
            $b_players_elo = null;
818
819
            // Only bother if we have players reported for both teams
820
            if (!empty($a_players) && !empty($b_players)) {
821
                $a_players_elo = self::getAveragePlayerElo($a_players);
822
                $b_players_elo = self::getAveragePlayerElo($b_players);
823
824
                $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
            $a_team_elo = ($team_a->isValid()) ? $team_a->getElo() : $a_players_elo;
829
            $b_team_elo = ($team_b->isValid()) ? $team_b->getElo() : $b_players_elo;
830
831
            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
            $teamEloDiff = self::calculateEloDiff($a_team_elo, $b_team_elo, $a_points, $b_points, $duration);
836
837
            $matchData['elo_diff'] = $teamEloDiff;
838
            $matchData['player_elo_diff'] = $playerEloDiff;
839
840
            // Update team ELOs
841
            if ($team_a->isValid()) {
842
                $team_a->adjustElo($teamEloDiff);
843
844
                $matchData['team_a'] = $a;
845
                $matchData['team_a_elo_new'] = $team_a->getElo();
846
            }
847
            if ($team_b->isValid()) {
848
                $team_b->adjustElo(-$teamEloDiff);
849
850
                $matchData['team_b'] = $b;
851
                $matchData['team_b_elo_new'] = $team_b->getElo();
852
            }
853
        }
854
855
        $match = self::create($matchData, 'updated');
856
        $match->updateMatchCount();
857
858
        $players = $match->getPlayers();
859
860
        $db = Database::getInstance();
861
        $db->startTransaction();
862
863
        /** @var Player $player */
864
        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...
865
            $diff = $playerEloDiff;
866
867
            if ($playerEloDiff !== null && !in_array($player->getId(), $a_players)) {
868
                $diff = -$playerEloDiff;
869
            }
870
871
            $player->setMatchParticipation($match, $diff);
872
        }
873
874
        $db->finishTransaction();
875
876
        return $match;
877
    }
878
879
    /**
880
     * Calculate the ELO score difference
881
     *
882
     * Computes the ELO score difference on each team after a match, based on
883
     * GU League's rules.
884
     *
885
     * @param  int $a_elo    Team A's current ELO score
886
     * @param  int $b_elo    Team B's current ELO score
887
     * @param  int $a_points Team A's match points
888
     * @param  int $b_points Team B's match points
889
     * @param  int $duration The match duration in minutes
890
     * @return int The ELO score difference
891
     */
892
    public static function calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration)
893
    {
894
        $prob = 1.0 / (1 + pow(10, (($b_elo - $a_elo) / 400.0)));
895
        if ($a_points > $b_points) {
896
            $diff = 50 * (1 - $prob);
897
        } elseif ($a_points == $b_points) {
898
            $diff = 50 * (0.5 - $prob);
899
        } else {
900
            $diff = 50 * (0 - $prob);
901
        }
902
903
        // Apply ELO modifiers from `config.yml`
904
        $durations = Service::getParameter('bzion.league.duration');
905
        $diff *= (isset($durations[$duration])) ? $durations[$duration] : 1;
906
907
        if (abs($diff) < 1 && $diff != 0) {
908
            // ELOs such as 0.75 should round up to 1...
909
            return ($diff > 0) ? 1 : -1;
910
        }
911
912
        // ...everything else is rounded down (-3.7 becomes -3 and 48.1 becomes 48)
913
        return intval($diff);
914
    }
915
916
    /**
917
     * Find if a match's stored ELO is correct
918
     */
919
    public function isEloCorrect()
920
    {
921
        return $this->elo_diff === $this->calculateEloDiff(
922
            $this->getTeamAEloOld(),
923
            $this->getTeamBEloOld(),
924
            $this->getTeamAPoints(),
925
            $this->getTeamBPoints(),
926
            $this->getDuration()
927
        );
928
    }
929
930
    /**
931
     * Recalculate the match's elo and adjust the team ELO values
932
     */
933
    public function recalculateElo()
934
    {
935
        if ($this->match_type !== self::OFFICIAL) {
936
            return;
937
        }
938
939
        $a = $this->getTeamA();
940
        $b = $this->getTeamB();
941
942
        $elo = $this->calculateEloDiff(
943
            $a->getElo(),
944
            $b->getElo(),
945
            $this->getTeamAPoints(),
946
            $this->getTeamBPoints(),
947
            $this->getDuration()
948
        );
949
950
        $this->updateProperty($this->elo_diff, "elo_diff", $elo);
951
952
        $a->changeElo($elo);
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...
953
        $b->changeElo(-$elo);
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...
954
955
        $this->updateProperty($this->team_a_elo_new, "team_a_elo_new", $a->getElo());
956
        $this->updateProperty($this->team_b_elo_new, "team_b_elo_new", $b->getElo());
957
    }
958
959
    /**
960
     * Get all the matches in the database
961
     */
962
    public static function getMatches()
963
    {
964
        return self::getQueryBuilder()->active()->getModels();
965
    }
966
967
    /**
968
     * Get a query builder for matches
969
     * @return MatchQueryBuilder
970
     */
971
    public static function getQueryBuilder()
972
    {
973
        return new MatchQueryBuilder('Match', array(
974
            'columns' => array(
975
                'firstTeam'        => 'team_a',
976
                'secondTeam'       => 'team_b',
977
                'firstTeamPoints'  => 'team_a_points',
978
                'secondTeamPoints' => 'team_b_points',
979
                'time'             => 'timestamp',
980
                'map'              => 'map',
981
                'type'             => 'match_type',
982
                'status'           => 'status'
983
            ),
984
        ));
985
    }
986
987
    /**
988
     * {@inheritdoc}
989
     */
990
    public function delete()
991
    {
992
        $this->updateMatchCount(true);
993
994
        return parent::delete();
995
    }
996
997
    /**
998
     * {@inheritdoc}
999
     */
1000
    public static function getActiveStatuses()
1001
    {
1002
        return array('entered');
1003
    }
1004
1005
    /**
1006
     * {@inheritdoc}
1007
     */
1008
    public function getName()
1009
    {
1010
        switch ($this->getMatchType()) {
1011
            case self::OFFICIAL:
1012
                $description = "(+/- " . $this->getEloDiff() . ")";
1013
                break;
1014
            case self::FUN:
1015
                $description = "Fun Match:";
1016
                break;
1017
            case self::SPECIAL:
1018
                $description = "Special Match:";
1019
                break;
1020
            default:
1021
                $description = "";
1022
        }
1023
1024
        return sprintf("%s %s [%d] vs [%d] %s",
1025
            $description,
1026
            $this->getWinner()->getName(),
1027
            $this->getScore($this->getWinner()),
1028
            $this->getScore($this->getLoser()),
1029
            $this->getLoser()->getName()
1030
        );
1031
    }
1032
1033
    /**
1034
     * Get the average ELO for an array of players
1035
     *
1036
     * @param int[] $players
1037
     *
1038
     * @return float|int
1039
     */
1040
    private static function getAveragePlayerElo($players)
1041
    {
1042
        $getElo = function ($n) {
1043
            return Player::get($n)->getElo();
1044
        };
1045
1046
        return array_sum(array_map($getElo, $players)) / count($players);
1047
    }
1048
1049
    /**
1050
     * Update the match count of the teams participating in the match
1051
     *
1052
     * @param bool $decrement Whether to decrement instead of incrementing the match count
1053
     */
1054
    private function updateMatchCount($decrement = false)
1055
    {
1056
        if ($this->match_type !== self::OFFICIAL) {
1057
            return;
1058
        }
1059
1060
        $diff = ($decrement) ? -1 : 1;
1061
1062
        if ($this->isDraw()) {
1063
            $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...
1064
            $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...
1065
        } else {
1066
            $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...
1067
            $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...
1068
        }
1069
    }
1070
}
1071