SqlUserRepository::rolesToArray()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
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\Infrastructure\Persistence;
14
15
use BenGorUser\User\Domain\Model\User;
16
use BenGorUser\User\Domain\Model\UserEmail;
17
use BenGorUser\User\Domain\Model\UserId;
18
use BenGorUser\User\Domain\Model\UserPassword;
19
use BenGorUser\User\Domain\Model\UserRepository;
20
use BenGorUser\User\Domain\Model\UserRole;
21
use BenGorUser\User\Domain\Model\UserToken;
22
use BenGorUser\User\Infrastructure\Domain\Model\UserEventBus;
23
24
/**
25
 * Sql user repository class.
26
 *
27
 * @author Beñat Espiña <[email protected]>
28
 * @author Gorka Laucirica <[email protected]>
29
 */
30
final class SqlUserRepository implements UserRepository
31
{
32
    const DATE_FORMAT = 'Y-m-d H:i:s';
33
34
    /**
35
     * The pdo instance.
36
     *
37
     * @var \PDO
38
     */
39
    private $pdo;
40
41
    /**
42
     * The user event bus, it can be null.
43
     *
44
     * @var UserEventBus|null
45
     */
46
    private $eventBus;
47
48
    /**
49
     * Constructor.
50
     *
51
     * @param \PDO              $aPdo       The pdo instance
52
     * @param UserEventBus|null $anEventBus The user event bus, it can be null
53
     */
54
    public function __construct(\PDO $aPdo, UserEventBus $anEventBus = null)
55
    {
56
        $this->pdo = $aPdo;
57
        $this->eventBus = $anEventBus;
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 View Code Duplication
    public function userOfId(UserId $anId)
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...
64
    {
65
        $statement = $this->execute('SELECT * FROM user WHERE id = :id', ['id' => $anId->id()]);
66
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
67
            return $this->buildUser($row);
68
        }
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 View Code Duplication
    public function all()
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...
75
    {
76
        $statement = $this->execute('SELECT * FROM user', []);
77
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
78
            return $this->buildUser($row);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->buildUser($row); (BenGorUser\User\Domain\Model\User) is incompatible with the return type declared by the interface BenGorUser\User\Domain\Model\UserRepository::all of type BenGorUser\User\Domain\Model\User[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
79
        }
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 View Code Duplication
    public function userOfEmail(UserEmail $anEmail)
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...
86
    {
87
        $statement = $this->execute('SELECT * FROM user WHERE email = :email', ['email' => $anEmail->email()]);
88
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
89
            return $this->buildUser($row);
90
        }
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96 View Code Duplication
    public function userOfConfirmationToken(UserToken $aConfirmationToken)
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...
97
    {
98
        $statement = $this->execute('SELECT * FROM user WHERE confirmation_token_token = :confirmationToken', [
99
            'confirmationToken' => $aConfirmationToken->token(),
100
        ]);
101
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
102
            return $this->buildUser($row);
103
        }
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 View Code Duplication
    public function userOfInvitationToken(UserToken $anInvitationToken)
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...
110
    {
111
        $statement = $this->execute('SELECT * FROM user WHERE invitation_token_token = :invitationToken', [
112
            'invitationToken' => $anInvitationToken->token(),
113
        ]);
114
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
115
            return $this->buildUser($row);
116
        }
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 View Code Duplication
    public function userOfRememberPasswordToken(UserToken $aRememberPasswordToken)
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...
123
    {
124
        $statement = $this->execute('SELECT * FROM user WHERE remember_password_token_token = :rememberPasswordToken', [
125
            'rememberPasswordToken' => $aRememberPasswordToken->token(),
126
        ]);
127
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
128
            return $this->buildUser($row);
129
        }
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function persist(User $aUser)
136
    {
137
        ($this->exist($aUser)) ? $this->update($aUser) : $this->insert($aUser);
138
139
        if ($this->eventBus instanceof UserEventBus) {
140
            $this->handle($aUser->events());
141
        }
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147
    public function remove(User $aUser)
148
    {
149
        $this->execute('DELETE FROM user WHERE id = :id', ['id' => $aUser->id()->id()]);
150
151
        if ($this->eventBus instanceof UserEventBus) {
152
            $this->handle($aUser->events());
153
        }
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159
    public function size()
160
    {
161
        return $this->pdo->query('SELECT COUNT(*) FROM user')->fetchColumn();
162
    }
163
164
    /**
165
     * Loads the user schema into database create the table
166
     * with user attribute properties as columns.
167
     */
168
    public function initSchema()
169
    {
170
        $this->pdo->exec(<<<'SQL'
171
DROP TABLE IF EXISTS user;
172
CREATE TABLE user (
173
    id CHAR(36) PRIMARY KEY,
174
    confirmation_token_token VARCHAR(36),
175
    confirmation_token_created_on DATETIME,
176
    created_on DATETIME NOT NULL,
177
    email VARCHAR(36) NOT NULL,
178
    invitation_token_token VARCHAR(36),
179
    invitation_token_created_on DATETIME,
180
    last_login DATETIME,
181
    password VARCHAR(30),
182
    remember_password_token_token VARCHAR(36),
183
    remember_password_token_created_on DATETIME,
184
    roles LONGTEXT NOT NULL COMMENT '(DC2Type:user_roles)',
185
    updated_on DATETIME NOT NULL
186
)
187
SQL
188
        );
189
    }
190
191
    /**
192
     * Checks if the user given exists in the database.
193
     *
194
     * @param User $aUser The user
195
     *
196
     * @return bool
197
     */
198
    private function exist(User $aUser)
199
    {
200
        $count = $this->execute(
201
            'SELECT COUNT(*) FROM user WHERE id = :id', [':id' => $aUser->id()->id()]
202
        )->fetchColumn();
203
204
        return (int) $count === 1;
205
    }
206
207
    /**
208
     * Prepares the insert SQL with the user given.
209
     *
210
     * @param User $aUser The user
211
     */
212
    private function insert(User $aUser)
213
    {
214
        $sql = 'INSERT INTO user (
215
            id,
216
            confirmation_token_token,
217
            confirmation_token_created_on,
218
            created_on,
219
            email,
220
            invitation_token_token,
221
            invitation_token_created_on,
222
            last_login,
223
            password,
224
            salt,
225
            remember_password_token_token,
226
            remember_password_token_created_on,
227
            roles,
228
            updated_on
229
        ) VALUES (
230
            :id,
231
            :confirmationTokenToken,
232
            :confirmationTokenCreatedOn,
233
            :createdOn,
234
            :email,
235
            :invitationTokenToken,
236
            :invitationTokenCreatedOn,
237
            :lastLogin,
238
            :password,
239
            :salt,
240
            :rememberPasswordTokenToken,
241
            :rememberPasswordTokenCreatedOn,
242
            :roles,
243
            :updatedOn
244
        )';
245
        $this->execute($sql, [
246
            'id'                             => $aUser->id()->id(),
247
            'confirmationTokenToken'         => $aUser->confirmationToken() ? $aUser->confirmationToken()->token() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 122 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
248
            'confirmationTokenCreatedOn'     => $aUser->confirmationToken() ? $aUser->confirmationToken()->createdOn() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 126 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
249
            'createdOn'                      => $aUser->createdOn()->format(self::DATE_FORMAT),
250
            'email'                          => $aUser->email()->email(),
251
            'invitationTokenToken'           => $aUser->invitationToken() ? $aUser->invitationToken()->token() : null,
252
            'invitationTokenCreatedOn'       => $aUser->invitationToken() ? $aUser->invitationToken()->createdOn() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 122 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
253
            'lastLogin'                      => $aUser->lastLogin() ? $aUser->lastLogin()->format(self::DATE_FORMAT) : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 124 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
254
            'password'                       => $aUser->password()->encodedPassword(),
255
            'salt'                           => $aUser->password()->salt(),
256
            'rememberPasswordTokenToken'     => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->token() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 130 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
257
            'rememberPasswordTokenCreatedOn' => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->createdOn() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 134 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
258
            'roles'                          => $this->rolesToString($aUser->roles()),
259
            'updatedOn'                      => $aUser->updatedOn()->format(self::DATE_FORMAT),
260
        ]);
261
    }
262
263
    /**
264
     * Prepares the update SQL with the user given.
265
     *
266
     * @param User $aUser The user
267
     */
268
    private function update(User $aUser)
269
    {
270
        $sql = 'UPDATE user SET
271
            confirmation_token_token = :confirmationTokenToken,
272
            confirmation_token_created_on = :confirmationTokenCreatedOn,
273
            invitation_token_token = :invitationTokenToken,
274
            invitation_token_created_on = :invitationTokenCreatedOn,
275
            last_login = :lastLogin,
276
            password = :password,
277
            remember_password_token_token = :rememberPasswordTokenToken,
278
            remember_password_token_created_on = :rememberPasswordTokenCreatedOn,
279
            roles = :roles,
280
            updated_on = :updatedOn
281
            WHERE id = :id';
282
        $this->execute($sql, [
283
            'id'                             => $aUser->id()->id(),
284
            'confirmationTokenToken'         => $aUser->confirmationToken() ? $aUser->confirmationToken()->token() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 122 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
285
            'confirmationTokenCreatedOn'     => $aUser->confirmationToken() ? $aUser->confirmationToken()->createdOn() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 126 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
286
            'invitationTokenToken'           => $aUser->invitationToken() ? $aUser->invitationToken()->token() : null,
287
            'invitationTokenCreatedOn'       => $aUser->invitationToken() ? $aUser->invitationToken()->createdOn() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 122 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
288
            'lastLogin'                      => $aUser->lastLogin() ? $aUser->lastLogin()->format(self::DATE_FORMAT) : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 124 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
289
            'password'                       => $aUser->password()->encodedPassword(),
290
            'rememberPasswordTokenToken'     => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->token() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 130 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
291
            'rememberPasswordTokenCreatedOn' => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->createdOn() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 134 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
292
            'roles'                          => $this->rolesToString($aUser->roles()),
293
            'updatedOn'                      => $aUser->updatedOn()->format(self::DATE_FORMAT),
294
        ]);
295
    }
296
297
    /**
298
     * Wrapper that encapsulates the same
299
     * logic about execute the query in PDO.
300
     *
301
     * @param string $aSql       The SQL
302
     * @param array  $parameters Array which contains the parameters of SQL
303
     *
304
     * @return \PDOStatement
305
     */
306
    private function execute($aSql, array $parameters)
307
    {
308
        $statement = $this->pdo->prepare($aSql);
309
        $statement->execute($parameters);
310
311
        return $statement;
312
    }
313
314
    /**
315
     * Builds the user with the given sql row attributes.
316
     *
317
     * @param array $row Array which contains attributes of user
318
     *
319
     * @return User
320
     */
321
    private function buildUser($row)
322
    {
323
        $createdOn = new \DateTimeImmutable($row['created_on']);
324
        $updatedOn = new \DateTimeImmutable($row['updated_on']);
325
        $lastLogin = null === $row['last_login']
326
            ? null
327
            : new \DateTimeImmutable($row['last_login']);
328
329
        $confirmationToken = null;
330 View Code Duplication
        if (null !== $row['confirmation_token_token']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
331
            $confirmationToken = new UserToken($row['confirmation_token_token']);
332
            $this->set($confirmationToken, 'createdOn', new \DateTimeImmutable($row['confirmation_token_created_on']));
333
        }
334
        $invitationToken = null;
335 View Code Duplication
        if (null !== $row['invitation_token_token']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
336
            $invitationToken = new UserToken($row['invitation_token_token']);
337
            $this->set($invitationToken, 'createdOn', new \DateTimeImmutable($row['invitation_token_created_on']));
338
        }
339
        $rememberPasswordToken = null;
340 View Code Duplication
        if (null !== $row['remember_password_token_token']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
341
            $rememberPasswordToken = new UserToken($row['remember_password_token_token']);
342
            $this->set($rememberPasswordToken, 'createdOn', new \DateTimeImmutable($row['remember_password_token_created_on']));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
343
        }
344
345
        $user = User::signUp(
346
            new UserId($row['id']),
347
            new UserEmail($row['email']),
348
            UserPassword::fromEncoded($row['password'], $row['salt']),
349
            $this->rolesToArray($row['roles'])
350
        );
351
352
        $user = $this->set($user, 'createdOn', $createdOn);
353
        $user = $this->set($user, 'updatedOn', $updatedOn);
354
        $user = $this->set($user, 'lastLogin', $lastLogin);
355
        $user = $this->set($user, 'confirmationToken', $confirmationToken);
356
        $user = $this->set($user, 'invitationToken', $invitationToken);
357
        $user = $this->set($user, 'rememberPasswordToken', $rememberPasswordToken);
358
359
        return $user;
360
    }
361
362
    /**
363
     * Transforms given user roles into encoded plain json array.
364
     *
365
     * @param array $userRoles Array which contains the user roles
366
     *
367
     * @return string
368
     */
369
    private function rolesToString(array $userRoles)
370
    {
371
        return json_encode(
372
            array_map(function (UserRole $userRole) {
373
                return $userRole->role();
374
            }, $userRoles)
375
        );
376
    }
377
378
    /**
379
     * Transforms given user roles encoded array into user roles collection.
380
     *
381
     * @param array $userRoles Encoded json array
382
     *
383
     * @return UserRole[]
384
     */
385
    private function rolesToArray($userRoles)
386
    {
387
        return array_map(function ($userRole) {
388
            return new UserRole($userRole);
389
        }, json_decode($userRoles));
390
    }
391
392
    /**
393
     * Populates by Reflection the domain object with the given SQL plain values.
394
     *
395
     * @param object $object        The domain object
396
     * @param string $propertyName  The property name
397
     * @param mixed  $propertyValue The property value
398
     *
399
     * @return object
400
     */
401
    private function set($object, $propertyName, $propertyValue)
402
    {
403
        $reflectionUser = new \ReflectionClass($object);
404
405
        $reflectionProperty = $reflectionUser->getProperty($propertyName);
406
        $reflectionProperty->setAccessible(true);
407
        $reflectionProperty->setValue($object, $propertyValue);
408
409
        return $object;
410
    }
411
412
    /**
413
     * Handles the given events with event bus.
414
     *
415
     * @param array $events A collection of user domain events
416
     */
417
    private function handle($events)
418
    {
419
        foreach ($events as $event) {
420
            $this->eventBus->handle($event);
421
        }
422
    }
423
}
424