diff --git a/system/BaseModel.php b/system/BaseModel.php index 179964379937..a1859a11b450 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -909,22 +909,7 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch if (is_array($set)) { foreach ($set as &$row) { - // If $row is using a custom class with public or protected - // properties representing the collection elements, we need to grab - // them as an array. - if (is_object($row) && ! $row instanceof stdClass) { - $row = $this->objectToArray($row, false, true); - } - - // If it's still a stdClass, go ahead and convert to - // an array so doProtectFields and other model methods - // don't have to do special checks. - if (is_object($row)) { - $row = (array) $row; - } - - // Convert any Time instances to appropriate $dateFormat - $row = $this->timeToString($row); + $row = $this->transformDataRowToArray($row); // Validate every row. if (! $this->skipValidation && ! $this->validate($row)) { @@ -1051,21 +1036,7 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc { if (is_array($set)) { foreach ($set as &$row) { - // If $row is using a custom class with public or protected - // properties representing the collection elements, we need to grab - // them as an array. - if (is_object($row) && ! $row instanceof stdClass) { - // For updates the index field is needed even if it is not changed. - // So set $onlyChanged to false. - $row = $this->objectToArray($row, false, true); - } - - // If it's still a stdClass, go ahead and convert to - // an array so doProtectFields and other model methods - // don't have to do special checks. - if (is_object($row)) { - $row = (array) $row; - } + $row = $this->transformDataRowToArray($row); // Validate data before saving. if (! $this->skipValidation && ! $this->validate($row)) { @@ -1696,6 +1667,52 @@ protected function trigger(string $event, array $eventData) return $eventData; } + /** + * If the model is using casts, this will convert the data + * in $row according to the rules defined in `$casts`. + * + * @param object|row_array|null $row Row data + * + * @return object|row_array|null Converted row data + * + * @used-by insertBatch() + * @used-by updateBatch() + * + * @throws ReflectionException + * @deprecated Since 4.6.4, temporary solution - will be removed in 4.7 + */ + protected function transformDataRowToArray(array|object|null $row): array|object|null + { + // If casts are used, convert the data first + if ($this->useCasts()) { + if (is_array($row)) { + $row = $this->converter->toDataSource($row); + } elseif ($row instanceof stdClass) { + $row = (array) $row; + $row = $this->converter->toDataSource($row); + } elseif ($row instanceof Entity) { + $row = $this->converter->extract($row); + } elseif (is_object($row)) { + $row = $this->converter->extract($row); + } + } elseif (is_object($row) && ! $row instanceof stdClass) { + // If $row is using a custom class with public or protected + // properties representing the collection elements, we need to grab + // them as an array. + $row = $this->objectToArray($row, false, true); + } + + // If it's still a stdClass, go ahead and convert to + // an array so doProtectFields and other model methods + // don't have to do special checks. + if (is_object($row)) { + $row = (array) $row; + } + + // Convert any Time instances to appropriate $dateFormat + return $this->timeToString($row); + } + /** * Sets the return type of the results to be as an associative array. * diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index d4a680d7696b..11b079be3ab4 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -22,6 +22,7 @@ use stdClass; use Tests\Support\Entity\User; use Tests\Support\Models\JobModel; +use Tests\Support\Models\UserCastsTimestampModel; use Tests\Support\Models\UserModel; use Tests\Support\Models\UserObjModel; use Tests\Support\Models\WithoutAutoIncrementModel; @@ -376,4 +377,34 @@ public function testInsertWithDefaultValue(): void $id = $this->model->getInsertID(); $this->assertSame($entity->country, $this->model->find($id)->country); } + + public function testInsertBatchWithCasts(): void + { + $userData = [ + [ + 'name' => 'Smriti', + 'email' => [ + 'personal' => 'smriti@india.com', + 'work' => 'smriti@company.com', + ], + 'country' => 'India', + ], + [ + 'name' => 'Rahul', + 'email' => [ + 'personal' => 'rahul123@india.com', + 'work' => 'rahul@company123.com', + ], + 'country' => 'India', + ], + ]; + $this->createModel(UserCastsTimestampModel::class); + + $numRows = $this->model->insertBatch($userData); // @phpstan-ignore argument.type + + $this->assertSame(2, $numRows); + + $this->seeInDatabase('user', ['email' => json_encode($userData[0]['email'])]); + $this->seeInDatabase('user', ['email' => json_encode($userData[1]['email'])]); + } } diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index cb378ea02e1f..2f01bb6a13f8 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -26,6 +26,7 @@ use Tests\Support\Models\EventModel; use Tests\Support\Models\JobModel; use Tests\Support\Models\SecondaryModel; +use Tests\Support\Models\UserCastsTimestampModel; use Tests\Support\Models\UserModel; use Tests\Support\Models\UserTimestampModel; use Tests\Support\Models\UUIDPkeyModel; @@ -602,4 +603,43 @@ public function testUpdateEntityUpdateOnlyChangedFalse(): void $this->assertTrue($result); $this->assertNotSame($updateAtBefore, $updateAtAfter); } + + public function testUpdateBatchWithCasts(): void + { + $this->createModel(UserCastsTimestampModel::class); + + $rows = $this->db->table('user')->limit(2)->orderBy('id', 'asc')->get()->getResultArray(); + + $this->assertNotEmpty($rows); + $this->assertCount(2, $rows); + + $row1 = $rows[0]; + $row2 = $rows[1]; + + $this->assertNotNull($row1); + $this->assertNotNull($row2); + + $updateData = [ + [ + 'id' => $row1['id'], + 'email' => [ + 'personal' => 'email1.new@country.com', + 'work' => 'email1.new@company.com', + ], + ], + [ + 'id' => $row2['id'], + 'email' => [ + 'personal' => 'email2.new@country.com', + 'work' => 'email2.new@company.com', + ], + ], + ]; + + $numRows = $this->model->updateBatch($updateData, 'id'); // @phpstan-ignore argument.type + $this->assertSame(2, $numRows); + + $this->seeInDatabase('user', ['email' => json_encode($updateData[0]['email'])]); + $this->seeInDatabase('user', ['email' => json_encode($updateData[1]['email'])]); + } } diff --git a/user_guide_src/source/changelogs/v4.6.4.rst b/user_guide_src/source/changelogs/v4.6.4.rst index 121cafb9c814..83783c84ce6a 100644 --- a/user_guide_src/source/changelogs/v4.6.4.rst +++ b/user_guide_src/source/changelogs/v4.6.4.rst @@ -34,6 +34,7 @@ Bugs Fixed - **Database:** Fixed a bug in ``Connection::getFieldData()`` for ``SQLSRV`` and ``OCI8`` where extra characters were returned in column default values (specific to those handlers), instead of following the convention used by other drivers. - **Forge:** Fixed a bug in ``Postgre`` and ``SQLSRV`` where changing a column's default value using ``Forge::modifyColumn()`` method produced incorrect SQL syntax. - **Model:** Fixed a bug in ``Model::replace()`` where ``created_at`` field (when available) wasn't set correctly. +- **Model:** Fixed a bug in ``Model::insertBatch()`` and ``Model::updateBatch()`` where casts were not applied to inserted or updated values. See the repo's `CHANGELOG.md `_ diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon index 46719d10694c..c124b9b496bb 100644 --- a/utils/phpstan-baseline/missingType.property.neon +++ b/utils/phpstan-baseline/missingType.property.neon @@ -243,82 +243,82 @@ parameters: path: ../../tests/system/Database/Live/MySQLi/NumberNativeTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:193\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:193\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:193\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$created_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:193\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:193\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:193\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:193\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:193\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$updated_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:287\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:287\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:287\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$created_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:287\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:287\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:287\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:287\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:287\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$updated_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php @@ -393,122 +393,122 @@ parameters: path: ../../tests/system/Models/SaveModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:198\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:198\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:198\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$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\:198\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:198\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:198\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:198\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:198\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$updated_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:217\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:217\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:217\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$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\:217\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:217\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:217\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:217\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:217\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$updated_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:350\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:351\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:350\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:351\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:350\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:351\:\:\$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\:350\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:351\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:350\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:351\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:350\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:351\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:350\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:351\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:350\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:351\:\:\$updated_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php