From 96276b1d50a3ed645873cc15a5dab4354aa5b90a Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 12 Dec 2025 20:31:48 +0100 Subject: [PATCH 1/9] feat: primary key validation in model --- system/BaseModel.php | 75 ++++++++++++-- system/Model.php | 13 ++- tests/system/Models/DeleteModelTest.php | 128 ++++++++++++++++++++---- tests/system/Models/InsertModelTest.php | 79 +++++++++++++++ tests/system/Models/UpdateModelTest.php | 64 +++++++++++- 5 files changed, 320 insertions(+), 39 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index 6b2edc43947f..dee14241dd36 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -780,6 +780,61 @@ public function getInsertID() return is_numeric($this->insertID) ? (int) $this->insertID : $this->insertID; } + /** + * Validates that the primary key values are valid for update/delete/insert operations. + * Throws exception if invalid. + * + * @param int|list|string $id + * @param bool $allowArray Whether to allow array of IDs (true for update/delete, false for insert) + * + * @throws InvalidArgumentException + */ + protected function validateID($id, bool $allowArray = true): void + { + if (is_array($id)) { + // Check if arrays are allowed + if (! $allowArray) { + throw new InvalidArgumentException( + 'Invalid primary key: only a single value is allowed, not an array.' + ); + } + + // Check for empty array + if ($id === []) { + throw new InvalidArgumentException('Invalid primary key: cannot be an empty array.'); + } + + // Validate each ID in the array recursively + foreach ($id as $key => $valueId) { + if (is_array($valueId)) { + throw new InvalidArgumentException( + sprintf('Invalid primary key at index %s: nested arrays are not allowed.', $key) + ); + } + + // Recursive call for each value (single values only in recursion) + $this->validateID($valueId, false); + } + + return; + } + + // Check for invalid single values + if (in_array($id, [null, 0, '0', '', true, false], true)) { + $type = is_bool($id) ? 'boolean ' . var_export($id, true) : var_export($id, true); + throw new InvalidArgumentException( + sprintf('Invalid primary key: %s is not allowed.', $type) + ); + } + + // Only allow int and string at this point + if (! is_int($id) && ! is_string($id)) { + throw new InvalidArgumentException( + sprintf('Invalid primary key: must be int or string, %s given.', get_debug_type($id)) + ); + } + } + /** * Inserts data into the database. If an object is provided, * it will attempt to convert it to an array. @@ -969,12 +1024,12 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch */ public function update($id = null, $row = null): bool { - if (is_bool($id)) { - throw new InvalidArgumentException('update(): argument #1 ($id) should not be boolean.'); - } + if ($id !== null) { + if (! is_array($id)) { + $id = [$id]; + } - if (is_numeric($id) || is_string($id)) { - $id = [$id]; + $this->validateID($id); } $row = $this->transformDataToArray($row, 'update'); @@ -1100,12 +1155,12 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc */ public function delete($id = null, bool $purge = false) { - if (is_bool($id)) { - throw new InvalidArgumentException('delete(): argument #1 ($id) should not be boolean.'); - } + if ($id !== null) { + if (! is_array($id)) { + $id = [$id]; + } - if (! in_array($id, [null, 0, '0'], true) && (is_numeric($id) || is_string($id))) { - $id = [$id]; + $this->validateID($id); } $eventData = [ diff --git a/system/Model.php b/system/Model.php index 108ff17cf849..38a3f3b24ca4 100644 --- a/system/Model.php +++ b/system/Model.php @@ -311,8 +311,13 @@ protected function doInsert(array $row) // Require non-empty primaryKey when // not using auto-increment feature - if (! $this->useAutoIncrement && ! isset($row[$this->primaryKey])) { - throw DataException::forEmptyPrimaryKey('insert'); + if (! $this->useAutoIncrement) { + if (! isset($row[$this->primaryKey])) { + throw DataException::forEmptyPrimaryKey('insert'); + } + + // Validate the primary key value (arrays not allowed for insert) + $this->validateID($row[$this->primaryKey], false); } $builder = $this->builder(); @@ -381,7 +386,7 @@ protected function doUpdate($id = null, $row = null): bool $builder = $this->builder(); - if (! in_array($id, [null, '', 0, '0', []], true)) { + if (is_array($id) && $id !== []) { $builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id); } @@ -409,7 +414,7 @@ protected function doDelete($id = null, bool $purge = false) $set = []; $builder = $this->builder(); - if (! in_array($id, [null, '', 0, '0', []], true)) { + if (is_array($id) && $id !== []) { $builder = $builder->whereIn($this->primaryKey, $id); } diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index 0294ab32aed9..feb085f6abe6 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Models; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\ModelException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; @@ -152,29 +153,37 @@ public function testOnlyDeleted(): void /** * Given an explicit empty value in the WHERE condition - * When executing a soft delete + * When executing a soft delete with where() clause * Then an exception should not be thrown * + * This test uses where() so values go into WHERE clause, not through validateID(). + * * @param int|string|null $emptyValue */ - #[DataProvider('emptyPkValues')] + #[DataProvider('emptyPkValuesWithWhereClause')] public function testDontThrowExceptionWhenSoftDeleteConditionIsSetWithEmptyValue($emptyValue): void { $this->createModel(UserModel::class); $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); $this->model->where('id', $emptyValue)->delete(); - $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); + // Special case: true converted to 1 + if ($emptyValue === true) { + $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NOT NULL' => null]); + } else { + $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); + } } /** * @param int|string|null $emptyValue + * @param class-string $exception */ #[DataProvider('emptyPkValues')] - public function testThrowExceptionWhenSoftDeleteParamIsEmptyValue($emptyValue): void + public function testThrowExceptionWhenSoftDeleteParamIsEmptyValue($emptyValue, string $exception, string $exceptionMessage): void { - $this->expectException(DatabaseException::class); - $this->expectExceptionMessage('Deletes are not allowed unless they contain a "where" or "like" clause.'); + $this->expectException($exception); + $this->expectExceptionMessage($exceptionMessage); $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); @@ -183,16 +192,17 @@ public function testThrowExceptionWhenSoftDeleteParamIsEmptyValue($emptyValue): /** * @param int|string|null $emptyValue + * @param class-string $exception */ #[DataProvider('emptyPkValues')] - public function testDontDeleteRowsWhenSoftDeleteParamIsEmpty($emptyValue): void + public function testDontDeleteRowsWhenSoftDeleteParamIsEmpty($emptyValue, string $exception, string $exceptionMessage): void { $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); try { $this->createModel(UserModel::class)->delete($emptyValue); - } catch (DatabaseException) { - // Do nothing. + } catch (DatabaseException | InvalidArgumentException) { + // Do nothing - both exceptions are expected for different values. } $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); @@ -233,15 +243,13 @@ public function testPurgeDeletedWithSoftDeleteFalse(): void /** * @param int|string|null $id + * @param class-string $exception */ #[DataProvider('emptyPkValues')] - public function testDeleteThrowDatabaseExceptionWithoutWhereClause($id): void + public function testDeleteThrowDatabaseExceptionWithoutWhereClause($id, string $exception, string $exceptionMessage): void { - // BaseBuilder throws Exception. - $this->expectException(DatabaseException::class); - $this->expectExceptionMessage( - 'Deletes are not allowed unless they contain a "where" or "like" clause.', - ); + $this->expectException($exception); + $this->expectExceptionMessage($exceptionMessage); // $useSoftDeletes = false $this->createModel(JobModel::class); @@ -251,15 +259,13 @@ public function testDeleteThrowDatabaseExceptionWithoutWhereClause($id): void /** * @param int|string|null $id + * @param class-string $exception */ #[DataProvider('emptyPkValues')] - public function testDeleteWithSoftDeleteThrowDatabaseExceptionWithoutWhereClause($id): void + public function testDeleteWithSoftDeleteThrowDatabaseExceptionWithoutWhereClause($id, string $exception, string $exceptionMessage): void { - // Model throws Exception. - $this->expectException(DatabaseException::class); - $this->expectExceptionMessage( - 'Deletes are not allowed unless they contain a "where" or "like" clause.', - ); + $this->expectException($exception); + $this->expectExceptionMessage($exceptionMessage); // $useSoftDeletes = true $this->createModel(UserModel::class); @@ -268,11 +274,91 @@ public function testDeleteWithSoftDeleteThrowDatabaseExceptionWithoutWhereClause } public static function emptyPkValues(): iterable + { + return [ + 'null' => [ + null, + DatabaseException::class, + 'Deletes are not allowed unless they contain a "where" or "like" clause.', + ], + 'false' => [ + false, + InvalidArgumentException::class, + 'Invalid primary key: boolean false is not allowed.', + ], + '0 integer' => [ + 0, + InvalidArgumentException::class, + 'Invalid primary key: 0 is not allowed.', + ], + "'0' string" => [ + '0', + InvalidArgumentException::class, + "Invalid primary key: '0' is not allowed.", + ], + 'empty string' => [ + '', + InvalidArgumentException::class, + "Invalid primary key: '' is not allowed.", + ], + 'true' => [ + true, + InvalidArgumentException::class, + 'Invalid primary key: boolean true is not allowed.', + ], + 'empty array' => [ + [], + InvalidArgumentException::class, + 'Invalid primary key: cannot be an empty array.', + ], + 'nested array' => [ + [[1, 2]], + InvalidArgumentException::class, + 'Invalid primary key at index 0: nested arrays are not allowed.', + ], + 'array with null' => [ + [1, null, 3], + InvalidArgumentException::class, + 'Invalid primary key: NULL is not allowed.', + ], + 'array with 0' => [ + [1, 0, 3], + InvalidArgumentException::class, + 'Invalid primary key: 0 is not allowed.', + ], + "array with '0'" => [ + [1, '0', 3], + InvalidArgumentException::class, + "Invalid primary key: '0' is not allowed.", + ], + 'array with empty string' => [ + [1, '', 3], + InvalidArgumentException::class, + "Invalid primary key: '' is not allowed.", + ], + 'array with boolean' => [ + [1, false, 3], + InvalidArgumentException::class, + 'Invalid primary key: boolean false is not allowed.', + ], + ]; + } + + /** + * Data provider for tests using where() clause. + * These values go into WHERE clause, not through validateID(). + * + * @return iterable + */ + public static function emptyPkValuesWithWhereClause(): iterable { return [ [0], [null], ['0'], + [''], + [true], + [false], ]; } } diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index 11b079be3ab4..45612e04fb9f 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -15,9 +15,11 @@ use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Entity\Entity; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\I18n\Time; use CodeIgniter\Model; use Config\Database; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use stdClass; use Tests\Support\Entity\User; @@ -407,4 +409,81 @@ public function testInsertBatchWithCasts(): void $this->seeInDatabase('user', ['email' => json_encode($userData[0]['email'])]); $this->seeInDatabase('user', ['email' => json_encode($userData[1]['email'])]); } + + /** + * @param mixed $invalidKey + * @param class-string $exception + */ + #[DataProvider('provideInvalidPrimaryKeyValues')] + public function testInsertWithInvalidPrimaryKeyWhenAutoIncrementDisabled( + $invalidKey, + string $exception, + string $exceptionMessage + ): void { + $this->expectException($exception); + $this->expectExceptionMessage($exceptionMessage); + + $insert = [ + 'key' => $invalidKey, + 'value' => 'some value', + ]; + + $this->createModel(WithoutAutoIncrementModel::class)->insert($insert); + } + + public static function provideInvalidPrimaryKeyValues(): iterable + { + return [ + 'null' => [ + null, + DataException::class, + 'There is no primary key defined when trying to make insert', + ], + '0 integer' => [ + 0, + InvalidArgumentException::class, + 'Invalid primary key: 0 is not allowed.', + ], + "'0' string" => [ + '0', + InvalidArgumentException::class, + "Invalid primary key: '0' is not allowed.", + ], + 'empty string' => [ + '', + InvalidArgumentException::class, + "Invalid primary key: '' is not allowed.", + ], + 'true' => [ + true, + InvalidArgumentException::class, + 'Invalid primary key: boolean true is not allowed.', + ], + 'false' => [ + false, + InvalidArgumentException::class, + 'Invalid primary key: boolean false is not allowed.', + ], + 'array with null' => [ + [null], + InvalidArgumentException::class, + 'Invalid primary key: only a single value is allowed, not an array.', + ], + 'array with 0 integer' => [ + [0], + InvalidArgumentException::class, + 'Invalid primary key: only a single value is allowed, not an array.', + ], + "array with '0' string" => [ + ['0'], + InvalidArgumentException::class, + "Invalid primary key: only a single value is allowed, not an array.", + ], + 'array with empty array' => [ + [[]], + InvalidArgumentException::class, + "Invalid primary key: only a single value is allowed, not an array.", + ], + ]; + } } diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index a9380f34c771..578f3d366282 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -550,7 +550,8 @@ public function testUpdateWithSetAndEscape(): void } /** - * @param false|null $id + * @param bool|int|string|null $id + * @param class-string $exception */ #[DataProvider('provideUpdateThrowDatabaseExceptionWithoutWhereClause')] public function testUpdateThrowDatabaseExceptionWithoutWhereClause($id, string $exception, string $exceptionMessage): void @@ -567,15 +568,70 @@ public function testUpdateThrowDatabaseExceptionWithoutWhereClause($id, string $ public static function provideUpdateThrowDatabaseExceptionWithoutWhereClause(): iterable { yield from [ - [ + 'null' => [ null, DatabaseException::class, 'Updates are not allowed unless they contain a "where" or "like" clause.', ], - [ + 'false' => [ false, InvalidArgumentException::class, - 'update(): argument #1 ($id) should not be boolean.', + 'Invalid primary key: boolean false is not allowed.', + ], + 'true' => [ + true, + InvalidArgumentException::class, + 'Invalid primary key: boolean true is not allowed.', + ], + '0 integer' => [ + 0, + InvalidArgumentException::class, + 'Invalid primary key: 0 is not allowed.', + ], + "'0' string" => [ + '0', + InvalidArgumentException::class, + "Invalid primary key: '0' is not allowed.", + ], + 'empty string' => [ + '', + InvalidArgumentException::class, + "Invalid primary key: '' is not allowed.", + ], + 'empty array' => [ + [], + InvalidArgumentException::class, + 'Invalid primary key: cannot be an empty array.', + ], + 'nested array' => [ + [[1, 2]], + InvalidArgumentException::class, + 'Invalid primary key at index 0: nested arrays are not allowed.', + ], + 'array with null' => [ + [1, null, 3], + InvalidArgumentException::class, + 'Invalid primary key: NULL is not allowed.', + ], + 'array with 0' => [ + [1, 0, 3], + InvalidArgumentException::class, + 'Invalid primary key: 0 is not allowed.', + ], + "array with '0'" => [ + [1, '0', 3], + InvalidArgumentException::class, + "Invalid primary key: '0' is not allowed.", + ], + 'array with empty string' => [ + [1, '', 3], + InvalidArgumentException::class, + "Invalid primary key: '' is not allowed.", + ], + 'array with boolean' => [ + [1, false, 3], + InvalidArgumentException::class, + 'Invalid primary key: boolean false is not allowed.', ], ]; } From 42bd025c3ebb65ccb07d7b898ac264a523266145 Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 12 Dec 2025 20:34:14 +0100 Subject: [PATCH 2/9] phpstan baseline --- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 7 +++- .../missingType.property.neon | 32 +++++++++---------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 50220a8e8989..c2815348b4f8 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2708 errors +# total 2709 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 9d944d2ed357..f7283f834887 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1320 errors +# total 1321 errors parameters: ignoreErrors: @@ -5812,6 +5812,11 @@ parameters: count: 1 path: ../../tests/system/Models/FindModelTest.php + - + message: '#^Method CodeIgniter\\Models\\InsertModelTest\:\:provideInvalidPrimaryKeyValues\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Models/InsertModelTest.php + - message: '#^Method CodeIgniter\\Models\\TimestampModelTest\:\:allowDatesPrepareOneRecord\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon index 48a8f1e1f48d..a90e2f83b727 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\:194\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:196\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:196\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:196\:\:\$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\:194\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:196\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:196\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:196\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:196\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:194\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:196\:\:\$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\:288\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:290\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:290\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:290\:\:\$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\:288\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:290\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:290\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:290\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:290\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:288\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/InsertModelTest\.php\:290\:\:\$updated_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/InsertModelTest.php From 83921419bdc081ff2b13886efe08fe7883cf69d8 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 14 Dec 2025 10:29:45 +0100 Subject: [PATCH 3/9] primary key validation in insertBatch --- system/Model.php | 3 +++ tests/system/Models/InsertModelTest.php | 24 ++++++++++++++++++++++++ tests/system/Models/UpdateModelTest.php | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/system/Model.php b/system/Model.php index 38a3f3b24ca4..dc3e4db94db2 100644 --- a/system/Model.php +++ b/system/Model.php @@ -373,6 +373,9 @@ protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $ if (! isset($row[$this->primaryKey])) { throw DataException::forEmptyPrimaryKey('insertBatch'); } + + // Validate the primary key value + $this->validateID($row[$this->primaryKey], false); } } diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index 45612e04fb9f..aec4ce0967af 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -431,6 +431,30 @@ public function testInsertWithInvalidPrimaryKeyWhenAutoIncrementDisabled( $this->createModel(WithoutAutoIncrementModel::class)->insert($insert); } + /** + * @param mixed $invalidKey + * @param class-string $exception + */ + #[DataProvider('provideInvalidPrimaryKeyValues')] + public function testInsertBatchWithInvalidPrimaryKeyWhenAutoIncrementDisabled($invalidKey, string $exception, string $exceptionMessage): void + { + $this->expectException($exception); + $this->expectExceptionMessage($exceptionMessage); + + $insertData = [ + [ + 'key' => 'valid_key_1', + 'value' => 'value1', + ], + [ + 'key' => $invalidKey, // Invalid key in second row + 'value' => 'value2', + ], + ]; + + $this->createModel(WithoutAutoIncrementModel::class)->insertBatch($insertData); + } + public static function provideInvalidPrimaryKeyValues(): iterable { return [ diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index 578f3d366282..0a41ba5e974e 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -562,7 +562,7 @@ public function testUpdateThrowDatabaseExceptionWithoutWhereClause($id, string $ // $useSoftDeletes = false $this->createModel(JobModel::class); - $this->model->update($id, ['name' => 'Foo Bar']); // @phpstan-ignore argument.type + $this->model->update($id, ['name' => 'Foo Bar']); } public static function provideUpdateThrowDatabaseExceptionWithoutWhereClause(): iterable From 62668d6308e56bfeabde47dc4b6b7b050e85e36e Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 14 Dec 2025 10:50:41 +0100 Subject: [PATCH 4/9] add changelog --- user_guide_src/source/changelogs/v4.7.0.rst | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 70b27b15a600..2f0abc1b6197 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -40,6 +40,43 @@ The ``insertBatch()`` and ``updateBatch()`` methods now honor model settings lik ``updateOnlyChanged`` and ``allowEmptyInserts``. This change ensures consistent handling across all insert/update operations. +Primary Key Validation +^^^^^^^^^^^^^^^^^^^^^^ + +The ``insert()`` and ``insertBatch()`` (when ``useAutoIncrement`` is disabled), ``update()``, +and ``delete()`` methods now validate primary key values **before** executing database queries. +Invalid primary key values will now throw ``InvalidArgumentException`` instead of +``DatabaseException``. + +**What changed:** + +- **Exception type:** Invalid primary keys now throw ``InvalidArgumentException`` with + specific error messages instead of generic ``DatabaseException`` from the database layer. +- **Validation timing:** + + - For ``update()`` and ``delete()``: Validation happens **before** the ``beforeUpdate``/``beforeDelete`` + events and before any database queries are executed. + - For ``insert()`` and ``insertBatch()`` (when auto-increment is disabled): Validation happens + **after** the ``beforeInsert`` event but **before** database queries are executed. + +- **Invalid values:** The following values are now explicitly rejected as primary keys: + + - ``null`` (for insert when auto-increment is disabled) + - ``0`` (integer zero) + - ``'0'`` (string zero) + - ``''`` (empty string) + - ``true`` and ``false`` (booleans) + - ``[]`` (empty array) + - Nested arrays (e.g., ``[[1, 2]]``) + - Arrays containing any of the above invalid values + +This change improves error reporting by providing specific validation messages +(e.g., "Invalid primary key: 0 is not allowed") instead of generic database errors, and +prevents invalid queries from reaching the database. + +If you need to allow some of these values for the primary key, you can override the +``validateID()`` method in your model. + Entity ------ From 760bd2a83b77cdbe3f658ec3d82d74ec6ca15bb9 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 14 Dec 2025 11:12:38 +0100 Subject: [PATCH 5/9] cs fix --- system/BaseModel.php | 11 +++---- tests/system/Models/DeleteModelTest.php | 40 ++++++++++++------------- tests/system/Models/InsertModelTest.php | 6 ++-- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index dee14241dd36..7b3924751ffa 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -795,7 +795,7 @@ protected function validateID($id, bool $allowArray = true): void // Check if arrays are allowed if (! $allowArray) { throw new InvalidArgumentException( - 'Invalid primary key: only a single value is allowed, not an array.' + 'Invalid primary key: only a single value is allowed, not an array.', ); } @@ -808,7 +808,7 @@ protected function validateID($id, bool $allowArray = true): void foreach ($id as $key => $valueId) { if (is_array($valueId)) { throw new InvalidArgumentException( - sprintf('Invalid primary key at index %s: nested arrays are not allowed.', $key) + sprintf('Invalid primary key at index %s: nested arrays are not allowed.', $key), ); } @@ -822,15 +822,16 @@ protected function validateID($id, bool $allowArray = true): void // Check for invalid single values if (in_array($id, [null, 0, '0', '', true, false], true)) { $type = is_bool($id) ? 'boolean ' . var_export($id, true) : var_export($id, true); + throw new InvalidArgumentException( - sprintf('Invalid primary key: %s is not allowed.', $type) + sprintf('Invalid primary key: %s is not allowed.', $type), ); } // Only allow int and string at this point if (! is_int($id) && ! is_string($id)) { throw new InvalidArgumentException( - sprintf('Invalid primary key: must be int or string, %s given.', get_debug_type($id)) + sprintf('Invalid primary key: must be int or string, %s given.', get_debug_type($id)), ); } } @@ -1026,7 +1027,7 @@ public function update($id = null, $row = null): bool { if ($id !== null) { if (! is_array($id)) { - $id = [$id]; + $id = [$id]; } $this->validateID($id); diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index feb085f6abe6..12a79c269325 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -160,7 +160,7 @@ public function testOnlyDeleted(): void * * @param int|string|null $emptyValue */ - #[DataProvider('emptyPkValuesWithWhereClause')] + #[DataProvider('provideDontThrowExceptionWhenSoftDeleteConditionIsSetWithEmptyValue')] public function testDontThrowExceptionWhenSoftDeleteConditionIsSetWithEmptyValue($emptyValue): void { $this->createModel(UserModel::class); @@ -175,6 +175,24 @@ public function testDontThrowExceptionWhenSoftDeleteConditionIsSetWithEmptyValue } } + /** + * Data provider for tests using where() clause. + * These values go into WHERE clause, not through validateID(). + * + * @return iterable + */ + public static function provideDontThrowExceptionWhenSoftDeleteConditionIsSetWithEmptyValue(): iterable + { + return [ + [0], + [null], + ['0'], + [''], + [true], + [false], + ]; + } + /** * @param int|string|null $emptyValue * @param class-string $exception @@ -201,7 +219,7 @@ public function testDontDeleteRowsWhenSoftDeleteParamIsEmpty($emptyValue, string try { $this->createModel(UserModel::class)->delete($emptyValue); - } catch (DatabaseException | InvalidArgumentException) { + } catch (DatabaseException|InvalidArgumentException) { // Do nothing - both exceptions are expected for different values. } @@ -343,22 +361,4 @@ public static function emptyPkValues(): iterable ], ]; } - - /** - * Data provider for tests using where() clause. - * These values go into WHERE clause, not through validateID(). - * - * @return iterable - */ - public static function emptyPkValuesWithWhereClause(): iterable - { - return [ - [0], - [null], - ['0'], - [''], - [true], - [false], - ]; - } } diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index aec4ce0967af..6f2160a6e1c7 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -418,7 +418,7 @@ public function testInsertBatchWithCasts(): void public function testInsertWithInvalidPrimaryKeyWhenAutoIncrementDisabled( $invalidKey, string $exception, - string $exceptionMessage + string $exceptionMessage, ): void { $this->expectException($exception); $this->expectExceptionMessage($exceptionMessage); @@ -501,12 +501,12 @@ public static function provideInvalidPrimaryKeyValues(): iterable "array with '0' string" => [ ['0'], InvalidArgumentException::class, - "Invalid primary key: only a single value is allowed, not an array.", + 'Invalid primary key: only a single value is allowed, not an array.', ], 'array with empty array' => [ [[]], InvalidArgumentException::class, - "Invalid primary key: only a single value is allowed, not an array.", + 'Invalid primary key: only a single value is allowed, not an array.', ], ]; } From b19145746ce05bfb41b142657d91f54e445a8f58 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 14 Dec 2025 11:13:00 +0100 Subject: [PATCH 6/9] update user guide --- user_guide_src/source/models/model.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index c9be9878e094..f1383b45f803 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -648,6 +648,19 @@ converted to strings with the format defined in ``dateFormat['datetime']`` and .. note:: Prior to v4.5.0, the date/time formats were hard coded as ``Y-m-d H:i:s`` and ``Y-m-d`` in the Model class. +Primary Key Validation +---------------------- + +.. versionadded:: 4.7.0 + +The ``insert()``, ``insertBatch()`` (when `$useAutoIncrement`_ is ``false``), ``update()``, +and ``delete()`` methods validate primary key values before executing database queries. +Invalid values such as ``null``, ``0``, ``'0'``, empty strings, booleans, empty arrays, +or nested arrays will throw an ``InvalidArgumentException`` with a specific error message. + +If you need to customize this behavior (e.g., to allow ``0`` as a valid primary key for +legacy systems), you can override the ``validateID()`` method in your model. + Deleting Data ============= From 6e7abd788c7312b7c2dbfb2bb2b9774db8c46b23 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 14 Dec 2025 11:46:21 +0100 Subject: [PATCH 7/9] fix tests for postgre --- tests/system/Models/DeleteModelTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index 12a79c269325..191ca99def6e 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -164,6 +164,11 @@ public function testOnlyDeleted(): void public function testDontThrowExceptionWhenSoftDeleteConditionIsSetWithEmptyValue($emptyValue): void { $this->createModel(UserModel::class); + + if ($this->db->DBDriver === 'Postgre' && in_array($emptyValue, ['', true, false], true)) { + $this->markTestSkipped('PostgreSQL does not allow empty string, true, or false for integer columns'); + } + $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); $this->model->where('id', $emptyValue)->delete(); From e21d252559e4d10c626b0c8697a7ed78d9d4733d Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 14 Dec 2025 13:08:27 +0100 Subject: [PATCH 8/9] allow RawSql for the primary key --- system/BaseModel.php | 18 ++++--- tests/system/Models/DeleteModelTest.php | 12 +++++ tests/system/Models/UpdateModelTest.php | 11 +++++ user_guide_src/source/changelogs/v4.7.0.rst | 3 ++ .../missingType.property.neon | 48 +++++++++---------- 5 files changed, 62 insertions(+), 30 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index 7b3924751ffa..1ab9a2019d8e 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -19,6 +19,7 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Database\Query; +use CodeIgniter\Database\RawSql; use CodeIgniter\DataCaster\Cast\CastInterface; use CodeIgniter\DataConverter\DataConverter; use CodeIgniter\Entity\Cast\CastInterface as EntityCastInterface; @@ -784,8 +785,8 @@ public function getInsertID() * Validates that the primary key values are valid for update/delete/insert operations. * Throws exception if invalid. * - * @param int|list|string $id - * @param bool $allowArray Whether to allow array of IDs (true for update/delete, false for insert) + * @param int|list|RawSql|string $id + * @param bool $allowArray Whether to allow array of IDs (true for update/delete, false for insert) * * @throws InvalidArgumentException */ @@ -819,6 +820,11 @@ protected function validateID($id, bool $allowArray = true): void return; } + // Allow RawSql objects for complex scenarios + if ($id instanceof RawSql) { + return; + } + // Check for invalid single values if (in_array($id, [null, 0, '0', '', true, false], true)) { $type = is_bool($id) ? 'boolean ' . var_export($id, true) : var_export($id, true); @@ -1018,8 +1024,8 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch * Updates a single record in the database. If an object is provided, * it will attempt to convert it into an array. * - * @param int|list|string|null $id - * @param object|row_array|null $row + * @param int|list|RawSql|string|null $id + * @param object|row_array|null $row * * @throws ReflectionException */ @@ -1147,8 +1153,8 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc /** * Deletes a single record from the database where $id matches. * - * @param int|list|string|null $id The rows primary key(s). - * @param bool $purge Allows overriding the soft deletes setting. + * @param int|list|RawSql|string|null $id The rows primary key(s). + * @param bool $purge Allows overriding the soft deletes setting. * * @return bool|string Returns a SQL string if in test mode. * diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index 191ca99def6e..cee283fced55 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Models; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\RawSql; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\ModelException; use PHPUnit\Framework\Attributes\DataProvider; @@ -38,6 +39,17 @@ public function testDeleteBasics(): void $this->dontSeeInDatabase('job', ['name' => 'Developer']); } + public function testDeleteWithRawSql(): void + { + $this->createModel(JobModel::class); + $this->seeInDatabase('job', ['name' => 'Developer']); + + // RawSql objects should be allowed as primary key values + $result = $this->model->delete(new RawSql('1')); + $this->assertTrue($result); + $this->dontSeeInDatabase('job', ['name' => 'Developer']); + } + public function testDeleteFail(): void { // WARNING this value will persist! take care to roll it back. diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index 0a41ba5e974e..7b472c3180fd 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; +use CodeIgniter\Database\RawSql; use CodeIgniter\Entity\Entity; use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database; @@ -107,6 +108,16 @@ public function testUpdateArray(): void $this->seeInDatabase('user', ['id' => 2, 'name' => 'Foo Bar']); } + public function testUpdateWithRawSql(): void + { + $this->createModel(UserModel::class); + + // RawSql objects should be allowed as primary key values + $result = $this->model->update(new RawSql('1'), ['name' => 'RawSql User']); + $this->assertTrue($result); + $this->seeInDatabase('user', ['id' => 1, 'name' => 'RawSql User']); + } + public function testUpdateResultFail(): void { // WARNING this value will persist! take care to roll it back. diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 2f0abc1b6197..05b667d68fda 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -70,6 +70,9 @@ Invalid primary key values will now throw ``InvalidArgumentException`` instead o - Nested arrays (e.g., ``[[1, 2]]``) - Arrays containing any of the above invalid values + **Note:** ``RawSql`` objects are allowed as primary key values for complex scenarios + where you need to use raw SQL expressions. + This change improves error reporting by providing specific validation messages (e.g., "Invalid primary key: 0 is not allowed") instead of generic database errors, and prevents invalid queries from reaching the database. diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon index a90e2f83b727..139386d26a14 100644 --- a/utils/phpstan-baseline/missingType.property.neon +++ b/utils/phpstan-baseline/missingType.property.neon @@ -393,122 +393,122 @@ parameters: path: ../../tests/system/Models/SaveModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$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\:199\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:199\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$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\:218\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$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\:218\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:218\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$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\:353\:\:\$_options has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$_options has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:353\:\:\$country has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$country has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:353\:\:\$created_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$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\:353\:\:\$deleted has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$deleted has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:353\:\:\$email has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$email has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:353\:\:\$id has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$id has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:353\:\:\$name has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:353\:\:\$updated_at has no type specified\.$#' + message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:364\:\:\$updated_at has no type specified\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php From 262a9960a0f826e3ac4c418d0bc3c4b421d1e11e Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 14 Dec 2025 18:00:25 +0100 Subject: [PATCH 9/9] update phpstan --- tests/system/Models/DeleteModelTest.php | 3 +++ tests/system/Models/InsertModelTest.php | 3 +++ tests/system/Models/UpdateModelTest.php | 3 +++ utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 17 +---------------- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index cee283fced55..9d97ca50fa74 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -308,6 +308,9 @@ public function testDeleteWithSoftDeleteThrowDatabaseExceptionWithoutWhereClause $this->model->delete($id); } + /** + * @return iterable + */ public static function emptyPkValues(): iterable { return [ diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index 6f2160a6e1c7..baf6b45ea7f0 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -455,6 +455,9 @@ public function testInsertBatchWithInvalidPrimaryKeyWhenAutoIncrementDisabled($i $this->createModel(WithoutAutoIncrementModel::class)->insertBatch($insertData); } + /** + * @return iterable + */ public static function provideInvalidPrimaryKeyValues(): iterable { return [ diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index 7b472c3180fd..94f8df12ea73 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -576,6 +576,9 @@ public function testUpdateThrowDatabaseExceptionWithoutWhereClause($id, string $ $this->model->update($id, ['name' => 'Foo Bar']); } + /** + * @return iterable + */ public static function provideUpdateThrowDatabaseExceptionWithoutWhereClause(): iterable { yield from [ diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index c2815348b4f8..611842cc1be2 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2709 errors +# total 2706 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index f7283f834887..b7be6baeffff 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1321 errors +# total 1318 errors parameters: ignoreErrors: @@ -5797,11 +5797,6 @@ parameters: count: 1 path: ../../tests/system/I18n/TimeTest.php - - - message: '#^Method CodeIgniter\\Models\\DeleteModelTest\:\:emptyPkValues\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Models/DeleteModelTest.php - - message: '#^Method CodeIgniter\\Models\\FindModelTest\:\:provideAggregateAndGroupBy\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 @@ -5812,11 +5807,6 @@ parameters: count: 1 path: ../../tests/system/Models/FindModelTest.php - - - message: '#^Method CodeIgniter\\Models\\InsertModelTest\:\:provideInvalidPrimaryKeyValues\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Method CodeIgniter\\Models\\TimestampModelTest\:\:allowDatesPrepareOneRecord\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -5827,11 +5817,6 @@ parameters: count: 1 path: ../../tests/system/Models/TimestampModelTest.php - - - message: '#^Method CodeIgniter\\Models\\UpdateModelTest\:\:provideUpdateThrowDatabaseExceptionWithoutWhereClause\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Method CodeIgniter\\Publisher\\PublisherRestrictionsTest\:\:provideDefaultPublicRestrictions\(\) return type has no value type specified in iterable type iterable\.$#' count: 1