Completed
Push — master ( aa536b...5904f4 )
by Beñat
02:33
created

SqlUserRepository::handle()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 2
eloc 3
nc 2
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 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...
75
    {
76
        $statement = $this->execute('SELECT * FROM user WHERE email = :email', ['email' => $anEmail->email()]);
77
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
78
            return $this->buildUser($row);
79
        }
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 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...
86
    {
87
        $statement = $this->execute('SELECT * FROM user WHERE confirmation_token = :confirmationToken', [
88
            'confirmationToken' => $aConfirmationToken->token(),
89
        ]);
90
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
91
            return $this->buildUser($row);
92
        }
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 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...
99
    {
100
        $statement = $this->execute('SELECT * FROM user WHERE invitation_token = :invitationToken', [
101
            'invitationToken' => $anInvitationToken->token(),
102
        ]);
103
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
104
            return $this->buildUser($row);
105
        }
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 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...
112
    {
113
        $statement = $this->execute('SELECT * FROM user WHERE remember_password_token = :rememberPasswordToken', [
114
            'rememberPasswordToken' => $aRememberPasswordToken->token(),
115
        ]);
116
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
117
            return $this->buildUser($row);
118
        }
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function persist(User $aUser)
125
    {
126
        ($this->exist($aUser)) ? $this->update($aUser) : $this->insert($aUser);
127
128
        if ($this->eventBus instanceof UserEventBus) {
129
            $this->handle($aUser->events());
130
        }
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function remove(User $aUser)
137
    {
138
        $this->execute('DELETE FROM user WHERE id = :id', ['id' => $aUser->id()->id()]);
139
140
        if ($this->eventBus instanceof UserEventBus) {
141
            $this->handle($aUser->events());
142
        }
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    public function size()
149
    {
150
        return $this->pdo->query('SELECT COUNT(*) FROM user')->fetchColumn();
151
    }
152
153
    /**
154
     * Loads the user schema into database create the table
155
     * with user attribute properties as columns.
156
     */
157
    public function initSchema()
158
    {
159
        $this->pdo->exec(<<<SQL
160
DROP TABLE IF EXISTS user;
161
CREATE TABLE user (
162
    id CHAR(36) PRIMARY KEY,
163
    confirmation_token VARCHAR(36),
164
    created_on DATETIME NOT NULL,
165
    email VARCHAR(36) NOT NULL,
166
    invitation_token VARCHAR(36),
167
    last_login DATETIME,
168
    password VARCHAR(30),
169
    remember_password_token VARCHAR(36),
170
    roles LONGTEXT NOT NULL COMMENT '(DC2Type:user_roles)',
171
    updated_on DATETIME NOT NULL
172
)
173
SQL
174
        );
175
    }
176
177
    /**
178
     * Checks if the user given exists in the database.
179
     *
180
     * @param User $aUser The user
181
     *
182
     * @return bool
183
     */
184
    private function exist(User $aUser)
185
    {
186
        $count = $this->execute(
187
            'SELECT COUNT(*) FROM user WHERE id = :id', [':id' => $aUser->id()->id()]
188
        )->fetchColumn();
189
190
        return (int)$count === 1;
191
    }
192
193
    /**
194
     * Prepares the insert SQL with the user given.
195
     *
196
     * @param User $aUser The user
197
     */
198
    private function insert(User $aUser)
199
    {
200
        $sql = 'INSERT INTO user (
201
            id,
202
            confirmation_token,
203
            created_on,
204
            email,
205
            invitation_token,
206
            last_login,
207
            password,
208
            salt,
209
            remember_password_token,
210
            roles,
211
            updated_on
212
        ) VALUES (
213
            :id,
214
            :token,
215
            :createdOn,
216
            :email,
217
            :invitationToken,
218
            :lastLogin,
219
            :password,
220
            :salt,
221
            :rememberPasswordToken,
222
            :roles,
223
            :updatedOn
224
        )';
225
        $this->execute($sql, [
226
            'id'                    => $aUser->id()->id(),
227
            'token'                 => $aUser->confirmationToken(),
228
            'createdOn'             => $aUser->createdOn()->format(self::DATE_FORMAT),
229
            'email'                 => $aUser->email()->email(),
230
            'invitationToken'       => $aUser->invitationToken() ? $aUser->invitationToken()->token() : null,
231
            'lastLogin'             => $aUser->lastLogin() ? $aUser->lastLogin()->format(self::DATE_FORMAT) : null,
232
            'password'              => $aUser->password()->encodedPassword(),
233
            'salt'                  => $aUser->password()->salt(),
234
            'rememberPasswordToken' => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->token() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 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...
235
            'roles'                 => $this->rolesToString($aUser->roles()),
236
            'updatedOn'             => $aUser->updatedOn()->format(self::DATE_FORMAT),
237
        ]);
238
    }
239
240
    /**
241
     * Prepares the update SQL with the user given.
242
     *
243
     * @param User $aUser The user
244
     */
245
    private function update(User $aUser)
246
    {
247
        $sql = 'UPDATE user SET
248
            confirmation_token = :confirmationToken,
249
            invitation_token = :invitationToken,
250
            last_login = :lastLogin,
251
            password = :password,
252
            remember_password_token = :rememberPasswordToken,
253
            roles = :roles,
254
            updated_on = :updatedOn
255
            WHERE id = :id';
256
        $this->execute($sql, [
257
            'id'                    => $aUser->id()->id(),
258
            'confirmationToken'     => $aUser->confirmationToken() ? $aUser->confirmationToken()->token() : null,
259
            'invitationToken'       => $aUser->invitationToken() ? $aUser->invitationToken()->token() : null,
260
            'lastLogin'             => $aUser->lastLogin() ? $aUser->lastLogin()->format(self::DATE_FORMAT) : null,
261
            'password'              => $aUser->password()->encodedPassword(),
262
            'rememberPasswordToken' => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->token() : null,
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 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...
263
            'roles'                 => $this->rolesToString($aUser->roles()),
264
            'updatedOn'             => $aUser->updatedOn()->format(self::DATE_FORMAT),
265
        ]);
266
    }
267
268
    /**
269
     * Wrapper that encapsulates the same
270
     * logic about execute the query in PDO.
271
     *
272
     * @param string $aSql       The SQL
273
     * @param array  $parameters Array which contains the parameters of SQL
274
     *
275
     * @return \PDOStatement
276
     */
277
    private function execute($aSql, array $parameters)
278
    {
279
        $statement = $this->pdo->prepare($aSql);
280
        $statement->execute($parameters);
281
282
        return $statement;
283
    }
284
285
    /**
286
     * Builds the user with the given sql row attributes.
287
     *
288
     * @param array $row Array which contains attributes of user
289
     *
290
     * @return User
291
     */
292
    private function buildUser($row)
293
    {
294
        $createdOn = new \DateTimeImmutable($row['created_on']);
295
        $updatedOn = new \DateTimeImmutable($row['updated_on']);
296
        $lastLogin = null === $row['last_login']
297
            ? null
298
            : new \DateTimeImmutable($row['last_login']);
299
        $confirmationToken = null === $row['confirmation_token']
300
            ? null
301
            : new UserToken($row['confirmation_token']);
302
        $invitationToken = null === $row['invitation_token']
303
            ? null
304
            : new UserToken($row['invitation_token']);
305
        $rememberPasswordToken = null === $row['remember_password_token']
306
            ? null
307
            : new UserToken($row['remember_password_token']);
308
309
        $user = User::signUp(
310
            new UserId($row['id']),
311
            new UserEmail($row['email']),
312
            UserPassword::fromEncoded($row['password'], $row['salt']),
313
            $this->rolesToArray($row['roles'])
314
        );
315
316
        $user = $this->set($user, 'createdOn', $createdOn);
317
        $user = $this->set($user, 'updatedOn', $updatedOn);
318
        $user = $this->set($user, 'lastLogin', $lastLogin);
319
        $user = $this->set($user, 'confirmationToken', $confirmationToken);
320
        $user = $this->set($user, 'invitationToken', $invitationToken);
321
        $user = $this->set($user, 'rememberPasswordToken', $rememberPasswordToken);
322
323
        return $user;
324
    }
325
326
    /**
327
     * Transforms given user roles into encoded plain json array.
328
     *
329
     * @param array $userRoles Array which contains the user roles
330
     *
331
     * @return string
332
     */
333
    private function rolesToString(array $userRoles)
334
    {
335
        return json_encode(
336
            array_map(function (UserRole $userRole) {
337
                return $userRole->role();
338
            }, $userRoles)
339
        );
340
    }
341
342
    /**
343
     * Transforms given user roles encoded array into user roles collection.
344
     *
345
     * @param array $userRoles Encoded json array
346
     *
347
     * @return UserRole[]
348
     */
349
    private function rolesToArray($userRoles)
350
    {
351
        return array_map(function ($userRole) {
352
            return new UserRole($userRole);
353
        }, json_decode($userRoles));
354
    }
355
356
    /**
357
     * Populates by Reflection the domain object with the given SQL plain values.
358
     *
359
     * @param User   $user          The user domain object
360
     * @param string $propertyName  The property name
361
     * @param mixed  $propertyValue The property value
362
     *
363
     * @return User
364
     */
365
    private function set(User $user, $propertyName, $propertyValue)
366
    {
367
        $reflectionUser = new \ReflectionClass($user);
368
369
        $reflectionCreatedOn = $reflectionUser->getProperty($propertyName);
370
        $reflectionCreatedOn->setAccessible(true);
371
        $reflectionCreatedOn->setValue($user, $propertyValue);
372
373
        return $user;
374
    }
375
376
    /**
377
     * Handles the given events with event bus.
378
     *
379
     * @param array $events A collection of user domain events
380
     */
381
    private function handle($events)
382
    {
383
        foreach ($events as $event) {
384
            $this->eventBus->handle($event);
385
        }
386
    }
387
}
388