User   F
last analyzed

Complexity

Total Complexity 58

Size/Duplication

Total Lines 602
Duplicated Lines 4.65 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 58
lcom 1
cbo 21
dl 28
loc 602
rs 1.8998
c 0
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 2
A signUp() 14 14 1
A invite() 14 14 1
A id() 0 4 1
A acceptInvitation() 0 17 3
A changePassword() 0 6 1
A cleanInvitationToken() 0 6 2
A cleanRememberPasswordToken() 0 6 2
A confirmationToken() 0 9 3
A createdOn() 0 4 1
A email() 0 4 1
A enableAccount() 0 12 1
A grant() 0 19 3
A invitationToken() 0 9 3
A isEnabled() 0 4 1
A isGranted() 0 10 3
A isInvitationTokenAccepted() 0 4 1
A isInvitationTokenExpired() 0 10 2
A isRememberPasswordTokenExpired() 0 10 2
A isRoleAllowed() 0 4 1
A lastLogin() 0 4 1
A login() 0 17 3
A logout() 0 13 2
A password() 0 4 1
A regenerateInvitationToken() 0 15 2
A rememberPasswordToken() 0 9 3
A rememberPassword() 0 12 1
B revoke() 0 29 5
A roles() 0 4 1
A updatedOn() 0 4 1
A availableRoles() 0 4 1
A invitationTokenLifetime() 0 4 1
A rememberPasswordTokenLifetime() 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 User 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 User, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the BenGorUser package.
5
 *
6
 * (c) Beñat Espiña <[email protected]>
7
 * (c) Gorka Laucirica <[email protected]>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace BenGorUser\User\Domain\Model;
14
15
use BenGorUser\User\Domain\Model\Event\UserEnabled;
16
use BenGorUser\User\Domain\Model\Event\UserInvitationTokenRegenerated;
17
use BenGorUser\User\Domain\Model\Event\UserInvited;
18
use BenGorUser\User\Domain\Model\Event\UserLoggedIn;
19
use BenGorUser\User\Domain\Model\Event\UserLoggedOut;
20
use BenGorUser\User\Domain\Model\Event\UserRegistered;
21
use BenGorUser\User\Domain\Model\Event\UserRememberPasswordRequested;
22
use BenGorUser\User\Domain\Model\Event\UserRoleGranted;
23
use BenGorUser\User\Domain\Model\Event\UserRoleRevoked;
24
use BenGorUser\User\Domain\Model\Exception\UserInactiveException;
25
use BenGorUser\User\Domain\Model\Exception\UserInvitationAlreadyAcceptedException;
26
use BenGorUser\User\Domain\Model\Exception\UserPasswordInvalidException;
27
use BenGorUser\User\Domain\Model\Exception\UserRoleAlreadyGrantedException;
28
use BenGorUser\User\Domain\Model\Exception\UserRoleAlreadyRevokedException;
29
use BenGorUser\User\Domain\Model\Exception\UserRoleInvalidException;
30
use BenGorUser\User\Domain\Model\Exception\UserTokenExpiredException;
31
use BenGorUser\User\Domain\Model\Exception\UserTokenNotFoundException;
32
33
/**
34
 * User domain class.
35
 *
36
 * @author Beñat Espiña <[email protected]>
37
 * @author Gorka Laucirica <[email protected]>
38
 */
39
class User extends UserAggregateRoot
40
{
41
    /**
42
     * The id.
43
     *
44
     * @var UserId
45
     */
46
    protected $id;
47
48
    /**
49
     * The confirmation token.
50
     *
51
     * @var UserToken
52
     */
53
    protected $confirmationToken;
54
55
    /**
56
     * Created on.
57
     *
58
     * @var \DateTimeInterface
59
     */
60
    protected $createdOn;
61
62
    /**
63
     * The email.
64
     *
65
     * @var UserEmail
66
     */
67
    protected $email;
68
69
    /**
70
     * The invitation token.
71
     *
72
     * @var UserToken
73
     */
74
    protected $invitationToken;
75
76
    /**
77
     * The last login.
78
     *
79
     * @var \DateTimeInterface|null
80
     */
81
    protected $lastLogin;
82
83
    /**
84
     * The password.
85
     *
86
     * @var UserPassword
87
     */
88
    protected $password;
89
90
    /**
91
     * The remember password token.
92
     *
93
     * @var UserToken
94
     */
95
    protected $rememberPasswordToken;
96
97
    /**
98
     * Array which contains roles.
99
     *
100
     * @var UserRole[]
101
     */
102
    protected $roles;
103
104
    /**
105
     * Updated on.
106
     *
107
     * @var \DateTimeInterface
108
     */
109
    protected $updatedOn;
110
111
    /**
112
     * Constructor.
113
     *
114
     * @param UserId            $anId      The id
115
     * @param UserEmail         $anEmail   The email
116
     * @param array             $userRoles Array which contains the roles
117
     * @param UserPassword|null $aPassword The encoded password
118
     */
119
    protected function __construct(UserId $anId, UserEmail $anEmail, array $userRoles, UserPassword $aPassword = null)
120
    {
121
        $this->id = $anId;
122
        $this->email = $anEmail;
123
        $this->password = $aPassword;
124
        $this->createdOn = new \DateTimeImmutable();
125
        $this->updatedOn = new \DateTimeImmutable();
126
127
        $this->roles = [];
128
        foreach ($userRoles as $userRole) {
129
            $this->grant($userRole);
130
        }
131
    }
132
133
    /**
134
     * Sign up user.
135
     *
136
     * @param UserId       $anId      The id
137
     * @param UserEmail    $anEmail   The email
138
     * @param UserPassword $aPassword The encoded password
139
     * @param array        $userRoles Array which contains the roles
140
     *
141
     * @return static
142
     */
143 View Code Duplication
    public static function signUp(UserId $anId, UserEmail $anEmail, UserPassword $aPassword, array $userRoles)
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...
144
    {
145
        $user = new static($anId, $anEmail, $userRoles, $aPassword);
146
        $user->confirmationToken = new UserToken();
147
        $user->publish(
148
            new UserRegistered(
149
                $user->id(),
150
                $user->email(),
151
                $user->confirmationToken()
152
            )
153
        );
154
155
        return $user;
156
    }
157
158
    /**
159
     * Invites user.
160
     *
161
     * @param UserId    $anId      The id
162
     * @param UserEmail $anEmail   The email
163
     * @param array     $userRoles Array which contains the roles
164
     *
165
     * @return static
166
     */
167 View Code Duplication
    public static function invite(UserId $anId, UserEmail $anEmail, array $userRoles)
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...
168
    {
169
        $user = new static($anId, $anEmail, $userRoles);
170
        $user->invitationToken = new UserToken();
171
        $user->publish(
172
            new UserInvited(
173
                $user->id(),
174
                $user->email(),
175
                $user->invitationToken()
0 ignored issues
show
Bug introduced by
It seems like $user->invitationToken() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
176
            )
177
        );
178
179
        return $user;
180
    }
181
182
    /**
183
     * Gets the id.
184
     *
185
     * @return UserId
186
     */
187
    public function id()
188
    {
189
        return $this->id;
190
    }
191
192
    /**
193
     * Accepts the invitation request.
194
     *
195
     * @throws UserTokenExpiredException when the token is expired
196
     */
197
    public function acceptInvitation()
198
    {
199
        if ($this->isInvitationTokenAccepted()) {
200
            throw new UserInvitationAlreadyAcceptedException();
201
        }
202
        if ($this->isInvitationTokenExpired()) {
203
            throw new UserTokenExpiredException();
204
        }
205
        $this->invitationToken = null;
206
        $this->updatedOn = new \DateTimeImmutable();
207
        $this->publish(
208
            new UserRegistered(
209
                $this->id(),
210
                $this->email()
211
            )
212
        );
213
    }
214
215
    /**
216
     * Updates the user password.
217
     *
218
     * @param UserPassword $aPassword The old password
219
     *
220
     * @throws UserTokenExpiredException when the token is expired
221
     */
222
    public function changePassword(UserPassword $aPassword)
223
    {
224
        $this->password = $aPassword;
225
        $this->rememberPasswordToken = null;
226
        $this->updatedOn = new \DateTimeImmutable();
227
    }
228
229
    /**
230
     * Cleans the invitation token.
231
     */
232
    public function cleanInvitationToken()
233
    {
234
        if ($this->isInvitationTokenExpired()) {
235
            $this->invitationToken = null;
236
        }
237
    }
238
239
    /**
240
     * Cleans the remember password token.
241
     */
242
    public function cleanRememberPasswordToken()
243
    {
244
        if ($this->isRememberPasswordTokenExpired()) {
245
            $this->rememberPasswordToken = null;
246
        }
247
    }
248
249
    /**
250
     * Gets the confirmation token.
251
     *
252
     * @return UserToken|null
253
     */
254
    public function confirmationToken()
255
    {
256
        // This ternary is a hack that avoids the
257
        // DoctrineORM limitation with nullable embeddables
258
        return $this->confirmationToken instanceof UserToken
259
        && null === $this->confirmationToken->token()
260
            ? null
261
            : $this->confirmationToken;
262
    }
263
264
    /**
265
     * Gets the created on.
266
     *
267
     * @return \DateTimeInterface
268
     */
269
    public function createdOn()
270
    {
271
        return $this->createdOn;
272
    }
273
274
    /**
275
     * Gets the email.
276
     *
277
     * @return UserEmail
278
     */
279
    public function email()
280
    {
281
        return $this->email;
282
    }
283
284
    /**
285
     * Enables the user account.
286
     */
287
    public function enableAccount()
288
    {
289
        $this->confirmationToken = null;
290
        $this->updatedOn = new \DateTimeImmutable();
291
292
        $this->publish(
293
            new UserEnabled(
294
                $this->id,
295
                $this->email
296
            )
297
        );
298
    }
299
300
    /**
301
     * Adds the given role.
302
     *
303
     * @param UserRole $aRole The user role
304
     *
305
     * @throws UserRoleInvalidException        when the user is role is invalid
306
     * @throws UserRoleAlreadyGrantedException when the user role is already granted
307
     */
308
    public function grant(UserRole $aRole)
309
    {
310
        if (false === $this->isRoleAllowed($aRole)) {
311
            throw new UserRoleInvalidException();
312
        }
313
        if (true === $this->isGranted($aRole)) {
314
            throw new UserRoleAlreadyGrantedException();
315
        }
316
        $this->roles[] = $aRole;
317
        $this->updatedOn = new \DateTimeImmutable();
318
319
        $this->publish(
320
            new UserRoleGranted(
321
                $this->id,
322
                $this->email,
323
                $aRole
324
            )
325
        );
326
    }
327
328
    /**
329
     * Gets the invitation token.
330
     *
331
     * @return UserToken|null
332
     */
333
    public function invitationToken()
334
    {
335
        // This ternary is a hack that avoids the
336
        // DoctrineORM limitation with nullable embeddables
337
        return $this->invitationToken instanceof UserToken
338
        && null === $this->invitationToken->token()
339
            ? null
340
            : $this->invitationToken;
341
    }
342
343
    /**
344
     * Checks if the user is enabled or not.
345
     *
346
     * @return bool
347
     */
348
    public function isEnabled()
349
    {
350
        return null === $this->confirmationToken();
351
    }
352
353
    /**
354
     * Checks if the user has the given role.
355
     *
356
     * @param UserRole $aRole The user role
357
     *
358
     * @return bool
359
     */
360
    public function isGranted(UserRole $aRole)
361
    {
362
        foreach ($this->roles as $role) {
363
            if ($role->equals($aRole)) {
364
                return true;
365
            }
366
        }
367
368
        return false;
369
    }
370
371
    /**
372
     * Checks if the invitation token is accepted or not.
373
     *
374
     * @return bool
375
     */
376
    public function isInvitationTokenAccepted()
377
    {
378
        return null === $this->invitationToken();
379
    }
380
381
    /**
382
     * Checks if the invitation token is expired or not.
383
     *
384
     * @throws UserTokenNotFoundException when the invitation token does not exist
385
     *
386
     * @return bool
387
     */
388
    public function isInvitationTokenExpired()
389
    {
390
        if (!$this->invitationToken() instanceof UserToken) {
391
            throw new UserTokenNotFoundException();
392
        }
393
394
        return $this->invitationToken->isExpired(
395
            $this->invitationTokenLifetime()
396
        );
397
    }
398
399
    /**
400
     * Checks if the remember password token is expired or not.
401
     *
402
     * @throws UserTokenNotFoundException when the remember password token does not exist
403
     *
404
     * @return bool
405
     */
406
    public function isRememberPasswordTokenExpired()
407
    {
408
        if (!$this->rememberPasswordToken() instanceof UserToken) {
409
            throw new UserTokenNotFoundException();
410
        }
411
412
        return $this->rememberPasswordToken->isExpired(
413
            $this->rememberPasswordTokenLifetime()
414
        );
415
    }
416
417
    /**
418
     * Checks if the role given appears between allowed roles.
419
     *
420
     * @param UserRole $aRole The user role
421
     *
422
     * @return bool
423
     */
424
    public function isRoleAllowed(UserRole $aRole)
425
    {
426
        return in_array($aRole->role(), static::availableRoles(), true);
427
    }
428
429
    /**
430
     * Gets the last login.
431
     *
432
     * @return \DateTimeInterface
0 ignored issues
show
Documentation introduced by
Should the return type not be \DateTimeInterface|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
433
     */
434
    public function lastLogin()
435
    {
436
        return $this->lastLogin;
437
    }
438
439
    /**
440
     * Validates user login for the given password.
441
     *
442
     * @param string              $aPlainPassword Plain password used to log in
443
     * @param UserPasswordEncoder $anEncoder      The encoder used to encode the password
444
     *
445
     * @throws UserInactiveException        when the user is not enabled
446
     * @throws UserPasswordInvalidException when the user password is invalid
447
     */
448
    public function login($aPlainPassword, UserPasswordEncoder $anEncoder)
449
    {
450
        if (false === $this->isEnabled()) {
451
            throw new UserInactiveException();
452
        }
453
        if (false === $this->password()->equals($aPlainPassword, $anEncoder)) {
454
            throw new UserPasswordInvalidException();
455
        }
456
        $this->lastLogin = new \DateTimeImmutable();
457
458
        $this->publish(
459
            new UserLoggedIn(
460
                $this->id,
461
                $this->email
462
            )
463
        );
464
    }
465
466
    /**
467
     * Updated the user state after logout.
468
     *
469
     * @throws UserInactiveException when the user is not enabled
470
     */
471
    public function logout()
472
    {
473
        if (false === $this->isEnabled()) {
474
            throw new UserInactiveException();
475
        }
476
477
        $this->publish(
478
            new UserLoggedOut(
479
                $this->id,
480
                $this->email
481
            )
482
        );
483
    }
484
485
    /**
486
     * Gets the password.
487
     *
488
     * @return UserPassword
489
     */
490
    public function password()
491
    {
492
        return $this->password;
493
    }
494
495
    /**
496
     * Updates the invitation token in case a user has
497
     * been already invited and has lost the token.
498
     *
499
     * @throws UserInvitationAlreadyAcceptedException in case user has already accepted the invitation
500
     */
501
    public function regenerateInvitationToken()
502
    {
503
        if (null === $this->invitationToken()) {
504
            throw new UserInvitationAlreadyAcceptedException();
505
        }
506
        $this->invitationToken = new UserToken();
507
508
        $this->publish(
509
            new UserInvitationTokenRegenerated(
510
                $this->id,
511
                $this->email,
512
                $this->invitationToken
513
            )
514
        );
515
    }
516
517
    /**
518
     * Gets the remember password token.
519
     *
520
     * @return UserToken
521
     */
522
    public function rememberPasswordToken()
523
    {
524
        // This ternary is a hack that avoids the
525
        // DoctrineORM limitation with nullable embeddables
526
        return $this->rememberPasswordToken instanceof UserToken
527
        && null === $this->rememberPasswordToken->token()
528
            ? null
529
            : $this->rememberPasswordToken;
530
    }
531
532
    /**
533
     * Remembers the password.
534
     */
535
    public function rememberPassword()
536
    {
537
        $this->rememberPasswordToken = new UserToken();
538
539
        $this->publish(
540
            new UserRememberPasswordRequested(
541
                $this->id,
542
                $this->email,
543
                $this->rememberPasswordToken
544
            )
545
        );
546
    }
547
548
    /**
549
     * Removes the given role.
550
     *
551
     * @param UserRole $aRole The user role
552
     *
553
     * @throws UserRoleInvalidException        when the role is invalid
554
     * @throws UserRoleAlreadyRevokedException when the role is already revoked
555
     */
556
    public function revoke(UserRole $aRole)
557
    {
558
        if (false === $this->isRoleAllowed($aRole)) {
559
            throw new UserRoleInvalidException();
560
        }
561
562
        $numberOfRoles = count($this->roles);
563
        for ($index = 0; $index < $numberOfRoles; $index++) {
564
            $role = $this->roles[$index];
565
566
            if ($role->equals($aRole)) {
567
                unset($this->roles[$index]);
568
                $this->roles = array_values($this->roles);
569
                break;
570
            }
571
        }
572
        if ($index === $numberOfRoles) {
573
            throw new UserRoleAlreadyRevokedException();
574
        }
575
576
        $this->updatedOn = new \DateTimeImmutable();
577
        $this->publish(
578
            new UserRoleRevoked(
579
                $this->id,
580
                $this->email,
581
                $aRole
582
            )
583
        );
584
    }
585
586
    /**
587
     * Gets the roles.
588
     *
589
     * @return UserRole[]
590
     */
591
    public function roles()
592
    {
593
        return $this->roles;
594
    }
595
596
    /**
597
     * Gets the updated on.
598
     *
599
     * @return \DateTimeInterface
600
     */
601
    public function updatedOn()
602
    {
603
        return $this->updatedOn;
604
    }
605
606
    /**
607
     * Gets the available roles in scalar type.
608
     *
609
     * This method is an extension point that it allows
610
     * to add more roles easily in the domain.
611
     *
612
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
613
     */
614
    public static function availableRoles()
615
    {
616
        return ['ROLE_USER', 'ROLE_ADMIN'];
617
    }
618
619
    /**
620
     * Extension point that determines the lifetime
621
     * of the invitation token in seconds.
622
     *
623
     * @return int
624
     */
625
    protected function invitationTokenLifetime()
626
    {
627
        return 604800; // 1 week
628
    }
629
630
    /**
631
     * Extension point that determines the lifetime
632
     * of the remember password token in seconds.
633
     *
634
     * @return int
635
     */
636
    protected function rememberPasswordTokenLifetime()
637
    {
638
        return 3600; // 1 hour
639
    }
640
}
641