It is also possible to write rules down manually in a dedicated method on the data object. This can come in handy when you want to construct a custom rule object which isn’t possible with attributes:
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function rules(): array
{
return [
'title' => ['required', 'string'],
'artist' => ['required', 'string'],
];
}
}
By overwriting a property’s rules within the rules method, no other rules will be inferred automatically anymore for that property.
This means that in the following example, only a max:20 rule will be added, and not a string and required rule:
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function rules(): array
{
return [
'title' => ['max:20'],
'artist' => ['max:20'],
];
}
}
// The generated rules will look like this
[
'title' => ['max:20'],
'artist' => ['max:20'],
]
As a rule of thumb always follow these rules:Always use the array syntax for defining rules and not a single string which splits the rules by | characters.
This is needed when using regexes since those | can be seen as part of the regex.
Merging manual rules
Writing manual rules doesn’t mean that you can’t use the automatic rules inferring anymore. By adding the MergeValidationRules attribute to your data class, the rules will be merged:
use Spatie\LaravelData\Attributes\MergeValidationRules;
#[MergeValidationRules]
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function rules(): array
{
return [
'title' => ['max:20'],
'artist' => ['max:20'],
];
}
}
// The generated rules will look like this
[
'title' => ['required', 'string', 'max:20'],
'artist' => ['required', 'string', 'max:20'],
]
Using attributes
It is even possible to use the validationAttribute objects within the rules method:
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\StringType;
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function rules(): array
{
return [
'title' => [new Required(), new StringType()],
'artist' => [new Required(), new StringType()],
];
}
}
You can even add dependencies to be automatically injected:
use SongSettingsRepository;
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function rules(SongSettingsRepository $settings): array
{
return [
'title' => [new RequiredIf($settings->forUser(auth()->user())->title_required), new StringType()],
'artist' => [new Required(), new StringType()],
];
}
}
Using context
Sometimes a bit more context is required, in such a case a ValidationContext parameter can be injected as such:
use Spatie\LaravelData\Support\Validation\ValidationContext;
use Illuminate\Validation\Rule;
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function rules(ValidationContext $context): array
{
return [
'title' => ['required'],
'artist' => Rule::requiredIf($context->fullPayload['title'] !== 'Never Gonna Give You Up'),
];
}
}
Full payload vs relative payload
By default, the provided payload is the whole request payload provided to the data object. If you want to generate rules in nested data objects, then a relative payload can be more useful:
class AlbumData extends Data
{
/**
* @param array<SongData> $songs
*/
public function __construct(
public string $title,
public array $songs,
) {
}
}
class SongData extends Data
{
public function __construct(
public string $title,
public ?string $artist,
) {
}
public static function rules(ValidationContext $context): array
{
return [
'title' => ['required'],
'artist' => Rule::requiredIf($context->payload['title'] !== 'Never Gonna Give You Up'),
];
}
}
When providing such a payload:
[
'title' => 'Best songs ever made',
'songs' => [
['title' => 'Never Gonna Give You Up'],
['title' => 'Heroes', 'artist' => 'David Bowie'],
],
];
The rules will be:
[
'title' => ['string', 'required'],
'songs' => ['present', 'array'],
'songs.*.title' => ['string', 'required'],
'songs.*.artist' => ['string', 'nullable'],
'songs.*' => [NestedRules(...)],
]
Accessing the validation path
It is also possible to retrieve the current path in the data object chain we’re generating rules for right now by calling $context->path. In the case of our previous example this would be songs.0 and songs.1.
Make sure the name of the parameter is $context in the rules method, otherwise no context will be injected.