Completed
Push — feature/player-elo ( 88ae6a...994bf4 )
by Vladimir
11:06
created

Player::addRole()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.0092

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 20
ccs 11
cts 12
cp 0.9167
rs 9.2
cc 4
eloc 10
nc 6
nop 1
crap 4.0092
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
    private $eloSeason;
156
    private $eloSeasonHistory;
157
158
    private $matchActivity;
159
160
    /**
161
     * The name of the database table used for queries
162
     */
163
    const TABLE = "players";
164
165
    /**
166
     * The location where avatars will be stored
167
     */
168
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
169
170
    const EDIT_PERMISSION = Permission::EDIT_USER;
171
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
172
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 44
    protected function assignResult($player)
178
    {
179 44
        $this->bzid = $player['bzid'];
180 44
        $this->name = $player['username'];
181 44
        $this->alias = $player['alias'];
182 44
        $this->team = $player['team'];
183 44
        $this->status = $player['status'];
184 44
        $this->avatar = $player['avatar'];
185 44
        $this->country = $player['country'];
186
187 44
        if (key_exists('activity', $player)) {
188
            $this->matchActivity = ($player['activity'] != null) ? $player['activity'] : 0.0;
189
        }
190 44
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 44
    protected function assignLazyResult($player)
196
    {
197 44
        $this->email = $player['email'];
198 44
        $this->verified = $player['verified'];
199 44
        $this->receives = $player['receives'];
200 44
        $this->confirmCode = $player['confirm_code'];
201 44
        $this->outdated = $player['outdated'];
202 44
        $this->description = $player['description'];
203 44
        $this->timezone = $player['timezone'];
204 44
        $this->joined = TimeDate::fromMysql($player['joined']);
205 44
        $this->last_login = TimeDate::fromMysql($player['last_login']);
206 44
        $this->last_match = Match::get($player['last_match']);
207 44
        $this->admin_notes = $player['admin_notes'];
208 44
        $this->ban = Ban::getBan($this->id);
209
210 44
        $this->cachedMatchSummary = [];
211
212 44
        $this->updateUserPermissions();
213 44
    }
214
215
    /**
216
     * Add a player a new role
217
     *
218
     * @param Role|int $role_id The role ID to add a player to
219
     *
220
     * @return bool Whether the operation was successful or not
221
     */
222 44
    public function addRole($role_id)
223
    {
224 44
        if ($role_id instanceof Role) {
225 1
            $role_id = $role_id->getId();
226 1
        }
227
228 44
        $this->lazyLoad();
229
230
        // Make sure the player doesn't already have the role
231 44
        foreach ($this->roles as $playerRole) {
232 14
            if ($playerRole->getId() == $role_id) {
233
                return false;
234
            }
235 44
        }
236
237 44
        $status = $this->modifyRole($role_id, "add");
238 44
        $this->refresh();
239
240 44
        return $status;
241
    }
242
243
    /**
244
     * Get the notes admins have left about a player
245
     * @return string The notes
246
     */
247
    public function getAdminNotes()
248
    {
249
        $this->lazyLoad();
250
251
        return $this->admin_notes;
252
    }
253
254
    /**
255
     * Get the player's BZID
256
     * @return int The BZID
257
     */
258 1
    public function getBZID()
259
    {
260 1
        return $this->bzid;
261
    }
262
263
    /**
264
     * Get the country a player belongs to
265
     *
266
     * @return Country The country belongs to
267
     */
268 1
    public function getCountry()
269
    {
270 1
        return Country::get($this->country);
271
    }
272
273
    /**
274
     * Get the e-mail address of the player
275
     *
276
     * @return string The address
277
     */
278
    public function getEmailAddress()
279
    {
280
        $this->lazyLoad();
281
282
        return $this->email;
283
    }
284
285
    /**
286
     * Build a key that we'll use for caching season Elo data in this model
287
     *
288
     * @param  string|null $season The season to get
289
     * @param  int|null    $year   The year of the season to get
290
     *
291
     * @return string
292
     */
293 5
    private function buildSeasonCacheKey(&$season, &$year)
294
    {
295 5
        if ($season === null) {
296 5
            $season = Season::getCurrentSeason();
297 5
        }
298
299 5
        if ($year === null) {
300 5
            $year = Carbon::now()->year;
301 5
        }
302
303 5
        return sprintf('%s-%s', $year, $season);
304
    }
305
306
    /**
307
     * Get the Elo changes for a player for a given season
308
     *
309
     * @param  string|null $season The season to get
310
     * @param  int|null    $year   The year of the season to get
311
     *
312
     * @return array
313
     */
314 5
    public function getEloSeasonHistory($season = null, $year = null)
315
    {
316 5
        $seasonKey = $this->buildSeasonCacheKey($season, $year);
317
318
        // This season's already been cached
319 5
        if ($this->eloSeasonHistory !== null && array_key_exists($seasonKey, $this->eloSeasonHistory)) {
320 5
            return $this->eloSeasonHistory[$seasonKey];
321
        }
322
323 5
        $results = $this->db->query('
324
          SELECT
325
            elo_new AS elo,
326
            MONTH(matches.timestamp) AS `month`,
327
            YEAR(matches.timestamp) AS `year`
328
          FROM
329
            player_elo
330
            LEFT JOIN matches ON player_elo.match_id = matches.id
331
          WHERE
332
            user_id = ? AND season_period = ? AND season_year = ?
333
          ORDER BY
334
            match_id ASC
335 5
        ', [ $this->getId(), $season, $year ]);
336
337 5
        $this->eloSeasonHistory[$seasonKey] = $results;
338 5
        $season = &$this->eloSeasonHistory[$seasonKey];
339
340 5
        if (!empty($season)) {
341
            $elo = end($season);
342
            $this->eloSeason[$seasonKey] = ($elo !== false) ? $elo['elo'] : 1200;
343
        } else {
344 5
            $this->eloSeason[$seasonKey] = 1200;
345
        }
346
347 5
        return $this->eloSeasonHistory[$seasonKey];
348
    }
349
350
    /**
351
     * Get the player's Elo for a season.
352
     *
353
     * With the default arguments, it will fetch the Elo for the current season.
354
     *
355
     * @param string|null $season The season we're looking for: winter, spring, summer, or fall
356
     * @param int|null    $year   The year of the season we're looking for
357
     *
358
     * @return int The player's Elo
359
     */
360 5
    public function getElo($season = null, $year = null)
361
    {
362 5
        $this->getEloSeasonHistory($season, $year);
363 5
        $seasonKey = $this->buildSeasonCacheKey($season, $year);
364
365 5
        return $this->eloSeason[$seasonKey];
366
    }
367
368
    /**
369
     * Adjust the Elo of a player for the current season based on a Match
370
     *
371
     * **Warning:** If $match is null, the Elo for the player will be modified but the value will not be persisted to
372
     * the database.
373
     *
374
     * @param int        $adjust The value to be added to the current ELO (negative to subtract)
375
     * @param Match|null $match  The match where this Elo change took place
376
     */
377 5
    public function adjustElo($adjust, Match $match = null)
378
    {
379 5
        $timestamp = ($match !== null) ? $match->getTimestamp() : (Carbon::now());
380 5
        $seasonInfo = Season::getSeason($timestamp);
381
382
        // Get the current Elo for the player, even if it's the default 1200. We need the value for adjusting
383 5
        $elo = $this->getElo($seasonInfo['season'], $seasonInfo['year']);
384 5
        $seasonKey = sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
385
386 5
        $this->eloSeason[$seasonKey] += $adjust;
387
388 5
        if ($match !== null && $this->isValid()) {
389 4
            $this->db->execute('
390
              INSERT INTO player_elo VALUES (?, ?, ?, ?, ?, ?)
391 4
            ', [ $this->getId(), $match->getId(), $seasonInfo['season'], $seasonInfo['year'], $elo, $this->eloSeason[$seasonKey] ]);
392 4
        }
393 5
    }
394
395
    /**
396
     * Returns whether the player has verified their e-mail address
397
     *
398
     * @return bool `true` for verified players
399
     */
400
    public function isVerified()
401
    {
402
        $this->lazyLoad();
403
404
        return $this->verified;
405
    }
406
407
    /**
408
     * Returns the confirmation code for the player's e-mail address verification
409
     *
410
     * @return string The player's confirmation code
411
     */
412
    public function getConfirmCode()
413
    {
414
        $this->lazyLoad();
415
416
        return $this->confirmCode;
417
    }
418
419
    /**
420
     * Returns what kind of events the player should be e-mailed about
421
     *
422
     * @return string The type of notifications
423
     */
424
    public function getReceives()
425
    {
426
        $this->lazyLoad();
427
428
        return $this->receives;
429
    }
430
431
    /**
432
     * Finds out whether the specified player wants and can receive an e-mail
433
     * message
434
     *
435
     * @param  string  $type
436
     * @return bool `true` if the player should be sent an e-mail
437
     */
438 1
    public function canReceive($type)
439
    {
440 1
        $this->lazyLoad();
441
442 1
        if (!$this->email || !$this->isVerified()) {
443
            // Unverified e-mail means the user will receive nothing
444 1
            return false;
445
        }
446
447
        if ($this->receives == 'everything') {
448
            return true;
449
        }
450
451
        return $this->receives == $type;
452
    }
453
454
    /**
455
     * Find out whether the specified confirmation code is correct
456
     *
457
     * This method protects against timing attacks
458
     *
459
     * @param  string $code The confirmation code to check
460
     * @return bool `true` for a correct e-mail verification code
461
     */
462
    public function isCorrectConfirmCode($code)
463
    {
464
        $this->lazyLoad();
465
466
        if ($this->confirmCode === null) {
467
            return false;
468
        }
469
470
        return StringUtils::equals($code, $this->confirmCode);
471
    }
472
473
    /**
474
     * Get the player's sanitized description
475
     * @return string The description
476
     */
477
    public function getDescription()
478
    {
479
        $this->lazyLoad();
480
481
        return $this->description;
482
    }
483
484
    /**
485
     * Get the joined date of the player
486
     * @return TimeDate The joined date of the player
487
     */
488
    public function getJoinedDate()
489
    {
490
        $this->lazyLoad();
491
492
        return $this->joined->copy();
493
    }
494
495
    /**
496
     * Get all of the known IPs used by the player
497
     *
498
     * @return string[][] An array containing IPs and hosts
499
     */
500
    public function getKnownIPs()
501
    {
502
        return $this->db->query(
503
            'SELECT DISTINCT ip, host FROM visits WHERE player = ? GROUP BY ip, host ORDER BY MAX(timestamp) DESC LIMIT 10',
504
            array($this->getId())
505
        );
506
    }
507
508
    /**
509
     * Get the last login for a player
510
     * @return TimeDate The date of the last login
511
     */
512
    public function getLastLogin()
513
    {
514
        $this->lazyLoad();
515
516
        return $this->last_login->copy();
517
    }
518
519
    /**
520
     * Get the last match
521
     * @return Match
522
     */
523
    public function getLastMatch()
524
    {
525
        $this->lazyLoad();
526
527
        return $this->last_match;
528
    }
529
530
    /**
531
     * Get all of the callsigns a player has used to log in to the website
532
     * @return string[] An array containing all of the past callsigns recorded for a player
533
     */
534
    public function getPastCallsigns()
535
    {
536
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
537
    }
538
539
    /**
540
     * Get the player's team
541
     * @return Team The object representing the team
542
     */
543 3
    public function getTeam()
544
    {
545 3
        return Team::get($this->team);
546
    }
547
548
    /**
549
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
550
     * @return string The timezone
551
     */
552 1
    public function getTimezone()
553
    {
554 1
        $this->lazyLoad();
555
556 1
        return ($this->timezone) ?: date_default_timezone_get();
557
    }
558
559
    /**
560
     * Get the roles of the player
561
     * @return Role[]
562
     */
563
    public function getRoles()
564
    {
565
        $this->lazyLoad();
566
567
        return $this->roles;
568
    }
569
570
    /**
571
     * Rebuild the list of permissions a user has been granted
572
     */
573 44
    private function updateUserPermissions()
574
    {
575 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...
576 44
        $this->permissions = array();
577
578 44
        foreach ($this->roles as $role) {
579 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...
580 44
        }
581 44
    }
582
583
    /**
584
     * Check if a player has a specific permission
585
     *
586
     * @param string|null $permission The permission to check for
587
     *
588
     * @return bool Whether or not the player has the permission
589
     */
590 2
    public function hasPermission($permission)
591
    {
592 2
        if ($permission === null) {
593 1
            return false;
594
        }
595
596 2
        $this->lazyLoad();
597
598 2
        return isset($this->permissions[$permission]);
599
    }
600
601
    /**
602
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
603
     * active
604
     *
605
     * @return bool True if the player has been active
606
     */
607
    public function hasBeenActive()
608
    {
609
        $this->lazyLoad();
610
611
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
612
        $lastLogin = $this->last_login->copy()->modify($interval);
613
614
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
615
616
        if ($this->last_match->isValid()) {
617
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
618
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
619
        }
620
621
        return $hasBeenActive;
622
    }
623
624
    /**
625
     * Check whether the callsign of the player is outdated
626
     *
627
     * Returns true if this player has probably changed their callsign, making
628
     * the current username stored in the database obsolete
629
     *
630
     * @return bool Whether or not the player is disabled
631
     */
632
    public function isOutdated()
633
    {
634
        $this->lazyLoad();
635
636
        return $this->outdated;
637
    }
638
639
    /**
640
     * Check if a player's account has been disabled
641
     *
642
     * @return bool Whether or not the player is disabled
643
     */
644
    public function isDisabled()
645
    {
646
        return $this->status == "disabled";
647
    }
648
649
    /**
650
     * Check if everyone can log in as this user on a test environment
651
     *
652
     * @return bool
653
     */
654 1
    public function isTestUser()
655
    {
656 1
        return $this->status == "test";
657
    }
658
659
    /**
660
     * Check if a player is teamless
661
     *
662
     * @return bool True if the player is teamless
663
     */
664 23
    public function isTeamless()
665
    {
666 23
        return empty($this->team);
667
    }
668
669
    /**
670
     * Mark a player's account as banned
671
     */
672 1
    public function markAsBanned()
673
    {
674 1
        if ($this->status != 'active') {
675
            return $this;
676
        }
677
678 1
        return $this->updateProperty($this->status, "status", "banned");
679
    }
680
681
    /**
682
     * Mark a player's account as unbanned
683
     */
684
    public function markAsUnbanned()
685
    {
686
        if ($this->status != 'banned') {
687
            return $this;
688
        }
689
690
        return $this->updateProperty($this->status, "status", "active");
691
    }
692
693
    /**
694
     * Find out if a player is banned
695
     *
696
     * @return bool
697
     */
698 2
    public function isBanned()
699
    {
700 2
        return Ban::getBan($this->id) !== null;
701
    }
702
703
    /**
704
     * Get the ban of the player
705
     *
706
     * This method performs a load of all the lazy parameters of the Player
707
     *
708
     * @return Ban|null The current ban of the player, or null if the player is
709
     *                  is not banned
710
     */
711
    public function getBan()
712
    {
713
        $this->lazyLoad();
714
715
        return $this->ban;
716
    }
717
718
    /**
719
     * Remove a player from a role
720
     *
721
     * @param int $role_id The role ID to add or remove
722
     *
723
     * @return bool Whether the operation was successful or not
724
     */
725
    public function removeRole($role_id)
726
    {
727
        $status = $this->modifyRole($role_id, "remove");
728
        $this->refresh();
729
730
        return $status;
731
    }
732
733
    /**
734
     * Set the player's email address and reset their verification status
735
     * @param string $email The address
736
     */
737
    public function setEmailAddress($email)
738
    {
739
        $this->lazyLoad();
740
741
        if ($this->email == $email) {
742
            // The e-mail hasn't changed, don't do anything
743
            return;
744
        }
745
746
        $this->setVerified(false);
747
        $this->generateNewConfirmCode();
748
749
        $this->updateProperty($this->email, 'email', $email);
750
    }
751
752
    /**
753
     * Set whether the player has verified their e-mail address
754
     *
755
     * @param  bool $verified Whether the player is verified or not
756
     * @return self
757
     */
758
    public function setVerified($verified)
759
    {
760
        $this->lazyLoad();
761
762
        if ($verified) {
763
            $this->setConfirmCode(null);
764
        }
765
766
        return $this->updateProperty($this->verified, 'verified', $verified);
767
    }
768
769
    /**
770
     * Generate a new random confirmation token for e-mail address verification
771
     *
772
     * @return self
773
     */
774
    public function generateNewConfirmCode()
775
    {
776
        $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...
777
        $random = $generator->nextBytes(16);
778
779
        return $this->setConfirmCode(bin2hex($random));
780
    }
781
782
    /**
783
     * Set the confirmation token for e-mail address verification
784
     *
785
     * @param  string $code The confirmation code
786
     * @return self
787
     */
788
    private function setConfirmCode($code)
789
    {
790
        $this->lazyLoad();
791
792
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
793
    }
794
795
    /**
796
     * Set what kind of events the player should be e-mailed about
797
     *
798
     * @param  string $receives The type of notification
799
     * @return self
800
     */
801
    public function setReceives($receives)
802
    {
803
        $this->lazyLoad();
804
805
        return $this->updateProperty($this->receives, 'receives', $receives);
806
    }
807
808
    /**
809
     * Set whether the callsign of the player is outdated
810
     *
811
     * @param  bool $outdated Whether the callsign is outdated
812
     * @return self
813
     */
814 44
    public function setOutdated($outdated)
815
    {
816 44
        $this->lazyLoad();
817
818 44
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
819
    }
820
821
    /**
822
     * Set the player's description
823
     * @param string $description The description
824
     */
825
    public function setDescription($description)
826
    {
827
        $this->updateProperty($this->description, "description", $description);
828
    }
829
830
    /**
831
     * Set the player's timezone
832
     * @param string $timezone The timezone
833
     */
834
    public function setTimezone($timezone)
835
    {
836
        $this->updateProperty($this->timezone, "timezone", $timezone);
837
    }
838
839
    /**
840
     * Set the player's team
841
     * @param int $team The team's ID
842
     */
843 23
    public function setTeam($team)
844
    {
845 23
        $this->updateProperty($this->team, "team", $team);
846 23
    }
847
848
    /**
849
     * Set the match the player last participated in
850
     *
851
     * @param int $match The match's ID
852
     */
853 14
    public function setLastMatch($match)
854
    {
855 14
        $this->updateProperty($this->last_match, 'last_match', $match);
856 14
    }
857
858
    /**
859
     * Set the player's status
860
     * @param string $status The new status
861
     */
862
    public function setStatus($status)
863
    {
864
        $this->updateProperty($this->status, 'status', $status);
865
    }
866
867
    /**
868
     * Set the player's admin notes
869
     * @param  string $admin_notes The new admin notes
870
     * @return self
871
     */
872
    public function setAdminNotes($admin_notes)
873
    {
874
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
875
    }
876
877
    /**
878
     * Set the player's country
879
     * @param  int   $country The ID of the new country
880
     * @return self
881
     */
882
    public function setCountry($country)
883
    {
884
        return $this->updateProperty($this->country, 'country', $country);
885
    }
886
887
    /**
888
     * Updates this player's last login
889
     */
890
    public function updateLastLogin()
891
    {
892
        $this->update("last_login", TimeDate::now()->toMysql());
893
    }
894
895
    /**
896
     * Get the player's username
897
     * @return string The username
898
     */
899 1
    public function getUsername()
900
    {
901 1
        return $this->name;
902
    }
903
904
    /**
905
     * Get the player's username, safe for use in your HTML
906
     * @return string The username
907
     */
908 1
    public function getEscapedUsername()
909
    {
910 1
        return $this->getEscapedName();
911
    }
912
913
    /**
914
     * Alias for Player::setUsername()
915
     *
916
     * @param  string $username The new username
917
     * @return self
918
     */
919
    public function setName($username)
920
    {
921
        return $this->setUsername($username);
922
    }
923
924
    /**
925
     * Mark all the unread messages of a player as read
926
     *
927
     * @return void
928
     */
929
    public function markMessagesAsRead()
930
    {
931
        $this->db->execute(
932
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
933
            array($this->id)
934
        );
935
    }
936
937
    /**
938
     * Set the roles of a user
939
     *
940
     * @todo   Is it worth making this faster?
941
     * @param  Role[] $roles The new roles of the user
942
     * @return self
943
     */
944
    public function setRoles($roles)
945
    {
946
        $this->lazyLoad();
947
948
        $oldRoles = Role::mapToIds($this->roles);
949
        $this->roles = $roles;
950
        $roleIds = Role::mapToIds($roles);
951
952
        $newRoles     = array_diff($roleIds, $oldRoles);
953
        $removedRoles = array_diff($oldRoles, $roleIds);
954
955
        foreach ($newRoles as $role) {
956
            $this->modifyRole($role, 'add');
957
        }
958
959
        foreach ($removedRoles as $role) {
960
            $this->modifyRole($role, 'remove');
961
        }
962
963
        $this->refresh();
964
965
        return $this;
966
    }
967
968
    /**
969
     * Give or remove a role to/form a player
970
     *
971
     * @param int    $role_id The role ID to add or remove
972
     * @param string $action  Whether to "add" or "remove" a role for a player
973
     *
974
     * @return bool Whether the operation was successful or not
975
     */
976 44
    private function modifyRole($role_id, $action)
977
    {
978 44
        $role = Role::get($role_id);
979
980 44
        if ($role->isValid()) {
981 44
            if ($action == "add") {
982 44
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
983 44
            } elseif ($action == "remove") {
984
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
985
            } else {
986
                throw new Exception("Unrecognized role action");
987
            }
988
989 44
            return true;
990
        }
991
992
        return false;
993
    }
994
995
    /**
996
     * Given a player's BZID, get a player object
997
     *
998
     * @param  int    $bzid The player's BZID
999
     * @return Player
1000
     */
1001 2
    public static function getFromBZID($bzid)
1002
    {
1003 2
        return self::get(self::fetchIdFrom($bzid, "bzid"));
1004
    }
1005
1006
    /**
1007
     * Get a single player by their username
1008
     *
1009
     * @param  string $username The username to look for
1010
     * @return Player
1011
     */
1012 1
    public static function getFromUsername($username)
1013
    {
1014 1
        $player = static::get(self::fetchIdFrom($username, 'username'));
1015
1016 1
        return $player->inject('name', $username);
1017
    }
1018
1019
    /**
1020
     * Get all the players in the database that have an active status
1021
     * @return Player[] An array of player BZIDs
1022
     */
1023
    public static function getPlayers()
1024
    {
1025
        return self::arrayIdToModel(
1026
            self::fetchIdsFrom("status", array("active", "test"), false)
1027
        );
1028
    }
1029
1030
    /**
1031
     * Show the number of notifications the user hasn't read yet
1032
     * @return int
1033
     */
1034 1
    public function countUnreadNotifications()
1035
    {
1036 1
        return Notification::countUnreadNotifications($this->id);
1037
    }
1038
1039
    /**
1040
     * Count the number of matches a player has participated in
1041
     * @return int
1042
     */
1043
    public function getMatchCount()
1044
    {
1045
        if ($this->cachedMatchCount === null) {
1046
            $this->cachedMatchCount = Match::getQueryBuilder()
1047
                ->active()
1048
                ->with($this)
1049
                ->count();
1050
        }
1051
1052
        return $this->cachedMatchCount;
1053
    }
1054
1055
    /**
1056
     * Get the (victory/total matches) ratio of the player
1057
     * @return float
1058
     */
1059
    public function getMatchWinRatio()
1060
    {
1061
        $count = $this->getMatchCount();
1062
1063
        if ($count == 0) {
1064
            return 0;
1065
        }
1066
1067
        $wins = Match::getQueryBuilder()
1068
            ->active()
1069
            ->with($this, 'win')
1070
            ->count();
1071
1072
        return $wins / $count;
1073
    }
1074
1075
    /**
1076
     * Get the (total caps made by team/total matches) ratio of the player
1077
     * @return float
1078
     */
1079
    public function getMatchAverageCaps()
1080
    {
1081
        $count = $this->getMatchCount();
1082
1083
        if ($count == 0) {
1084
            return 0;
1085
        }
1086
1087
        // Get the sum of team A points if the player was in team A, team B
1088
        // points if the player was in team B, and their average if the player
1089
        // was on both teams for some reason
1090
        $query = $this->db->query(
1091
            "SELECT SUM(
1092
                IF(
1093
                    FIND_IN_SET(?, team_a_players) AND FIND_IN_SET(?, team_b_players),
1094
                    (team_a_points+team_b_points)/2,
1095
                    IF(FIND_IN_SET(?, team_a_players), team_a_points, team_b_points)
1096
                )
1097
            ) AS sum FROM matches WHERE status='entered' AND (FIND_IN_SET(?, team_a_players) OR FIND_IN_SET(?, team_b_players))",
1098
            array_fill(0, 5, $this->id)
1099
        );
1100
1101
        return $query[0]['sum'] / $count;
1102
    }
1103
1104
    /**
1105
     * Get the match activity in matches per day for a player
1106
     *
1107
     * @return float
1108
     */
1109
    public function getMatchActivity()
1110
    {
1111
        if ($this->matchActivity !== null) {
1112
            return $this->matchActivity;
1113
        }
1114
1115
        $activity = 0.0;
1116
1117
        $matches = Match::getQueryBuilder()
1118
            ->active()
1119
            ->with($this)
1120
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
1121
            ->getModels($fast = true);
1122
1123
        foreach ($matches as $match) {
1124
            $activity += $match->getActivity();
1125
        }
1126
1127
        return $activity;
1128
    }
1129
1130
    /**
1131
     * Return an array of matches this player participated in per month.
1132
     *
1133
     * ```
1134
     * ['yyyy-mm'] = <number of matches>
1135
     * ```
1136
     *
1137
     * @param TimeDate|string $timePeriod
1138
     *
1139
     * @return int[]
1140
     */
1141
    public function getMatchSummary($timePeriod = '1 year ago')
1142
    {
1143
        $since = ($timePeriod instanceof TimeDate) ? $timePeriod : TimeDate::from($timePeriod);
1144
1145
        if (!isset($this->cachedMatchSummary[(string)$timePeriod])) {
1146
            $this->cachedMatchSummary[(string)$timePeriod] = Match::getQueryBuilder()
1147
                ->active()
1148
                ->with($this)
1149
                ->where('time')->isAfter($since)
1150
                ->getSummary($since)
1151
            ;
1152
        }
1153
1154
        return $this->cachedMatchSummary[(string)$timePeriod];
1155
    }
1156
1157
    /**
1158
     * Show the number of messages the user hasn't read yet
1159
     * @return int
1160
     */
1161 1
    public function countUnreadMessages()
1162
    {
1163 1
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1164 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...
1165 1
        );
1166
    }
1167
1168
    /**
1169
     * Routine maintenance for a player when they participate in a match.
1170
     *
1171
     * This function updates the last match played, changes the ELO, and creates a historic ELO change.
1172
     *
1173
     * @param Match $match
1174
     * @param int   $eloDiff
1175
     */
1176 14
    public function setMatchParticipation(Match $match, $eloDiff)
1177
    {
1178 14
        $this->setLastMatch($match->getId());
1179
1180 14
        if ($match->isOfficial() && $eloDiff !== null) {
1181 4
            $this->adjustElo($eloDiff, $match);
1182 4
        }
1183 14
    }
1184
1185
    /**
1186
     * Get all of the members belonging to a team
1187
     * @param  int      $teamID The ID of the team to fetch the members of
1188
     * @return Player[] An array of Player objects of the team members
1189
     */
1190 2
    public static function getTeamMembers($teamID)
1191
    {
1192 2
        return self::arrayIdToModel(
1193 2
            self::fetchIds("WHERE team = ?", array($teamID))
1194 2
        );
1195
    }
1196
1197
    /**
1198
     * {@inheritdoc}
1199
     */
1200 1
    public static function getActiveStatuses()
1201
    {
1202 1
        return array('active', 'reported', 'test');
1203
    }
1204
1205
    /**
1206
     * {@inheritdoc}
1207
     */
1208 44
    public static function getEagerColumns($prefix = null)
1209
    {
1210
        $columns = [
1211 44
            'id',
1212 44
            'bzid',
1213 44
            'team',
1214 44
            'username',
1215 44
            'alias',
1216 44
            'status',
1217 44
            'avatar',
1218 44
            'country',
1219 44
        ];
1220
1221 44
        return self::formatColumns($prefix, $columns);
1222
    }
1223
1224
    /**
1225
     * {@inheritdoc}
1226
     */
1227 44
    public static function getLazyColumns($prefix = null)
1228
    {
1229
        $columns = [
1230 44
            'email',
1231 44
            'verified',
1232 44
            'receives',
1233 44
            'confirm_code',
1234 44
            'outdated',
1235 44
            'description',
1236 44
            'timezone',
1237 44
            'joined',
1238 44
            'last_login',
1239 44
            'last_match',
1240 44
            'admin_notes',
1241 44
        ];
1242
1243 44
        return self::formatColumns($prefix, $columns);
1244
    }
1245
1246
    /**
1247
     * Get a query builder for players
1248
     * @return PlayerQueryBuilder
1249
     */
1250
    public static function getQueryBuilder()
1251
    {
1252
        return new PlayerQueryBuilder('Player', array(
1253
            'columns' => array(
1254
                'name'     => 'username',
1255
                'team'     => 'team',
1256
                'outdated' => 'outdated',
1257
                'status'   => 'status',
1258
            ),
1259
            'name' => 'name',
1260
        ));
1261
    }
1262
1263
    /**
1264
     * Enter a new player to the database
1265
     * @param  int              $bzid        The player's bzid
1266
     * @param  string           $username    The player's username
1267
     * @param  int              $team        The player's team
1268
     * @param  string           $status      The player's status
1269
     * @param  int              $role_id     The player's role when they are first created
1270
     * @param  string           $avatar      The player's profile avatar
1271
     * @param  string           $description The player's profile description
1272
     * @param  int              $country     The player's country
1273
     * @param  string           $timezone    The player's timezone
1274
     * @param  string|\TimeDate $joined      The date the player joined
1275
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1276
     * @return Player           An object representing the player that was just entered
1277
     */
1278 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")
1279
    {
1280 44
        $joined = TimeDate::from($joined);
1281 44
        $last_login = TimeDate::from($last_login);
1282 44
        $timezone = ($timezone) ?: date_default_timezone_get();
1283
1284 44
        $player = self::create(array(
1285 44
            'bzid'        => $bzid,
1286 44
            'team'        => $team,
1287 44
            'username'    => $username,
1288 44
            'alias'       => self::generateAlias($username),
1289 44
            'status'      => $status,
1290 44
            'avatar'      => $avatar,
1291 44
            'description' => $description,
1292 44
            'country'     => $country,
1293 44
            'timezone'    => $timezone,
1294 44
            'joined'      => $joined->toMysql(),
1295 44
            'last_login'  => $last_login->toMysql(),
1296 44
        ));
1297
1298 44
        $player->addRole($role_id);
1299 44
        $player->getIdenticon($player->getId());
1300 44
        $player->setUsername($username);
1301
1302 44
        return $player;
1303
    }
1304
1305
    /**
1306
     * Determine if a player exists in the database
1307
     * @param  int  $bzid The player's bzid
1308
     * @return bool Whether the player exists in the database
1309
     */
1310
    public static function playerBZIDExists($bzid)
1311
    {
1312
        return self::getFromBZID($bzid)->isValid();
1313
    }
1314
1315
    /**
1316
     * Change a player's callsign and add it to the database if it does not
1317
     * exist as a past callsign
1318
     *
1319
     * @param  string $username The new username of the player
1320
     * @return self
1321
     */
1322 44
    public function setUsername($username)
1323
    {
1324
        // The player's username was just fetched from BzDB, it's definitely not
1325
        // outdated
1326 44
        $this->setOutdated(false);
1327
1328
        // Players who have this player's username are considered outdated
1329 44
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1330
1331 44
        if ($username === $this->name) {
1332
            // The player's username hasn't changed, no need to do anything
1333 44
            return $this;
1334
        }
1335
1336
        // Players who used to have our player's username are not outdated anymore,
1337
        // unless they are more than one.
1338
        // Even though we are sure that the old and new usernames are not equal,
1339
        // MySQL makes a different type of string equality tests, which is why we
1340
        // also check IDs to make sure not to affect our own player's outdatedness.
1341
        $this->db->execute("
1342
            UPDATE {$this->table} SET outdated =
1343
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1344
            WHERE username = ? AND id != ?",
1345
            array($this->name, $this->id, $this->name, $this->id));
1346
1347
        $this->updateProperty($this->name, 'username', $username);
1348
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1349
        $this->resetAlias();
1350
1351
        return $this;
1352
    }
1353
1354
    /**
1355
     * Alphabetical order function for use in usort (case-insensitive)
1356
     * @return Closure The sort function
1357
     */
1358
    public static function getAlphabeticalSort()
1359
    {
1360 1
        return function (Player $a, Player $b) {
1361 1
            return strcasecmp($a->getUsername(), $b->getUsername());
1362 1
        };
1363
    }
1364
1365
    /**
1366
     * {@inheritdoc}
1367
     * @todo Add a constraint that does this automatically
1368
     */
1369 44
    public function wipe()
1370
    {
1371 44
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1372
1373 44
        parent::wipe();
1374 44
    }
1375
1376
    /**
1377
     * Find whether the player can delete a model
1378
     *
1379
     * @param  PermissionModel $model       The model that will be seen
1380
     * @param  bool         $showDeleted Whether to show deleted models to admins
1381
     * @return bool
1382
     */
1383 1
    public function canSee($model, $showDeleted = false)
1384
    {
1385 1
        return $model->canBeSeenBy($this, $showDeleted);
1386
    }
1387
1388
    /**
1389
     * Find whether the player can delete a model
1390
     *
1391
     * @param  PermissionModel $model The model that will be deleted
1392
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1393
     *                                to soft-delete ones
1394
     * @return bool
1395
     */
1396 1
    public function canDelete($model, $hard = false)
1397
    {
1398 1
        if ($hard) {
1399
            return $model->canBeHardDeletedBy($this);
1400
        } else {
1401 1
            return $model->canBeSoftDeletedBy($this);
1402
        }
1403
    }
1404
1405
    /**
1406
     * Find whether the player can create a model
1407
     *
1408
     * @param  string  $modelName The PHP class identifier of the model type
1409
     * @return bool
1410
     */
1411 1
    public function canCreate($modelName)
1412
    {
1413 1
        return $modelName::canBeCreatedBy($this);
1414
    }
1415
1416
    /**
1417
     * Find whether the player can edit a model
1418
     *
1419
     * @param  PermissionModel $model The model which will be edited
1420
     * @return bool
1421
     */
1422 1
    public function canEdit($model)
1423
    {
1424 1
        return $model->canBeEditedBy($this);
1425
    }
1426
}
1427