Skip to content

Commit 86278d9

Browse files
maniabapaulbalandan
authored andcommitted
feat: Add optional dbGroup support in validation rules for multiple database connections
cs fixer docs format fix rules add test testIsNotUniqueWithDBConnectionAsParameter docs: update v4.5.6.rst docs: fix tabs docs update for v4.6.0 Update validation.rst Update user_guide_src/source/changelogs/v4.6.0.rst Co-authored-by: Michal Sniatala <[email protected]> Add a test case for `InvalidArgumentException` and remove the duplicate comment. refactor: code for is_unique and is_not_unique methods; extracted common code into prepareUniqueQuery method rector and phpstan fix
1 parent 9fba8c7 commit 86278d9

File tree

5 files changed

+113
-93
lines changed

5 files changed

+113
-93
lines changed

system/Validation/Rules.php

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace CodeIgniter\Validation;
1515

16+
use CodeIgniter\Database\BaseBuilder;
1617
use CodeIgniter\Exceptions\InvalidArgumentException;
1718
use CodeIgniter\Helpers\Array\ArrayHelper;
1819
use Config\Database;
@@ -110,42 +111,25 @@ public function greater_than_equal_to($str, string $min): bool
110111
* accept only one filter).
111112
*
112113
* Example:
114+
* is_not_unique[dbGroup.table.field,where_field,where_value]
113115
* is_not_unique[table.field,where_field,where_value]
114116
* is_not_unique[menu.id,active,1]
115117
*
116118
* @param string|null $str
117119
*/
118120
public function is_not_unique($str, string $field, array $data): bool
119121
{
120-
if (! is_string($str) && $str !== null) {
121-
$str = (string) $str;
122-
}
123-
124-
// Grab any data for exclusion of a single row.
125-
[$field, $whereField, $whereValue] = array_pad(
126-
explode(',', $field),
127-
3,
128-
null
129-
);
130-
131-
// Break the table and field apart
132-
sscanf($field, '%[^.].%[^.]', $table, $field);
133-
134-
$row = Database::connect($data['DBGroup'] ?? null)
135-
->table($table)
136-
->select('1')
137-
->where($field, $str)
138-
->limit(1);
122+
[$builder, $whereField, $whereValue] = $this->prepareUniqueQuery($str, $field, $data);
139123

140124
if (
141125
$whereField !== null && $whereField !== ''
142126
&& $whereValue !== null && $whereValue !== ''
143127
&& ! preg_match('/^\{(\w+)\}$/', $whereValue)
144128
) {
145-
$row = $row->where($whereField, $whereValue);
129+
$builder = $builder->where($whereField, $whereValue);
146130
}
147131

148-
return $row->get()->getRow() !== null;
132+
return $builder->get()->getRow() !== null;
149133
}
150134

151135
/**
@@ -170,40 +154,66 @@ public function in_list($value, string $list): bool
170154
* record updates.
171155
*
172156
* Example:
157+
* is_unique[dbGroup.table.field,ignore_field,ignore_value]
173158
* is_unique[table.field,ignore_field,ignore_value]
174159
* is_unique[users.email,id,5]
175160
*
176161
* @param string|null $str
177162
*/
178163
public function is_unique($str, string $field, array $data): bool
179164
{
180-
if (! is_string($str) && $str !== null) {
181-
$str = (string) $str;
182-
}
183-
184-
[$field, $ignoreField, $ignoreValue] = array_pad(
185-
explode(',', $field),
186-
3,
187-
null
188-
);
189-
190-
sscanf($field, '%[^.].%[^.]', $table, $field);
191-
192-
$row = Database::connect($data['DBGroup'] ?? null)
193-
->table($table)
194-
->select('1')
195-
->where($field, $str)
196-
->limit(1);
165+
[$builder, $ignoreField, $ignoreValue] = $this->prepareUniqueQuery($str, $field, $data);
197166

198167
if (
199168
$ignoreField !== null && $ignoreField !== ''
200169
&& $ignoreValue !== null && $ignoreValue !== ''
201170
&& ! preg_match('/^\{(\w+)\}$/', $ignoreValue)
202171
) {
203-
$row = $row->where("{$ignoreField} !=", $ignoreValue);
172+
$builder = $builder->where("{$ignoreField} !=", $ignoreValue);
204173
}
205174

206-
return $row->get()->getRow() === null;
175+
return $builder->get()->getRow() === null;
176+
}
177+
178+
/**
179+
* Prepares the database query for uniqueness checks.
180+
*
181+
* @param mixed $value The value to check.
182+
* @param string $field The field parameters.
183+
* @param array<string, mixed> $data Additional data.
184+
*
185+
* @return array{0: BaseBuilder, 1: string|null, 2: string|null}
186+
*/
187+
private function prepareUniqueQuery($value, string $field, array $data): array
188+
{
189+
if (! is_string($value) && $value !== null) {
190+
$value = (string) $value;
191+
}
192+
193+
// Split the field parameters and pad the array to ensure three elements.
194+
[$field, $extraField, $extraValue] = array_pad(explode(',', $field), 3, null);
195+
196+
// Parse the field string to extract dbGroup, table, and field.
197+
$parts = explode('.', $field, 3);
198+
$numParts = count($parts);
199+
200+
if ($numParts === 3) {
201+
[$dbGroup, $table, $field] = $parts;
202+
} elseif ($numParts === 2) {
203+
[$table, $field] = $parts;
204+
} else {
205+
throw new InvalidArgumentException('The field must be in the format "table.field" or "dbGroup.table.field".');
206+
}
207+
208+
// Connect to the database.
209+
$dbGroup ??= $data['DBGroup'] ?? null;
210+
$builder = Database::connect($dbGroup)
211+
->table($table)
212+
->select('1')
213+
->where($field, $value)
214+
->limit(1);
215+
216+
return [$builder, $extraField, $extraValue];
207217
}
208218

209219
/**

system/Validation/StrictRules/Rules.php

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
use CodeIgniter\Helpers\Array\ArrayHelper;
1717
use CodeIgniter\Validation\Rules as NonStrictRules;
18-
use Config\Database;
1918

2019
/**
2120
* Validation Rules.
@@ -134,6 +133,7 @@ public function greater_than_equal_to($str, string $min): bool
134133
* accept only one filter).
135134
*
136135
* Example:
136+
* is_not_unique[dbGroup.table.field,where_field,where_value]
137137
* is_not_unique[table.field,where_field,where_value]
138138
* is_not_unique[menu.id,active,1]
139139
*
@@ -145,31 +145,7 @@ public function is_not_unique($str, string $field, array $data): bool
145145
return false;
146146
}
147147

148-
// Grab any data for exclusion of a single row.
149-
[$field, $whereField, $whereValue] = array_pad(
150-
explode(',', $field),
151-
3,
152-
null
153-
);
154-
155-
// Break the table and field apart
156-
sscanf($field, '%[^.].%[^.]', $table, $field);
157-
158-
$row = Database::connect($data['DBGroup'] ?? null)
159-
->table($table)
160-
->select('1')
161-
->where($field, $str)
162-
->limit(1);
163-
164-
if (
165-
$whereField !== null && $whereField !== ''
166-
&& $whereValue !== null && $whereValue !== ''
167-
&& ! preg_match('/^\{(\w+)\}$/', $whereValue)
168-
) {
169-
$row = $row->where($whereField, $whereValue);
170-
}
171-
172-
return $row->get()->getRow() !== null;
148+
return $this->nonStrictRules->is_not_unique($str, $field, $data);
173149
}
174150

175151
/**
@@ -196,6 +172,7 @@ public function in_list($value, string $list): bool
196172
* record updates.
197173
*
198174
* Example:
175+
* is_unique[dbGroup.table.field,ignore_field,ignore_value]
199176
* is_unique[table.field,ignore_field,ignore_value]
200177
* is_unique[users.email,id,5]
201178
*
@@ -207,29 +184,7 @@ public function is_unique($str, string $field, array $data): bool
207184
return false;
208185
}
209186

210-
[$field, $ignoreField, $ignoreValue] = array_pad(
211-
explode(',', $field),
212-
3,
213-
null
214-
);
215-
216-
sscanf($field, '%[^.].%[^.]', $table, $field);
217-
218-
$row = Database::connect($data['DBGroup'] ?? null)
219-
->table($table)
220-
->select('1')
221-
->where($field, $str)
222-
->limit(1);
223-
224-
if (
225-
$ignoreField !== null && $ignoreField !== ''
226-
&& $ignoreValue !== null && $ignoreValue !== ''
227-
&& ! preg_match('/^\{(\w+)\}$/', $ignoreValue)
228-
) {
229-
$row = $row->where("{$ignoreField} !=", $ignoreValue);
230-
}
231-
232-
return $row->get()->getRow() === null;
187+
return $this->nonStrictRules->is_unique($str, $field, $data);
233188
}
234189

235190
/**

tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ public function testIsUniqueWithDBConnection(): void
9696
$this->assertTrue($result);
9797
}
9898

99+
public function testIsUniqueWithDBConnectionAsParameter(): void
100+
{
101+
$this->validation->setRules(['email' => 'is_unique[tests.user.email]']);
102+
103+
$data = ['email' => '[email protected]'];
104+
105+
$result = $this->validation->run($data);
106+
107+
$this->assertTrue($result);
108+
}
109+
110+
public function testIsUniqueWrongParametersThrowInvalidArgumentException(): void
111+
{
112+
$this->validation->setRules(['email' => 'is_unique[invalid_parameters]']);
113+
114+
$data = ['email' => '[email protected]'];
115+
116+
$this->expectException(\InvalidArgumentException::class);
117+
$this->expectExceptionMessage('The field must be in the format "table.field" or "dbGroup.table.field".');
118+
119+
$this->validation->run($data);
120+
}
121+
99122
public function testIsUniqueWithInvalidDBGroup(): void
100123
{
101124
$this->expectException(InvalidArgumentException::class);
@@ -295,4 +318,31 @@ public function testIsNotUniqueByManualRun(): void
295318

296319
$this->assertTrue($this->createRules()->is_not_unique('[email protected]', 'user.email,id,{id}', []));
297320
}
321+
322+
public function testIsNotUniqueWithDBConnectionAsParameter(): void
323+
{
324+
Database::connect()
325+
->table('user')
326+
->insert([
327+
'name' => 'Derek Travis',
328+
'email' => '[email protected]',
329+
'country' => 'Elbonia',
330+
]);
331+
332+
$data = ['email' => '[email protected]'];
333+
$this->validation->setRules(['email' => 'is_not_unique[tests.user.email]']);
334+
$this->assertTrue($this->validation->run($data));
335+
}
336+
337+
public function testIsNotUniqueWrongParametersThrowInvalidArgumentException(): void
338+
{
339+
$this->validation->setRules(['email' => 'is_not_unique[invalid_parameters]']);
340+
341+
$data = ['email' => '[email protected]'];
342+
343+
$this->expectException(\InvalidArgumentException::class);
344+
$this->expectExceptionMessage('The field must be in the format "table.field" or "dbGroup.table.field".');
345+
346+
$this->validation->run($data);
347+
}
298348
}

user_guide_src/source/changelogs/v4.6.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ Libraries
219219
See :ref:`FileCollection::retainMultiplePatterns() <file-collections-retain-multiple-patterns>`.
220220
- **Validation:** Added ``min_dims`` validation rule to ``FileRules`` class. See
221221
:ref:`Validation <rules-for-file-uploads>`.
222+
- **Validation:** Rules: ``is_unique`` and ``is_not_unique`` now accept the optional
223+
``dbGroup`` as part of the first parameter. See :ref:`Validation <rules-for-general-use>`.
222224

223225
Helpers and Functions
224226
=====================

user_guide_src/source/libraries/validation.rst

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -949,13 +949,16 @@ is_natural No Fails if field contains anything other than
949949
is_natural_no_zero No Fails if field contains anything other than
950950
a natural number, except zero: ``1``, ``2``,
951951
``3``, etc.
952-
is_not_unique Yes Checks the database to see if the given value ``is_not_unique[table.field,where_field,where_value]``
952+
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]``
953953
exists. Can ignore records by field/value to
954954
filter (currently accept only one filter).
955-
is_unique Yes Checks if this field value exists in the ``is_unique[table.field,ignore_field,ignore_value]``
955+
(Since v4.6.0, you can optionally pass
956+
the dbGroup as a parameter)
957+
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]``
956958
database. Optionally set a column and value
957959
to ignore, useful when updating records to
958-
ignore itself.
960+
ignore itself. (Since v4.6.0, you can
961+
optionally pass the dbGroup as a parameter)
959962
less_than Yes Fails if field is greater than or equal to ``less_than[8]``
960963
the parameter value or not numeric.
961964
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
10941097
parameter is the field name. The second is
10951098
the width, and the third is the height. Will
10961099
also fail if the file cannot be determined
1097-
to be an image. (This rule was added in
1100+
to be an image. (This rule was added in
10981101
v4.6.0.)
10991102
mime_in Yes Fails if the file's mime type is not one ``mime_in[field_name,image/png,image/jpeg]``
11001103
listed in the parameters.

0 commit comments

Comments
 (0)