Completed
Push — feature/player-elo ( a3fab4...3fea36 )
by Vladimir
05:55 queued 03:34
created

Player::getElo()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 34
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.0488

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 14
cts 16
cp 0.875
rs 8.439
c 0
b 0
f 0
cc 5
eloc 14
nc 9
nop 2
crap 5.0488
1
<?php
2
/**
3
 * This file contains functionality relating to a league player
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
use Carbon\Carbon;
10
use Symfony\Component\Security\Core\Util\SecureRandom;
11
use Symfony\Component\Security\Core\Util\StringUtils;
12
13
/**
14
 * A league player
15
 * @package    BZiON\Models
16
 */
17
class Player extends AvatarModel implements NamedModel, DuplexUrlInterface, EloInterface
18
{
19
    /**
20
     * These are built-in roles that cannot be deleted via the web interface so we will be storing these values as
21
     * constant variables. Hopefully, a user won't be silly enough to delete them manually from the database.
22
     *
23
     * @TODO Deprecate these and use the Role constants
24
     */
25
    const DEVELOPER    = Role::DEVELOPER;
26
    const ADMIN        = Role::ADMINISTRATOR;
27
    const COP          = Role::COP;
28
    const REFEREE      = Role::REFEREE;
29
    const S_ADMIN      = Role::SYSADMIN;
30
    const PLAYER       = Role::PLAYER;
31
    const PLAYER_NO_PM = Role::PLAYER_NO_PM;
32
33
    /**
34
     * The bzid of the player
35
     * @var int
36
     */
37
    protected $bzid;
38
39
    /**
40
     * The id of the player's team
41
     * @var int
42
     */
43
    protected $team;
44
45
    /**
46
     * The player's status
47
     * @var string
48
     */
49
    protected $status;
50
51
    /**
52
     * The player's e-mail address
53
     * @var string
54
     */
55
    protected $email;
56
57
    /**
58
     * Whether the player has verified their e-mail address
59
     * @var bool
60
     */
61
    protected $verified;
62
63
    /**
64
     * What kind of events the player should be e-mailed about
65
     * @var string
66
     */
67
    protected $receives;
68
69
    /**
70
     * A confirmation code for the player's e-mail address verification
71
     * @var string
72
     */
73
    protected $confirmCode;
74
75
    /**
76
     * Whether the callsign of the player is outdated
77
     * @var bool
78
     */
79
    protected $outdated;
80
81
    /**
82
     * The player's profile description
83
     * @var string
84
     */
85
    protected $description;
86
87
    /**
88
     * The id of the player's country
89
     * @var int
90
     */
91
    protected $country;
92
93
    /**
94
     * The player's timezone PHP identifier, e.g. "Europe/Paris"
95
     * @var string
96
     */
97
    protected $timezone;
98
99
    /**
100
     * The date the player joined the site
101
     * @var TimeDate
102
     */
103
    protected $joined;
104
105
    /**
106
     * The date of the player's last login
107
     * @var TimeDate
108
     */
109
    protected $last_login;
110
111
    /**
112
     * The date of the player's last match
113
     * @var Match
114
     */
115
    protected $last_match;
116
117
    /**
118
     * The roles a player belongs to
119
     * @var Role[]
120
     */
121
    protected $roles;
122
123
    /**
124
     * The permissions a player has
125
     * @var Permission[]
126
     */
127
    protected $permissions;
128
129
    /**
130
     * A section for admins to write notes about players
131
     * @var string
132
     */
133
    protected $admin_notes;
134
135
    /**
136
     * The ban of the player, or null if the player is not banned
137
     * @var Ban|null
138
     */
139
    protected $ban;
140
141
    /**
142
     * Cached results for match summaries
143
     *
144
     * @var array
145
     */
146
    private $cachedMatchSummary;
147
148
    /**
149
     * The cached match count for a player
150
     *
151
     * @var int
152
     */
153
    private $cachedMatchCount = null;
154
155
    /**
156
     * The player's current Elo
157
     *
158
     * @var int
159
     */
160
    private $elo;
161
162
    private $matchActivity;
163
164
    /**
165
     * The name of the database table used for queries
166
     */
167
    const TABLE = "players";
168
169
    /**
170
     * The location where avatars will be stored
171
     */
172
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
173
174
    const EDIT_PERMISSION = Permission::EDIT_USER;
175
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
176
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
177
178
    /**
179
     * {@inheritdoc}
180
     */
181 44
    protected function assignResult($player)
182
    {
183 44
        $this->bzid = $player['bzid'];
184 44
        $this->name = $player['username'];
185 44
        $this->alias = $player['alias'];
186 44
        $this->team = $player['team'];
187 44
        $this->status = $player['status'];
188 44
        $this->avatar = $player['avatar'];
189 44
        $this->country = $player['country'];
190
191 44
        if (key_exists('activity', $player)) {
192
            $this->matchActivity = ($player['activity'] != null) ? $player['activity'] : 0.0;
193
        }
194 44
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199 44
    protected function assignLazyResult($player)
200
    {
201 44
        $this->email = $player['email'];
202 44
        $this->verified = $player['verified'];
203 44
        $this->receives = $player['receives'];
204 44
        $this->confirmCode = $player['confirm_code'];
205 44
        $this->outdated = $player['outdated'];
206 44
        $this->description = $player['description'];
207 44
        $this->timezone = $player['timezone'];
208 44
        $this->joined = TimeDate::fromMysql($player['joined']);
209 44
        $this->last_login = TimeDate::fromMysql($player['last_login']);
210 44
        $this->last_match = Match::get($player['last_match']);
211 44
        $this->admin_notes = $player['admin_notes'];
212 44
        $this->ban = Ban::getBan($this->id);
213
214 44
        $this->cachedMatchSummary = [];
215
216 44
        $this->updateUserPermissions();
217 44
    }
218
219
    /**
220
     * Add a player a new role
221
     *
222
     * @param Role|int $role_id The role ID to add a player to
223
     *
224
     * @return bool Whether the operation was successful or not
225
     */
226 44
    public function addRole($role_id)
227
    {
228 44
        if ($role_id instanceof Role) {
229 1
            $role_id = $role_id->getId();
230 1
        }
231
232 44
        $this->lazyLoad();
233
234
        // Make sure the player doesn't already have the role
235 44
        foreach ($this->roles as $playerRole) {
236 14
            if ($playerRole->getId() == $role_id) {
237
                return false;
238
            }
239 44
        }
240
241 44
        $status = $this->modifyRole($role_id, "add");
242 44
        $this->refresh();
243
244 44
        return $status;
245
    }
246
247
    /**
248
     * Get the notes admins have left about a player
249
     * @return string The notes
250
     */
251
    public function getAdminNotes()
252
    {
253
        $this->lazyLoad();
254
255
        return $this->admin_notes;
256
    }
257
258
    /**
259
     * Get the player's BZID
260
     * @return int The BZID
261
     */
262 1
    public function getBZID()
263
    {
264 1
        return $this->bzid;
265
    }
266
267
    /**
268
     * Get the country a player belongs to
269
     *
270
     * @return Country The country belongs to
271
     */
272 1
    public function getCountry()
273
    {
274 1
        return Country::get($this->country);
275
    }
276
277
    /**
278
     * Get the e-mail address of the player
279
     *
280
     * @return string The address
281
     */
282
    public function getEmailAddress()
283
    {
284
        $this->lazyLoad();
285
286
        return $this->email;
287
    }
288
289
    /**
290
     * Get the player's Elo for a season.
291
     *
292
     * With the default arguments, it will fetch the Elo for the current season.
293
     *
294
     * @param string|null $season The season we're looking for: winter, spring, summer, or fall
295
     * @param int|null    $year   The year of the season we're looking for
296
     *
297
     * @return int The player's Elo
298
     */
299 5
    public function getElo($season = null, $year = null)
300
    {
301 5
        if ($this->elo !== null) {
302 5
            return $this->elo;
303
        }
304
305 5
        if ($season === null) {
306 5
            $season = Season::getCurrentSeason();
307 5
        }
308
309 5
        if ($year === null) {
310 5
            $year = Carbon::now()->year;
311 5
        }
312
313 5
        $query = $this->db->query('
314
          SELECT
315
            elo_new AS elo
316
          FROM
317
            player_elo
318
          WHERE
319
            user_id = ? AND season_year = ? AND season_period = ?
320
          ORDER BY
321
            match_id DESC
322
          LIMIT 1
323 5
        ', [$this->getId(), $year, $season]);
324
325 5
        if (count($query) > 0) {
326
            $this->elo = $query[0]['elo'];
327
        } else {
328 5
            $this->elo = 1200;
329
        }
330
331 5
        return $this->elo;
332
    }
333
334
    /**
335
     * Adjust the Elo of a player for the current season based on a Match
336
     *
337
     * **Warning:** If $match is null, the Elo for the player will be modified but the value will not be persisted to
338
     * the database.
339
     *
340
     * @param int        $adjust The value to be added to the current ELO (negative to subtract)
341
     * @param Match|null $match  The match where this Elo change took place
342
     */
343 5
    public function adjustElo($adjust, Match $match = null)
344
    {
345 5
        $elo = $this->getElo();
346 5
        $this->elo += $adjust;
347
348 5
        if ($match !== null) {
349 4
            $this->db->execute('
350
              INSERT INTO player_elo VALUES (?, ?, ?, ?, ?, ?)
351 4
            ', [ $this->getId(), $match->getId(), Season::getCurrentSeason(), Carbon::now()->year, $elo, $this->elo ]);
352 4
        }
353 5
    }
354
355
    /**
356
     * Returns whether the player has verified their e-mail address
357
     *
358
     * @return bool `true` for verified players
359
     */
360
    public function isVerified()
361
    {
362
        $this->lazyLoad();
363
364
        return $this->verified;
365
    }
366
367
    /**
368
     * Returns the confirmation code for the player's e-mail address verification
369
     *
370
     * @return string The player's confirmation code
371
     */
372
    public function getConfirmCode()
373
    {
374
        $this->lazyLoad();
375
376
        return $this->confirmCode;
377
    }
378
379
    /**
380
     * Returns what kind of events the player should be e-mailed about
381
     *
382
     * @return string The type of notifications
383
     */
384
    public function getReceives()
385
    {
386
        $this->lazyLoad();
387
388
        return $this->receives;
389
    }
390
391
    /**
392
     * Finds out whether the specified player wants and can receive an e-mail
393
     * message
394
     *
395
     * @param  string  $type
396
     * @return bool `true` if the player should be sent an e-mail
397
     */
398 1
    public function canReceive($type)
399
    {
400 1
        $this->lazyLoad();
401
402 1
        if (!$this->email || !$this->isVerified()) {
403
            // Unverified e-mail means the user will receive nothing
404 1
            return false;
405
        }
406
407
        if ($this->receives == 'everything') {
408
            return true;
409
        }
410
411
        return $this->receives == $type;
412
    }
413
414
    /**
415
     * Find out whether the specified confirmation code is correct
416
     *
417
     * This method protects against timing attacks
418
     *
419
     * @param  string $code The confirmation code to check
420
     * @return bool `true` for a correct e-mail verification code
421
     */
422
    public function isCorrectConfirmCode($code)
423
    {
424
        $this->lazyLoad();
425
426
        if ($this->confirmCode === null) {
427
            return false;
428
        }
429
430
        return StringUtils::equals($code, $this->confirmCode);
431
    }
432
433
    /**
434
     * Get the player's sanitized description
435
     * @return string The description
436
     */
437
    public function getDescription()
438
    {
439
        $this->lazyLoad();
440
441
        return $this->description;
442
    }
443
444
    /**
445
     * Get the joined date of the player
446
     * @return TimeDate The joined date of the player
447
     */
448
    public function getJoinedDate()
449
    {
450
        $this->lazyLoad();
451
452
        return $this->joined->copy();
453
    }
454
455
    /**
456
     * Get all of the known IPs used by the player
457
     *
458
     * @return string[][] An array containing IPs and hosts
459
     */
460
    public function getKnownIPs()
461
    {
462
        return $this->db->query(
463
            'SELECT DISTINCT ip, host FROM visits WHERE player = ? GROUP BY ip, host ORDER BY MAX(timestamp) DESC LIMIT 10',
464
            array($this->getId())
465
        );
466
    }
467
468
    /**
469
     * Get the last login for a player
470
     * @return TimeDate The date of the last login
471
     */
472
    public function getLastLogin()
473
    {
474
        $this->lazyLoad();
475
476
        return $this->last_login->copy();
477
    }
478
479
    /**
480
     * Get the last match
481
     * @return Match
482
     */
483
    public function getLastMatch()
484
    {
485
        $this->lazyLoad();
486
487
        return $this->last_match;
488
    }
489
490
    /**
491
     * Get all of the callsigns a player has used to log in to the website
492
     * @return string[] An array containing all of the past callsigns recorded for a player
493
     */
494
    public function getPastCallsigns()
495
    {
496
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
497
    }
498
499
    /**
500
     * Get the player's team
501
     * @return Team The object representing the team
502
     */
503 3
    public function getTeam()
504
    {
505 3
        return Team::get($this->team);
506
    }
507
508
    /**
509
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
510
     * @return string The timezone
511
     */
512 1
    public function getTimezone()
513
    {
514 1
        $this->lazyLoad();
515
516 1
        return ($this->timezone) ?: date_default_timezone_get();
517
    }
518
519
    /**
520
     * Get the roles of the player
521
     * @return Role[]
522
     */
523
    public function getRoles()
524
    {
525
        $this->lazyLoad();
526
527
        return $this->roles;
528
    }
529
530
    /**
531
     * Rebuild the list of permissions a user has been granted
532
     */
533 44
    private function updateUserPermissions()
534
    {
535 44
        $this->roles = Role::getRoles($this->id);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Role::getRoles($this->id) of type array<integer,object<Model>> is incompatible with the declared type array<integer,object<Role>> of property $roles.

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

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

Loading history...
536 44
        $this->permissions = array();
537
538 44
        foreach ($this->roles as $role) {
539 44
            $this->permissions = array_merge($this->permissions, $role->getPerms());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Model as the method getPerms() does only exist in the following sub-classes of Model: Permission, Role. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
Documentation Bug introduced by
It seems like array_merge($this->permi...ons, $role->getPerms()) of type array is incompatible with the declared type array<integer,object<Permission>> of property $permissions.

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

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

Loading history...
540 44
        }
541 44
    }
542
543
    /**
544
     * Check if a player has a specific permission
545
     *
546
     * @param string|null $permission The permission to check for
547
     *
548
     * @return bool Whether or not the player has the permission
549
     */
550 2
    public function hasPermission($permission)
551
    {
552 2
        if ($permission === null) {
553 1
            return false;
554
        }
555
556 2
        $this->lazyLoad();
557
558 2
        return isset($this->permissions[$permission]);
559
    }
560
561
    /**
562
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
563
     * active
564
     *
565
     * @return bool True if the player has been active
566
     */
567
    public function hasBeenActive()
568
    {
569
        $this->lazyLoad();
570
571
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
572
        $lastLogin = $this->last_login->copy()->modify($interval);
573
574
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
575
576
        if ($this->last_match->isValid()) {
577
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
578
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
579
        }
580
581
        return $hasBeenActive;
582
    }
583
584
    /**
585
     * Check whether the callsign of the player is outdated
586
     *
587
     * Returns true if this player has probably changed their callsign, making
588
     * the current username stored in the database obsolete
589
     *
590
     * @return bool Whether or not the player is disabled
591
     */
592
    public function isOutdated()
593
    {
594
        $this->lazyLoad();
595
596
        return $this->outdated;
597
    }
598
599
    /**
600
     * Check if a player's account has been disabled
601
     *
602
     * @return bool Whether or not the player is disabled
603
     */
604
    public function isDisabled()
605
    {
606
        return $this->status == "disabled";
607
    }
608
609
    /**
610
     * Check if everyone can log in as this user on a test environment
611
     *
612
     * @return bool
613
     */
614 1
    public function isTestUser()
615
    {
616 1
        return $this->status == "test";
617
    }
618
619
    /**
620
     * Check if a player is teamless
621
     *
622
     * @return bool True if the player is teamless
623
     */
624 23
    public function isTeamless()
625
    {
626 23
        return empty($this->team);
627
    }
628
629
    /**
630
     * Mark a player's account as banned
631
     */
632 1
    public function markAsBanned()
633
    {
634 1
        if ($this->status != 'active') {
635
            return $this;
636
        }
637
638 1
        return $this->updateProperty($this->status, "status", "banned");
639
    }
640
641
    /**
642
     * Mark a player's account as unbanned
643
     */
644
    public function markAsUnbanned()
645
    {
646
        if ($this->status != 'banned') {
647
            return $this;
648
        }
649
650
        return $this->updateProperty($this->status, "status", "active");
651
    }
652
653
    /**
654
     * Find out if a player is banned
655
     *
656
     * @return bool
657
     */
658 2
    public function isBanned()
659
    {
660 2
        return Ban::getBan($this->id) !== null;
661
    }
662
663
    /**
664
     * Get the ban of the player
665
     *
666
     * This method performs a load of all the lazy parameters of the Player
667
     *
668
     * @return Ban|null The current ban of the player, or null if the player is
669
     *                  is not banned
670
     */
671
    public function getBan()
672
    {
673
        $this->lazyLoad();
674
675
        return $this->ban;
676
    }
677
678
    /**
679
     * Remove a player from a role
680
     *
681
     * @param int $role_id The role ID to add or remove
682
     *
683
     * @return bool Whether the operation was successful or not
684
     */
685
    public function removeRole($role_id)
686
    {
687
        $status = $this->modifyRole($role_id, "remove");
688
        $this->refresh();
689
690
        return $status;
691
    }
692
693
    /**
694
     * Set the player's email address and reset their verification status
695
     * @param string $email The address
696
     */
697
    public function setEmailAddress($email)
698
    {
699
        $this->lazyLoad();
700
701
        if ($this->email == $email) {
702
            // The e-mail hasn't changed, don't do anything
703
            return;
704
        }
705
706
        $this->setVerified(false);
707
        $this->generateNewConfirmCode();
708
709
        $this->updateProperty($this->email, 'email', $email);
710
    }
711
712
    /**
713
     * Set whether the player has verified their e-mail address
714
     *
715
     * @param  bool $verified Whether the player is verified or not
716
     * @return self
717
     */
718
    public function setVerified($verified)
719
    {
720
        $this->lazyLoad();
721
722
        if ($verified) {
723
            $this->setConfirmCode(null);
724
        }
725
726
        return $this->updateProperty($this->verified, 'verified', $verified);
727
    }
728
729
    /**
730
     * Generate a new random confirmation token for e-mail address verification
731
     *
732
     * @return self
733
     */
734
    public function generateNewConfirmCode()
735
    {
736
        $generator = new SecureRandom();
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Component\Security\Core\Util\SecureRandom has been deprecated with message: since version 2.8, to be removed in 3.0. Use the random_bytes function instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
737
        $random = $generator->nextBytes(16);
738
739
        return $this->setConfirmCode(bin2hex($random));
740
    }
741
742
    /**
743
     * Set the confirmation token for e-mail address verification
744
     *
745
     * @param  string $code The confirmation code
746
     * @return self
747
     */
748
    private function setConfirmCode($code)
749
    {
750
        $this->lazyLoad();
751
752
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
753
    }
754
755
    /**
756
     * Set what kind of events the player should be e-mailed about
757
     *
758
     * @param  string $receives The type of notification
759
     * @return self
760
     */
761
    public function setReceives($receives)
762
    {
763
        $this->lazyLoad();
764
765
        return $this->updateProperty($this->receives, 'receives', $receives);
766
    }
767
768
    /**
769
     * Set whether the callsign of the player is outdated
770
     *
771
     * @param  bool $outdated Whether the callsign is outdated
772
     * @return self
773
     */
774 44
    public function setOutdated($outdated)
775
    {
776 44
        $this->lazyLoad();
777
778 44
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
779
    }
780
781
    /**
782
     * Set the player's description
783
     * @param string $description The description
784
     */
785
    public function setDescription($description)
786
    {
787
        $this->updateProperty($this->description, "description", $description);
788
    }
789
790
    /**
791
     * Set the player's timezone
792
     * @param string $timezone The timezone
793
     */
794
    public function setTimezone($timezone)
795
    {
796
        $this->updateProperty($this->timezone, "timezone", $timezone);
797
    }
798
799
    /**
800
     * Set the player's team
801
     * @param int $team The team's ID
802
     */
803 23
    public function setTeam($team)
804
    {
805 23
        $this->updateProperty($this->team, "team", $team);
806 23
    }
807
808
    /**
809
     * Set the match the player last participated in
810
     *
811
     * @param int $match The match's ID
812
     */
813 14
    public function setLastMatch($match)
814
    {
815 14
        $this->updateProperty($this->last_match, 'last_match', $match);
816 14
    }
817
818
    /**
819
     * Set the player's status
820
     * @param string $status The new status
821
     */
822
    public function setStatus($status)
823
    {
824
        $this->updateProperty($this->status, 'status', $status);
825
    }
826
827
    /**
828
     * Set the player's admin notes
829
     * @param  string $admin_notes The new admin notes
830
     * @return self
831
     */
832
    public function setAdminNotes($admin_notes)
833
    {
834
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
835
    }
836
837
    /**
838
     * Set the player's country
839
     * @param  int   $country The ID of the new country
840
     * @return self
841
     */
842
    public function setCountry($country)
843
    {
844
        return $this->updateProperty($this->country, 'country', $country);
845
    }
846
847
    /**
848
     * Updates this player's last login
849
     */
850
    public function updateLastLogin()
851
    {
852
        $this->update("last_login", TimeDate::now()->toMysql());
853
    }
854
855
    /**
856
     * Get the player's username
857
     * @return string The username
858
     */
859 1
    public function getUsername()
860
    {
861 1
        return $this->name;
862
    }
863
864
    /**
865
     * Get the player's username, safe for use in your HTML
866
     * @return string The username
867
     */
868 1
    public function getEscapedUsername()
869
    {
870 1
        return $this->getEscapedName();
871
    }
872
873
    /**
874
     * Alias for Player::setUsername()
875
     *
876
     * @param  string $username The new username
877
     * @return self
878
     */
879
    public function setName($username)
880
    {
881
        return $this->setUsername($username);
882
    }
883
884
    /**
885
     * Mark all the unread messages of a player as read
886
     *
887
     * @return void
888
     */
889
    public function markMessagesAsRead()
890
    {
891
        $this->db->execute(
892
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
893
            array($this->id)
894
        );
895
    }
896
897
    /**
898
     * Set the roles of a user
899
     *
900
     * @todo   Is it worth making this faster?
901
     * @param  Role[] $roles The new roles of the user
902
     * @return self
903
     */
904
    public function setRoles($roles)
905
    {
906
        $this->lazyLoad();
907
908
        $oldRoles = Role::mapToIds($this->roles);
909
        $this->roles = $roles;
910
        $roleIds = Role::mapToIds($roles);
911
912
        $newRoles     = array_diff($roleIds, $oldRoles);
913
        $removedRoles = array_diff($oldRoles, $roleIds);
914
915
        foreach ($newRoles as $role) {
916
            $this->modifyRole($role, 'add');
917
        }
918
919
        foreach ($removedRoles as $role) {
920
            $this->modifyRole($role, 'remove');
921
        }
922
923
        $this->refresh();
924
925
        return $this;
926
    }
927
928
    /**
929
     * Give or remove a role to/form a player
930
     *
931
     * @param int    $role_id The role ID to add or remove
932
     * @param string $action  Whether to "add" or "remove" a role for a player
933
     *
934
     * @return bool Whether the operation was successful or not
935
     */
936 44
    private function modifyRole($role_id, $action)
937
    {
938 44
        $role = Role::get($role_id);
939
940 44
        if ($role->isValid()) {
941 44
            if ($action == "add") {
942 44
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
943 44
            } elseif ($action == "remove") {
944
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
945
            } else {
946
                throw new Exception("Unrecognized role action");
947
            }
948
949 44
            return true;
950
        }
951
952
        return false;
953
    }
954
955
    /**
956
     * Given a player's BZID, get a player object
957
     *
958
     * @param  int    $bzid The player's BZID
959
     * @return Player
960
     */
961 2
    public static function getFromBZID($bzid)
962
    {
963 2
        return self::get(self::fetchIdFrom($bzid, "bzid"));
964
    }
965
966
    /**
967
     * Get a single player by their username
968
     *
969
     * @param  string $username The username to look for
970
     * @return Player
971
     */
972 1
    public static function getFromUsername($username)
973
    {
974 1
        $player = static::get(self::fetchIdFrom($username, 'username'));
975
976 1
        return $player->inject('name', $username);
977
    }
978
979
    /**
980
     * Get all the players in the database that have an active status
981
     * @return Player[] An array of player BZIDs
982
     */
983
    public static function getPlayers()
984
    {
985
        return self::arrayIdToModel(
986
            self::fetchIdsFrom("status", array("active", "test"), false)
987
        );
988
    }
989
990
    /**
991
     * Show the number of notifications the user hasn't read yet
992
     * @return int
993
     */
994 1
    public function countUnreadNotifications()
995
    {
996 1
        return Notification::countUnreadNotifications($this->id);
997
    }
998
999
    /**
1000
     * Count the number of matches a player has participated in
1001
     * @return int
1002
     */
1003
    public function getMatchCount()
1004
    {
1005
        if ($this->cachedMatchCount === null) {
1006
            $this->cachedMatchCount = Match::getQueryBuilder()
1007
                ->active()
1008
                ->with($this)
1009
                ->count();
1010
        }
1011
1012
        return $this->cachedMatchCount;
1013
    }
1014
1015
    /**
1016
     * Get the (victory/total matches) ratio of the player
1017
     * @return float
1018
     */
1019
    public function getMatchWinRatio()
1020
    {
1021
        $count = $this->getMatchCount();
1022
1023
        if ($count == 0) {
1024
            return 0;
1025
        }
1026
1027
        $wins = Match::getQueryBuilder()
1028
            ->active()
1029
            ->with($this, 'win')
1030
            ->count();
1031
1032
        return $wins / $count;
1033
    }
1034
1035
    /**
1036
     * Get the (total caps made by team/total matches) ratio of the player
1037
     * @return float
1038
     */
1039
    public function getMatchAverageCaps()
1040
    {
1041
        $count = $this->getMatchCount();
1042
1043
        if ($count == 0) {
1044
            return 0;
1045
        }
1046
1047
        // Get the sum of team A points if the player was in team A, team B
1048
        // points if the player was in team B, and their average if the player
1049
        // was on both teams for some reason
1050
        $query = $this->db->query(
1051
            "SELECT SUM(
1052
                IF(
1053
                    FIND_IN_SET(?, team_a_players) AND FIND_IN_SET(?, team_b_players),
1054
                    (team_a_points+team_b_points)/2,
1055
                    IF(FIND_IN_SET(?, team_a_players), team_a_points, team_b_points)
1056
                )
1057
            ) AS sum FROM matches WHERE status='entered' AND (FIND_IN_SET(?, team_a_players) OR FIND_IN_SET(?, team_b_players))",
1058
            array_fill(0, 5, $this->id)
1059
        );
1060
1061
        return $query[0]['sum'] / $count;
1062
    }
1063
1064
    /**
1065
     * Get the match activity in matches per day for a player
1066
     *
1067
     * @return float
1068
     */
1069
    public function getMatchActivity()
1070
    {
1071
        if ($this->matchActivity !== null) {
1072
            return $this->matchActivity;
1073
        }
1074
1075
        $activity = 0.0;
1076
1077
        $matches = Match::getQueryBuilder()
1078
            ->active()
1079
            ->with($this)
1080
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
1081
            ->getModels($fast = true);
1082
1083
        foreach ($matches as $match) {
1084
            $activity += $match->getActivity();
1085
        }
1086
1087
        return $activity;
1088
    }
1089
1090
    /**
1091
     * Return an array of matches this player participated in per month.
1092
     *
1093
     * ```
1094
     * ['yyyy-mm'] = <number of matches>
1095
     * ```
1096
     *
1097
     * @param TimeDate|string $timePeriod
1098
     *
1099
     * @return int[]
1100
     */
1101
    public function getMatchSummary($timePeriod = '1 year ago')
1102
    {
1103
        $since = ($timePeriod instanceof TimeDate) ? $timePeriod : TimeDate::from($timePeriod);
1104
1105
        if (!isset($this->cachedMatchSummary[(string)$timePeriod])) {
1106
            $this->cachedMatchSummary[(string)$timePeriod] = Match::getQueryBuilder()
1107
                ->active()
1108
                ->with($this)
1109
                ->where('time')->isAfter($since)
1110
                ->getSummary($since)
1111
            ;
1112
        }
1113
1114
        return $this->cachedMatchSummary[(string)$timePeriod];
1115
    }
1116
1117
    /**
1118
     * Show the number of messages the user hasn't read yet
1119
     * @return int
1120
     */
1121 1
    public function countUnreadMessages()
1122
    {
1123 1
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1124 1
            $this->id, 'player_conversations'
0 ignored issues
show
Documentation introduced by
$this->id is of type integer, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1125 1
        );
1126
    }
1127
1128
    /**
1129
     * Routine maintenance for a player when they participate in a match.
1130
     *
1131
     * This function updates the last match played, changes the ELO, and creates a historic ELO change.
1132
     *
1133
     * @param Match $match
1134
     * @param int   $eloDiff
1135
     */
1136 14
    public function setMatchParticipation(Match $match, $eloDiff)
1137
    {
1138 14
        $this->setLastMatch($match->getId());
1139
1140 14
        if ($match->isOfficial() && $eloDiff !== null) {
1141 4
            $this->adjustElo($eloDiff, $match);
1142 4
        }
1143 14
    }
1144
1145
    /**
1146
     * Get all of the members belonging to a team
1147
     * @param  int      $teamID The ID of the team to fetch the members of
1148
     * @return Player[] An array of Player objects of the team members
1149
     */
1150 2
    public static function getTeamMembers($teamID)
1151
    {
1152 2
        return self::arrayIdToModel(
1153 2
            self::fetchIds("WHERE team = ?", array($teamID))
1154 2
        );
1155
    }
1156
1157
    /**
1158
     * {@inheritdoc}
1159
     */
1160 1
    public static function getActiveStatuses()
1161
    {
1162 1
        return array('active', 'reported', 'test');
1163
    }
1164
1165
    /**
1166
     * {@inheritdoc}
1167
     */
1168 44
    public static function getEagerColumns($prefix = null)
1169
    {
1170
        $columns = [
1171 44
            'id',
1172 44
            'bzid',
1173 44
            'team',
1174 44
            'username',
1175 44
            'alias',
1176 44
            'status',
1177 44
            'avatar',
1178 44
            'country',
1179 44
        ];
1180
1181 44
        return self::formatColumns($prefix, $columns);
1182
    }
1183
1184
    /**
1185
     * {@inheritdoc}
1186
     */
1187 44
    public static function getLazyColumns($prefix = null)
1188
    {
1189
        $columns = [
1190 44
            'email',
1191 44
            'verified',
1192 44
            'receives',
1193 44
            'confirm_code',
1194 44
            'outdated',
1195 44
            'description',
1196 44
            'timezone',
1197 44
            'joined',
1198 44
            'last_login',
1199 44
            'last_match',
1200 44
            'admin_notes',
1201 44
        ];
1202
1203 44
        return self::formatColumns($prefix, $columns);
1204
    }
1205
1206
    /**
1207
     * Get a query builder for players
1208
     * @return PlayerQueryBuilder
1209
     */
1210
    public static function getQueryBuilder()
1211
    {
1212
        return new PlayerQueryBuilder('Player', array(
1213
            'columns' => array(
1214
                'name'     => 'username',
1215
                'team'     => 'team',
1216
                'outdated' => 'outdated',
1217
                'status'   => 'status',
1218
            ),
1219
            'name' => 'name',
1220
        ));
1221
    }
1222
1223
    /**
1224
     * Enter a new player to the database
1225
     * @param  int              $bzid        The player's bzid
1226
     * @param  string           $username    The player's username
1227
     * @param  int              $team        The player's team
1228
     * @param  string           $status      The player's status
1229
     * @param  int              $role_id     The player's role when they are first created
1230
     * @param  string           $avatar      The player's profile avatar
1231
     * @param  string           $description The player's profile description
1232
     * @param  int              $country     The player's country
1233
     * @param  string           $timezone    The player's timezone
1234
     * @param  string|\TimeDate $joined      The date the player joined
1235
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1236
     * @return Player           An object representing the player that was just entered
1237
     */
1238 44
    public static function newPlayer($bzid, $username, $team = null, $status = "active", $role_id = self::PLAYER, $avatar = "", $description = "", $country = 1, $timezone = null, $joined = "now", $last_login = "now")
1239
    {
1240 44
        $joined = TimeDate::from($joined);
1241 44
        $last_login = TimeDate::from($last_login);
1242 44
        $timezone = ($timezone) ?: date_default_timezone_get();
1243
1244 44
        $player = self::create(array(
1245 44
            'bzid'        => $bzid,
1246 44
            'team'        => $team,
1247 44
            'username'    => $username,
1248 44
            'alias'       => self::generateAlias($username),
1249 44
            'status'      => $status,
1250 44
            'avatar'      => $avatar,
1251 44
            'description' => $description,
1252 44
            'country'     => $country,
1253 44
            'timezone'    => $timezone,
1254 44
            'joined'      => $joined->toMysql(),
1255 44
            'last_login'  => $last_login->toMysql(),
1256 44
        ));
1257
1258 44
        $player->addRole($role_id);
1259 44
        $player->getIdenticon($player->getId());
1260 44
        $player->setUsername($username);
1261
1262 44
        return $player;
1263
    }
1264
1265
    /**
1266
     * Determine if a player exists in the database
1267
     * @param  int  $bzid The player's bzid
1268
     * @return bool Whether the player exists in the database
1269
     */
1270
    public static function playerBZIDExists($bzid)
1271
    {
1272
        return self::getFromBZID($bzid)->isValid();
1273
    }
1274
1275
    /**
1276
     * Change a player's callsign and add it to the database if it does not
1277
     * exist as a past callsign
1278
     *
1279
     * @param  string $username The new username of the player
1280
     * @return self
1281
     */
1282 44
    public function setUsername($username)
1283
    {
1284
        // The player's username was just fetched from BzDB, it's definitely not
1285
        // outdated
1286 44
        $this->setOutdated(false);
1287
1288
        // Players who have this player's username are considered outdated
1289 44
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1290
1291 44
        if ($username === $this->name) {
1292
            // The player's username hasn't changed, no need to do anything
1293 44
            return $this;
1294
        }
1295
1296
        // Players who used to have our player's username are not outdated anymore,
1297
        // unless they are more than one.
1298
        // Even though we are sure that the old and new usernames are not equal,
1299
        // MySQL makes a different type of string equality tests, which is why we
1300
        // also check IDs to make sure not to affect our own player's outdatedness.
1301
        $this->db->execute("
1302
            UPDATE {$this->table} SET outdated =
1303
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1304
            WHERE username = ? AND id != ?",
1305
            array($this->name, $this->id, $this->name, $this->id));
1306
1307
        $this->updateProperty($this->name, 'username', $username);
1308
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1309
        $this->resetAlias();
1310
1311
        return $this;
1312
    }
1313
1314
    /**
1315
     * Alphabetical order function for use in usort (case-insensitive)
1316
     * @return Closure The sort function
1317
     */
1318
    public static function getAlphabeticalSort()
1319
    {
1320 1
        return function (Player $a, Player $b) {
1321 1
            return strcasecmp($a->getUsername(), $b->getUsername());
1322 1
        };
1323
    }
1324
1325
    /**
1326
     * {@inheritdoc}
1327
     * @todo Add a constraint that does this automatically
1328
     */
1329 44
    public function wipe()
1330
    {
1331 44
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1332
1333 44
        parent::wipe();
1334 44
    }
1335
1336
    /**
1337
     * Find whether the player can delete a model
1338
     *
1339
     * @param  PermissionModel $model       The model that will be seen
1340
     * @param  bool         $showDeleted Whether to show deleted models to admins
1341
     * @return bool
1342
     */
1343 1
    public function canSee($model, $showDeleted = false)
1344
    {
1345 1
        return $model->canBeSeenBy($this, $showDeleted);
1346
    }
1347
1348
    /**
1349
     * Find whether the player can delete a model
1350
     *
1351
     * @param  PermissionModel $model The model that will be deleted
1352
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1353
     *                                to soft-delete ones
1354
     * @return bool
1355
     */
1356 1
    public function canDelete($model, $hard = false)
1357
    {
1358 1
        if ($hard) {
1359
            return $model->canBeHardDeletedBy($this);
1360
        } else {
1361 1
            return $model->canBeSoftDeletedBy($this);
1362
        }
1363
    }
1364
1365
    /**
1366
     * Find whether the player can create a model
1367
     *
1368
     * @param  string  $modelName The PHP class identifier of the model type
1369
     * @return bool
1370
     */
1371 1
    public function canCreate($modelName)
1372
    {
1373 1
        return $modelName::canBeCreatedBy($this);
1374
    }
1375
1376
    /**
1377
     * Find whether the player can edit a model
1378
     *
1379
     * @param  PermissionModel $model The model which will be edited
1380
     * @return bool
1381
     */
1382 1
    public function canEdit($model)
1383
    {
1384 1
        return $model->canBeEditedBy($this);
1385
    }
1386
}
1387