Completed
Push — feature/player-list ( 9d1e0c...62d21d )
by Vladimir
04:17
created

Player   F

Complexity

Total Complexity 110

Size/Duplication

Total Lines 1255
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 15

Test Coverage

Coverage 43.02%

Importance

Changes 0
Metric Value
wmc 110
lcom 2
cbo 15
dl 0
loc 1255
ccs 148
cts 344
cp 0.4302
rs 0.6314
c 0
b 0
f 0

77 Methods

Rating   Name   Duplication   Size   Complexity  
A countUnreadMessages() 0 6 1
A getAlphabeticalSort() 0 6 1
A canCreate() 0 4 1
A removeRole() 0 7 1
A markMessagesAsRead() 0 7 1
A getPlayers() 0 6 1
A getMatchCount() 0 11 2
A assignResult() 0 14 3
A assignLazyResult() 0 19 1
A addRole() 0 20 4
A getAdminNotes() 0 6 1
A getBZID() 0 4 1
A getCountry() 0 4 1
A getEmailAddress() 0 6 1
A isVerified() 0 6 1
A getConfirmCode() 0 6 1
A getReceives() 0 6 1
A canReceive() 0 15 4
A isCorrectConfirmCode() 0 10 2
A getDescription() 0 6 1
A getJoinedDate() 0 6 1
A getKnownIPs() 0 7 1
A getLastLogin() 0 6 1
A getLastMatch() 0 6 1
A getPastCallsigns() 0 4 1
A getTeam() 0 4 1
A getTimezone() 0 6 2
A getRoles() 0 6 1
A updateUserPermissions() 0 9 2
A hasPermission() 0 10 2
A hasBeenActive() 0 16 3
A isOutdated() 0 6 1
A isDisabled() 0 4 1
A isTestUser() 0 4 1
A isTeamless() 0 4 1
A markAsBanned() 0 8 2
A markAsUnbanned() 0 8 2
A isBanned() 0 4 1
A getBan() 0 6 1
A setEmailAddress() 0 14 2
A setVerified() 0 10 2
A generateNewConfirmCode() 0 7 1
A setConfirmCode() 0 6 1
A setReceives() 0 6 1
A setOutdated() 0 6 1
A setDescription() 0 4 1
A setTimezone() 0 4 1
A setTeam() 0 4 1
A setLastMatch() 0 4 1
A setStatus() 0 4 1
A setAdminNotes() 0 4 1
A setCountry() 0 4 1
A updateLastLogin() 0 4 1
A getUsername() 0 4 1
A getEscapedUsername() 0 4 1
A setName() 0 4 1
A setRoles() 0 23 3
A modifyRole() 0 18 4
A getFromBZID() 0 4 1
A getFromUsername() 0 6 1
A countUnreadNotifications() 0 4 1
A getMatchWinRatio() 0 15 2
B getMatchAverageCaps() 0 24 2
A getMatchActivity() 0 20 3
A getMatchSummary() 0 15 3
A getTeamMembers() 0 6 1
A getActiveStatuses() 0 4 1
A getEagerColumns() 0 4 1
A getLazyColumns() 0 4 1
A getQueryBuilder() 0 12 1
B newPlayer() 0 26 2
A playerBZIDExists() 0 4 1
B setUsername() 0 31 2
A wipe() 0 6 1
A canSee() 0 4 1
A canDelete() 0 8 2
A canEdit() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Player often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Player, and based on these observations, apply Extract Interface, too.

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