Skip to content

Commit

Permalink
feat: Add optional dbGroup support in validation rules for multiple d…
Browse files Browse the repository at this point in the history
…atabase 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
  • Loading branch information
maniaba authored and paulbalandan committed Dec 1, 2024
1 parent 9fba8c7 commit 86278d9
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 93 deletions.
90 changes: 50 additions & 40 deletions system/Validation/Rules.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace CodeIgniter\Validation;

use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\Helpers\Array\ArrayHelper;
use Config\Database;
Expand Down Expand Up @@ -110,42 +111,25 @@ 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]
*
* @param string|null $str
*/
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;
}

/**
Expand All @@ -170,40 +154,66 @@ 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]
*
* @param string|null $str
*/
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<string, mixed> $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];
}

/**
Expand Down
53 changes: 4 additions & 49 deletions system/Validation/StrictRules/Rules.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

use CodeIgniter\Helpers\Array\ArrayHelper;
use CodeIgniter\Validation\Rules as NonStrictRules;
use Config\Database;

/**
* Validation Rules.
Expand Down Expand Up @@ -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]
*
Expand All @@ -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);
}

/**
Expand All @@ -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]
*
Expand All @@ -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);
}

/**
Expand Down
50 changes: 50 additions & 0 deletions tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,29 @@ public function testIsUniqueWithDBConnection(): void
$this->assertTrue($result);
}

public function testIsUniqueWithDBConnectionAsParameter(): void
{
$this->validation->setRules(['email' => 'is_unique[tests.user.email]']);

$data = ['email' => '[email protected]'];

$result = $this->validation->run($data);

$this->assertTrue($result);
}

public function testIsUniqueWrongParametersThrowInvalidArgumentException(): void
{
$this->validation->setRules(['email' => 'is_unique[invalid_parameters]']);

$data = ['email' => '[email protected]'];

$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);
Expand Down Expand Up @@ -295,4 +318,31 @@ public function testIsNotUniqueByManualRun(): void

$this->assertTrue($this->createRules()->is_not_unique('[email protected]', 'user.email,id,{id}', []));
}

public function testIsNotUniqueWithDBConnectionAsParameter(): void
{
Database::connect()
->table('user')
->insert([
'name' => 'Derek Travis',
'email' => '[email protected]',
'country' => 'Elbonia',
]);

$data = ['email' => '[email protected]'];
$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' => '[email protected]'];

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The field must be in the format "table.field" or "dbGroup.table.field".');

$this->validation->run($data);
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.6.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ Libraries
See :ref:`FileCollection::retainMultiplePatterns() <file-collections-retain-multiple-patterns>`.
- **Validation:** Added ``min_dims`` validation rule to ``FileRules`` class. See
:ref:`Validation <rules-for-file-uploads>`.
- **Validation:** Rules: ``is_unique`` and ``is_not_unique`` now accept the optional
``dbGroup`` as part of the first parameter. See :ref:`Validation <rules-for-general-use>`.

Helpers and Functions
=====================
Expand Down
11 changes: 7 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]``
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 86278d9

Please sign in to comment.