Skip to content

Commit c650a35

Browse files
committed
refactor: align insert-only fields with field protection
Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
1 parent 266a97e commit c650a35

8 files changed

Lines changed: 119 additions & 42 deletions

File tree

system/BaseModel.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ abstract class BaseModel
160160
*
161161
* @var list<string>
162162
*/
163-
protected $insertOnlyFields = [];
163+
protected array $insertOnlyFields = [];
164164

165165
/**
166166
* If true, will set created_at, and updated_at
@@ -1171,7 +1171,7 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc
11711171
// strip out updated_at values.
11721172
$ignoredFields = $index === null ? [] : [$index];
11731173

1174-
$this->ensureNoInsertOnlyFields($row, $ignoredFields);
1174+
$row = $this->doProtectInsertOnlyFieldsForUpdate($row, $ignoredFields);
11751175
$this->ensureNoDisallowedFields($row, $ignoredFields);
11761176
$row = $this->doProtectFields($row);
11771177

@@ -1487,17 +1487,19 @@ protected function ensureNoDisallowedFields(array $row, array $ignoredFields = [
14871487
}
14881488

14891489
/**
1490-
* Throws when update data contains fields that may only be inserted.
1490+
* Removes fields from update data when they may only be inserted.
14911491
*
14921492
* @param row_array $row
14931493
* @param list<string> $ignoredFields
14941494
*
1495+
* @return row_array
1496+
*
14951497
* @throws DataException
14961498
*/
1497-
protected function ensureNoInsertOnlyFields(array $row, array $ignoredFields = []): void
1499+
protected function doProtectInsertOnlyFieldsForUpdate(array $row, array $ignoredFields = []): array
14981500
{
14991501
if (! $this->protectFields || $this->allowedFields === [] || $this->insertOnlyFields === []) {
1500-
return;
1502+
return $row;
15011503
}
15021504

15031505
$insertOnlyFields = [];
@@ -1509,12 +1511,15 @@ protected function ensureNoInsertOnlyFields(array $row, array $ignoredFields = [
15091511

15101512
if (in_array($key, $this->insertOnlyFields, true)) {
15111513
$insertOnlyFields[] = $key;
1514+
unset($row[$key]);
15121515
}
15131516
}
15141517

1515-
if ($insertOnlyFields !== []) {
1518+
if ($insertOnlyFields !== [] && $this->throwOnDisallowedFields) {
15161519
throw DataException::forInsertOnlyFields(static::class, $insertOnlyFields);
15171520
}
1521+
1522+
return $row;
15181523
}
15191524

15201525
/**
@@ -1551,7 +1556,7 @@ protected function doProtectFieldsForInsert(array $row): array
15511556
*/
15521557
protected function doProtectFieldsForUpdate(array $row): array
15531558
{
1554-
$this->ensureNoInsertOnlyFields($row);
1559+
$row = $this->doProtectInsertOnlyFieldsForUpdate($row);
15551560
$this->ensureNoDisallowedFields($row);
15561561

15571562
return $this->doProtectFields($row);

system/Commands/Generators/Views/model.tpl.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
class {class} extends Model
88
{
99
<?php if (is_string($dbGroup)): ?>
10-
protected $DBGroup = '{dbGroup}';
10+
protected $DBGroup = '{dbGroup}';
1111
<?php endif; ?>
12-
protected $table = '{table}';
13-
protected $primaryKey = 'id';
14-
protected $useAutoIncrement = true;
15-
protected $returnType = {return};
16-
protected $useSoftDeletes = false;
17-
protected $protectFields = true;
18-
protected $allowedFields = [];
19-
protected $insertOnlyFields = [];
12+
protected $table = '{table}';
13+
protected $primaryKey = 'id';
14+
protected $useAutoIncrement = true;
15+
protected $returnType = {return};
16+
protected $useSoftDeletes = false;
17+
protected $protectFields = true;
18+
protected $allowedFields = [];
19+
protected array $insertOnlyFields = [];
2020

2121
protected bool $throwOnDisallowedFields = false;
2222
protected bool $allowEmptyInserts = false;

system/Model.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ protected function doProtectFieldsForInsert(array $row): array
785785

786786
protected function doProtectFieldsForUpdate(array $row): array
787787
{
788-
$this->ensureNoInsertOnlyFields($row, [$this->primaryKey]);
788+
$row = $this->doProtectInsertOnlyFieldsForUpdate($row, [$this->primaryKey]);
789789
$this->ensureNoDisallowedFields($row, [$this->primaryKey]);
790790

791791
return $this->doProtectFields($row);

tests/system/Models/InsertOnlyFieldsModelTest.php

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,46 +54,110 @@ public function testInsertBatchAllowsInsertOnlyFields(): void
5454
]);
5555
}
5656

57-
public function testUpdateThrowsOnInsertOnlyFields(): void
57+
public function testUpdateDiscardsInsertOnlyFieldsByDefault(): void
58+
{
59+
$result = $this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->update(1, [
60+
'name' => 'Insert Only Update',
61+
'email' => 'insert-only-update@example.com',
62+
]);
63+
64+
$this->assertTrue($result);
65+
$this->seeInDatabase('user', [
66+
'id' => 1,
67+
'name' => 'Insert Only Update',
68+
'email' => 'derek@world.com',
69+
]);
70+
}
71+
72+
public function testThrowOnDisallowedFieldsThrowsOnInsertOnlyFields(): void
5873
{
5974
$this->expectException(DataException::class);
6075
$this->expectExceptionMessage('Fields cannot be updated for model "Tests\Support\Models\UserModel": email');
6176

62-
$this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->update(1, [
77+
$this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->throwOnDisallowedFields()->update(1, [
6378
'name' => 'Insert Only Update',
6479
'email' => 'insert-only-update@example.com',
6580
]);
6681
}
6782

68-
public function testSaveUpdateThrowsOnInsertOnlyFields(): void
83+
public function testSaveUpdateDiscardsInsertOnlyFieldsByDefault(): void
84+
{
85+
$result = $this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->save([
86+
'id' => 1,
87+
'name' => 'Insert Only Save',
88+
'email' => 'insert-only-save@example.com',
89+
]);
90+
91+
$this->assertTrue($result);
92+
$this->seeInDatabase('user', [
93+
'id' => 1,
94+
'name' => 'Insert Only Save',
95+
'email' => 'derek@world.com',
96+
]);
97+
}
98+
99+
public function testSaveUpdateThrowsOnInsertOnlyFieldsWhenThrowingOnDisallowedFields(): void
69100
{
70101
$this->expectException(DataException::class);
71102
$this->expectExceptionMessage('Fields cannot be updated for model "Tests\Support\Models\UserModel": email');
72103

73-
$this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->save([
104+
$this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->throwOnDisallowedFields()->save([
74105
'id' => 1,
75106
'name' => 'Insert Only Save',
76107
'email' => 'insert-only-save@example.com',
77108
]);
78109
}
79110

80-
public function testSetUpdateThrowsOnInsertOnlyFields(): void
111+
public function testSetUpdateDiscardsInsertOnlyFieldsByDefault(): void
112+
{
113+
$result = $this->createModel(UserModel::class)->setInsertOnlyFields(['email'])
114+
->where('id', 1)
115+
->set('email', 'insert-only-set@example.com')
116+
->update(null, ['name' => 'Insert Only Set']);
117+
118+
$this->assertTrue($result);
119+
$this->seeInDatabase('user', [
120+
'id' => 1,
121+
'name' => 'Insert Only Set',
122+
'email' => 'derek@world.com',
123+
]);
124+
}
125+
126+
public function testSetUpdateThrowsOnInsertOnlyFieldsWhenThrowingOnDisallowedFields(): void
81127
{
82128
$this->expectException(DataException::class);
83129
$this->expectExceptionMessage('Fields cannot be updated for model "Tests\Support\Models\UserModel": email');
84130

85-
$this->createModel(UserModel::class)->setInsertOnlyFields(['email'])
131+
$this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->throwOnDisallowedFields()
86132
->where('id', 1)
87133
->set('email', 'insert-only-set@example.com')
88134
->update(null, ['name' => 'Insert Only Set']);
89135
}
90136

91-
public function testUpdateBatchThrowsOnInsertOnlyFields(): void
137+
public function testUpdateBatchDiscardsInsertOnlyFieldsByDefault(): void
138+
{
139+
$result = $this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->updateBatch([
140+
[
141+
'id' => 1,
142+
'name' => 'Insert Only Batch',
143+
'email' => 'insert-only-update-batch@example.com',
144+
],
145+
], 'id');
146+
147+
$this->assertSame(1, $result);
148+
$this->seeInDatabase('user', [
149+
'id' => 1,
150+
'name' => 'Insert Only Batch',
151+
'email' => 'derek@world.com',
152+
]);
153+
}
154+
155+
public function testUpdateBatchThrowsOnInsertOnlyFieldsWhenThrowingOnDisallowedFields(): void
92156
{
93157
$this->expectException(DataException::class);
94158
$this->expectExceptionMessage('Fields cannot be updated for model "Tests\Support\Models\UserModel": email');
95159

96-
$this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->updateBatch([
160+
$this->createModel(UserModel::class)->setInsertOnlyFields(['email'])->throwOnDisallowedFields()->updateBatch([
97161
[
98162
'id' => 1,
99163
'name' => 'Insert Only Batch',
@@ -118,29 +182,32 @@ public function testUpdateBatchAllowsInsertOnlyFieldAsIndex(): void
118182
]);
119183
}
120184

121-
public function testEntityUpdateThrowsOnChangedInsertOnlyFields(): void
185+
public function testEntityUpdateDiscardsChangedInsertOnlyFieldsByDefault(): void
122186
{
123187
$model = new class ($this->db) extends UserModel {
124-
protected $returnType = User::class;
125-
protected $insertOnlyFields = ['email'];
188+
protected $returnType = User::class;
189+
protected array $insertOnlyFields = ['email'];
126190
};
127191

128192
$user = $model->find(1);
129193
$this->assertInstanceOf(User::class, $user);
130194

131195
$user->email = 'insert-only-entity@example.com';
196+
$user->name = 'Insert Only Entity';
132197

133-
$this->expectException(DataException::class);
134-
$this->expectExceptionMessage('Fields cannot be updated for model "' . $model::class . '": email');
135-
136-
$model->update($user->id, $user);
198+
$this->assertTrue($model->update($user->id, $user));
199+
$this->seeInDatabase('user', [
200+
'id' => 1,
201+
'name' => 'Insert Only Entity',
202+
'email' => 'derek@world.com',
203+
]);
137204
}
138205

139206
public function testEntityUpdateAllowsUnchangedInsertOnlyFields(): void
140207
{
141208
$model = new class ($this->db) extends UserModel {
142-
protected $returnType = User::class;
143-
protected $insertOnlyFields = ['email'];
209+
protected $returnType = User::class;
210+
protected array $insertOnlyFields = ['email'];
144211
};
145212

146213
$user = $model->find(1);

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ Model
274274

275275
- Added new ``chunkRows()`` method to ``CodeIgniter\Model`` for processing large datasets in smaller chunks.
276276
- Added new ``firstOrInsert()`` method to ``CodeIgniter\Model`` that finds the first row matching the given attributes or inserts a new one. See :ref:`model-first-or-insert`.
277-
- Added ``$insertOnlyFields`` and ``setInsertOnlyFields()`` to ``CodeIgniter\Model`` to prevent configured fields from being submitted during Model update operations. See :ref:`model-insert-only-fields`.
277+
- Added ``$insertOnlyFields`` and ``setInsertOnlyFields()`` to ``CodeIgniter\Model`` to remove configured fields from Model update operations while allowing them during inserts. See :ref:`model-insert-only-fields`.
278278
- Added ``$throwOnDisallowedFields`` and ``throwOnDisallowedFields()`` to ``CodeIgniter\Model`` to throw a ``DataException`` when write data contains fields that would otherwise be discarded by ``$allowedFields``. See :ref:`model-throw-on-disallowed-fields`.
279279

280280
Libraries

user_guide_src/source/models/model.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ $insertOnlyFields
171171
.. versionadded:: 4.8.0
172172

173173
This array may contain fields that can be set during ``insert()`` and
174-
``insertBatch()`` calls, but cannot be submitted during ``update()``,
175-
``updateBatch()``, or update-side ``save()`` calls.
174+
``insertBatch()`` calls, but should be removed from ``update()``,
175+
``updateBatch()``, and update-side ``save()`` calls.
176176

177177
This is useful for values that should be created once and then left unchanged
178178
through normal Model writes, such as public IDs, external references, or
@@ -183,8 +183,13 @@ generated slugs.
183183
Fields listed here must also be listed in `$allowedFields`_ when field
184184
protection is enabled. This is Model-level protection only. It does not create a
185185
database constraint, does not inspect previous database values, and does not
186-
intercept direct Query Builder writes. Calling ``protect(false)`` disables this
187-
protection.
186+
intercept ``replace()`` or direct Query Builder writes. Calling
187+
``protect(false)`` disables this protection.
188+
189+
By default, insert-only fields are discarded from update data, the same way
190+
fields outside `$allowedFields`_ are discarded. When `$throwOnDisallowedFields`_
191+
is enabled, submitting an insert-only field during an update operation throws a
192+
``DataException``.
188193

189194
You may also change this setting with the ``setInsertOnlyFields()`` method.
190195

user_guide_src/source/models/model/005.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class UserModel extends Model
1414
protected $returnType = 'array';
1515
protected $useSoftDeletes = true;
1616

17-
protected $allowedFields = ['name', 'email'];
18-
protected $insertOnlyFields = [];
17+
protected $allowedFields = ['name', 'email'];
18+
protected array $insertOnlyFields = [];
1919

2020
protected bool $allowEmptyInserts = false;
2121
protected bool $updateOnlyChanged = true;

user_guide_src/source/models/model/069.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ class UserModel extends Model
88
{
99
protected $allowedFields = ['public_id', 'name', 'email'];
1010

11-
protected $insertOnlyFields = ['public_id'];
11+
protected array $insertOnlyFields = ['public_id'];
1212
}

0 commit comments

Comments
 (0)