AbstractUser   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 354
Duplicated Lines 0 %

Test Coverage

Coverage 81.89%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 143
dl 0
loc 354
ccs 104
cts 127
cp 0.8189
rs 9.2
c 1
b 0
f 0
wmc 40

36 Methods

Rating   Name   Duplication   Size   Complexity  
A getEmailAddress() 0 3 1
A getNewPasswordConfirmationToken() 0 3 1
A getOldPassword() 0 3 1
A setEmailAddressVerified() 0 5 1
A __construct() 0 8 1
A getNewEmailAddress() 0 3 1
A getNewEmailConfirmationToken() 0 3 1
A setNewEmailAddress() 0 8 2
A getPlainPassword() 0 3 1
A getRoles() 0 3 1
A __toString() 0 3 1
A setEmailAddress() 0 8 2
A setEnabled() 0 5 1
A setPlainPassword() 0 9 2
A setNewPasswordConfirmationToken() 0 5 1
A isEnabled() 0 3 1
A eraseCredentials() 0 3 1
A getPasswordRequestedAt() 0 3 1
A setPasswordRequestedAt() 0 5 1
A getPassword() 0 3 1
A getUserIdentifier() 0 3 1
A setPassword() 0 5 1
A setUsername() 0 5 1
A getSalt() 0 4 1
A setEmailAddressVerifyToken() 0 3 1
A setRoles() 0 5 1
A isEmailAddressVerified() 0 3 1
A setNewEmailConfirmationToken() 0 5 1
A getNewEmailAddressChangeRequestedAt() 0 3 1
A getUsername() 0 3 1
A setOldPassword() 0 5 1
A getEmailAddressVerifyToken() 0 3 1
A serialize() 0 10 1
A unserialize() 0 14 1
A createFromPayload() 0 17 1
A isPasswordRequestLimitReached() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractUser 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.

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 AbstractUser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentsBundle\Entity\User;
15
16
use ApiPlatform\Metadata\ApiProperty;
17
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserInterface;
18
use Ramsey\Uuid\Uuid;
19
use Silverback\ApiComponentsBundle\Annotation as Silverback;
20
use Silverback\ApiComponentsBundle\Entity\Utility\IdTrait;
21
use Silverback\ApiComponentsBundle\Entity\Utility\TimestampedTrait;
22
use Silverback\ApiComponentsBundle\Validator\Constraints as AcbAssert;
23
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
24
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
25
use Symfony\Component\Security\Core\User\UserInterface as SymfonyUserInterface;
26
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
27
use Symfony\Component\Serializer\Annotation\Groups;
28
use Symfony\Component\Validator\Constraints as Assert;
29
30
/**
31
 * @author Daniel West <[email protected]>
32
 */
33
#[Silverback\Timestamped]
34
#[UniqueEntity(fields: ['username'], message: 'Sorry, that user already exists in the database.', errorPath: 'username')]
35
#[UniqueEntity(fields: ['emailAddress'], message: 'Sorry, that email address already exists in the database.', errorPath: 'emailAddress')]
36
#[AcbAssert\NewEmailAddress(groups: ['User:emailAddress', 'Default'])]
37
abstract class AbstractUser implements SymfonyUserInterface, PasswordAuthenticatedUserInterface, JWTUserInterface
38
{
39
    use IdTrait;
40
    use TimestampedTrait;
41
42
    #[Assert\NotBlank(message: 'Please enter a username.', allowNull: false, groups: ['Default'])]
43
    #[Groups(['User:superAdmin', 'User:output', 'Form:cwa_resource:read'])]
44
    protected ?string $username;
45
46
    #[Assert\NotBlank(message: 'Please enter your email address.', allowNull: false, groups: ['Default'])]
47
    #[Assert\Email]
48
    #[Groups(['User:superAdmin', 'User:output', 'Form:cwa_resource:read'])]
49
    protected ?string $emailAddress;
50
51
    #[Groups(['User:superAdmin', 'User:output', 'Form:cwa_resource:read'])]
52
    protected array $roles;
53
54
    #[Groups(['User:superAdmin'])]
55
    protected bool $enabled;
56
57
    #[ApiProperty(readable: false, writable: false)]
58
    protected string $password;
59
60
    #[Assert\NotBlank(message: 'Please enter your desired password.', groups: ['User:password:create'])]
61
    #[Assert\Length(min: 6, max: 4096, minMessage: 'Your password must be more than 6 characters long.', maxMessage: 'Your password cannot be over 4096 characters', groups: ['User:password:create'])]
62
    #[ApiProperty(readable: false)]
63
    #[Groups(['User:input'])]
64
    protected ?string $plainPassword = null;
65
66
    /**
67
     * Random string sent to the user email address in order to verify it.
68
     */
69
    #[ApiProperty(readable: false, writable: false)]
70
    protected ?string $newPasswordConfirmationToken = null;
71
72
    #[ApiProperty(readable: false, writable: false)]
73
    public ?string $plainNewPasswordConfirmationToken = null;
74
75
    #[ApiProperty(readable: false, writable: false)]
76
    protected ?\DateTime $passwordRequestedAt = null;
77
78
    #[UserPassword(message: 'You have not entered your current password correctly. Please try again.', groups: ['User:password:change'])]
79
    #[ApiProperty(readable: false)]
80
    #[Groups(['User:input'])]
81
    protected ?string $oldPassword = null;
82
83
    #[ApiProperty(readable: false, writable: false)]
84
    protected ?\DateTime $passwordUpdatedAt = null;
85
86
    #[Assert\NotBlank(allowNull: true, groups: ['User:emailAddress', 'Default'])]
87
    #[Assert\Email]
88
    #[Groups(['User:input', 'User:output', 'User:emailAddress', 'Form:cwa_resource:read:role_user'])]
89
    protected ?string $newEmailAddress = null;
90
91
    /**
92
     * Random string sent to the user's new email address in order to verify it.
93
     */
94
    #[ApiProperty(readable: false, writable: false)]
95
    protected ?string $newEmailConfirmationToken = null;
96
97
    #[ApiProperty(readable: false, writable: false)]
98
    #[Groups(['User:output'])]
99
    protected ?\DateTime $newEmailAddressChangeRequestedAt = null;
100
101
    #[ApiProperty(readable: false, writable: false)]
102
    public ?string $plainNewEmailConfirmationToken = null;
103
104
    #[ApiProperty(readable: false, writable: false)]
105
    protected bool $emailAddressVerified = false;
106
107
    /**
108
     * Random string sent to previous email address when email is changed to permit email restore and password change.
109
     */
110
    #[ApiProperty(readable: false, writable: false)]
111
    protected ?string $emailAddressVerifyToken = null;
112
113
    #[ApiProperty(readable: false, writable: false)]
114
    public ?string $plainEmailAddressVerifyToken = null;
115
116
    #[ApiProperty(readable: false, writable: false)]
117
    protected ?\DateTime $emailLastUpdatedAt = null;
118
119
    /**
120
     * `final` to make `createFromPayload` safe. Could instead make an interface? Or abstract and force child to define constructor?
121
     */
122 49
    public function __construct(string $username = '', string $emailAddress = '', bool $emailAddressVerified = false, array $roles = ['ROLE_USER'], string $password = '', bool $enabled = true)
123
    {
124 49
        $this->username = $username;
125 49
        $this->emailAddress = $emailAddress;
126 49
        $this->emailAddressVerified = $emailAddressVerified;
127 49
        $this->roles = $roles;
128 49
        $this->password = $password;
129 49
        $this->enabled = $enabled;
130
    }
131
132 23
    public function getUsername(): string
133
    {
134 23
        return $this->username;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->username could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
135
    }
136
137 16
    public function setUsername(string $username): self
138
    {
139 16
        $this->username = $username;
140
141 16
        return $this;
142
    }
143
144 18
    public function getEmailAddress(): ?string
145
    {
146 18
        return $this->emailAddress;
147
    }
148
149 18
    public function setEmailAddress(?string $emailAddress): self
150
    {
151 18
        $this->emailAddress = $emailAddress;
152 18
        if ($emailAddress) {
153 18
            $this->emailLastUpdatedAt = new \DateTime();
154
        }
155
156 18
        return $this;
157
    }
158
159 4
    public function getRoles(): array
160
    {
161 4
        return $this->roles;
162
    }
163
164 1
    public function setRoles(array $roles): self
165
    {
166 1
        $this->roles = $roles;
167
168 1
        return $this;
169
    }
170
171 6
    public function isEnabled(): bool
172
    {
173 6
        return $this->enabled;
174
    }
175
176 4
    public function setEnabled(bool $enabled): self
177
    {
178 4
        $this->enabled = $enabled;
179
180 4
        return $this;
181
    }
182
183 3
    public function getPassword(): ?string
184
    {
185 3
        return $this->password;
186
    }
187
188 2
    public function setPassword(string $password): self
189
    {
190 2
        $this->password = $password;
191
192 2
        return $this;
193
    }
194
195 1
    public function getPlainPassword(): ?string
196
    {
197 1
        return $this->plainPassword;
198
    }
199
200 1
    public function setPlainPassword(?string $plainPassword): self
201
    {
202 1
        $this->plainPassword = $plainPassword;
203 1
        if ($plainPassword) {
204
            // Needs to update mapped field to trigger update event which will encode the plain password
205 1
            $this->passwordUpdatedAt = new \DateTime();
206
        }
207
208 1
        return $this;
209
    }
210
211 1
    public function getNewPasswordConfirmationToken(): ?string
212
    {
213 1
        return $this->newPasswordConfirmationToken;
214
    }
215
216 1
    public function setNewPasswordConfirmationToken(?string $newPasswordConfirmationToken): self
217
    {
218 1
        $this->newPasswordConfirmationToken = $newPasswordConfirmationToken;
219
220 1
        return $this;
221
    }
222
223 2
    public function getPasswordRequestedAt(): ?\DateTime
224
    {
225 2
        return $this->passwordRequestedAt;
226
    }
227
228 2
    public function setPasswordRequestedAt(?\DateTime $passwordRequestedAt): self
229
    {
230 2
        $this->passwordRequestedAt = $passwordRequestedAt;
231
232 2
        return $this;
233
    }
234
235 1
    public function getOldPassword(): ?string
236
    {
237 1
        return $this->oldPassword;
238
    }
239
240 1
    public function setOldPassword(?string $oldPassword): self
241
    {
242 1
        $this->oldPassword = $oldPassword;
243
244 1
        return $this;
245
    }
246
247 6
    public function getNewEmailAddress(): ?string
248
    {
249 6
        return $this->newEmailAddress;
250
    }
251
252 4
    public function setNewEmailAddress(?string $newEmailAddress): self
253
    {
254 4
        $this->newEmailAddress = $newEmailAddress;
255 4
        if ($newEmailAddress) {
256 4
            $this->newEmailAddressChangeRequestedAt = new \DateTime();
257
        }
258
259 4
        return $this;
260
    }
261
262 1
    public function getNewEmailConfirmationToken(): ?string
263
    {
264 1
        return $this->newEmailConfirmationToken;
265
    }
266
267 1
    public function setNewEmailConfirmationToken(?string $newEmailConfirmationToken): self
268
    {
269 1
        $this->newEmailConfirmationToken = $newEmailConfirmationToken;
270
271 1
        return $this;
272
    }
273
274
    public function getNewEmailAddressChangeRequestedAt(): ?\DateTime
275
    {
276
        return $this->newEmailAddressChangeRequestedAt;
277
    }
278
279 6
    public function isEmailAddressVerified(): bool
280
    {
281 6
        return $this->emailAddressVerified;
282
    }
283
284 5
    public function setEmailAddressVerified(bool $emailAddressVerified): self
285
    {
286 5
        $this->emailAddressVerified = $emailAddressVerified;
287
288 5
        return $this;
289
    }
290
291
    public function getEmailAddressVerifyToken(): ?string
292
    {
293
        return $this->emailAddressVerifyToken;
294
    }
295
296
    public function setEmailAddressVerifyToken(?string $emailAddressVerifyToken): void
297
    {
298
        $this->emailAddressVerifyToken = $emailAddressVerifyToken;
299
    }
300
301 1
    public function isPasswordRequestLimitReached($ttl): bool
302
    {
303 1
        $lastRequest = $this->getPasswordRequestedAt();
304
305 1
        return $lastRequest instanceof \DateTime
306 1
            && $lastRequest->getTimestamp() + $ttl > time();
307
    }
308
309
    /** @see \Serializable::serialize() */
310 1
    public function serialize(): string
311
    {
312 1
        return serialize(
313 1
            [
314 1
                (string) $this->id,
315 1
                $this->username,
316 1
                $this->emailAddress,
317 1
                $this->password,
318 1
                $this->enabled,
319 1
                $this->roles,
320 1
            ]
321 1
        );
322
    }
323
324
    /**
325
     * @see \Serializable::unserialize()
326
     */
327 2
    public function unserialize(string $serialized): self
328
    {
329 2
        $id = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $id is dead and can be removed.
Loading history...
330 2
        [
331 2
            $id,
332 2
            $this->username,
333 2
            $this->emailAddress,
334 2
            $this->password,
335 2
            $this->enabled,
336 2
            $this->roles,
337 2
        ] = unserialize($serialized, ['allowed_classes' => false]);
338 2
        $this->id = Uuid::fromString($id);
339
340 2
        return $this;
341
    }
342
343
    /**
344
     * Not needed - we use bcrypt.
345
     */
346
    #[ApiProperty(readable: false, writable: false)]
347
    public function getSalt(): ?string
348
    {
349
        return null;
350
    }
351
352
    /**
353
     * Remove sensitive data - e.g. plain passwords etc.
354
     */
355 1
    public function eraseCredentials(): void
356
    {
357 1
        $this->plainPassword = null;
358
    }
359
360 1
    public function __toString()
361
    {
362 1
        return (string) $this->id;
363
    }
364
365
    public static function createFromPayload($username, array $payload): JWTUserInterface
366
    {
367
        $newUser = new static(
368
            $username,
369
            $payload['emailAddress'],
370
            $payload['emailAddressVerified'],
371
            $payload['roles']
372
        );
373
374
        $newUser->setNewEmailAddress($payload['newEmailAddress']);
375
376
        $reflection = new \ReflectionClass(static::class);
377
        $idProperty = $reflection->getProperty('id');
378
        $idProperty->setAccessible(true);
379
        $idProperty->setValue($newUser, Uuid::fromString($payload['id']));
380
381
        return $newUser;
382
    }
383
384
    public function getUserIdentifier(): string
385
    {
386
        return $this->getUsername();
387
    }
388
}
389