Skip to content

Commit ad13858

Browse files
[12.x]: Add 'doesntContain' validation rule (#56467)
* feat: implement 'does not contain' validation rule * fix styling issues
1 parent 2476f3c commit ad13858

File tree

4 files changed

+175
-0
lines changed

4 files changed

+175
-0
lines changed

src/Illuminate/Validation/Concerns/ValidatesAttributes.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,29 @@ public function validateContains($attribute, $value, $parameters)
536536
return true;
537537
}
538538

539+
/**
540+
* Validate an attribute does not contain a list of values.
541+
*
542+
* @param string $attribute
543+
* @param mixed $value
544+
* @param array<int, int|string> $parameters
545+
* @return bool
546+
*/
547+
public function validateDoesntContain($attribute, $value, $parameters)
548+
{
549+
if (! is_array($value)) {
550+
return false;
551+
}
552+
553+
foreach ($parameters as $parameter) {
554+
if (in_array($parameter, $value)) {
555+
return false;
556+
}
557+
}
558+
559+
return true;
560+
}
561+
539562
/**
540563
* Validate that the password of the currently authenticated user matches the given value.
541564
*

src/Illuminate/Validation/Rule.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,21 @@ public static function contains($values)
275275
return new Rules\Contains(is_array($values) ? $values : func_get_args());
276276
}
277277

278+
/**
279+
* Get a "does not contain" rule builder instance.
280+
*
281+
* @param \Illuminate\Contracts\Support\Arrayable|\BackedEnum|\UnitEnum|array|string $values
282+
* @return \Illuminate\Validation\Rules\DoesntContain
283+
*/
284+
public static function doesntContain($values)
285+
{
286+
if ($values instanceof Arrayable) {
287+
$values = $values->toArray();
288+
}
289+
290+
return new Rules\DoesntContain(is_array($values) ? $values : func_get_args());
291+
}
292+
278293
/**
279294
* Compile a set of rules for an attribute.
280295
*
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Illuminate\Validation\Rules;
4+
5+
use Illuminate\Contracts\Support\Arrayable;
6+
use Stringable;
7+
8+
use function Illuminate\Support\enum_value;
9+
10+
class DoesntContain implements Stringable
11+
{
12+
/**
13+
* The values that should be contained in the attribute.
14+
*
15+
* @var array
16+
*/
17+
protected $values;
18+
19+
/**
20+
* Create a new doesntContain rule instance.
21+
*
22+
* @param \Illuminate\Contracts\Support\Arrayable|\BackedEnum|\UnitEnum|array|string $values
23+
*/
24+
public function __construct($values)
25+
{
26+
if ($values instanceof Arrayable) {
27+
$values = $values->toArray();
28+
}
29+
30+
$this->values = is_array($values) ? $values : func_get_args();
31+
}
32+
33+
/**
34+
* Convert the rule to a validation string.
35+
*
36+
* @return string
37+
*/
38+
public function __toString()
39+
{
40+
$values = array_map(function ($value) {
41+
$value = enum_value($value);
42+
43+
return '"'.str_replace('"', '""', $value).'"';
44+
}, $this->values);
45+
46+
return 'doesnt_contain:'.implode(',', $values);
47+
}
48+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Validation;
4+
5+
use Illuminate\Translation\ArrayLoader;
6+
use Illuminate\Translation\Translator;
7+
use Illuminate\Validation\Rule;
8+
use Illuminate\Validation\Validator;
9+
use PHPUnit\Framework\TestCase;
10+
11+
include_once 'Enums.php';
12+
13+
class ValidationRuleDoesntContainTest extends TestCase
14+
{
15+
public function testItCorrectlyFormatsAStringVersionOfTheRule()
16+
{
17+
$rule = Rule::doesntContain('Taylor');
18+
$this->assertSame('doesnt_contain:"Taylor"', (string) $rule);
19+
20+
$rule = Rule::doesntContain('Taylor', 'Abigail');
21+
$this->assertSame('doesnt_contain:"Taylor","Abigail"', (string) $rule);
22+
23+
$rule = Rule::doesntContain(['Taylor', 'Abigail']);
24+
$this->assertSame('doesnt_contain:"Taylor","Abigail"', (string) $rule);
25+
26+
$rule = Rule::doesntContain(collect(['Taylor', 'Abigail']));
27+
$this->assertSame('doesnt_contain:"Taylor","Abigail"', (string) $rule);
28+
29+
$rule = Rule::doesntContain([ArrayKeys::key_1, ArrayKeys::key_2]);
30+
$this->assertSame('doesnt_contain:"key_1","key_2"', (string) $rule);
31+
32+
$rule = Rule::doesntContain([ArrayKeysBacked::key_1, ArrayKeysBacked::key_2]);
33+
$this->assertSame('doesnt_contain:"key_1","key_2"', (string) $rule);
34+
35+
$rule = Rule::doesntContain(['Taylor', 'Taylor']);
36+
$this->assertSame('doesnt_contain:"Taylor","Taylor"', (string) $rule);
37+
38+
$rule = Rule::doesntContain([1, 2, 3]);
39+
$this->assertSame('doesnt_contain:"1","2","3"', (string) $rule);
40+
41+
$rule = Rule::doesntContain(['"foo"', '"bar"', '"baz"']);
42+
$this->assertSame('doesnt_contain:"""foo""","""bar""","""baz"""', (string) $rule);
43+
}
44+
45+
public function testDoesntContainValidation()
46+
{
47+
$trans = new Translator(new ArrayLoader, 'en');
48+
49+
// Test fails when value is string
50+
$v = new Validator($trans, ['roles' => 'admin'], ['roles' => Rule::doesntContain('admin')]);
51+
$this->assertTrue($v->fails());
52+
53+
// Test fails when array contains the value
54+
$v = new Validator($trans, ['roles' => ['admin', 'user']], ['roles' => Rule::doesntContain('admin')]);
55+
$this->assertTrue($v->fails());
56+
57+
// Test fails when array contains all the values (using array argument)
58+
$v = new Validator($trans, ['roles' => ['admin', 'user']], ['roles' => Rule::doesntContain(['admin', 'editor'])]);
59+
$this->assertTrue($v->fails());
60+
61+
// Test fails when array contains some of the values (using multiple arguments)
62+
$v = new Validator($trans, ['roles' => ['admin', 'user']], ['roles' => Rule::doesntContain('subscriber', 'admin')]);
63+
$this->assertTrue($v->fails());
64+
65+
// Test passes when array does not contain any value
66+
$v = new Validator($trans, ['roles' => ['subscriber', 'guest']], ['roles' => Rule::doesntContain(['admin', 'editor'])]);
67+
$this->assertTrue($v->passes());
68+
69+
// Test fails when array includes a value (using string-like format)
70+
$v = new Validator($trans, ['roles' => ['admin', 'user']], ['roles' => 'doesnt_contain:admin']);
71+
$this->assertTrue($v->fails());
72+
73+
// Test passes when array doesn't include a value (using string-like format)
74+
$v = new Validator($trans, ['roles' => ['admin', 'user']], ['roles' => 'doesnt_contain:editor']);
75+
$this->assertTrue($v->passes());
76+
77+
// Test fails when array doesn't contain the value
78+
$v = new Validator($trans, ['roles' => ['admin', 'user']], ['roles' => Rule::doesntContain('admin')]);
79+
$this->assertTrue($v->fails());
80+
81+
// Test with empty array
82+
$v = new Validator($trans, ['roles' => []], ['roles' => Rule::doesntContain('admin')]);
83+
$this->assertTrue($v->passes());
84+
85+
// Test with nullable field
86+
$v = new Validator($trans, ['roles' => null], ['roles' => ['nullable', Rule::doesntContain('admin')]]);
87+
$this->assertTrue($v->passes());
88+
}
89+
}

0 commit comments

Comments
 (0)