Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Add validation Rule::raw($rule, $parameters) #43779

Closed
wants to merge 9 commits into from
15 changes: 11 additions & 4 deletions src/Illuminate/Validation/Concerns/ValidatesAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -2136,12 +2136,19 @@ protected function compare($first, $second, $operator)
public function parseNamedParameters($parameters)
{
return array_reduce($parameters, function ($result, $item) {
[$key, $value] = array_pad(explode('=', $item, 2), 2, null);

$result[$key] = $value;
// If the item is a an array, we will assume the parameters have been
// passed in as groups of key value pairs. Otherwise, we can assume
// they are string based, separating them by their equals sign.
if (is_array($item)) {
array_push($result, $item);
} else {
[$key, $value] = array_pad(explode('=', $item, 2), 2, null);

$result[$key] = $value;
}

return $result;
});
}, []);
Copy link
Contributor Author

@stevebauman stevebauman Aug 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change has been added so that parameters that have been given as key => value pairs (such as with the dimensions rule), are properly pushed onto the result, rather than attempting to explode the array, throwing an exception.

A default empty array has also been added, so that the $result starts out as an array, allowing array_push to function. It previous would be null, until it being converted into an array via $result[$key].

}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/Illuminate/Validation/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,38 @@
namespace Illuminate\Validation;

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Validation\Rules\Dimensions;
use Illuminate\Validation\Rules\ExcludeIf;
use Illuminate\Validation\Rules\Exists;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\Rules\NotIn;
use Illuminate\Validation\Rules\ProhibitedIf;
use Illuminate\Validation\Rules\RawRule;
use Illuminate\Validation\Rules\RequiredIf;
use Illuminate\Validation\Rules\Unique;

class Rule
{
use Macroable;

/**
* Make a new raw validation rule.
*
* @param string $rule
* @param \Illuminate\Contracts\Support\Arrayable|string|array $parameters
* @return \Illuminate\Validation\Rules\RawRule
*/
public static function raw($rule, $parameters = [])
{
if ($parameters instanceof Arrayable) {
$parameters = $parameters->toArray();
}

return new RawRule($rule, Arr::wrap($parameters));
}

/**
* Create a new conditional rule set.
*
Expand Down
33 changes: 33 additions & 0 deletions src/Illuminate/Validation/Rules/RawRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Illuminate\Validation\Rules;

class RawRule
{
/**
* The validation rule.
*
* @var string
*/
public $rule;

/**
* The validation rule parameters.
*
* @var array
*/
public $parameters = [];

/**
* Constructor.
*
* @param string $rule
* @param array $parameters
* @return void
*/
public function __construct($rule, array $parameters)
{
$this->rule = $rule;
$this->parameters = $parameters;
}
}
8 changes: 8 additions & 0 deletions src/Illuminate/Validation/ValidationRuleParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Exists;
use Illuminate\Validation\Rules\RawRule;
use Illuminate\Validation\Rules\Unique;

class ValidationRuleParser
Expand Down Expand Up @@ -120,6 +121,7 @@ protected function prepareRule($rule, $attribute)
}

if (! is_object($rule) ||
$rule instanceof RawRule ||
$rule instanceof RuleContract ||
($rule instanceof Exists && $rule->queryCallbacks()) ||
($rule instanceof Unique && $rule->queryCallbacks())) {
Expand Down Expand Up @@ -228,6 +230,12 @@ public static function parse($rule)
return [$rule, []];
}

if ($rule instanceof RawRule) {
$rule = Arr::isAssoc($rule->parameters)
? [$rule->rule, $rule->parameters]
: [$rule->rule, ...$rule->parameters];
}

if (is_array($rule)) {
$rule = static::parseArrayRule($rule);
} else {
Expand Down
32 changes: 32 additions & 0 deletions tests/Validation/ValidationForEachTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,38 @@ public function testForEachCallbacksCanReturnDifferentRules()
], $v->getMessageBag()->toArray());
}

public function testForEachCallbacksCanReturnRawRules()
{
$data = [
'items' => [
['sku' => '|foo'],
['sku' => '|bar'],
['sku' => '|baz'],
],
];

$rules = [
'items.*.sku' => Rule::forEach(function () {
return [
Rule::raw('in', ['|foo', '|bar', '|baz']),
Rule::raw('ends_with', 'invalid'),
];
}),
];

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, $data, $rules);

$this->assertFalse($v->passes());

$this->assertEquals([
'items.0.sku' => ['validation.ends_with'],
'items.1.sku' => ['validation.ends_with'],
'items.2.sku' => ['validation.ends_with'],
], $v->getMessageBag()->toArray());
}

public function testForEachCallbacksDoNotBreakRegexRules()
{
$data = [
Expand Down
129 changes: 129 additions & 0 deletions tests/Validation/ValidationRawRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Illuminate\Tests\Validation;

use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
use Mockery as m;
use PHPUnit\Framework\TestCase;

class ValidationRawRuleTest extends TestCase
{
public function testRawRule()
{
$data = ['foo' => 'bar'];

$rules = [
'foo' => [
Rule::raw('required'),
Rule::raw('in', 'bar'),
],
];

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, $data, $rules);

$this->assertTrue($v->passes());
$this->assertEquals($data, $v->validated());
}

public function testRawRulesSupportReservedKeywords()
{
$data = [
'foo' => '|',
'bar' => ';',
'baz' => ',',
];

$rules = [
'foo' => Rule::raw('in', '|'),
'bar' => Rule::raw('in', ';'),
'baz' => Rule::raw('in', ','),
];

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, $data, $rules);

$this->assertTrue($v->passes());
$this->assertEquals($data, $v->validated());
}

public function testRawRulesCanBeAppliedToArrays()
{
$data = [
'items' => [
['|foo' => '...', ';bar' => '...', '.baz' => '...'],
['|foo' => '...', ';bar' => '...', '.baz' => '...'],
['|foo' => '...', ';bar' => '...', '.baz' => '...'],
],
];

$rules = [
'items' => ['array'],
'items.*.name' => [
'array',
Rule::raw('required_array_keys', ['|foo', ';bar', '.baz']),
],
];

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, $data, $rules);

$this->assertTrue($v->passes());
$this->assertEquals($data, $v->validated());
}

public function testRawRulesCanBeUsedWithReservedKeywordFields()
{
$data = [
'|foo' => null,
';baz' => 'zal',
',zee' => 'fur',
];

$rules = [
'|foo' => Rule::raw('required_if', [';baz', 'zal']),
',zee' => Rule::raw('prohibited_if', [';baz', 'zal']),
];

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, $data, $rules);

$this->assertFalse($v->passes());
$this->assertEquals([
'|foo' => ['validation.required_if'],
',zee' => ['validation.prohibited_if'],
], $v->getMessageBag()->toArray());
}

public function testRawRegexRule()
{
$data = ['x' => 'asdasdf'];

$rules = ['x' => Rule::raw('regex', '/^[a-z]+$/i')];

$trans = $this->getIlluminateArrayTranslator();

$v = new Validator($trans, $data, $rules);

$this->assertTrue($v->passes());
}

protected function getTranslator()
{
return m::mock(TranslatorContract::class);
}

public function getIlluminateArrayTranslator()
{
return new Translator(
new ArrayLoader, 'en'
);
}
}
15 changes: 15 additions & 0 deletions tests/Validation/ValidationValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Illuminate\Translation\Translator;
use Illuminate\Validation\DatabasePresenceVerifierInterface;
use Illuminate\Validation\Rules\Exists;
use Illuminate\Validation\Rules\RawRule;
use Illuminate\Validation\Rules\Unique;
use Illuminate\Validation\ValidationData;
use Illuminate\Validation\ValidationException;
Expand Down Expand Up @@ -3039,6 +3040,14 @@ public function testValidationExists()
$v->setPresenceVerifier($mock);
$this->assertTrue($v->passes());

$trans = $this->getIlluminateArrayTranslator();
$v = new Validator($trans, ['email' => 'foo'], ['email' => new RawRule('Exists', ['users', 'email', 'account_id', 1, 'name', 'taylor'])]);
$mock = m::mock(DatabasePresenceVerifierInterface::class);
$mock->shouldReceive('setConnection')->once()->with(null);
$mock->shouldReceive('getCount')->once()->with('users', 'email', 'foo', null, null, ['account_id' => 1, 'name' => 'taylor'])->andReturn(1);
$v->setPresenceVerifier($mock);
$this->assertTrue($v->passes());

$trans = $this->getIlluminateArrayTranslator();
$v = new Validator($trans, ['email' => 'foo'], ['email' => 'Exists:users,email,account_id,1,name,taylor']);
$mock = m::mock(DatabasePresenceVerifierInterface::class);
Expand Down Expand Up @@ -3673,6 +3682,12 @@ public function testValidateImageDimensions()
$v = new Validator($trans, ['x' => $uploadedFile], ['x' => 'dimensions:min_height=2,ratio=3/2']);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['x' => $uploadedFile], ['x' => new RawRule('dimensions', ['min_height' => 2, 'ratio' => '3/2'])]);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['x' => $uploadedFile], ['x' => new RawRule('dimensions', ['min_height=2', 'ratio=3/2'])]);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['x' => $uploadedFile], ['x' => 'dimensions:ratio=1.5']);
$this->assertTrue($v->passes());

Expand Down