Completed
Push — feature/player-elo ( e26eb0...e4ba22 )
by Vladimir
13:48
created

Player::setMatchParticipation()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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