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

Player::setMatchParticipation()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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