Completed
Push — v0_9 ( 8a86fc...4c1f3c )
by Vladimir
09:59 queued 03:46
created

Player   F

Complexity

Total Complexity 100

Size/Duplication

Total Lines 1173
Duplicated Lines 1.02 %

Coupling/Cohesion

Components 2
Dependencies 14

Test Coverage

Coverage 46.29%

Importance

Changes 0
Metric Value
wmc 100
lcom 2
cbo 14
dl 12
loc 1173
ccs 156
cts 337
cp 0.4629
rs 1.913
c 0
b 0
f 0

74 Methods

Rating   Name   Duplication   Size   Complexity  
A removeRole() 0 7 1
A setTimezone() 0 4 1
A assignResult() 0 10 1
A assignLazyResult() 0 17 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 4 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 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 setTeam() 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 markMessagesAsRead() 0 7 1
A setRoles() 0 23 3
A modifyRole() 0 18 4
A getFromBZID() 0 4 1
A getFromUsername() 0 6 1
A getPlayers() 0 6 1
A countUnreadNotifications() 0 4 1
A getMatchCount() 0 11 2
A getMatchWinRatio() 0 15 2
B getMatchAverageCaps() 0 24 2
A getMatchActivity() 0 16 2
A countUnreadMessages() 0 6 1
A getTeamMembers() 0 6 1
A getActiveStatuses() 0 4 1
A getEagerColumns() 0 4 1
A getLazyColumns() 0 4 1
A getQueryBuilder() 12 12 1
B newPlayer() 0 26 2
A playerBZIDExists() 0 4 1
B setUsername() 0 31 2
A getAlphabeticalSort() 0 6 1
A wipe() 0 6 1
A canSee() 0 4 1
A canDelete() 0 8 2
A canCreate() 0 4 1
A canEdit() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
     * The cached match count for a player
142
     *
143
     * @var int
144
     */
145
    private $cachedMatchCount = null;
146
147
    /**
148
     * The name of the database table used for queries
149
     */
150
    const TABLE = "players";
151
152
    /**
153
     * The location where avatars will be stored
154
     */
155
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
156
157
    const EDIT_PERMISSION = Permission::EDIT_USER;
158
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
159
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 39
    protected function assignResult($player)
165
    {
166 39
        $this->bzid = $player['bzid'];
167 39
        $this->name = $player['username'];
168 39
        $this->alias = $player['alias'];
169 39
        $this->team = $player['team'];
170 39
        $this->status = $player['status'];
171 39
        $this->avatar = $player['avatar'];
172 39
        $this->country = $player['country'];
173 39
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 39
    protected function assignLazyResult($player)
179
    {
180 39
        $this->email = $player['email'];
181 39
        $this->verified = $player['verified'];
182 39
        $this->receives = $player['receives'];
183 39
        $this->confirmCode = $player['confirm_code'];
184 39
        $this->outdated = $player['outdated'];
185 39
        $this->description = $player['description'];
186 39
        $this->timezone = $player['timezone'];
187 39
        $this->joined = TimeDate::fromMysql($player['joined']);
188 39
        $this->last_login = TimeDate::fromMysql($player['last_login']);
189 39
        $this->last_match = Match::get($player['last_match']);
190 39
        $this->admin_notes = $player['admin_notes'];
191 39
        $this->ban = Ban::getBan($this->id);
192
193 39
        $this->updateUserPermissions();
194 39
    }
195
196
    /**
197
     * Add a player a new role
198
     *
199
     * @param Role|int $role_id The role ID to add a player to
200
     *
201
     * @return bool Whether the operation was successful or not
202
     */
203 39
    public function addRole($role_id)
204
    {
205 39
        if ($role_id instanceof Role) {
206 1
            $role_id = $role_id->getId();
207 1
        }
208
209 39
        $this->lazyLoad();
210
211
        // Make sure the player doesn't already have the role
212 39
        foreach ($this->roles as $playerRole) {
213 14
            if ($playerRole->getId() == $role_id) {
214
                return false;
215
            }
216 39
        }
217
218 39
        $status = $this->modifyRole($role_id, "add");
219 39
        $this->refresh();
220
221 39
        return $status;
222
    }
223
224
    /**
225
     * Get the notes admins have left about a player
226
     * @return string The notes
227
     */
228
    public function getAdminNotes()
229
    {
230
        $this->lazyLoad();
231
232
        return $this->admin_notes;
233
    }
234
235
    /**
236
     * Get the player's BZID
237
     * @return int The BZID
238
     */
239
    public function getBZID()
240
    {
241
        return $this->bzid;
242
    }
243
244
    /**
245
     * Get the country a player belongs to
246
     *
247
     * @return Country The country belongs to
248
     */
249 1
    public function getCountry()
250
    {
251 1
        return Country::get($this->country);
252
    }
253
254
    /**
255
     * Get the e-mail address of the player
256
     *
257
     * @return string The address
258
     */
259
    public function getEmailAddress()
260
    {
261
        $this->lazyLoad();
262
263
        return $this->email;
264
    }
265
266
    /**
267
     * Returns whether the player has verified their e-mail address
268
     *
269
     * @return bool `true` for verified players
270
     */
271
    public function isVerified()
272
    {
273
        $this->lazyLoad();
274
275
        return $this->verified;
276
    }
277
278
    /**
279
     * Returns the confirmation code for the player's e-mail address verification
280
     *
281
     * @return string The player's confirmation code
282
     */
283
    public function getConfirmCode()
284
    {
285
        $this->lazyLoad();
286
287
        return $this->confirmCode;
288
    }
289
290
    /**
291
     * Returns what kind of events the player should be e-mailed about
292
     *
293
     * @return string The type of notifications
294
     */
295
    public function getReceives()
296
    {
297
        $this->lazyLoad();
298
299
        return $this->receives;
300
    }
301
302
    /**
303
     * Finds out whether the specified player wants and can receive an e-mail
304
     * message
305
     *
306
     * @param  string  $type
307
     * @return bool `true` if the player should be sent an e-mail
308
     */
309 1
    public function canReceive($type)
310
    {
311 1
        $this->lazyLoad();
312
313 1
        if (!$this->email || !$this->isVerified()) {
314
            // Unverified e-mail means the user will receive nothing
315 1
            return false;
316
        }
317
318
        if ($this->receives == 'everything') {
319
            return true;
320
        }
321
322
        return $this->receives == $type;
323
    }
324
325
    /**
326
     * Find out whether the specified confirmation code is correct
327
     *
328
     * This method protects against timing attacks
329
     *
330
     * @param  string $code The confirmation code to check
331
     * @return bool `true` for a correct e-mail verification code
332
     */
333
    public function isCorrectConfirmCode($code)
334
    {
335
        $this->lazyLoad();
336
337
        if ($this->confirmCode === null) {
338
            return false;
339
        }
340
341
        return StringUtils::equals($code, $this->confirmCode);
342
    }
343
344
    /**
345
     * Get the player's sanitized description
346
     * @return string The description
347
     */
348
    public function getDescription()
349
    {
350
        $this->lazyLoad();
351
352
        return $this->description;
353
    }
354
355
    /**
356
     * Get the joined date of the player
357
     * @return TimeDate The joined date of the player
358
     */
359
    public function getJoinedDate()
360
    {
361
        $this->lazyLoad();
362
363
        return $this->joined->copy();
364
    }
365
366
    /**
367
     * Get all of the known IPs used by the player
368
     *
369
     * @return string[][] An array containing IPs and hosts
370
     */
371
    public function getKnownIPs()
372
    {
373
        return $this->db->query("SELECT DISTINCT ip, host FROM visits WHERE player = ? LIMIT 10", array($this->getId()));
374
    }
375
376
    /**
377
     * Get the last login for a player
378
     * @return TimeDate The date of the last login
379
     */
380
    public function getLastLogin()
381
    {
382
        $this->lazyLoad();
383
384
        return $this->last_login->copy();
385
    }
386
387
    /**
388
     * Get the last match
389
     * @return Match
390
     */
391
    public function getLastMatch()
392
    {
393
        $this->lazyLoad();
394
395
        return $this->last_match;
396
    }
397
398
    /**
399
     * Get all of the callsigns a player has used to log in to the website
400
     * @return string[] An array containing all of the past callsigns recorded for a player
401
     */
402
    public function getPastCallsigns()
403
    {
404
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
405
    }
406
407
    /**
408
     * Get the player's team
409
     * @return Team The object representing the team
410
     */
411 2
    public function getTeam()
412
    {
413 2
        return Team::get($this->team);
414
    }
415
416
    /**
417
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
418
     * @return string The timezone
419
     */
420 1
    public function getTimezone()
421
    {
422 1
        $this->lazyLoad();
423
424 1
        return ($this->timezone) ?: date_default_timezone_get();
425
    }
426
427
    /**
428
     * Get the roles of the player
429
     * @return Role[]
430
     */
431
    public function getRoles()
432
    {
433
        $this->lazyLoad();
434
435
        return $this->roles;
436
    }
437
438
    /**
439
     * Rebuild the list of permissions a user has been granted
440
     */
441 39
    private function updateUserPermissions()
442
    {
443 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...
444 39
        $this->permissions = array();
445
446 39
        foreach ($this->roles as $role) {
447 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...
448 39
        }
449 39
    }
450
451
    /**
452
     * Check if a player has a specific permission
453
     *
454
     * @param string|null $permission The permission to check for
455
     *
456
     * @return bool Whether or not the player has the permission
457
     */
458 2
    public function hasPermission($permission)
459
    {
460 2
        if ($permission === null) {
461 1
            return false;
462
        }
463
464 2
        $this->lazyLoad();
465
466 2
        return isset($this->permissions[$permission]);
467
    }
468
469
    /**
470
     * Check whether the callsign of the player is outdated
471
     *
472
     * Returns true if this player has probably changed their callsign, making
473
     * the current username stored in the database obsolete
474
     *
475
     * @return bool Whether or not the player is disabled
476
     */
477
    public function isOutdated()
478
    {
479
        $this->lazyLoad();
480
481
        return $this->outdated;
482
    }
483
484
    /**
485
     * Check if a player's account has been disabled
486
     *
487
     * @return bool Whether or not the player is disabled
488
     */
489
    public function isDisabled()
490
    {
491
        return $this->status == "disabled";
492
    }
493
494
    /**
495
     * Check if everyone can log in as this user on a test environment
496
     *
497
     * @return bool
498
     */
499 1
    public function isTestUser()
500
    {
501 1
        return $this->status == "test";
502
    }
503
504
    /**
505
     * Check if a player is teamless
506
     *
507
     * @return bool True if the player is teamless
508
     */
509 18
    public function isTeamless()
510
    {
511 18
        return empty($this->team);
512
    }
513
514
    /**
515
     * Mark a player's account as banned
516
     */
517 1
    public function markAsBanned()
518
    {
519 1
        if ($this->status != 'active') {
520
            return $this;
521
        }
522
523 1
        return $this->updateProperty($this->status, "status", "banned");
524
    }
525
526
    /**
527
     * Mark a player's account as unbanned
528
     */
529
    public function markAsUnbanned()
530
    {
531
        if ($this->status != 'banned') {
532
            return $this;
533
        }
534
535
        return $this->updateProperty($this->status, "status", "active");
536
    }
537
538
    /**
539
     * Find out if a player is banned
540
     *
541
     * @return bool
542
     */
543 2
    public function isBanned()
544
    {
545 2
        return Ban::getBan($this->id) !== null;
546
    }
547
548
    /**
549
     * Get the ban of the player
550
     *
551
     * This method performs a load of all the lazy parameters of the Player
552
     *
553
     * @return Ban|null The current ban of the player, or null if the player is
554
     *                  is not banned
555
     */
556
    public function getBan()
557
    {
558
        $this->lazyLoad();
559
560
        return $this->ban;
561
    }
562
563
    /**
564
     * Remove a player from a role
565
     *
566
     * @param int $role_id The role ID to add or remove
567
     *
568
     * @return bool Whether the operation was successful or not
569
     */
570
    public function removeRole($role_id)
571
    {
572
        $status = $this->modifyRole($role_id, "remove");
573
        $this->refresh();
574
575
        return $status;
576
    }
577
578
    /**
579
     * Set the player's email address and reset their verification status
580
     * @param string $email The address
581
     */
582
    public function setEmailAddress($email)
583
    {
584
        $this->lazyLoad();
585
586
        if ($this->email == $email) {
587
            // The e-mail hasn't changed, don't do anything
588
            return;
589
        }
590
591
        $this->setVerified(false);
592
        $this->generateNewConfirmCode();
593
594
        $this->updateProperty($this->email, 'email', $email);
595
    }
596
597
    /**
598
     * Set whether the player has verified their e-mail address
599
     *
600
     * @param  bool $verified Whether the player is verified or not
601
     * @return self
602
     */
603
    public function setVerified($verified)
604
    {
605
        $this->lazyLoad();
606
607
        if ($verified) {
608
            $this->setConfirmCode(null);
609
        }
610
611
        return $this->updateProperty($this->verified, 'verified', $verified);
612
    }
613
614
    /**
615
     * Generate a new random confirmation token for e-mail address verification
616
     *
617
     * @return self
618
     */
619
    public function generateNewConfirmCode()
620
    {
621
        $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...
622
        $random = $generator->nextBytes(16);
623
624
        return $this->setConfirmCode(bin2hex($random));
625
    }
626
627
    /**
628
     * Set the confirmation token for e-mail address verification
629
     *
630
     * @param  string $code The confirmation code
631
     * @return self
632
     */
633
    private function setConfirmCode($code)
634
    {
635
        $this->lazyLoad();
636
637
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
638
    }
639
640
    /**
641
     * Set what kind of events the player should be e-mailed about
642
     *
643
     * @param  string $receives The type of notification
644
     * @return self
645
     */
646
    public function setReceives($receives)
647
    {
648
        $this->lazyLoad();
649
650
        return $this->updateProperty($this->receives, 'receives', $receives);
651
    }
652
653
    /**
654
     * Set whether the callsign of the player is outdated
655
     *
656
     * @param  bool $outdated Whether the callsign is outdated
657
     * @return self
658
     */
659 39
    public function setOutdated($outdated)
660
    {
661 39
        $this->lazyLoad();
662
663 39
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
664
    }
665
666
    /**
667
     * Set the player's description
668
     * @param string $description The description
669
     */
670
    public function setDescription($description)
671
    {
672
        $this->updateProperty($this->description, "description", $description);
673
    }
674
675
    /**
676
     * Set the player's timezone
677
     * @param string $timezone The timezone
678
     */
679
    public function setTimezone($timezone)
680
    {
681
        $this->updateProperty($this->timezone, "timezone", $timezone);
682
    }
683
684
    /**
685
     * Set the player's team
686
     * @param int $team The team's ID
687
     */
688 18
    public function setTeam($team)
689
    {
690 18
        $this->updateProperty($this->team, "team", $team);
691 18
    }
692
693
    /**
694
     * Set the player's status
695
     * @param string $status The new status
696
     */
697
    public function setStatus($status)
698
    {
699
        $this->updateProperty($this->status, 'status', $status);
700
    }
701
702
    /**
703
     * Set the player's admin notes
704
     * @param  string $admin_notes The new admin notes
705
     * @return self
706
     */
707
    public function setAdminNotes($admin_notes)
708
    {
709
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
710
    }
711
712
    /**
713
     * Set the player's country
714
     * @param  int   $country The ID of the new country
715
     * @return self
716
     */
717
    public function setCountry($country)
718
    {
719
        return $this->updateProperty($this->country, 'country', $country);
720
    }
721
722
    /**
723
     * Updates this player's last login
724
     */
725
    public function updateLastLogin()
726
    {
727
        $this->update("last_login", TimeDate::now()->toMysql());
728
    }
729
730
    /**
731
     * Get the player's username
732
     * @return string The username
733
     */
734 1
    public function getUsername()
735
    {
736 1
        return $this->name;
737
    }
738
739
    /**
740
     * Get the player's username, safe for use in your HTML
741
     * @return string The username
742
     */
743 1
    public function getEscapedUsername()
744
    {
745 1
        return $this->getEscapedName();
746
    }
747
748
    /**
749
     * Alias for Player::setUsername()
750
     *
751
     * @param  string $username The new username
752
     * @return self
753
     */
754
    public function setName($username)
755
    {
756
        return $this->setUsername($username);
757
    }
758
759
    /**
760
     * Mark all the unread messages of a player as read
761
     *
762
     * @return void
763
     */
764
    public function markMessagesAsRead()
765
    {
766
        $this->db->execute(
767
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
768
            array($this->id)
769
        );
770
    }
771
772
    /**
773
     * Set the roles of a user
774
     *
775
     * @todo   Is it worth making this faster?
776
     * @param  Role[] $roles The new roles of the user
777
     * @return self
778
     */
779
    public function setRoles($roles)
780
    {
781
        $this->lazyLoad();
782
783
        $oldRoles = Role::mapToIds($this->roles);
784
        $this->roles = $roles;
785
        $roleIds = Role::mapToIds($roles);
786
787
        $newRoles     = array_diff($roleIds, $oldRoles);
788
        $removedRoles = array_diff($oldRoles, $roleIds);
789
790
        foreach ($newRoles as $role) {
791
            $this->modifyRole($role, 'add');
792
        }
793
794
        foreach ($removedRoles as $role) {
795
            $this->modifyRole($role, 'remove');
796
        }
797
798
        $this->refresh();
799
800
        return $this;
801
    }
802
803
    /**
804
     * Give or remove a role to/form a player
805
     *
806
     * @param int    $role_id The role ID to add or remove
807
     * @param string $action  Whether to "add" or "remove" a role for a player
808
     *
809
     * @return bool Whether the operation was successful or not
810
     */
811 39
    private function modifyRole($role_id, $action)
812
    {
813 39
        $role = Role::get($role_id);
814
815 39
        if ($role->isValid()) {
816 39
            if ($action == "add") {
817 39
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
818 39
            } elseif ($action == "remove") {
819
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
820
            } else {
821
                throw new Exception("Unrecognized role action");
822
            }
823
824 39
            return true;
825
        }
826
827
        return false;
828
    }
829
830
    /**
831
     * Given a player's BZID, get a player object
832
     *
833
     * @param  int    $bzid The player's BZID
834
     * @return Player
835
     */
836 1
    public static function getFromBZID($bzid)
837
    {
838 1
        return self::get(self::fetchIdFrom($bzid, "bzid"));
839
    }
840
841
    /**
842
     * Get a single player by their username
843
     *
844
     * @param  string $username The username to look for
845
     * @return Player
846
     */
847 1
    public static function getFromUsername($username)
848
    {
849 1
        $player = static::get(self::fetchIdFrom($username, 'username'));
850
851 1
        return $player->inject('name', $username);
852
    }
853
854
    /**
855
     * Get all the players in the database that have an active status
856
     * @return Player[] An array of player BZIDs
857
     */
858
    public static function getPlayers()
859
    {
860
        return self::arrayIdToModel(
861
            self::fetchIdsFrom("status", array("active", "test"), false)
862
        );
863
    }
864
865
    /**
866
     * Show the number of notifications the user hasn't read yet
867
     * @return int
868
     */
869 1
    public function countUnreadNotifications()
870
    {
871 1
        return Notification::countUnreadNotifications($this->id);
872
    }
873
874
    /**
875
     * Count the number of matches a player has participated in
876
     * @return int
877
     */
878
    public function getMatchCount()
879
    {
880
        if ($this->cachedMatchCount === null) {
881
            $this->cachedMatchCount = Match::getQueryBuilder()
882
                ->active()
883
                ->with($this)
884
                ->count();
885
        }
886
887
        return $this->cachedMatchCount;
888
    }
889
890
    /**
891
     * Get the (victory/total matches) ratio of the player
892
     * @return float
893
     */
894
    public function getMatchWinRatio()
895
    {
896
        $count = $this->getMatchCount();
897
898
        if ($count == 0) {
899
            return 0;
900
        }
901
902
        $wins = Match::getQueryBuilder()
903
            ->active()
904
            ->with($this, 'win')
905
            ->count();
906
907
        return $wins / $count;
908
    }
909
910
    /**
911
     * Get the (total caps made by team/total matches) ratio of the player
912
     * @return float
913
     */
914
    public function getMatchAverageCaps()
915
    {
916
        $count = $this->getMatchCount();
917
918
        if ($count == 0) {
919
            return 0;
920
        }
921
922
        // Get the sum of team A points if the player was in team A, team B
923
        // points if the player was in team B, and their average if the player
924
        // was on both teams for some reason
925
        $query = $this->db->query(
926
            "SELECT SUM(
927
                IF(
928
                    FIND_IN_SET(?, team_a_players) AND FIND_IN_SET(?, team_b_players),
929
                    (team_a_points+team_b_points)/2,
930
                    IF(FIND_IN_SET(?, team_a_players), team_a_points, team_b_points)
931
                )
932
            ) AS sum FROM matches WHERE status='entered' AND (FIND_IN_SET(?, team_a_players) OR FIND_IN_SET(?, team_b_players))",
933
            array_fill(0, 5, $this->id)
934
        );
935
936
        return $query[0]['sum'] / $count;
937
    }
938
939
    /**
940
     * Get the match activity in matches per day for a player
941
     *
942
     * @return float
943
     */
944
    public function getMatchActivity()
945
    {
946
        $activity = 0.0;
947
948
        $matches = Match::getQueryBuilder()
949
            ->active()
950
            ->with($this)
951
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
952
            ->getModels($fast = true);
953
954
        foreach ($matches as $match) {
955
            $activity += $match->getActivity();
956
        }
957
958
        return $activity;
959
    }
960
961
    /**
962
     * Show the number of messages the user hasn't read yet
963
     * @return int
964
     */
965 1
    public function countUnreadMessages()
966
    {
967 1
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
968 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...
969 1
        );
970
    }
971
972
    /**
973
     * Get all of the members belonging to a team
974
     * @param  int      $teamID The ID of the team to fetch the members of
975
     * @return Player[] An array of Player objects of the team members
976
     */
977 2
    public static function getTeamMembers($teamID)
978
    {
979 2
        return self::arrayIdToModel(
980 2
            self::fetchIds("WHERE team = ?", array($teamID))
981 2
        );
982
    }
983
984
    /**
985
     * {@inheritdoc}
986
     */
987 1
    public static function getActiveStatuses()
988
    {
989 1
        return array('active', 'reported', 'test');
990
    }
991
992
    /**
993
     * {@inheritdoc}
994
     */
995 39
    public static function getEagerColumns()
996
    {
997 39
        return 'id,bzid,team,username,alias,status,avatar,country';
998
    }
999
1000
    /**
1001
     * {@inheritdoc}
1002
     */
1003 39
    public static function getLazyColumns()
1004
    {
1005 39
        return 'email,verified,receives,confirm_code,outdated,description,timezone,joined,last_login,last_match,admin_notes';
1006
    }
1007
1008
    /**
1009
     * Get a query builder for players
1010
     * @return QueryBuilder
1011
     */
1012 View Code Duplication
    public static function getQueryBuilder()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1013
    {
1014
        return new QueryBuilder('Player', array(
1015
            'columns' => array(
1016
                'name'     => 'username',
1017
                'team'     => 'team',
1018
                'outdated' => 'outdated',
1019
                'status'   => 'status'
1020
            ),
1021
            'name' => 'name',
1022
        ));
1023
    }
1024
1025
    /**
1026
     * Enter a new player to the database
1027
     * @param  int              $bzid        The player's bzid
1028
     * @param  string           $username    The player's username
1029
     * @param  int              $team        The player's team
1030
     * @param  string           $status      The player's status
1031
     * @param  int              $role_id     The player's role when they are first created
1032
     * @param  string           $avatar      The player's profile avatar
1033
     * @param  string           $description The player's profile description
1034
     * @param  int              $country     The player's country
1035
     * @param  string           $timezone    The player's timezone
1036
     * @param  string|\TimeDate $joined      The date the player joined
1037
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1038
     * @return Player           An object representing the player that was just entered
1039
     */
1040 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")
1041
    {
1042 39
        $joined = TimeDate::from($joined);
1043 39
        $last_login = TimeDate::from($last_login);
1044 39
        $timezone = ($timezone) ?: date_default_timezone_get();
1045
1046 39
        $player = self::create(array(
1047 39
            'bzid'        => $bzid,
1048 39
            'team'        => $team,
1049 39
            'username'    => $username,
1050 39
            'alias'       => self::generateAlias($username),
1051 39
            'status'      => $status,
1052 39
            'avatar'      => $avatar,
1053 39
            'description' => $description,
1054 39
            'country'     => $country,
1055 39
            'timezone'    => $timezone,
1056 39
            'joined'      => $joined->toMysql(),
1057 39
            'last_login'  => $last_login->toMysql(),
1058 39
        ));
1059
1060 39
        $player->addRole($role_id);
1061 39
        $player->getIdenticon($player->getId());
1062 39
        $player->setUsername($username);
1063
1064 39
        return $player;
1065
    }
1066
1067
    /**
1068
     * Determine if a player exists in the database
1069
     * @param  int  $bzid The player's bzid
1070
     * @return bool Whether the player exists in the database
1071
     */
1072
    public static function playerBZIDExists($bzid)
1073
    {
1074
        return self::getFromBZID($bzid)->isValid();
1075
    }
1076
1077
    /**
1078
     * Change a player's callsign and add it to the database if it does not
1079
     * exist as a past callsign
1080
     *
1081
     * @param  string $username The new username of the player
1082
     * @return self
1083
     */
1084 39
    public function setUsername($username)
1085
    {
1086
        // The player's username was just fetched from BzDB, it's definitely not
1087
        // outdated
1088 39
        $this->setOutdated(false);
1089
1090
        // Players who have this player's username are considered outdated
1091 39
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1092
1093 39
        if ($username === $this->name) {
1094
            // The player's username hasn't changed, no need to do anything
1095 1
            return $this;
1096
        }
1097
1098
        // Players who used to have our player's username are not outdated anymore,
1099
        // unless they are more than one.
1100
        // Even though we are sure that the old and new usernames are not equal,
1101
        // MySQL makes a different type of string equality tests, which is why we
1102
        // also check IDs to make sure not to affect our own player's outdatedness.
1103 38
        $this->db->execute("
1104 38
            UPDATE {$this->table} SET outdated =
1105 38
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1106 38
            WHERE username = ? AND id != ?",
1107 38
            array($this->name, $this->id, $this->name, $this->id));
1108
1109 38
        $this->updateProperty($this->name, 'username', $username);
1110 38
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1111 38
        $this->resetAlias();
1112
1113 38
        return $this;
1114
    }
1115
1116
    /**
1117
     * Alphabetical order function for use in usort (case-insensitive)
1118
     * @return Closure The sort function
1119
     */
1120
    public static function getAlphabeticalSort()
1121
    {
1122 1
        return function (Player $a, Player $b) {
1123 1
            return strcasecmp($a->getUsername(), $b->getUsername());
1124 1
        };
1125
    }
1126
1127
    /**
1128
     * {@inheritdoc}
1129
     * @todo Add a constraint that does this automatically
1130
     */
1131 39
    public function wipe()
1132
    {
1133 39
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1134
1135 39
        parent::wipe();
1136 39
    }
1137
1138
    /**
1139
     * Find whether the player can delete a model
1140
     *
1141
     * @param  PermissionModel $model       The model that will be seen
1142
     * @param  bool         $showDeleted Whether to show deleted models to admins
1143
     * @return bool
1144
     */
1145 1
    public function canSee($model, $showDeleted = false)
1146
    {
1147 1
        return $model->canBeSeenBy($this, $showDeleted);
1148
    }
1149
1150
    /**
1151
     * Find whether the player can delete a model
1152
     *
1153
     * @param  PermissionModel $model The model that will be deleted
1154
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1155
     *                                to soft-delete ones
1156
     * @return bool
1157
     */
1158 1
    public function canDelete($model, $hard = false)
1159
    {
1160 1
        if ($hard) {
1161
            return $model->canBeHardDeletedBy($this);
1162
        } else {
1163 1
            return $model->canBeSoftDeletedBy($this);
1164
        }
1165
    }
1166
1167
    /**
1168
     * Find whether the player can create a model
1169
     *
1170
     * @param  string  $modelName The PHP class identifier of the model type
1171
     * @return bool
1172
     */
1173 1
    public function canCreate($modelName)
1174
    {
1175 1
        return $modelName::canBeCreatedBy($this);
1176
    }
1177
1178
    /**
1179
     * Find whether the player can edit a model
1180
     *
1181
     * @param  PermissionModel $model The model which will be edited
1182
     * @return bool
1183
     */
1184 1
    public function canEdit($model)
1185
    {
1186 1
        return $model->canBeEditedBy($this);
1187
    }
1188
}
1189