Skip to content

Commit 93f607f

Browse files
authored
Merge pull request #275 from kenjis/fix-usermodel-save
fix: UserModel::save() can't update User's email/password
2 parents 951ca47 + 1ecdbdd commit 93f607f

File tree

8 files changed

+347
-63
lines changed

8 files changed

+347
-63
lines changed

docs/quickstart.md

+6-25
Original file line numberDiff line numberDiff line change
@@ -265,21 +265,20 @@ Since Shield uses a more complex user setup than many other systems, due to the
265265

266266
### Creating Users
267267

268-
By default, the only values stored in a user is the username. The first step is to create the user record with the username. If you don't have a username, be sure to set the value to `null` anyway, so that it passes CodeIgniter's empty data check.
268+
By default, the only values stored in the users table is the username. The first step is to create the user record with the username. If you don't have a username, be sure to set the value to `null` anyway, so that it passes CodeIgniter's empty data check.
269269

270270
```php
271271
$users = model('UserModel');
272272
$user = new User([
273-
'username' => 'foo-bar'
273+
'username' => 'foo-bar',
274+
'email' => '[email protected]',
275+
'password' => 'secret plain text password',
274276
]);
275277
$users->save($user);
276278

277-
// Get the updated user so we have the ID...
279+
// To get the complete user object with ID, we need to get from the database
278280
$user = $users->findById($users->getInsertID());
279281

280-
// Store the email/password identity for this user.
281-
$user->createEmailIdentity($this->request->getPost(['email', 'password']));
282-
283282
// Add to default group
284283
$users->addToDefaultGroup($user);
285284
```
@@ -297,7 +296,7 @@ NOTE: The User rows use [soft deletes](https://codeigniter.com/user_guide/models
297296

298297
### Editing A User
299298

300-
The `UserModel::save()` method has been modified to ensure that an email or password previously set on the `User` entity will be automatically updated in the correct `UserIdentity` record.
299+
The `UserModel::save()`, `update()` and `insert()` methods have been modified to ensure that an email or password previously set on the `User` entity will be automatically updated in the correct `UserIdentity` record.
301300

302301
```php
303302
$users = model('UserModel');
@@ -310,21 +309,3 @@ $user->fill([
310309
]);
311310
$users->save($user);
312311
```
313-
314-
If you prefer to use the `update()` method then you will have to update the user's appropriate UserIdentity manually.
315-
316-
```php
317-
$users = model('UserModel');
318-
$user = $users->findById(123);
319-
320-
$user->fill([
321-
'username' => 'JoeSmith111',
322-
'email' => '[email protected]',
323-
'password' => 'secret123'
324-
]);
325-
326-
// Saves the username field
327-
$users->update($user);
328-
// Updates the email and password
329-
$user->saveEmailIdentity();
330-
```

src/Controllers/RegisterController.php

+11-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use CodeIgniter\HTTP\RedirectResponse;
88
use CodeIgniter\Shield\Authentication\Authenticators\Session;
99
use CodeIgniter\Shield\Entities\User;
10+
use CodeIgniter\Shield\Exceptions\ValidationException;
1011
use CodeIgniter\Shield\Models\UserModel;
1112

1213
/**
@@ -57,26 +58,28 @@ public function registerAction(): RedirectResponse
5758
}
5859

5960
// Save the user
60-
$allowedPostFields = array_merge(setting('Auth.validFields'), setting('Auth.personalFields'));
61-
$user = $this->getUserEntity();
62-
61+
$allowedPostFields = array_merge(
62+
setting('Auth.validFields'),
63+
setting('Auth.personalFields'),
64+
['password']
65+
);
66+
$user = $this->getUserEntity();
6367
$user->fill($this->request->getPost($allowedPostFields));
6468

6569
// Workaround for email only registration/login
6670
if ($user->username === null) {
6771
$user->username = null;
6872
}
6973

70-
if (! $users->save($user)) {
74+
try {
75+
$users->save($user);
76+
} catch (ValidationException $e) {
7177
return redirect()->back()->withInput()->with('errors', $users->errors());
7278
}
7379

74-
// Get the updated user so we have the ID...
80+
// To get the complete user object with ID, we need to get from the database
7581
$user = $users->findById($users->getInsertID());
7682

77-
// Store the email/password identity for this user.
78-
$user->createEmailIdentity($this->request->getPost(['email', 'password']));
79-
8083
// Add to default group
8184
$users->addToDefaultGroup($user);
8285

src/Entities/User.php

+19-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace CodeIgniter\Shield\Entities;
44

5+
use CodeIgniter\Database\Exceptions\DataException;
56
use CodeIgniter\Entity\Entity;
67
use CodeIgniter\Shield\Authentication\Authenticators\Session;
78
use CodeIgniter\Shield\Authentication\Traits\HasAccessTokens;
@@ -122,7 +123,7 @@ public function getEmailIdentity(): ?UserIdentity
122123
}
123124

124125
/**
125-
* If $user, $password, or $password_hash have been updated,
126+
* If $email, $password, or $password_hash have been updated,
126127
* will update the user's email identity record with the
127128
* correct values.
128129
*/
@@ -159,7 +160,23 @@ public function saveEmailIdentity(): bool
159160
/** @var UserIdentityModel $identityModel */
160161
$identityModel = model(UserIdentityModel::class);
161162

162-
return $identityModel->save($identity);
163+
try {
164+
/** @throws DataException */
165+
$identityModel->save($identity);
166+
} catch (DataException $e) {
167+
// There may be no data to update.
168+
$messages = [
169+
lang('Database.emptyDataset', ['insert']),
170+
lang('Database.emptyDataset', ['update']),
171+
];
172+
if (in_array($e->getMessage(), $messages, true)) {
173+
return true;
174+
}
175+
176+
throw $e;
177+
}
178+
179+
return true;
163180
}
164181

165182
/**
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace CodeIgniter\Shield\Exceptions;
4+
5+
use RuntimeException;
6+
7+
class ValidationException extends RuntimeException
8+
{
9+
}

src/Models/CheckQueryReturnTrait.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace CodeIgniter\Shield\Models;
44

5-
use CodeIgniter\Shield\Exceptions\RuntimeException;
5+
use CodeIgniter\Shield\Exceptions\ValidationException;
66
use ReflectionObject;
77
use ReflectionProperty;
88

@@ -36,7 +36,7 @@ private function checkValidationError(): void
3636
$message .= ' [' . $field . '] ' . $error;
3737
}
3838

39-
throw new RuntimeException($message);
39+
throw new ValidationException($message);
4040
}
4141
}
4242

src/Models/UserModel.php

+97-24
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use CodeIgniter\Shield\Authentication\Authenticators\Session;
88
use CodeIgniter\Shield\Entities\User;
99
use CodeIgniter\Shield\Exceptions\InvalidArgumentException;
10-
use CodeIgniter\Shield\Exceptions\RuntimeException;
10+
use CodeIgniter\Shield\Exceptions\ValidationException;
1111
use Faker\Generator;
1212

1313
class UserModel extends Model
@@ -29,13 +29,20 @@ class UserModel extends Model
2929
];
3030
protected $useTimestamps = true;
3131
protected $afterFind = ['fetchIdentities'];
32+
protected $afterInsert = ['saveEmailIdentity'];
33+
protected $afterUpdate = ['saveEmailIdentity'];
3234

3335
/**
3436
* Whether identity records should be included
3537
* when user records are fetched from the database.
3638
*/
3739
protected bool $fetchIdentities = false;
3840

41+
/**
42+
* Save the User for afterInsert and afterUpdate
43+
*/
44+
protected ?User $tempUser = null;
45+
3946
/**
4047
* Mark the next find* query to include identities
4148
*
@@ -53,7 +60,7 @@ public function withIdentities(): self
5360
* returned from a find* method. Called
5461
* automatically when $this->fetchIdentities == true
5562
*
56-
* Model event callback called `afterFind`.
63+
* Model event callback called by `afterFind`.
5764
*/
5865
protected function fetchIdentities(array $data): array
5966
{
@@ -170,6 +177,7 @@ public function findByCredentials(array $credentials): ?User
170177
$user = new User($data);
171178
$user->email = $email;
172179
$user->password_hash = $password_hash;
180+
$user->syncOriginal();
173181

174182
return $user;
175183
}
@@ -184,48 +192,113 @@ public function activate(User $user): void
184192
{
185193
$user->active = true;
186194

187-
$return = $this->save($user);
188-
189-
$this->checkQueryReturn($return);
195+
$this->save($user);
190196
}
191197

192198
/**
193-
* Override the BaseModel's `save()` method to allow
194-
* updating of user email, password, or password_hash
195-
* fields if they've been modified.
199+
* Override the BaseModel's `insert()` method.
200+
* If you pass User object, also inserts Email Identity.
196201
*
197202
* @param array|User $data
198203
*
199-
* @TODO can't change the return type to void.
204+
* @throws ValidationException
205+
*
206+
* @retrun true|int|string Insert ID if $returnID is true
200207
*/
201-
public function save($data): bool
208+
public function insert($data = null, bool $returnID = true)
202209
{
203-
try {
204-
$result = parent::save($data);
210+
$this->tempUser = $data instanceof User ? $data : null;
205211

206-
if ($result && $data instanceof User) {
207-
/** @var User $user */
208-
$user = $data->id === null
209-
? $this->find($this->db->insertID())
210-
: $data;
212+
$result = parent::insert($data, $returnID);
211213

212-
if (! $user->saveEmailIdentity()) {
213-
throw new RuntimeException('Unable to save email identity.');
214-
}
215-
}
214+
$this->checkQueryReturn($result);
215+
216+
return $returnID ? $this->insertID : $result;
217+
}
216218

217-
return $result;
219+
/**
220+
* Override the BaseModel's `update()` method.
221+
* If you pass User object, also updates Email Identity.
222+
*
223+
* @param array|int|string|null $id
224+
* @param array|User $data
225+
*
226+
* @throws ValidationException
227+
*/
228+
public function update($id = null, $data = null): bool
229+
{
230+
$this->tempUser = $data instanceof User ? $data : null;
231+
232+
try {
233+
/** @throws DataException */
234+
$result = parent::update($id, $data);
218235
} catch (DataException $e) {
219236
$messages = [
220-
lang('Database.emptyDataset', ['insert']),
221237
lang('Database.emptyDataset', ['update']),
222238
];
239+
223240
if (in_array($e->getMessage(), $messages, true)) {
224-
// @TODO Why true? Shouldn't this workaround be removed?
241+
$this->tempUser->saveEmailIdentity();
242+
225243
return true;
226244
}
227245

228246
throw $e;
229247
}
248+
249+
$this->checkQueryReturn($result);
250+
251+
return true;
252+
}
253+
254+
/**
255+
* Override the BaseModel's `save()` method.
256+
* If you pass User object, also updates Email Identity.
257+
*
258+
* @param array|User $data
259+
*
260+
* @throws ValidationException
261+
*/
262+
public function save($data): bool
263+
{
264+
$result = parent::save($data);
265+
266+
$this->checkQueryReturn($result);
267+
268+
return true;
269+
}
270+
271+
/**
272+
* Save Email Identity
273+
*
274+
* Model event callback called by `afterInsert` and `afterUpdate`.
275+
*/
276+
protected function saveEmailIdentity(array $data): array
277+
{
278+
// If insert()/update() gets an array data, do nothing.
279+
if ($this->tempUser === null) {
280+
return $data;
281+
}
282+
283+
// Insert
284+
if ($this->tempUser->id === null) {
285+
/** @var User $user */
286+
$user = $this->find($this->db->insertID());
287+
288+
$user->email = $this->tempUser->email ?? '';
289+
$user->password = $this->tempUser->password ?? '';
290+
$user->password_hash = $this->tempUser->password_hash ?? '';
291+
292+
$user->saveEmailIdentity();
293+
$this->tempUser = null;
294+
295+
return $data;
296+
}
297+
298+
// Update
299+
$this->tempUser->saveEmailIdentity();
300+
$this->tempUser = null;
301+
302+
return $data;
230303
}
231304
}

tests/Unit/LoginModelTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Tests\Unit;
44

5-
use CodeIgniter\Shield\Exceptions\RuntimeException;
5+
use CodeIgniter\Shield\Exceptions\ValidationException;
66
use CodeIgniter\Shield\Models\LoginModel;
77
use CodeIgniter\Test\DatabaseTestTrait;
88
use Tests\Support\TestCase;
@@ -24,7 +24,7 @@ private function createLoginModel(): LoginModel
2424

2525
public function testRecordLoginAttemptThrowsException(): void
2626
{
27-
$this->expectException(RuntimeException::class);
27+
$this->expectException(ValidationException::class);
2828
$this->expectExceptionMessage(
2929
'Validation error: [ip_address] The ip_address field is required.'
3030
. ' [id_type] The id_type field is required.'

0 commit comments

Comments
 (0)