Skip to content

Commit 7aebad8

Browse files
authored
Type fixes (#49)
* refactor: allowed modifiers to optionally return a modified rule or not * docs: typed `excludeIf` correctly * test: added explicit check of both modifier flows * docs: fixed dimensions typehint * test: switch to gate facade
1 parent 02c06f8 commit 7aebad8

File tree

4 files changed

+83
-31
lines changed

4 files changed

+83
-31
lines changed

src/Rule.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ public static function digitsBetween(int $min, int $max): string
339339
* like *3/2* or a float like *1.5*.
340340
*
341341
* @link https://laravel.com/docs/11.x/validation#rule-dimensions
342-
* @param array<string, int|float> $constraints
342+
* @param array<string, int|float|string> $constraints
343343
*/
344344
public static function dimensions(array $constraints = []): Dimensions
345345
{

src/RuleSet.php

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -404,15 +404,15 @@ public function digitsBetween(int $min, int $max): self
404404
* pass a callback which accepts a {@see \Illuminate\Validation\Rules\Dimensions} instance.
405405
*
406406
* @link https://laravel.com/docs/11.x/validation#rule-dimensions
407-
* @param array<string, int|float> $constraints
408-
* @param ?callable(\Illuminate\Validation\Rules\Dimensions): void $modifier
407+
* @param array<string, int|float|string> $constraints
408+
* @param ?callable(\Illuminate\Validation\Rules\Dimensions): (\Illuminate\Validation\Rules\Dimensions|void) $modifier
409409
*/
410410
public function dimensions(array $constraints = [], ?callable $modifier = null): self
411411
{
412412
$rule = Rule::dimensions($constraints);
413413

414414
if ($modifier) {
415-
$modifier($rule);
415+
$rule = $this->modify($rule, $modifier);
416416
}
417417

418418
return $this->rule($rule);
@@ -473,14 +473,14 @@ public function endsWith(string ...$value): self
473473
*
474474
* @link https://laravel.com/docs/11.x/validation#rule-enum
475475
* @param class-string $type
476-
* @param ?callable(\Illuminate\Validation\Rules\Enum): void $modifier
476+
* @param ?callable(\Illuminate\Validation\Rules\Enum): (\Illuminate\Validation\Rules\Enum|void) $modifier
477477
*/
478478
public function enum(string $type, ?callable $modifier = null): self
479479
{
480480
$rule = Rule::enum($type);
481481

482482
if ($modifier) {
483-
$modifier($rule);
483+
$rule = $this->modify($rule, $modifier);
484484
}
485485

486486
return $this->rule($rule);
@@ -502,7 +502,7 @@ public function exclude(): self
502502
* methods if a true boolean is passed in or the passed in closure returns true.
503503
*
504504
* @link https://laravel.com/docs/11.x/validation#rule-exclude-if
505-
* @param callable|bool $callback
505+
* @param bool|callable(): bool $callback
506506
*/
507507
public function excludeIf(mixed $callback): self
508508
{
@@ -562,14 +562,14 @@ public function excludeWithout(string $anotherField): self
562562
* {@see RuleSet::rule} or pass a callback which accepts an {@see \Illuminate\Validation\Rules\Exists} instance.
563563
*
564564
* @link https://laravel.com/docs/11.x/validation#rule-exists
565-
* @param ?callable(\Illuminate\Validation\Rules\Exists): void $modifier
565+
* @param ?callable(\Illuminate\Validation\Rules\Exists): (\Illuminate\Validation\Rules\Exists|void) $modifier
566566
*/
567567
public function exists(string $table, string $column = 'NULL', ?callable $modifier = null): self
568568
{
569569
$rule = Rule::exists($table, $column);
570570

571571
if ($modifier) {
572-
$modifier($rule);
572+
$rule = $this->modify($rule, $modifier);
573573
}
574574

575575
return $this->rule($rule);
@@ -951,14 +951,14 @@ public function numeric(): self
951951
* use {@see Rule::password} with {@see RuleSet::rule}, or pass a callback which accepts a {@see Password} instance.
952952
*
953953
* @link https://laravel.com/docs/11.x/validation#validating-passwords
954-
* @param ?callable(\Illuminate\Validation\Rules\Password): void $modifier
954+
* @param ?callable(\Illuminate\Validation\Rules\Password): (\Illuminate\Validation\Rules\Password|void) $modifier
955955
*/
956956
public function password(?int $size = null, ?callable $modifier = null): self
957957
{
958958
$rule = Rule::password($size);
959959

960960
if ($modifier) {
961-
$modifier($rule);
961+
$rule = $this->modify($rule, $modifier);
962962
}
963963

964964
return $this->rule($rule);
@@ -1294,14 +1294,14 @@ public function ulid(): self
12941294
* {@see RuleSet::rule} or pass a callback which accepts a {@see \Illuminate\Validation\Rules\Unique} instance.
12951295
*
12961296
* @link https://laravel.com/docs/11.x/validation#rule-unique
1297-
* @param ?callable(\Illuminate\Validation\Rules\Unique): void $modifier
1297+
* @param ?callable(\Illuminate\Validation\Rules\Unique): (\Illuminate\Validation\Rules\Unique|void) $modifier
12981298
*/
12991299
public function unique(string $table, string $column = 'NULL', ?callable $modifier = null): self
13001300
{
13011301
$rule = Rule::unique($table, $column);
13021302

13031303
if ($modifier) {
1304-
$modifier($rule);
1304+
$rule = $this->modify($rule, $modifier);
13051305
}
13061306

13071307
return $this->rule($rule);
@@ -1353,4 +1353,16 @@ protected static function getDefinedRuleSets(): Contracts\DefinedRuleSets
13531353
{
13541354
return resolve(Contracts\DefinedRuleSets::class);
13551355
}
1356+
1357+
/**
1358+
* @param T $rule
1359+
* @param callable(T): (T|void) $modifier
1360+
* @return T
1361+
* @template T
1362+
*/
1363+
protected function modify($rule, callable $modifier)
1364+
{
1365+
/** @var T */
1366+
return $modifier($rule) ?: $rule;
1367+
}
13561368
}

tests/Unit/DatabaseRuleTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Illuminate\Foundation\Testing\WithFaker;
1010
use Illuminate\Support\Facades\DB;
1111
use Illuminate\Support\Facades\Validator;
12+
use Illuminate\Validation\Rule as LaravelRule;
1213
use Illuminate\Validation\Rules\Exists;
1314
use Illuminate\Validation\Rules\Unique;
1415
use PHPUnit\Framework\Attributes\DataProvider;
@@ -204,6 +205,51 @@ public static function databaseSetupProvider(): array
204205
],
205206
'fails' => true,
206207
],
208+
'not unique with modifier that does not return' => [
209+
'createData' => function () {
210+
$email = $this->faker->email;
211+
DB::table('users')->insert([
212+
'name' => 'modified',
213+
'email' => $email,
214+
'password' => $this->faker->password,
215+
]);
216+
217+
return ['value' => $email];
218+
},
219+
'createRules' => fn() => [
220+
'value' => RuleSet::create()->unique(
221+
'users',
222+
'email',
223+
function (Unique $rule) {
224+
$rule->where('name', 'modified');
225+
}
226+
),
227+
],
228+
'fails' => true,
229+
],
230+
'not unique with modifier that overwrites' => [
231+
'createData' => function () {
232+
$email = $this->faker->email;
233+
DB::table('users')->insert([
234+
'name' => 'overwritten',
235+
'email' => $email,
236+
'password' => $this->faker->password,
237+
]);
238+
239+
return ['value' => $email];
240+
},
241+
'createRules' => fn() => [
242+
'value' => RuleSet::create()->unique(
243+
'invalid_users',
244+
'email',
245+
function () {
246+
return LaravelRule::unique('users', 'email')
247+
->where('name', 'overwritten');
248+
}
249+
),
250+
],
251+
'fails' => true,
252+
],
207253
'unique with modifier' => [
208254
'createData' => function () {
209255
$email = $this->faker->email;

tests/Unit/RuleTest.php

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
use Closure;
1010
use DateTime;
1111
use Illuminate\Auth\AuthManager;
12-
use Illuminate\Contracts\Auth\Access\Gate;
1312
use Illuminate\Contracts\Auth\Guard;
1413
use Illuminate\Contracts\Hashing\Hasher;
1514
use Illuminate\Contracts\Validation\InvokableRule;
1615
use Illuminate\Contracts\Validation\ValidationRule;
1716
use Illuminate\Foundation\Auth\User;
17+
use Illuminate\Support\Facades\Gate;
1818
use Illuminate\Support\Facades\Validator;
1919
use Illuminate\Support\Str;
2020
use Illuminate\Validation\Rules\Dimensions;
@@ -40,7 +40,7 @@ protected function setUp(): void
4040
}
4141

4242
#[DataProvider('ruleDataProvider')]
43-
public function testRuleIntegration($data, Closure $rules, bool $fails, ?array $errors = null): void
43+
public function testRuleIntegration(mixed $data, Closure $rules, bool $fails, ?array $errors = null): void
4444
{
4545
// Arrange
4646
if ($data instanceof Closure) {
@@ -478,7 +478,11 @@ public static function ruleDataProvider(): array
478478
'data' => 'value-a',
479479
'rules' => function () {
480480
$mockUser = new User;
481-
$this->mockGateAllows(true, 'modify', [User::class, $mockUser, 'value-a']);
481+
482+
Gate::expects('allows')
483+
->once()
484+
->with('modify', [User::class, $mockUser, 'value-a'])
485+
->andReturn(true);
482486

483487
return RuleSet::create()->can('modify', User::class, $mockUser);
484488
},
@@ -488,7 +492,11 @@ public static function ruleDataProvider(): array
488492
'data' => 'value-b',
489493
'rules' => function () {
490494
$mockUser = new User;
491-
$this->mockGateAllows(false, 'modify', [User::class, $mockUser, 'value-b']);
495+
496+
Gate::expects('allows')
497+
->once()
498+
->with('modify', [User::class, $mockUser, 'value-b'])
499+
->andReturn(false);
492500

493501
return RuleSet::create()->can('modify', User::class, $mockUser);
494502
},
@@ -3114,18 +3122,4 @@ public function getClientOriginalExtension(): string
31143122
}
31153123
};
31163124
}
3117-
3118-
private function mockGateAllows(bool $return, ...$arguments): void
3119-
{
3120-
$gate = $this->mock(Gate::class);
3121-
3122-
$gate
3123-
->expects('allows')
3124-
->once()
3125-
->with(...$arguments)
3126-
->andReturn($return)
3127-
->getMock();
3128-
3129-
$this->app->instance(Gate::class, $gate);
3130-
}
31313125
}

0 commit comments

Comments
 (0)