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

Add the ability to optionally merge automatically inferred rules with manual rules #848

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
22 changes: 22 additions & 0 deletions docs/validation/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,28 @@ class SongData extends Data
}
```

The above example will override the automatically inferred rules. If you want the manual rules to be merged with the automatically inferred rules, you can use the `MergeRules` attribute:

```php
#[MergeRules]
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}

public static function rules(): array
{
return [
'title' => ['max:255'],
'artist' => ['max:255'],
];
}
}
```

You can read more about manual rules in its [dedicated chapter](/docs/laravel-data/v4/validation/manual-rules).

### Using the container
Expand Down
11 changes: 11 additions & 0 deletions src/Attributes/MergeRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Spatie\LaravelData\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class MergeRules
{
//
}
24 changes: 20 additions & 4 deletions src/Resolvers/DataValidationRulesResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Spatie\LaravelData\Attributes\MergeRules;
use Spatie\LaravelData\Attributes\Validation\ArrayType;
use Spatie\LaravelData\Attributes\Validation\Present;
use Spatie\LaravelData\Support\DataClass;
Expand Down Expand Up @@ -67,7 +68,7 @@ public function execute(
$dataRules->add($propertyPath, $rules);
}

$this->resolveOverwrittenRules(
$this->resolveRules(
$dataClass,
$fullPayload,
$path,
Expand Down Expand Up @@ -223,7 +224,7 @@ protected function resolveToplevelRules(
}


protected function resolveOverwrittenRules(
protected function resolveRules(
Copy link
Member

Choose a reason for hiding this comment

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

Let us rename this to appendOverwrittenRules, resolveRules is too general.

DataClass $class,
array $fullPayload,
ValidationPath $path,
Expand All @@ -240,9 +241,19 @@ protected function resolveOverwrittenRules(
$path
);

$overwrittenRules = app()->call([$class->name, 'rules'], ['context' => $validationContext]);
$manualRules = app()->call([$class->name, 'rules'], ['context' => $validationContext]);
Copy link
Member

Choose a reason for hiding this comment

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

Let's keep this as overwrittenRules.


foreach ($overwrittenRules as $key => $rules) {
if ($this->shouldMergeRules($class)) {
$manualRules = collect($manualRules)->map(
fn (string|array $rules) => is_array($rules) ? $rules : explode('|', $rules)
)->all();
Comment on lines +247 to +249
Copy link
Member

Choose a reason for hiding this comment

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

Why not use the RuleDenormalizer here?

Also: when we add an attribute not to validate the property it will still be validated we don't want that.

I think a better solution would be to implement the logic within the loop where addRules isn't called since it replaces them but rather rules are merged over there (or not if the property shouldn"t be validated).


$dataRules->rules = array_merge_recursive($dataRules->rules, $manualRules);

return;
}

foreach ($manualRules as $key => $rules) {
if (in_array($key, $withoutValidationProperties)) {
continue;
}
Expand Down Expand Up @@ -278,4 +289,9 @@ protected function inferRulesForDataProperty(
$path
);
}

protected function shouldMergeRules(DataClass $class): bool
{
return $class->attributes->contains(fn (object $attribute) => $attribute::class === MergeRules::class);
Copy link
Member

Choose a reason for hiding this comment

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

No need for an extra function here, just inline the variable

}
}
50 changes: 50 additions & 0 deletions tests/Attributes/Validation/MergeRulesetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

use Illuminate\Validation\ValidationException;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Tests\Fakes\DataWithMergedRuleset;
use Spatie\LaravelData\Tests\Fakes\DataWithMergedStringRuleset;

it('it will merge validation rules', function () {
try {
DataWithMergedRuleset::validate(['first_name' => str_repeat('a', 1)]);
} catch (ValidationException $exception) {
expect($exception->errors())->toMatchArray([
'first_name' => ['The first name field must be at least 2 characters.']
]);
}

try {
DataWithMergedRuleset::validate(['first_name' => str_repeat('a', 11)]);
} catch (ValidationException $exception) {
expect($exception->errors())->toMatchArray([
'first_name' => ['The first name field must not be greater than 10 characters.']
]);
}
});

it('it will merge validation rules using string rules', function () {
try {
Data::validate(['first_name' => str_repeat('a', 1)]);
} catch (ValidationException $exception) {
expect($exception->errors())->toMatchArray([
'first_name' => ['The first name field must be at least 2 characters.']
]);
}

try {
DataWithMergedStringRuleset::validate(['first_name' => str_repeat('a', 11)]);
} catch (ValidationException $exception) {
expect($exception->errors())->toMatchArray([
'first_name' => ['The first name field must not be greater than 10 characters.']
]);
}

try {
DataWithMergedStringRuleset::validate(['first_name' => 'a123']);
} catch (ValidationException $exception) {
expect($exception->errors())->toMatchArray([
'first_name' => ['The first name field must only contain letters.']
]);
}
});
24 changes: 24 additions & 0 deletions tests/Fakes/DataWithMergedRuleset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Spatie\LaravelData\Tests\Fakes;

use Spatie\LaravelData\Attributes\MergeRules;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Data;

#[MergeRules]
class DataWithMergedRuleset extends Data
{
public function __construct(
#[Max(10)]
public string $first_name,
) {
}

public static function rules(): array
{
return [
'first_name' => ['min:2']
];
Comment on lines +18 to +22
Copy link
Member

Choose a reason for hiding this comment

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

Inline these classes within the tests, that makes the fakes directory a bit less heavy.

}
}
24 changes: 24 additions & 0 deletions tests/Fakes/DataWithMergedStringRuleset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Spatie\LaravelData\Tests\Fakes;

use Spatie\LaravelData\Attributes\MergeRules;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Data;

#[MergeRules]
class DataWithMergedStringRuleset extends Data
{
public function __construct(
#[Max(10)]
public string $first_name,
) {
}

public static function rules(): array
{
return [
'first_name' => 'min:2|alpha'
];
}
}