diff --git a/system/BaseModel.php b/system/BaseModel.php index 06af2ded411f..ee3d1a859955 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1099,6 +1099,21 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc { if (is_array($set)) { foreach ($set as &$row) { + // Save the index value from the original row because + // transformDataToArray() may strip it when updateOnlyChanged + // is true. + $updateIndex = null; + + if ($this->updateOnlyChanged) { + if (is_array($row)) { + $updateIndex = $row[$index] ?? null; + } elseif ($row instanceof Entity) { + $updateIndex = $row->toRawArray()[$index] ?? null; + } elseif (is_object($row)) { + $updateIndex = $row->{$index} ?? null; + } + } + $row = $this->transformDataToArray($row, 'update'); // Validate data before saving. @@ -1106,8 +1121,13 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc return false; } - // Save updateIndex for later - $updateIndex = $row[$index] ?? null; + // When updateOnlyChanged is true, restore the pre-extracted + // index into $row. Otherwise read it from the transformed row. + if ($updateIndex !== null) { + $row[$index] = $updateIndex; + } else { + $updateIndex = $row[$index] ?? null; + } if ($updateIndex === null) { throw new InvalidArgumentException( diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index 94f8df12ea73..2495834ba978 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -265,6 +265,43 @@ public function testUpdateBatchWithEntity(): void $this->assertSame(2, $model->updateBatch([$entity1, $entity2], 'id')); } + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/9943 + */ + public function testUpdateBatchWithEntityAndUpdateOnlyChanged(): void + { + $entity1 = new User(); + $entity1->id = 1; + $entity1->name = 'Derek Jones'; + $entity1->email = 'derek@world.com'; + $entity1->country = 'US'; + $entity1->syncOriginal(); + $entity1->country = 'Greece'; + + $entity2 = new User(); + $entity2->id = 4; + $entity2->name = 'Chris Martin'; + $entity2->email = 'chris@world.com'; + $entity2->country = 'UK'; + $entity2->syncOriginal(); + $entity2->country = 'Finland'; + + // updateOnlyChanged is true by default. The index field 'id' is + // unchanged but must be preserved for the batch WHERE clause. + $model = $this->createModel(UserModel::class); + $this->assertTrue($this->getPrivateProperty($model, 'updateOnlyChanged')); + $this->assertSame(2, $model->updateBatch([$entity1, $entity2], 'id')); + + $this->seeInDatabase('user', [ + 'name' => 'Derek Jones', + 'country' => 'Greece', + ]); + $this->seeInDatabase('user', [ + 'name' => 'Chris Martin', + 'country' => 'Finland', + ]); + } + public function testUpdateNoPrimaryKey(): void { $this->db->table('secondary')->insert([ diff --git a/user_guide_src/source/changelogs/v4.7.1.rst b/user_guide_src/source/changelogs/v4.7.1.rst index 8a94a731a413..a6152e840301 100644 --- a/user_guide_src/source/changelogs/v4.7.1.rst +++ b/user_guide_src/source/changelogs/v4.7.1.rst @@ -33,6 +33,7 @@ Bugs Fixed ********** - **ContentSecurityPolicy:** Fixed a bug where ``generateNonces()`` produces corrupted JSON responses by replacing CSP nonce placeholders with unescaped double quotes. The method now automatically JSON-escapes nonce attributes when the response Content-Type is JSON. +- **Model:** Fixed a bug where ``BaseModel::updateBatch()`` threw an exception when ``updateOnlyChanged`` was ``true`` and the index field value did not change. - **Session:** Fixed a bug in ``MemcachedHandler`` where the constructor incorrectly threw an exception when ``savePath`` was not empty. See the repo's diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon index fdf4057fee76..2083602a9d54 100644 --- a/utils/phpstan-baseline/missingType.property.neon +++ b/utils/phpstan-baseline/missingType.property.neon @@ -468,42 +468,42 @@ parameters: path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$created_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$updated_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php