You can create a data object by the values given in the request.
For example, let's say you send a POST request to an endpoint with the following data:
{
"title" : "Never gonna give you up",
"artist" : "Rick Astley"
}
This package can automatically resolve a SongData
object from these values by using the SongData
class we saw in an earlier chapter:
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
}
You can now inject the SongData
class in your controller. It will already be filled with the values found in the request.
class SongController{
...
public function update(
Song $model,
SongData $data
){
$model->update($data->all());
return redirect()->back();
}
}
##Using validation
When creating a data object from a request, the package can also validate the values from the request that will be used to construct the data object.
It is possible to add rules as attributes to properties of a data object:
class SongData extends Data
{
public function __construct(
public string $title,
#[Max(20)]
public string $artist,
) {
}
}
When you provide an artist with a length of more than 20 characters, the validation will fail just like it would when you created a custom request class for the endpoint.
You can find a complete list of available rules here.
One special attribute is the Rule
attribute. With it, you can write rules just like you would when creating a custom Laravel request:
#[Rule(['required', 'string'])]
public string $property
#[Rule('required|string')]
public string $property
#[Rule('required', 'string')]
public string $property
It is also possible to write rules down 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'],
];
}
}
It is even possible to use the validationAttribute objects within the rules
method:
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()],
];
}
}
Rules defined within the rules
method will always overwrite automatically generated rules.
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()],
];
}
}
Additionally, if you need to access the data payload, you can use $payload
parameter:
class PaymentData extends Data
{
public function __construct(
public string $payment_method,
public ?string $paypal_email,
) {
}
public static function rules(array $payload): array
{
return [
'payment_method' => ['required'],
'paypal_email' => Rule::requiredIf($payload['payment_method'] === 'paypal'),
];
}
}
##Mapping a request onto a data object
By default, the package will do a one to one mapping from request to the data object, which means that for each property within the data object, a value with the same key will be searched within the request values.
If you want to customize this mapping, then you can always add a magical creation method like this:
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function fromRequest(Request $request): static
{
return new self(
$request->input('title_of_song'),
$request->input('artist_name')
);
}
}
##Getting the data object filled with request data from anywhere
You can resolve a data object from the container.
app(SongData::class);
We resolving a data object from the container, it's properties will allready be filled by the values of the request with matching key names.
If the request contains data that is not compatible with the data object, a validation exception will be thrown.
##Automatically inferring rules for properties
Since we have such strongly typed data objects, we can infer some validation rules from them. Rule inferrers will take information about the type of the property and will create validation rules from that information.
Rule inferrers are configured in the data.php
config file:
'rule_inferrers' => [
Spatie\LaravelData\RuleInferrers\NullableRuleInferrer::class,
Spatie\LaravelData\RuleInferrers\RequiredRuleInferrer::class,
Spatie\LaravelData\RuleInferrers\BuiltInTypesRuleInferrer::class,
Spatie\LaravelData\RuleInferrers\AttributesRuleInferrer::class,
],
By default, four rule inferrers are enabled:
-
NullableRuleInferrer will add a
nullable
rule when the property is nullable
-
RequiredRuleInferrer will add a
required
rule when the property is not nullable
-
BuiltInTypesRuleInferrer will add a rules which are based upon the built-in php types:
- An
int
or float
type will add the numeric
rule
- A
bool
type will add the boolean
rule
- A
string
type will add the string
rule
- A
array
type will add the array
rule
-
AttributesRuleInferrer will make sure that rule attributes we described above will also add their rules
It is possible to write your rule inferrers. You can find more information here.
##Skipping validation
Sometimes you don't want properties to be automatically validated, for instance when you're manually overwriting the rules method like this:
class SongData extends Data
{
public function __construct(
public string $name,
) {
}
public static function fromRequest(Request $request): static{
return new self("{$request->input('first_name')} {$request->input('last_name')}")
}
public static function rules(): array
{
return [
'first_name' => ['required', 'string'],
'last_name' => ['required', 'string'],
];
}
}
When a request is being validated, the rules will look like this:
[
'name' => ['required', 'string'],
'first_name' => ['required', 'string'],
'last_name' => ['required', 'string'],
]
We know we never want to validate the name
property since it won't be in the request payload, this can be done as such:
class SongData extends Data
{
public function __construct(
#[WithoutValidation]
public string $name,
) {
}
}
Now the validation rules will look like this:
[
'first_name' => ['required', 'string'],
'last_name' => ['required', 'string'],
]
##Overwriting the validator
Before validating the values, it is possible to plugin into the validator. This can be done as such:
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function withValidator(Validator $validator): void
{
$validator->after(function ($validator) {
$validator->errors()->add('field', 'Something is wrong with this field!');
});
}
}
##Overwriting messages
It is possible to overwrite the error messages that will be returned when an error fails:
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function messages(): array
{
return [
'title.required' => 'A title is required',
'artist.required' => 'An artist is required',
];
}
}
You can also provide dependencies to be injected:
use SongSettingsRepository;
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function messages(SongSettingsRepository $repository): array
{
return [
'title.required' => 'A '. $repository->forUser(auth()->user())->title_word .' is required',
'artist.required' => 'An artist is required',
];
}
}
##Overwriting attributes
In the default Laravel validation rules, you can overwrite the name of the attribute as such:
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function attributes(): array
{
return [
'title' => 'titel',
'artist' => 'artiest',
];
}
}
You can also provide dependencies to be injected:
use SongSettingsRepository;
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function attributes(SongSettingsRepository $repository): array
{
return [
'title' => 'titel',
'artist' => 'artiest',
] + $repository->getAttributes();
}
}
##Authorizing a request
Just like with Laravel requests, it is possible to authorize an action for certain people only:
class SongData extends Data
{
public function __construct(
public string $title,
public string $artist,
) {
}
public static function authorize(): bool
{
return Auth::user()->name === 'Ruben';
}
}
If the method returns false
, then an AuthorizationException
is thrown.
##Validating a collection of data objects:
Let's say we want to create a data object like this from a request:
class AlbumData extends Data
{
public function __construct(
public string $title,
/** @var SongData[] */
public DataCollection $songs,
) {
}
}
Since the SongData
has its own validation rules, the package will automatically apply them when resolving validation rules for this object.
In this case the validation rules for AlbumData
would look like this:
[
'title' => ['required', 'string'],
'songs' => ['required', 'array'],
'songs.*.title' => ['required', 'string'],
'songs.*.artist' => ['required', 'string'],
]
##Validating a data object without request
It is also possible to validate values for a data object without using a request:
SongData::validate(['title' => 'Never gonna give you up', 'artist' => 'Rick Astley']);
When the validation passes, a new data object is returned with the values. When the validation fails, a Laravel ValidationException
is thrown.