Completed
Pull Request — master (#44)
by Beñat
02:18
created

User::cleanInvitationToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 0
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
        $this->invitationToken = null;
235
    }
236
237
    /**
238
     * Cleans the remember password token.
239
     */
240
    public function cleanRememberPasswordToken()
241
    {
242
        $this->rememberPasswordToken = null;
243
    }
244
245
    /**
246
     * Gets the confirmation token.
247
     *
248
     * @return UserToken|null
249
     */
250
    public function confirmationToken()
251
    {
252
        // This ternary is a hack that avoids the
253
        // DoctrineORM limitation with nullable embeddables
254
        return $this->confirmationToken instanceof UserToken
255
        && null === $this->confirmationToken->token()
256
            ? null
257
            : $this->confirmationToken;
258
    }
259
260
    /**
261
     * Gets the created on.
262
     *
263
     * @return \DateTimeInterface
264
     */
265
    public function createdOn()
266
    {
267
        return $this->createdOn;
268
    }
269
270
    /**
271
     * Gets the email.
272
     *
273
     * @return UserEmail
274
     */
275
    public function email()
276
    {
277
        return $this->email;
278
    }
279
280
    /**
281
     * Enables the user account.
282
     */
283
    public function enableAccount()
284
    {
285
        $this->confirmationToken = null;
286
        $this->updatedOn = new \DateTimeImmutable();
287
288
        $this->publish(
289
            new UserEnabled(
290
                $this->id,
291
                $this->email
292
            )
293
        );
294
    }
295
296
    /**
297
     * Adds the given role.
298
     *
299
     * @param UserRole $aRole The user role
300
     *
301
     * @throws UserRoleInvalidException        when the user is role is invalid
302
     * @throws UserRoleAlreadyGrantedException when the user role is already granted
303
     */
304
    public function grant(UserRole $aRole)
305
    {
306
        if (false === $this->isRoleAllowed($aRole)) {
307
            throw new UserRoleInvalidException();
308
        }
309
        if (true === $this->isGranted($aRole)) {
310
            throw new UserRoleAlreadyGrantedException();
311
        }
312
        $this->roles[] = $aRole;
313
        $this->updatedOn = new \DateTimeImmutable();
314
315
        $this->publish(
316
            new UserRoleGranted(
317
                $this->id,
318
                $this->email,
319
                $aRole
320
            )
321
        );
322
    }
323
324
    /**
325
     * Gets the invitation token.
326
     *
327
     * @return UserToken|null
328
     */
329
    public function invitationToken()
330
    {
331
        // This ternary is a hack that avoids the
332
        // DoctrineORM limitation with nullable embeddables
333
        return $this->invitationToken instanceof UserToken
334
        && null === $this->invitationToken->token()
335
            ? null
336
            : $this->invitationToken;
337
    }
338
339
    /**
340
     * Checks if the user is enabled or not.
341
     *
342
     * @return bool
343
     */
344
    public function isEnabled()
345
    {
346
        return null === $this->confirmationToken();
347
    }
348
349
    /**
350
     * Checks if the user has the given role.
351
     *
352
     * @param UserRole $aRole The user role
353
     *
354
     * @return bool
355
     */
356
    public function isGranted(UserRole $aRole)
357
    {
358
        foreach ($this->roles as $role) {
359
            if ($role->equals($aRole)) {
360
                return true;
361
            }
362
        }
363
364
        return false;
365
    }
366
367
    /**
368
     * Checks if the invitation token is accepted or not.
369
     *
370
     * @return bool
371
     */
372
    public function isInvitationTokenAccepted()
373
    {
374
        return null === $this->invitationToken();
375
    }
376
377
    /**
378
     * Checks if the invitation token is expired or not.
379
     *
380
     * @throws UserTokenNotFoundException when the invitation token does not exist
381
     *
382
     * @return bool
383
     */
384
    public function isInvitationTokenExpired()
385
    {
386
        if (!$this->invitationToken() instanceof UserToken) {
387
            throw new UserTokenNotFoundException();
388
        }
389
390
        return $this->invitationToken->isExpired(
391
            $this->invitationTokenLifetime()
392
        );
393
    }
394
395
    /**
396
     * Checks if the remember password token is expired or not.
397
     *
398
     * @throws UserTokenNotFoundException when the remember password token does not exist
399
     *
400
     * @return bool
401
     */
402
    public function isRememberPasswordTokenExpired()
403
    {
404
        if (!$this->rememberPasswordToken() instanceof UserToken) {
405
            throw new UserTokenNotFoundException();
406
        }
407
408
        return $this->rememberPasswordToken->isExpired(
409
            $this->rememberPasswordTokenLifetime()
410
        );
411
    }
412
413
    /**
414
     * Checks if the role given appears between allowed roles.
415
     *
416
     * @param UserRole $aRole The user role
417
     *
418
     * @return bool
419
     */
420
    public function isRoleAllowed(UserRole $aRole)
421
    {
422
        return in_array($aRole->role(), static::availableRoles(), true);
423
    }
424
425
    /**
426
     * Gets the last login.
427
     *
428
     * @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...
429
     */
430
    public function lastLogin()
431
    {
432
        return $this->lastLogin;
433
    }
434
435
    /**
436
     * Validates user login for the given password.
437
     *
438
     * @param string              $aPlainPassword Plain password used to log in
439
     * @param UserPasswordEncoder $anEncoder      The encoder used to encode the password
440
     *
441
     * @throws UserInactiveException        when the user is not enabled
442
     * @throws UserPasswordInvalidException when the user password is invalid
443
     */
444
    public function login($aPlainPassword, UserPasswordEncoder $anEncoder)
445
    {
446
        if (false === $this->isEnabled()) {
447
            throw new UserInactiveException();
448
        }
449
        if (false === $this->password()->equals($aPlainPassword, $anEncoder)) {
450
            throw new UserPasswordInvalidException();
451
        }
452
        $this->lastLogin = new \DateTimeImmutable();
453
454
        $this->publish(
455
            new UserLoggedIn(
456
                $this->id,
457
                $this->email
458
            )
459
        );
460
    }
461
462
    /**
463
     * Updated the user state after logout.
464
     *
465
     * @throws UserInactiveException when the user is not enabled
466
     */
467
    public function logout()
468
    {
469
        if (false === $this->isEnabled()) {
470
            throw new UserInactiveException();
471
        }
472
473
        $this->publish(
474
            new UserLoggedOut(
475
                $this->id,
476
                $this->email
477
            )
478
        );
479
    }
480
481
    /**
482
     * Gets the password.
483
     *
484
     * @return UserPassword
485
     */
486
    public function password()
487
    {
488
        return $this->password;
489
    }
490
491
    /**
492
     * Updates the invitation token in case a user has
493
     * been already invited and has lost the token.
494
     *
495
     * @throws UserInvitationAlreadyAcceptedException in case user has already accepted the invitation
496
     */
497
    public function regenerateInvitationToken()
498
    {
499
        if (null === $this->invitationToken()) {
500
            throw new UserInvitationAlreadyAcceptedException();
501
        }
502
        $this->invitationToken = new UserToken();
503
504
        $this->publish(
505
            new UserInvitationTokenRegenerated(
506
                $this->id,
507
                $this->email,
508
                $this->invitationToken
509
            )
510
        );
511
    }
512
513
    /**
514
     * Gets the remember password token.
515
     *
516
     * @return UserToken
517
     */
518
    public function rememberPasswordToken()
519
    {
520
        // This ternary is a hack that avoids the
521
        // DoctrineORM limitation with nullable embeddables
522
        return $this->rememberPasswordToken instanceof UserToken
523
        && null === $this->rememberPasswordToken->token()
524
            ? null
525
            : $this->rememberPasswordToken;
526
    }
527
528
    /**
529
     * Remembers the password.
530
     */
531
    public function rememberPassword()
532
    {
533
        $this->rememberPasswordToken = new UserToken();
534
535
        $this->publish(
536
            new UserRememberPasswordRequested(
537
                $this->id,
538
                $this->email,
539
                $this->rememberPasswordToken
540
            )
541
        );
542
    }
543
544
    /**
545
     * Removes the given role.
546
     *
547
     * @param UserRole $aRole The user role
548
     *
549
     * @throws UserRoleInvalidException        when the role is invalid
550
     * @throws UserRoleAlreadyRevokedException when the role is already revoked
551
     */
552
    public function revoke(UserRole $aRole)
553
    {
554
        if (false === $this->isRoleAllowed($aRole)) {
555
            throw new UserRoleInvalidException();
556
        }
557
        foreach ($this->roles as $key => $role) {
558
            if ($role->equals($aRole)) {
559
                unset($this->roles[$key]);
560
                $this->roles = array_values($this->roles);
561
                break;
562
            }
563
            throw new UserRoleAlreadyRevokedException();
564
        }
565
        $this->updatedOn = new \DateTimeImmutable();
566
        $this->publish(
567
            new UserRoleRevoked(
568
                $this->id,
569
                $this->email,
570
                $aRole
571
            )
572
        );
573
    }
574
575
    /**
576
     * Gets the roles.
577
     *
578
     * @return UserRole[]
579
     */
580
    public function roles()
581
    {
582
        return $this->roles;
583
    }
584
585
    /**
586
     * Gets the updated on.
587
     *
588
     * @return \DateTimeInterface
589
     */
590
    public function updatedOn()
591
    {
592
        return $this->updatedOn;
593
    }
594
595
    /**
596
     * Gets the available roles in scalar type.
597
     *
598
     * This method is an extension point that it allows
599
     * to add more roles easily in the domain.
600
     *
601
     * @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...
602
     */
603
    public static function availableRoles()
604
    {
605
        return ['ROLE_USER', 'ROLE_ADMIN'];
606
    }
607
608
    /**
609
     * Extension point that determines the lifetime
610
     * of the invitation token in seconds.
611
     *
612
     * @return int
613
     */
614
    protected function invitationTokenLifetime()
615
    {
616
        return 604800; // 1 week
617
    }
618
619
    /**
620
     * Extension point that determines the lifetime
621
     * of the remember password token in seconds.
622
     *
623
     * @return int
624
     */
625
    protected function rememberPasswordTokenLifetime()
626
    {
627
        return 3600; // 1 hour
628
    }
629
}
630