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

[8.x] Conditional rules #38361

Merged
merged 5 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/Illuminate/Validation/ConditionalRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Illuminate\Validation;

class ConditionalRules
{
/**
* The boolean condition indicating if the rules should be added to the attribute.
*
* @var callable|bool
*/
protected $condition;

/**
* The rules to be added to the attribute.
*
* @var array
Copy link
Contributor

@sebdesign sebdesign Aug 18, 2021

Choose a reason for hiding this comment

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

This should be array|string.

Copy link
Member

Choose a reason for hiding this comment

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

Can you send in a PR?

*/
protected $rules;

/**
* Create a new conditional rules instance.
*
* @param callable|bool $condition
* @param array|string $rules
* @return void
*/
public function __construct($condition, $rules)
{
$this->condition = $condition;
$this->rules = $rules;
}

/**
* Determine if the conditional rules should be added.
*
* @param array $data
* @return bool
*/
public function passes(array $data = [])
{
return is_callable($this->condition)
? call_user_func($this->condition, $data)
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrapping $data in Illuminate\Support\Fluent would make accessing the inputs slightly cleaner ($inputs->key) through the __get helper that checks if the property exists and otherwise return null, as opposed to needing to use Arr::get($inputs, 'key') or $inputs['key'] ?? null.

Suggested change
? call_user_func($this->condition, $data)
? call_user_func($this->condition, new Fluent($data))

Not sure if it's best to put it here or in the Validator on line 1085. Applied here, the setters would have no side-effects on the other conditionals.

: $this->condition;
}

/**
* Get the rules.
*
* @return array
*/
public function rules()
{
return is_string($this->rules) ? explode('|', $this->rules) : $this->rules;
}
}
12 changes: 12 additions & 0 deletions src/Illuminate/Validation/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ class Rule
{
use Macroable;

/**
* Create a new conditional rule set.
*
* @param callable|bool $condition
* @param array|string $rules
* @return \Illuminate\Validation\ConditionalRules
*/
public static function when($condition, $rules)
{
return new ConditionalRules($condition, $rules);
}

/**
* Get a dimensions constraint builder instance.
*
Expand Down
29 changes: 29 additions & 0 deletions src/Illuminate/Validation/ValidationRuleParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,33 @@ protected static function normalizeRule($rule)
return $rule;
}
}

/**
* Expand and conditional rules in the given array of rules.
*
* @param array $rules
* @param array $data
* @return array
*/
public static function filterConditionalRules($rules, array $data = [])
{
return collect($rules)->mapWithKeys(function ($attributeRules, $attribute) use ($data) {
if (! is_array($attributeRules) &&
! $attributeRules instanceof ConditionalRules) {
return [$attribute => $attributeRules];
}

if ($attributeRules instanceof ConditionalRules) {
return [$attribute => $attributeRules->passes($data) ? $attributeRules->rules() : null];
}

return [$attribute => collect($attributeRules)->map(function ($rule) use ($data) {
if (! $rule instanceof ConditionalRules) {
return [$rule];
}

return $rule->passes($data) ? $rule->rules() : null;
})->filter()->flatten(1)->values()->all()];
})->filter()->all();
}
}
2 changes: 1 addition & 1 deletion src/Illuminate/Validation/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@ public function addRules($rules)
// of the explicit rules needed for the given data. For example the rule
// names.* would get expanded to names.0, names.1, etc. for this data.
$response = (new ValidationRuleParser($this->data))
->explode($rules);
->explode(ValidationRuleParser::filterConditionalRules($rules, $this->data));

$this->rules = array_merge_recursive(
$this->rules, $response->rules
Expand Down
32 changes: 32 additions & 0 deletions tests/Validation/ValidationRuleParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Illuminate\Tests\Validation;

use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationRuleParser;
use PHPUnit\Framework\TestCase;

class ValidationRuleParserTest extends TestCase
{
public function test_conditional_rules_are_properly_expanded_and_filtered()
{
$rules = ValidationRuleParser::filterConditionalRules([
'name' => Rule::when(true, ['required', 'min:2']),
'email' => Rule::when(false, ['required', 'min:2']),
'password' => Rule::when(true, 'required|min:2'),
'username' => ['required', Rule::when(true, ['min:2'])],
'address' => ['required', Rule::when(false, ['min:2'])],
'city' => ['required', Rule::when(function (array $input) {
return true;
}, ['min:2'])],
]);

$this->assertEquals([
'name' => ['required', 'min:2'],
'password' => ['required', 'min:2'],
'username' => ['required', 'min:2'],
'address' => ['required'],
'city' => ['required', 'min:2'],
], $rules);
}
}