Completed
Push — master ( bce3ff...b78e58 )
by Konstantinos
04:07
created

Match::setServerAddress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 7
ccs 0
cts 4
cp 0
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
crap 2
1
<?php
2
/**
3
 * This file contains functionality relating to the official matches played in the league
4
 *
5
 * @package    BZiON\Models
6
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
7
 */
8
9
/**
10
 * A match played between two teams
11
 * @package    BZiON\Models
12
 */
13
class Match extends PermissionModel implements NamedModel
14
{
15
    /**
16
     * The ID of the first team of the match
17
     * @var int
18
     */
19
    protected $team_a;
20
21
    /**
22
     * The ID of the second team of the match
23
     * @var int
24
     */
25
    protected $team_b;
26
27
    /**
28
     * The match points (usually the number of flag captures) Team A scored
29
     * @var int
30
     */
31
    protected $team_a_points;
32
33
    /**
34
     * The match points Team B scored
35
     * @var int
36
     */
37
    protected $team_b_points;
38
39
    /**
40
     * The BZIDs of players part of Team A who participated in the match, separated by commas
41
     * @var string
42
     */
43
    protected $team_a_players;
44
45
    /**
46
     * The BZIDs of players part of Team B who participated in the match, separated by commas
47
     * @var string
48
     */
49
    protected $team_b_players;
50
51
    /**
52
     * The ELO score of Team A after the match
53
     * @var int
54
     */
55
    protected $team_a_elo_new;
56
57
    /**
58
     * The ELO score of Team B after the match
59
     * @var int
60
     */
61
    protected $team_b_elo_new;
62
63
    /**
64
     * The map ID used in the match if the league supports more than one map
65
     * @var int
66
     */
67
    protected $map;
68
69
    /**
70
     * A JSON string of events that happened during a match, such as captures and substitutions
71
     * @var string
72
     */
73
    protected $match_details;
74
75
    /**
76
     * The port of the server where the match took place
77
     * @var int
78
     */
79
    protected $port;
80
81
    /**
82
     * The server location of there the match took place
83
     * @var string
84
     */
85
    protected $server;
86
87
    /**
88
     * The file name of the replay file of the match
89
     * @var string
90
     */
91
    protected $replay_file;
92
93
    /**
94
     * The absolute value of the ELO score difference
95
     * @var int
96
     */
97
    protected $elo_diff;
98
99
    /**
100
     * The timestamp representing when the match was played
101
     * @var TimeDate
102
     */
103
    protected $timestamp;
104
105
    /**
106
     * The timestamp representing when the match information was last updated
107
     * @var TimeDate
108
     */
109
    protected $updated;
110
111
    /**
112
     * The duration of the match in minutes
113
     * @var int
114
     */
115
    protected $duration;
116
117
    /**
118
     * The ID of the person (i.e. referee) who last updated the match information
119
     * @var string
120
     */
121
    protected $entered_by;
122
123
    /**
124
     * The status of the match. Can be 'entered', 'disabled', 'deleted' or 'reported'
125
     * @var string
126
     */
127
    protected $status;
128
129
    /**
130
     * The name of the database table used for queries
131
     */
132
    const TABLE = "matches";
133
134
    const CREATE_PERMISSION = Permission::ENTER_MATCH;
135
    const EDIT_PERMISSION = Permission::EDIT_MATCH;
136
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_MATCH;
137
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_MATCH;
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 9
    protected function assignResult($match)
143
    {
144 9
        $this->team_a = $match['team_a'];
145 9
        $this->team_b = $match['team_b'];
146 9
        $this->team_a_points = $match['team_a_points'];
147 9
        $this->team_b_points = $match['team_b_points'];
148 9
        $this->team_a_players = $match['team_a_players'];
149 9
        $this->team_b_players = $match['team_b_players'];
150 9
        $this->team_a_elo_new = $match['team_a_elo_new'];
151 9
        $this->team_b_elo_new = $match['team_b_elo_new'];
152 9
        $this->map = $match['map'];
153 9
        $this->match_details = $match['match_details'];
154 9
        $this->port = $match['port'];
155 9
        $this->server = $match['server'];
156 9
        $this->replay_file = $match['replay_file'];
157 9
        $this->elo_diff = $match['elo_diff'];
158 9
        $this->timestamp = TimeDate::fromMysql($match['timestamp']);
159 9
        $this->updated = TimeDate::fromMysql($match['updated']);
160 9
        $this->duration = $match['duration'];
161 9
        $this->entered_by = $match['entered_by'];
162 9
        $this->status = $match['status'];
163 9
    }
164
165
    /**
166
     * Get the name of the route that shows the object
167
     * @param  string $action The route's suffix
168
     * @return string
169
     */
170 1
    public static function getRouteName($action = 'list')
171
    {
172 1
        return "match_$action";
173
    }
174
175
    /**
176
     * Get a one word description of a match relative to a team (i.e. win, loss, or draw)
177
     *
178
     * @param int $teamID The team ID we want the noun for
179
     *
180
     * @return string Either "win", "loss", or "draw" relative to the team
181
     */
182 1
    public function getMatchDescription($teamID)
183
    {
184 1
        if ($this->getScore($teamID) > $this->getOpponentScore($teamID)) {
185 1
            return "win";
186 1
        } elseif ($this->getScore($teamID) < $this->getOpponentScore($teamID)) {
187 1
            return "loss";
188
        }
189
190 1
        return "tie";
191
    }
192
193
    /**
194
     * Get a one letter description of a match relative to a team (i.e. W, L, or T)
195
     *
196
     * @param int $teamID The team ID we want the noun for
197
     *
198
     * @return string Either "W", "L", or "T" relative to the team
199
     */
200 1
    public function getMatchLetter($teamID)
201
    {
202 1
        return strtoupper(substr($this->getMatchDescription($teamID), 0, 1));
203
    }
204
205
    /**
206
     * Get the score of a specific team
207
     *
208
     * @param int|Team $teamID The team we want the score for
209
     *
210
     * @return int The score that team received
211
     */
212 2 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...
213
    {
214 2
        if ($teamID instanceof Team) {
215
            // Oh no! The caller gave us a Team model instead of an ID!
216 1
            $teamID = $teamID->getId();
217
        }
218
219 2
        if ($this->getTeamA()->getId() == $teamID) {
220 2
            return $this->getTeamAPoints();
221
        }
222
223 1
        return $this->getTeamBPoints();
224
    }
225
226
    /**
227
     * Get the score of the opponent relative to a team
228
     *
229
     * @param int $teamID The opponent of the team we want the score for
230
     *
231
     * @return int The score of the opponent
232
     */
233 2 View Code Duplication
    public function getOpponentScore($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...
234
    {
235 2
        if ($teamID instanceof Team) {
236
            $teamID = $teamID->getId();
237
        }
238
239 2
        if ($this->getTeamA()->getId() != $teamID) {
240 1
            return $this->getTeamAPoints();
241
        }
242
243 2
        return $this->getTeamBPoints();
244
    }
245
246
    /**
247
     * Get the opponent of a match relative to a team ID
248
     *
249
     * @param int $teamID The team who is known in a match
250
     *
251
     * @return Team The opponent team
252
     */
253 8
    public function getOpponent($teamID)
254
    {
255 8
        if ($this->getTeamA()->getId() == $teamID) {
256 6
            return $this->getTeamB();
257
        }
258
259 4
        return $this->getTeamA();
260
    }
261
262
    /**
263
     * Get the timestamp of the match
264
     **
265
     * @return TimeDate The match's timestamp
266
     */
267 2
    public function getTimestamp()
268
    {
269 2
        return $this->timestamp;
270
    }
271
272
    /**
273
     * Set the timestamp of the match
274
     *
275
     * @param  mixed The match's new timestamp
276
     * @return $this
277
     */
278
    public function setTimestamp($timestamp)
279
    {
280
        $this->timestamp = TimeDate::from($timestamp);
281
        $this->update("timestamp", $this->timestamp->toMysql(), "s");
282
283
        return $this;
284
    }
285
286
    /**
287
     * Get the first team involved in the match
288
     * @return Team Team A's id
289
     */
290 9
    public function getTeamA()
291
    {
292 9
        return Team::get($this->team_a);
293
    }
294
295
    /**
296
     * Get the second team involved in the match
297
     * @return Team Team B's id
298
     */
299 9
    public function getTeamB()
300
    {
301 9
        return Team::get($this->team_b);
302
    }
303
304
    /**
305
     * Get the list of players on Team A who participated in this match
306
     * @return Player[]|null Returns null if there were no players recorded for this match
307
     */
308
    public function getTeamAPlayers()
309
    {
310
        return $this->parsePlayers($this->team_a_players);
311
    }
312
313
    /**
314
     * Get the list of players on Team B who participated in this match
315
     * @return Player[]|null Returns null if there were no players recorded for this match
316
     */
317
    public function getTeamBPlayers()
318
    {
319
        return $this->parsePlayers($this->team_b_players);
320
    }
321
322
    /**
323
     * Set the players of the match's teams
324
     *
325
     * @param int[] $teamAPlayers An array of player IDs
326
     * @param int[] $teamBPlayers An array of player IDs
327
     * @return self
328
     */
329
    public function setTeamPlayers($teamAPlayers = array(), $teamBPlayers = array())
330
    {
331
        $this->updateProperty($this->team_a_players, "team_a_players", implode(',', $teamAPlayers), "s");
332
        $this->updateProperty($this->team_b_players, "team_b_players", implode(',', $teamBPlayers), "s");
333
334
        return $this;
335
    }
336
337
    /**
338
     * Get an array of players based on a string representation
339
     * @param string $playerString
340
     * @return Player[]|null Returns null if there were no players recorded for this match
341
     */
342
    private static function parsePlayers($playerString)
343
    {
344
        if ($playerString == null) {
345
            return null;
346
        }
347
348
        return Player::arrayIdToModel(explode(",", $playerString));
349
    }
350
351
    /**
352
     * Get the first team's points
353
     * @return int Team A's points
354
     */
355 4
    public function getTeamAPoints()
356
    {
357 4
        return $this->team_a_points;
358
    }
359
360
    /**
361
     * Get the second team's points
362
     * @return int Team B's points
363
     */
364 4
    public function getTeamBPoints()
365
    {
366 4
        return $this->team_b_points;
367
    }
368
369
    /**
370
     * Set the match team points
371
     *
372
     * @param  int $teamAPoints Team A's points
373
     * @param  int $teamBPoints Team B's points
374
     * @return self
375
     */
376
    public function setTeamPoints($teamAPoints, $teamBPoints)
377
    {
378
        $this->updateProperty($this->team_a_points, "team_a_points", $teamAPoints, "i");
379
        $this->updateProperty($this->team_b_points, "team_b_points", $teamBPoints, "i");
380
381
        return $this;
382
    }
383
384
    /**
385
     * Get the ELO difference applied to each team's old ELO
386
     * @return int The ELO difference
387
     */
388 7
    public function getEloDiff()
389
    {
390 7
        return abs($this->elo_diff);
391
    }
392
393
    /**
394
     * Get the first team's new ELO
395
     * @return int Team A's new ELO
396
     */
397 6
    public function getTeamAEloNew()
398
    {
399 6
        return $this->team_a_elo_new;
400
    }
401
402
    /**
403
     * Get the second team's new ELO
404
     * @return int Team B's new ELO
405
     */
406 6
    public function getTeamBEloNew()
407
    {
408 6
        return $this->team_b_elo_new;
409
    }
410
411
    /**
412
     * Get the first team's old ELO
413
     * @return int
414
     */
415 5
    public function getTeamAEloOld()
416
    {
417 5
        return $this->team_a_elo_new - $this->elo_diff;
418
    }
419
420
    /**
421
     * Get the second team's old ELO
422
     * @return int
423
     */
424 5
    public function getTeamBEloOld()
425
    {
426 5
        return $this->team_b_elo_new + $this->elo_diff;
427
    }
428
429
    /**
430
     * Get the map where the match was played on
431
     * @return Map Returns an invalid map if no map was found
432
     */
433 1
    public function getMap()
434
    {
435 1
        return Map::get($this->map);
436
    }
437
438
    /**
439
     * Set the map where the match was played
440
     * @param  int $map The ID of the map
441
     * @return self
442
     */
443
    public function setMap($map)
444
    {
445
        $this->updateProperty($this->map, "map", $map, "s");
446
    }
447
448
    /**
449
     * Get a JSON decoded array of events that occurred during the match
450
     * @return mixed|null Returns null if there were no events recorded for the match
451
     */
452
    public function getMatchDetails()
453
    {
454
        return json_decode($this->match_details);
455
    }
456
457
    /**
458
     * Get the server address of the server where this match took place
459
     * @return string|null Returns null if there was no server address recorded
460
     */
461
    public function getServerAddress()
462
    {
463
        if ($this->port == null || $this->server == null) {
464
            return null;
465
        }
466
467
        return $this->server . ":" . $this->port;
468
    }
469
470
    /**
471
     * Set the server address of the server where this match took place
472
     *
473
     * @param  string|null $server The server hostname
474
     * @param  int|null    $port   The server port
475
     * @return self
476
     */
477
    public function setServerAddress($server = null, $port = 5154)
478
    {
479
        $this->updateProperty($this->server, "server", $server, "s");
480
        $this->updateProperty($this->port, "port", $port, "i");
481
482
        return $this;
483
    }
484
485
    /**
486
     * Get the name of the replay file for this specific map
487
     * @param  int    $length The length of the replay file name; it will be truncated
488
     * @return string Returns null if there was no replay file name recorded
489
     */
490
    public function getReplayFileName($length = 0)
491
    {
492
        if ($length > 0) {
493
            return substr($this->replay_file, 0, $length);
494
        }
495
496
        return $this->replay_file;
497
    }
498
499
    /**
500
     * Get the match duration
501
     * @return int The duration of the match in minutes
502
     */
503 2
    public function getDuration()
504
    {
505 2
        return $this->duration;
506
    }
507
508
    /**
509
     * Set the match duration
510
     *
511
     * @param  int  $duration The new duration of the match in minutes
512
     * @return self
513
     */
514
    public function setDuration($duration)
515
    {
516
        return $this->updateProperty($this->duration, "duration", $duration, "i");
517
    }
518
519
    /**
520
     * Get the user who entered the match
521
     * @return Player
522
     */
523 1
    public function getEnteredBy()
524
    {
525 1
        return Player::get($this->entered_by);
526
    }
527
528
    /**
529
     * Get the loser of the match
530
     *
531
     * @return Team The team that was the loser or the team with the lower elo if the match was a draw
532
     */
533 8
    public function getLoser()
534
    {
535
        // Get the winner of the match
536 8
        $winner = $this->getWinner();
537
538
        // Get the team that wasn't the winner... Duh
539 8
        return $this->getOpponent($winner->getId());
540
    }
541
542
    /**
543
     * Get the winner of a match
544
     *
545
     * @return Team The team that was the victor or the team with the lower elo if the match was a draw
546
     */
547 8
    public function getWinner()
548
    {
549
        // Get the team that had its ELO increased
550 8
        if ($this->elo_diff > 0) {
551 6
            return $this->getTeamA();
552 2
        } elseif ($this->elo_diff < 0) {
553 2
            return $this->getTeamB();
554
        }
555
556
        // If the ELOs are the same, return Team A because well, fuck you that's why
557
        return $this->getTeamA();
558
    }
559
560
    /**
561
     * Determine whether the match was a draw
562
     * @return bool True if the match ended without any winning teams
563
     */
564 9
    public function isDraw()
565
    {
566 9
        return $this->team_a_points == $this->team_b_points;
567
    }
568
569
    /**
570
     * Reset the ELOs of the teams participating in the match
571
     *
572
     * @return self
573
     */
574
    public function resetELOs()
575
    {
576
        $this->getTeamA()->changeELO(-$this->elo_diff);
577
        $this->getTeamB()->changeELO(+$this->elo_diff);
578
    }
579
580
    /**
581
     * Enter a new match to the database
582
     * @param  int             $a          Team A's ID
583
     * @param  int             $b          Team B's ID
584
     * @param  int             $a_points   Team A's match points
585
     * @param  int             $b_points   Team B's match points
586
     * @param  int             $duration   The match duration in minutes
587
     * @param  int|null        $entered_by The ID of the player reporting the match
588
     * @param  string|DateTime $timestamp  When the match was played
589
     * @param  int[]           $a_players  The IDs of the first team's players
590
     * @param  int[]           $b_players  The IDs of the second team's players
591
     * @param  string|null     $server     The address of the server where the match was played
592
     * @param  int|null        $port       The port of the server where the match was played
593
     * @param  string          $replayFile The name of the replay file of the match
594
     * @param  int             $map        The ID of the map where the match was played, only for rotational leagues
595
     * @return Match           An object representing the match that was just entered
596
     */
597 9
    public static function enterMatch(
598
        $a, $b, $a_points, $b_points, $duration, $entered_by, $timestamp = "now",
599
        $a_players = array(), $b_players = array(), $server = null, $port = null,
600
        $replayFile = null, $map = null
601
    ) {
602 9
        $team_a = Team::get($a);
603 9
        $team_b = Team::get($b);
604 9
        $a_elo = $team_a->getElo();
605 9
        $b_elo = $team_b->getElo();
606
607 9
        $diff = self::calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration);
608
609
        // Update team ELOs
610 9
        $team_a->changeElo($diff);
611 9
        $team_b->changeElo(-$diff);
612
613 9
        $match = self::create(array(
614 9
            'team_a'         => $a,
615 9
            'team_b'         => $b,
616 9
            'team_a_points'  => $a_points,
617 9
            'team_b_points'  => $b_points,
618 9
            'team_a_players' => implode(',', $a_players),
619 9
            'team_b_players' => implode(',', $b_players),
620 9
            'team_a_elo_new' => $team_a->getElo(),
621 9
            'team_b_elo_new' => $team_b->getElo(),
622 9
            'elo_diff'       => $diff,
623 9
            'timestamp'      => TimeDate::from($timestamp)->toMysql(),
624 9
            'duration'       => $duration,
625 9
            'entered_by'     => $entered_by,
626 9
            'server'         => $server,
627 9
            'port'           => $port,
628 9
            'replay_file'    => $replayFile,
629 9
            'map'            => $map,
630 9
            'status'         => 'entered'
631 9
        ), 'iiiissiiisiisisis', 'updated');
632
633 9
        $match->updateMatchCount();
634
635 9
        return $match;
636
    }
637
638
    /**
639
     * Calculate the ELO score difference
640
     *
641
     * Computes the absolute value of the ELO score difference on each team
642
     * after a match, based on GU League's rules.
643
     *
644
     * @param  int $a_elo    Team A's current ELO score
645
     * @param  int $b_elo    Team B's current ELO score
646
     * @param  int $a_points Team A's match points
647
     * @param  int $b_points Team B's match points
648
     * @param  int $duration The match duration in minutes
649
     * @return int The ELO score difference
650
     */
651 9
    public static function calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration)
652
    {
653 9
        $prob = 1.0 / (1 + pow(10, (($b_elo - $a_elo) / 400.0)));
654 9
        if ($a_points > $b_points) {
655 5
            $diff = 50 * (1 - $prob);
656 5
        } elseif ($a_points == $b_points) {
657 4
            $diff = 50 * (0.5 - $prob);
658
        } else {
659 1
            $diff = 50 * (0 - $prob);
660
        }
661
662
        // Apply ELO modifiers from `config.yml`
663 9
        $durations = Service::getParameter('bzion.league.duration');
664 9
        $diff *= (isset($durations[$duration])) ? $durations[$duration] : 1;
665
666 9
        if (abs($diff) < 1 && $diff != 0) {
667
            // ELOs such as 0.75 should round up to 1...
668 2
            return ($diff > 0) ? 1 : -1;
669
        }
670
671
        // ...everything else is rounded down (-3.7 becomes -3 and 48.1 becomes 48)
672 7
        return intval($diff);
673
    }
674
675
    /**
676
     * Find if a match's stored ELO is correct
677
     */
678
    public function isEloCorrect()
679
    {
680
        return $this->elo_diff === $this->calculateEloDiff(
681
            $this->getTeamAEloOld(),
682
            $this->getTeamBEloOld(),
683
            $this->getTeamAPoints(),
684
            $this->getTeamBPoints(),
685
            $this->getDuration()
686
        );
687
    }
688
689
    /**
690
     * Recalculate the match's elo and adjust the team ELO values
691
     */
692
    public function recalculateElo()
693
    {
694
        $a = $this->getTeamA();
695
        $b = $this->getTeamB();
696
697
        $elo = $this->calculateEloDiff(
698
            $a->getElo(),
699
            $b->getElo(),
700
            $this->getTeamAPoints(),
701
            $this->getTeamBPoints(),
702
            $this->getDuration()
703
        );
704
705
        $this->updateProperty($this->elo_diff, "elo_diff", $elo, "i");
706
707
        $a->changeElo($elo);
708
        $b->changeElo(-$elo);
709
710
        $this->updateProperty($this->team_a_elo_new, "team_a_elo_new", $a->getElo(), "i");
711
        $this->updateProperty($this->team_b_elo_new, "team_b_elo_new", $b->getElo(), "i");
712
    }
713
714
    /**
715
     * Get all the matches in the database
716
     */
717 1
    public static function getMatches()
718
    {
719 1
        return self::getQueryBuilder()->active()->getModels();
720
    }
721
722
    /**
723
     * Get a query builder for matches
724
     * @return MatchQueryBuilder
725
     */
726 3
    public static function getQueryBuilder()
727
    {
728 3
        return new MatchQueryBuilder('Match', array(
729
            'columns' => array(
730
                'firstTeam'        => 'team_a',
731
                'secondTeam'       => 'team_b',
732
                'firstTeamPoints'  => 'team_a_points',
733
                'secondTeamPoints' => 'team_b_points',
734
                'time'             => 'timestamp',
735
                'status'           => 'status'
736 3
            ),
737
        ));
738
    }
739
740
    /**
741
     * {@inheritdoc}
742
     */
743
    public function delete()
744
    {
745
        $this->updateMatchCount(true);
746
747
        return parent::delete();
748
    }
749
750
    /**
751
     * {@inheritdoc}
752
     */
753 3
    public static function getActiveStatuses()
754
    {
755 3
        return array('entered');
756
    }
757
758
    /**
759
     * {@inheritdoc}
760
     */
761 1
    public function getName()
762
    {
763 1
        return sprintf("(+/- %d) %s [%d] vs [%d] %s",
764 1
            $this->getEloDiff(),
765 1
            $this->getWinner()->getName(),
766 1
            $this->getScore($this->getWinner()),
0 ignored issues
show
Bug introduced by
It seems like $this->getWinner() can be null; however, getScore() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
767 1
            $this->getScore($this->getLoser()),
0 ignored issues
show
Bug introduced by
It seems like $this->getLoser() can be null; however, getScore() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
768 1
            $this->getLoser()->getName()
769
        );
770
    }
771
772
    /**
773
     * Update the match count of the teams participating in the match
774
     *
775
     * @param bool $decrement Whether to decrement instead of incrementing the match count
776
     */
777 9
    private function updateMatchCount($decrement = false)
778
    {
779 9
        $diff = ($decrement) ? -1 : 1;
780
781 9
        if ($this->isDraw()) {
782 4
            $this->getTeamA()->changeMatchCount($diff, 'draw');
783 4
            $this->getTeamB()->changeMatchCount($diff, 'draw');
784
        } else {
785 6
            $this->getWinner()->changeMatchCount($diff, 'win');
786 6
            $this->getLoser()->changeMatchCount($diff, 'loss');
787
        }
788 9
    }
789
}
790