Skip to content

Commit

Permalink
Validation: support placeholders for anything
Browse files Browse the repository at this point in the history
  • Loading branch information
paulbalandan committed Jan 5, 2022
1 parent c1fc842 commit c1974ee
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 27 deletions.
71 changes: 45 additions & 26 deletions system/Validation/Validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CodeIgniter\View\RendererInterface;
use Config\Validation as ValidationConfig;
use InvalidArgumentException;
use TypeError;

/**
* Validator
Expand Down Expand Up @@ -364,18 +365,30 @@ public function withRequest(RequestInterface $request): ValidationInterface
* 'rule' => 'message'
* ]
*
* @param array|string $rules
*
* @throws TypeError
*
* @return $this
*/
public function setRule(string $field, ?string $label, string $rules, array $errors = [])
public function setRule(string $field, ?string $label, $rules, array $errors = [])
{
$this->rules[$field] = [
'label' => $label,
'rules' => $rules,
if (! is_array($rules) && ! is_string($rules)) {
throw new TypeError('$rules must be of type string|array');
}

$ruleSet = [
$field => [
'label' => $label,
'rules' => $rules,
],
];

$this->customErrors = array_merge($this->customErrors, [
$field => $errors,
]);
if ($errors) {
$ruleSet[$field]['errors'] = $errors;
}

$this->setRules($ruleSet + $this->getRules());

return $this;
}
Expand Down Expand Up @@ -403,16 +416,18 @@ public function setRules(array $rules, array $errors = []): ValidationInterface
$this->customErrors = $errors;

foreach ($rules as $field => &$rule) {
if (! is_array($rule)) {
continue;
}
if (is_array($rule)) {
if (array_key_exists('errors', $rule)) {
$this->customErrors[$field] = $rule['errors'];
unset($rule['errors']);
}

if (! array_key_exists('errors', $rule)) {
continue;
// if $rule is already a rule collection, just move it to "rules"
// transforming [foo => [required, foobar]] to [foo => [rules => [required, foobar]]]
if (! array_key_exists('rules', $rule)) {
$rule = ['rules' => $rule];
}
}

$this->customErrors[$field] = $rule['errors'];
unset($rule['errors']);
}

$this->rules = $rules;
Expand Down Expand Up @@ -583,23 +598,27 @@ protected function fillPlaceholders(array $rules, array $data): array
$replacements["{{$key}}"] = $value;
}

if (! empty($replacements)) {
if ($replacements !== []) {
foreach ($rules as &$rule) {
if (is_array($rule)) {
foreach ($rule as &$row) {
// Should only be an `errors` array
// which doesn't take placeholders.
if (is_array($row)) {
continue;
}
$ruleSet = $rule['rules'] ?? $rule;

$row = strtr($row ?? '', $replacements);
if (is_array($ruleSet)) {
foreach ($ruleSet as &$row) {
if (is_string($row)) {
$row = strtr($row, $replacements);
}
}
}

continue;
if (is_string($ruleSet)) {
$ruleSet = strtr($ruleSet, $replacements);
}

$rule = strtr($rule ?? '', $replacements);
if (isset($rule['rules'])) {
$rule['rules'] = $ruleSet;
} else {
$rule = $ruleSet;
}
}
}

Expand Down
204 changes: 203 additions & 1 deletion tests/system/Validation/ValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
use Config\App;
use Config\Services;
use Generator;
use PHPUnit\Framework\ExpectationFailedException;
use Tests\Support\Validation\TestRules;
use TypeError;

/**
* @internal
Expand Down Expand Up @@ -87,7 +89,109 @@ public function testSetRulesStoresRules(): void
$this->assertSame($rules, $this->validation->getRules());
}

public function testRunReturnsFalseWithNothingToDo(): void
public function testSetRuleStoresRule()
{
$this->validation->setRules([]);
$this->validation->setRule('foo', null, 'bar|baz');

$this->assertSame([
'foo' => [
'label' => null,
'rules' => 'bar|baz',
],
], $this->validation->getRules());
}

public function testSetRuleAddsRule()
{
$this->validation->setRules([
'bar' => [
'label' => null,
'rules' => 'bar|baz',
],
]);
$this->validation->setRule('foo', null, 'foo|foz');

$this->assertSame([
'foo' => [
'label' => null,
'rules' => 'foo|foz',
],
'bar' => [
'label' => null,
'rules' => 'bar|baz',
],
], $this->validation->getRules());
}

public function testSetRuleOverwritesRule()
{
$this->validation->setRules([
'foo' => [
'label' => null,
'rules' => 'bar|baz',
],
]);
$this->validation->setRule('foo', null, 'foo|foz');

$this->assertSame([
'foo' => [
'label' => null,
'rules' => 'foo|foz',
],
], $this->validation->getRules());
}

/**
* @dataProvider setRuleRulesFormatCaseProvider
*
* @param mixed $rules
*/
public function testSetRuleRulesFormat(bool $expected, $rules): void
{
if (! $expected) {
$this->expectException(TypeError::class);
$this->expectExceptionMessage('$rules must be of type string|array');
}

$this->validation->setRule('foo', null, $rules);
$this->addToAssertionCount(1);
}

public function setRuleRulesFormatCaseProvider(): iterable
{
yield 'fail-simple-object' => [
false,
(object) ['required'],
];

yield 'pass-single-string' => [
true,
'required',
];

yield 'pass-single-array' => [
true,
['required'],
];

yield 'fail-deep-object' => [
false,
$this->validation,
];

yield 'pass-multiple-string' => [
true,
'required|alpha',
];

yield 'pass-multiple-array' => [
true,
['required', 'alpha'],
];
}

public function testRunReturnsFalseWithNothingToDo()
{
$this->validation->setRules([]);
$this->assertFalse($this->validation->run([]));
Expand Down Expand Up @@ -1008,4 +1112,102 @@ public function provideStringRulesCases(): iterable
['required', 'regex_match[/^(01|2689|09)[0-9]{8}$/]', 'numeric'],
];
}

/**
* internal method to simplify placeholder replacement test
* REQUIRES THE RULES TO BE SET FOR THE FIELD "foo"
*
* @param array|null $data optional POST data, needs to contain the key $placeholderField to pass
*
* @source https://github.com/codeigniter4/CodeIgniter4/pull/3910#issuecomment-784922913
*/
private function placeholderReplacementResultDetermination(string $placeholder = 'id', ?array $data = null)
{
if ($data === null) {
$data = [$placeholder => 'placeholder-value'];
}

$validationRules = $this->getPrivateMethodInvoker($this->validation, 'fillPlaceholders')($this->validation->getRules(), $data);
$fieldRules = $validationRules['foo']['rules'] ?? $validationRules['foo'];
if (is_string($fieldRules)) {
$fieldRules = $this->getPrivateMethodInvoker($this->validation, 'splitRules')($fieldRules);
}

// loop all rules for this field
foreach ($fieldRules as $rule) {
// only string type rules are supported
if (is_string($rule)) {
$this->assertStringNotContainsString('{' . $placeholder . '}', $rule);
}
}
}

/**
* @see ValidationTest::placeholderReplacementResultDetermination()
*/
public function testPlaceholderReplacementTestFails()
{
// to test if placeholderReplacementResultDetermination() works we provoke and expect an exception
$this->expectException(ExpectationFailedException::class);
$this->expectExceptionMessage('Failed asserting that \'filter[{id}]\' does not contain "{id}".');

$this->validation->setRule('foo', 'foo-label', 'required|filter[{id}]');

// calling with empty $data should produce an exception since {id} can't be replaced
$this->placeholderReplacementResultDetermination('id', []);
}

public function testPlaceholderReplacementSetSingleRuleString()
{
$this->validation->setRule('foo', null, 'required|filter[{id}]');

$this->placeholderReplacementResultDetermination();
}

public function testPlaceholderReplacementSetSingleRuleArray()
{
$this->validation->setRule('foo', null, ['required', 'filter[{id}]']);

$this->placeholderReplacementResultDetermination();
}

public function testPlaceholderReplacementSetMultipleRulesSimpleString()
{
$this->validation->setRules([
'foo' => 'required|filter[{id}]',
]);

$this->placeholderReplacementResultDetermination();
}

public function testPlaceholderReplacementSetMultipleRulesSimpleArray()
{
$this->validation->setRules([
'foo' => ['required', 'filter[{id}]'],
]);

$this->placeholderReplacementResultDetermination();
}

public function testPlaceholderReplacementSetMultipleRulesComplexString()
{
$this->validation->setRules([
'foo' => [
'rules' => 'required|filter[{id}]',
],
]);

$this->placeholderReplacementResultDetermination();
}

public function testPlaceholderReplacementSetMultipleRulesComplexArray()
{
$this->validation->setRules([
'foo' => [
'rules' => ['required', 'filter[{id}]'],
],
]);

$this->placeholderReplacementResultDetermination();
}
}

0 comments on commit c1974ee

Please sign in to comment.