diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 06586a64190d..30fab3539ecd 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Validation; +use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Helpers\Array\ArrayHelper; use Config\Database; @@ -110,6 +111,7 @@ public function greater_than_equal_to($str, string $min): bool * accept only one filter). * * Example: + * is_not_unique[dbGroup.table.field,where_field,where_value] * is_not_unique[table.field,where_field,where_value] * is_not_unique[menu.id,active,1] * @@ -117,35 +119,17 @@ public function greater_than_equal_to($str, string $min): bool */ public function is_not_unique($str, string $field, array $data): bool { - if (! is_string($str) && $str !== null) { - $str = (string) $str; - } - - // Grab any data for exclusion of a single row. - [$field, $whereField, $whereValue] = array_pad( - explode(',', $field), - 3, - null - ); - - // Break the table and field apart - sscanf($field, '%[^.].%[^.]', $table, $field); - - $row = Database::connect($data['DBGroup'] ?? null) - ->table($table) - ->select('1') - ->where($field, $str) - ->limit(1); + [$builder, $whereField, $whereValue] = $this->prepareUniqueQuery($str, $field, $data); if ( $whereField !== null && $whereField !== '' && $whereValue !== null && $whereValue !== '' && ! preg_match('/^\{(\w+)\}$/', $whereValue) ) { - $row = $row->where($whereField, $whereValue); + $builder = $builder->where($whereField, $whereValue); } - return $row->get()->getRow() !== null; + return $builder->get()->getRow() !== null; } /** @@ -170,6 +154,7 @@ public function in_list($value, string $list): bool * record updates. * * Example: + * is_unique[dbGroup.table.field,ignore_field,ignore_value] * is_unique[table.field,ignore_field,ignore_value] * is_unique[users.email,id,5] * @@ -177,33 +162,58 @@ public function in_list($value, string $list): bool */ public function is_unique($str, string $field, array $data): bool { - if (! is_string($str) && $str !== null) { - $str = (string) $str; - } - - [$field, $ignoreField, $ignoreValue] = array_pad( - explode(',', $field), - 3, - null - ); - - sscanf($field, '%[^.].%[^.]', $table, $field); - - $row = Database::connect($data['DBGroup'] ?? null) - ->table($table) - ->select('1') - ->where($field, $str) - ->limit(1); + [$builder, $ignoreField, $ignoreValue] = $this->prepareUniqueQuery($str, $field, $data); if ( $ignoreField !== null && $ignoreField !== '' && $ignoreValue !== null && $ignoreValue !== '' && ! preg_match('/^\{(\w+)\}$/', $ignoreValue) ) { - $row = $row->where("{$ignoreField} !=", $ignoreValue); + $builder = $builder->where("{$ignoreField} !=", $ignoreValue); } - return $row->get()->getRow() === null; + return $builder->get()->getRow() === null; + } + + /** + * Prepares the database query for uniqueness checks. + * + * @param mixed $value The value to check. + * @param string $field The field parameters. + * @param array $data Additional data. + * + * @return array{0: BaseBuilder, 1: string|null, 2: string|null} + */ + private function prepareUniqueQuery($value, string $field, array $data): array + { + if (! is_string($value) && $value !== null) { + $value = (string) $value; + } + + // Split the field parameters and pad the array to ensure three elements. + [$field, $extraField, $extraValue] = array_pad(explode(',', $field), 3, null); + + // Parse the field string to extract dbGroup, table, and field. + $parts = explode('.', $field, 3); + $numParts = count($parts); + + if ($numParts === 3) { + [$dbGroup, $table, $field] = $parts; + } elseif ($numParts === 2) { + [$table, $field] = $parts; + } else { + throw new InvalidArgumentException('The field must be in the format "table.field" or "dbGroup.table.field".'); + } + + // Connect to the database. + $dbGroup ??= $data['DBGroup'] ?? null; + $builder = Database::connect($dbGroup) + ->table($table) + ->select('1') + ->where($field, $value) + ->limit(1); + + return [$builder, $extraField, $extraValue]; } /** diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index ec02a4e0a0ac..e836f4e4c127 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -15,7 +15,6 @@ use CodeIgniter\Helpers\Array\ArrayHelper; use CodeIgniter\Validation\Rules as NonStrictRules; -use Config\Database; /** * Validation Rules. @@ -134,6 +133,7 @@ public function greater_than_equal_to($str, string $min): bool * accept only one filter). * * Example: + * is_not_unique[dbGroup.table.field,where_field,where_value] * is_not_unique[table.field,where_field,where_value] * is_not_unique[menu.id,active,1] * @@ -145,31 +145,7 @@ public function is_not_unique($str, string $field, array $data): bool return false; } - // Grab any data for exclusion of a single row. - [$field, $whereField, $whereValue] = array_pad( - explode(',', $field), - 3, - null - ); - - // Break the table and field apart - sscanf($field, '%[^.].%[^.]', $table, $field); - - $row = Database::connect($data['DBGroup'] ?? null) - ->table($table) - ->select('1') - ->where($field, $str) - ->limit(1); - - if ( - $whereField !== null && $whereField !== '' - && $whereValue !== null && $whereValue !== '' - && ! preg_match('/^\{(\w+)\}$/', $whereValue) - ) { - $row = $row->where($whereField, $whereValue); - } - - return $row->get()->getRow() !== null; + return $this->nonStrictRules->is_not_unique($str, $field, $data); } /** @@ -196,6 +172,7 @@ public function in_list($value, string $list): bool * record updates. * * Example: + * is_unique[dbGroup.table.field,ignore_field,ignore_value] * is_unique[table.field,ignore_field,ignore_value] * is_unique[users.email,id,5] * @@ -207,29 +184,7 @@ public function is_unique($str, string $field, array $data): bool return false; } - [$field, $ignoreField, $ignoreValue] = array_pad( - explode(',', $field), - 3, - null - ); - - sscanf($field, '%[^.].%[^.]', $table, $field); - - $row = Database::connect($data['DBGroup'] ?? null) - ->table($table) - ->select('1') - ->where($field, $str) - ->limit(1); - - if ( - $ignoreField !== null && $ignoreField !== '' - && $ignoreValue !== null && $ignoreValue !== '' - && ! preg_match('/^\{(\w+)\}$/', $ignoreValue) - ) { - $row = $row->where("{$ignoreField} !=", $ignoreValue); - } - - return $row->get()->getRow() === null; + return $this->nonStrictRules->is_unique($str, $field, $data); } /** diff --git a/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php b/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php index 5f8472ec29f0..bd0be4ccc223 100644 --- a/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php +++ b/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php @@ -97,6 +97,29 @@ public function testIsUniqueWithDBConnection(): void $this->assertTrue($result); } + public function testIsUniqueWithDBConnectionAsParameter(): void + { + $this->validation->setRules(['email' => 'is_unique[tests.user.email]']); + + $data = ['email' => 'derek@world.co.uk']; + + $result = $this->validation->run($data); + + $this->assertTrue($result); + } + + public function testIsUniqueWrongParametersThrowInvalidArgumentException(): void + { + $this->validation->setRules(['email' => 'is_unique[invalid_parameters]']); + + $data = ['email' => 'derek@world.co.uk']; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The field must be in the format "table.field" or "dbGroup.table.field".'); + + $this->validation->run($data); + } + public function testIsUniqueWithInvalidDBGroup(): void { $this->expectException(InvalidArgumentException::class); @@ -296,4 +319,31 @@ public function testIsNotUniqueByManualRun(): void $this->assertTrue($this->createRules()->is_not_unique('deva@example.com', 'user.email,id,{id}', [])); } + + public function testIsNotUniqueWithDBConnectionAsParameter(): void + { + Database::connect() + ->table('user') + ->insert([ + 'name' => 'Derek Travis', + 'email' => 'derek@world.com', + 'country' => 'Elbonia', + ]); + + $data = ['email' => 'derek@world.com']; + $this->validation->setRules(['email' => 'is_not_unique[tests.user.email]']); + $this->assertTrue($this->validation->run($data)); + } + + public function testIsNotUniqueWrongParametersThrowInvalidArgumentException(): void + { + $this->validation->setRules(['email' => 'is_not_unique[invalid_parameters]']); + + $data = ['email' => 'derek@world.co.uk']; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The field must be in the format "table.field" or "dbGroup.table.field".'); + + $this->validation->run($data); + } } diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index d3dc4529d512..0a3ed22fa37d 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -219,6 +219,8 @@ Libraries See :ref:`FileCollection::retainMultiplePatterns() `. - **Validation:** Added ``min_dims`` validation rule to ``FileRules`` class. See :ref:`Validation `. +- **Validation:** Rules: ``is_unique`` and ``is_not_unique`` now accept the optional + ``dbGroup`` as part of the first parameter. See :ref:`Validation `. Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 25111c2cdcef..96416355bb22 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -949,13 +949,16 @@ is_natural No Fails if field contains anything other than is_natural_no_zero No Fails if field contains anything other than a natural number, except zero: ``1``, ``2``, ``3``, etc. -is_not_unique Yes Checks the database to see if the given value ``is_not_unique[table.field,where_field,where_value]`` +is_not_unique Yes Checks the database to see if the given value ``is_not_unique[table.field,where_field,where_value]`` or ``is_not_unique[dbGroup.table.field,where_field,where_value]`` exists. Can ignore records by field/value to filter (currently accept only one filter). -is_unique Yes Checks if this field value exists in the ``is_unique[table.field,ignore_field,ignore_value]`` + (Since v4.6.0, you can optionally pass + the dbGroup as a parameter) +is_unique Yes Checks if this field value exists in the ``is_unique[table.field,ignore_field,ignore_value]`` or ``is_unique[dbGroup.table.field,ignore_field,ignore_value]`` database. Optionally set a column and value to ignore, useful when updating records to - ignore itself. + ignore itself. (Since v4.6.0, you can + optionally pass the dbGroup as a parameter) less_than Yes Fails if field is greater than or equal to ``less_than[8]`` the parameter value or not numeric. less_than_equal_to Yes Fails if field is greater than the parameter ``less_than_equal_to[8]`` @@ -1094,7 +1097,7 @@ min_dims Yes Fails if the minimum width and height of an parameter is the field name. The second is the width, and the third is the height. Will also fail if the file cannot be determined - to be an image. (This rule was added in + to be an image. (This rule was added in v4.6.0.) mime_in Yes Fails if the file's mime type is not one ``mime_in[field_name,image/png,image/jpeg]`` listed in the parameters.