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) |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
86
|
|
|
{ |
87
|
|
|
$statement = $this->execute('SELECT * FROM user WHERE confirmation_token_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) |
|
|
|
|
99
|
|
|
{ |
100
|
|
|
$statement = $this->execute('SELECT * FROM user WHERE invitation_token_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) |
|
|
|
|
112
|
|
|
{ |
113
|
|
|
$statement = $this->execute('SELECT * FROM user WHERE remember_password_token_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_token VARCHAR(36), |
164
|
|
|
confirmation_token_created_on DATETIME, |
165
|
|
|
created_on DATETIME NOT NULL, |
166
|
|
|
email VARCHAR(36) NOT NULL, |
167
|
|
|
invitation_token_token VARCHAR(36), |
168
|
|
|
invitation_token_created_on DATETIME, |
169
|
|
|
last_login DATETIME, |
170
|
|
|
password VARCHAR(30), |
171
|
|
|
remember_password_token_token VARCHAR(36), |
172
|
|
|
remember_password_token_created_on DATETIME, |
173
|
|
|
roles LONGTEXT NOT NULL COMMENT '(DC2Type:user_roles)', |
174
|
|
|
updated_on DATETIME NOT NULL |
175
|
|
|
) |
176
|
|
|
SQL |
177
|
|
|
); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Checks if the user given exists in the database. |
182
|
|
|
* |
183
|
|
|
* @param User $aUser The user |
184
|
|
|
* |
185
|
|
|
* @return bool |
186
|
|
|
*/ |
187
|
|
|
private function exist(User $aUser) |
188
|
|
|
{ |
189
|
|
|
$count = $this->execute( |
190
|
|
|
'SELECT COUNT(*) FROM user WHERE id = :id', [':id' => $aUser->id()->id()] |
191
|
|
|
)->fetchColumn(); |
192
|
|
|
|
193
|
|
|
return (int) $count === 1; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Prepares the insert SQL with the user given. |
198
|
|
|
* |
199
|
|
|
* @param User $aUser The user |
200
|
|
|
*/ |
201
|
|
|
private function insert(User $aUser) |
202
|
|
|
{ |
203
|
|
|
$sql = 'INSERT INTO user ( |
204
|
|
|
id, |
205
|
|
|
confirmation_token_token, |
206
|
|
|
confirmation_token_created_on, |
207
|
|
|
created_on, |
208
|
|
|
email, |
209
|
|
|
invitation_token_token, |
210
|
|
|
invitation_token_created_on, |
211
|
|
|
last_login, |
212
|
|
|
password, |
213
|
|
|
salt, |
214
|
|
|
remember_password_token_token, |
215
|
|
|
remember_password_token_created_on, |
216
|
|
|
roles, |
217
|
|
|
updated_on |
218
|
|
|
) VALUES ( |
219
|
|
|
:id, |
220
|
|
|
:confirmationTokenToken, |
221
|
|
|
:confirmationTokenCreatedOn, |
222
|
|
|
:createdOn, |
223
|
|
|
:email, |
224
|
|
|
:invitationTokenToken, |
225
|
|
|
:invitationTokenCreatedOn, |
226
|
|
|
:lastLogin, |
227
|
|
|
:password, |
228
|
|
|
:salt, |
229
|
|
|
:rememberPasswordTokenToken, |
230
|
|
|
:rememberPasswordTokenCreatedOn, |
231
|
|
|
:roles, |
232
|
|
|
:updatedOn |
233
|
|
|
)'; |
234
|
|
|
$this->execute($sql, [ |
235
|
|
|
'id' => $aUser->id()->id(), |
236
|
|
|
'confirmationTokenToken' => $aUser->confirmationToken() ? $aUser->confirmationToken()->token() : null, |
|
|
|
|
237
|
|
|
'confirmationTokenCreatedOn' => $aUser->confirmationToken() ? $aUser->confirmationToken()->createdOn() : null, |
|
|
|
|
238
|
|
|
'createdOn' => $aUser->createdOn()->format(self::DATE_FORMAT), |
239
|
|
|
'email' => $aUser->email()->email(), |
240
|
|
|
'invitationTokenToken' => $aUser->invitationToken() ? $aUser->invitationToken()->token() : null, |
241
|
|
|
'invitationTokenCreatedOn' => $aUser->invitationToken() ? $aUser->invitationToken()->createdOn() : null, |
|
|
|
|
242
|
|
|
'lastLogin' => $aUser->lastLogin() ? $aUser->lastLogin()->format(self::DATE_FORMAT) : null, |
|
|
|
|
243
|
|
|
'password' => $aUser->password()->encodedPassword(), |
244
|
|
|
'salt' => $aUser->password()->salt(), |
245
|
|
|
'rememberPasswordTokenToken' => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->token() : null, |
|
|
|
|
246
|
|
|
'rememberPasswordTokenCreatedOn' => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->createdOn() : null, |
|
|
|
|
247
|
|
|
'roles' => $this->rolesToString($aUser->roles()), |
248
|
|
|
'updatedOn' => $aUser->updatedOn()->format(self::DATE_FORMAT), |
249
|
|
|
]); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Prepares the update SQL with the user given. |
254
|
|
|
* |
255
|
|
|
* @param User $aUser The user |
256
|
|
|
*/ |
257
|
|
|
private function update(User $aUser) |
258
|
|
|
{ |
259
|
|
|
$sql = 'UPDATE user SET |
260
|
|
|
confirmation_token_token = :confirmationTokenToken, |
261
|
|
|
confirmation_token_created_on = :confirmationTokenCreatedOn, |
262
|
|
|
invitation_token_token = :invitationTokenToken, |
263
|
|
|
invitation_token_created_on = :invitationTokenCreatedOn, |
264
|
|
|
last_login = :lastLogin, |
265
|
|
|
password = :password, |
266
|
|
|
remember_password_token_token = :rememberPasswordTokenToken, |
267
|
|
|
remember_password_token_created_on = :rememberPasswordTokenCreatedOn, |
268
|
|
|
roles = :roles, |
269
|
|
|
updated_on = :updatedOn |
270
|
|
|
WHERE id = :id'; |
271
|
|
|
$this->execute($sql, [ |
272
|
|
|
'id' => $aUser->id()->id(), |
273
|
|
|
'confirmationTokenToken' => $aUser->confirmationToken() ? $aUser->confirmationToken()->token() : null, |
|
|
|
|
274
|
|
|
'confirmationTokenCreatedOn' => $aUser->confirmationToken() ? $aUser->confirmationToken()->createdOn() : null, |
|
|
|
|
275
|
|
|
'invitationTokenToken' => $aUser->invitationToken() ? $aUser->invitationToken()->token() : null, |
276
|
|
|
'invitationTokenCreatedOn' => $aUser->invitationToken() ? $aUser->invitationToken()->createdOn() : null, |
|
|
|
|
277
|
|
|
'lastLogin' => $aUser->lastLogin() ? $aUser->lastLogin()->format(self::DATE_FORMAT) : null, |
|
|
|
|
278
|
|
|
'password' => $aUser->password()->encodedPassword(), |
279
|
|
|
'rememberPasswordTokenToken' => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->token() : null, |
|
|
|
|
280
|
|
|
'rememberPasswordTokenCreatedOn' => $aUser->rememberPasswordToken() ? $aUser->rememberPasswordToken()->createdOn() : null, |
|
|
|
|
281
|
|
|
'roles' => $this->rolesToString($aUser->roles()), |
282
|
|
|
'updatedOn' => $aUser->updatedOn()->format(self::DATE_FORMAT), |
283
|
|
|
]); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* Wrapper that encapsulates the same |
288
|
|
|
* logic about execute the query in PDO. |
289
|
|
|
* |
290
|
|
|
* @param string $aSql The SQL |
291
|
|
|
* @param array $parameters Array which contains the parameters of SQL |
292
|
|
|
* |
293
|
|
|
* @return \PDOStatement |
294
|
|
|
*/ |
295
|
|
|
private function execute($aSql, array $parameters) |
296
|
|
|
{ |
297
|
|
|
$statement = $this->pdo->prepare($aSql); |
298
|
|
|
$statement->execute($parameters); |
299
|
|
|
|
300
|
|
|
return $statement; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Builds the user with the given sql row attributes. |
305
|
|
|
* |
306
|
|
|
* @param array $row Array which contains attributes of user |
307
|
|
|
* |
308
|
|
|
* @return User |
309
|
|
|
*/ |
310
|
|
|
private function buildUser($row) |
311
|
|
|
{ |
312
|
|
|
$createdOn = new \DateTimeImmutable($row['created_on']); |
313
|
|
|
$updatedOn = new \DateTimeImmutable($row['updated_on']); |
314
|
|
|
$lastLogin = null === $row['last_login'] |
315
|
|
|
? null |
316
|
|
|
: new \DateTimeImmutable($row['last_login']); |
317
|
|
|
|
318
|
|
|
$confirmationToken = null; |
319
|
|
|
if (null !== $row['confirmation_token_token']) { |
320
|
|
|
$confirmationToken = new UserToken($row['confirmation_token_token']); |
321
|
|
|
$this->set($confirmationToken, 'createdOn', $row['confirmation_token_created_on']); |
322
|
|
|
} |
323
|
|
|
$invitationToken = null; |
324
|
|
|
if (null !== $row['invitation_token_token']) { |
325
|
|
|
$invitationToken = new UserToken($row['invitation_token_token']); |
326
|
|
|
$this->set($invitationToken, 'createdOn', $row['invitation_token_created_on']); |
327
|
|
|
} |
328
|
|
|
$rememberPasswordToken = null; |
329
|
|
|
if (null !== $row['remember_password_token_token']) { |
330
|
|
|
$rememberPasswordToken = new UserToken($row['remember_password_token_token']); |
331
|
|
|
$this->set($rememberPasswordToken, 'createdOn', $row['remember_password_token_created_on']); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
$user = User::signUp( |
335
|
|
|
new UserId($row['id']), |
336
|
|
|
new UserEmail($row['email']), |
337
|
|
|
UserPassword::fromEncoded($row['password'], $row['salt']), |
338
|
|
|
$this->rolesToArray($row['roles']) |
339
|
|
|
); |
340
|
|
|
|
341
|
|
|
$user = $this->set($user, 'createdOn', $createdOn); |
342
|
|
|
$user = $this->set($user, 'updatedOn', $updatedOn); |
343
|
|
|
$user = $this->set($user, 'lastLogin', $lastLogin); |
344
|
|
|
$user = $this->set($user, 'confirmationToken', $confirmationToken); |
345
|
|
|
$user = $this->set($user, 'invitationToken', $invitationToken); |
346
|
|
|
$user = $this->set($user, 'rememberPasswordToken', $rememberPasswordToken); |
347
|
|
|
|
348
|
|
|
return $user; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Transforms given user roles into encoded plain json array. |
353
|
|
|
* |
354
|
|
|
* @param array $userRoles Array which contains the user roles |
355
|
|
|
* |
356
|
|
|
* @return string |
357
|
|
|
*/ |
358
|
|
|
private function rolesToString(array $userRoles) |
359
|
|
|
{ |
360
|
|
|
return json_encode( |
361
|
|
|
array_map(function (UserRole $userRole) { |
362
|
|
|
return $userRole->role(); |
363
|
|
|
}, $userRoles) |
364
|
|
|
); |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Transforms given user roles encoded array into user roles collection. |
369
|
|
|
* |
370
|
|
|
* @param array $userRoles Encoded json array |
371
|
|
|
* |
372
|
|
|
* @return UserRole[] |
373
|
|
|
*/ |
374
|
|
|
private function rolesToArray($userRoles) |
375
|
|
|
{ |
376
|
|
|
return array_map(function ($userRole) { |
377
|
|
|
return new UserRole($userRole); |
378
|
|
|
}, json_decode($userRoles)); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Populates by Reflection the domain object with the given SQL plain values. |
383
|
|
|
* |
384
|
|
|
* @param object $object The domain object |
385
|
|
|
* @param string $propertyName The property name |
386
|
|
|
* @param mixed $propertyValue The property value |
387
|
|
|
* |
388
|
|
|
* @return object |
389
|
|
|
*/ |
390
|
|
|
private function set($object, $propertyName, $propertyValue) |
391
|
|
|
{ |
392
|
|
|
$reflectionUser = new \ReflectionClass($object); |
393
|
|
|
|
394
|
|
|
$reflectionCreatedOn = $reflectionUser->getProperty($propertyName); |
395
|
|
|
$reflectionCreatedOn->setAccessible(true); |
396
|
|
|
$reflectionCreatedOn->setValue($object, $propertyValue); |
397
|
|
|
|
398
|
|
|
return $object; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* Handles the given events with event bus. |
403
|
|
|
* |
404
|
|
|
* @param array $events A collection of user domain events |
405
|
|
|
*/ |
406
|
|
|
private function handle($events) |
407
|
|
|
{ |
408
|
|
|
foreach ($events as $event) { |
409
|
|
|
$this->eventBus->handle($event); |
410
|
|
|
} |
411
|
|
|
} |
412
|
|
|
} |
413
|
|
|
|
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.