Laravel / nested validation - php

I am building a component-driven API for a basic page-builder system and have hit a stumbling block when it comes to validation.
First, I want to explain the use-case.
if we have a component (for example in Vue) in /components/ProfileCard.vue
<script>
export default {
props: {
name: String,
age: Number,
avatar: String
}
}
</script>
I am creating a component in the backend components.php config:
<?php
return [
'profile' => [
'component' => 'ProfileCard',
'rules' => [
'name' => [
'required',
],
'age' => [
'required',
'number',
],
'avatar' => [
'required',
]
],
],
];
Which checks and validates every time a profile card component is submitted.
Creating a custom validation rule for Component, I am able to say "the ProfileCard component is not valid" but I am not able to merge / nest the validation rules:
Component.php
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Validator;
class Component implements Rule
{
protected $validator = null;
public function passes($attribute, $value)
{
$components = config('components');
$component = $value['component'];
if (isset($components[$component])) {
return false;
}
$c = $components[$component];
$this->validator = Validator::make($value['data'], $c['rules'], $c['messages'] ?? '');
return $this->validator->passes();
}
public function message()
{
if (is_null($this->validator)) {
return 'The component does not exist';
}
return $this->validator->errors();
}
}
Has anybody got any experience doing anything like this or can anybody point me in the right direction towards a solution?
I am ideally looking for a solution which is applicable while using Laravel's FormRequest validation, like so:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Unique;
use App\Rules\Component;
class CreateUserRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'email' => [
'required',
'email',
new Unique('users', 'email'),
],
'profile' => [
'required',
new Component(),
]
];
}
}
The data would come in like so:
{
"email": "test#example.com",
"profile": {
"component": "ProfileCard",
"data": {
"name": "Test",
"age": 49,
"avatar": "https://example.com/avatar.png"
}
}
}
I have updated the question with the progress I have made myself, you can return a MessageBag in the messages method on the rule, however, this creates a slight problem, the response comes back as follows:
"message": "The given data was invalid.",
"errors": {
"profile": [
{
"name": [
"The name field is required."
],
"age": [
"The age field is required."
],
"avatar": [
"The avatar field is required."
],
},
":message"
]
}
Clearly this is an improvement but it's still not as usable, we don't have a ':message' and the validation errors are nested in an object in the "profile" array.

Your approach seems like you are over complicating a simple problem. I would never do a validation in a validation rule. Instead do rules that is dependent on the component and adjust it in the form request accordingly. You can easily do nested rules like so.
[
'profile.name' => 'string',
]
Do the rest of the logic in the form request. The strategy is to have rules based on the request input and your config file, based on what you already tried.
public function rules()
{
// i do not know how you determine this key
$componentKey = 'profile';
$rules = [
...,
$componentKey => [
'required',
]
];
$inputComponent= $this->input('profile')['component'];
$components = config('components');
// based on your data this seems wrong, but basically fetch the correct config entry
$component = $components[$inputComponent];
foreach ($component['rules'] as $key => $value) {
$rules[$componentKey . '.' . $key] => $value;
}
return $rules;
}
There is some parts of your code where i can't figure out what your data means, i do not know how you get the component key profile and your code based on the config and the component field seems wrong and should instead do a loop with a where condition. I think that this can get you in the right direction, this solution will solve your message problem and be way simpler.

Related

Creating two seperate validation messages for the same attribute

I'm trying to create two separate validation messages for the same validation attribute.
There are two rules that use "before_or_equal" - the end_time has to be "before_or_equal" start_time and also the end_time has to be after 5:00 (time). The validation works, but I can't seem to find a way to create a working custom message for the latter.
I tried to specify the rule by including it literally with the value, but it doesn't seem to work.
This is what the custom request validation looks like for the end_time at the moment.
public function rules()
{
return [
'end_time' => ['after_or_equal:start_time', 'after_or_equal:5:00'],
];
}
public function messages()
{
return [
'end_time.after_or_equal' => 'Message 1',
'end_time.after_or_equal:5:00' => 'Message 2',
];
}
You can use :date for your custom error messages.
Example:
public function rules()
{
return [
'end_time' => ['after_or_equal:start_time', 'after_or_equal:5:00'],
];
}
public function messages()
{
return [
'end_time.after_or_equal' => 'the :attribute time must be after :date',
];
}
The replaced value is actual value of first input of the validator
i don't know if i understand your question correctly, but are you looking for something like this ?
public function rules()
{
return $this->messages("end_time", [
"after_or_equal",
"after_or_equal:5:00",
]);
}
public function messages(string $Key, array $CustomAttributes)
{
$Exceptions = [
"end_time" => [
"after_or_equal" => "Message 1",
"after_or_equal:5:00" => "Message 2"
]
];
$Exception = [
$Key => []
];
foreach ($CustomAttributes as $Attribute) {
array_push($Exception[$Key], $Exceptions[$Key][$Attribute]);
}
return $Exception;
}

Laravel display validation error in multiple languages at the same time

I am working on returning validation errors in multiple languages at the same time.
I have a controller that injects a class that extends FormRequest and I am overriding 'failedValidation' and there I get the validator error messages.
public function store(SysUserStoreRequest $request)
{
// ...
}
class SystemUserStoreRequest extends ApiRequest
{
// This extends another class ApiRequest
Here I defined rules()
}
class APIRequest extends FormRequest
{
// Here I override the FailedValidation method.
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException($this->response($validator->getMessageBag()->toArray()));
}
}
The above code currently returns the error in the default language.
I can change the response to show in a different language by changing the locale in the middleware, but I am working on requirement where I need to return a new structure of validation error with each field errors in both en and fr.
I need the structure like below:
{
"detail": {
"email": {
"en-CA" : [
"The email has already been taken."
],
"fr-CA" : [
"The french text."
]
},
"first_name": {
"en-CA" : [
"The first name must be at least 5.",
"The first name must be an integer."
],
"fr-CA" : [
"The french text",
"The french text."
]
}
}
}
So i tired to override the failedValidation method and do something like below:
$validation_structure = [];
if ($validator->fails()) {
$failedRules = $validator->failed();
}
after getting the al the failed rules i can then get the string from the lang folder for each locale and get the string for the field and rule and generate the message using
$x[] = __('validation.unique', [], 'fr-CA');
$x[] = __('validation.unique', [], 'en-CA');
this will give me the string in both labguages but I do not know how to replace the :attributes, :values and various other string replacements.
You could overwrite the message bag the SystemUserStoreRequest will give back to format the messsages.
class SystemUserStoreRequest extends ApiRequest
{
public function rules()
{
return [
'email' => 'required|unique:users,email,' . $this->id,
];
}
public function messages()
{
return [
'email.required' => [
'nl' => __('validation.required', ['attribute' => __('portal.email', [],'nl')], 'nl'),
'en' => __('validation.required', ['attribute' => __('portal.email', [],'en')], 'en'),
],
'email.unique' => [
'nl' => __('validation.unique', ['attribute' => __('portal.email', [],'nl')], 'nl'),
'en' => __('validation.unique', ['attribute' => __('portal.email', [],'en')], 'en'),
]
];
}
}
Then the output would look like:
{
"message":"The given data was invalid.",
"errors":{
"email":[
{
"nl":"E-mailadres is verplicht.",
"en":"The email field is required."
}
]
}
}
Here is some more documentation about custom messages:
https://laravel.com/docs/8.x/validation#specifying-custom-messages-in-language-files

Required If validation for Laravel array with empty value

I have the following input
{
"password":"password",
"environment_roles":[
{
"environment_id":"",
"role_id":""
}
],
"admin":true
}
and have a Request class with following rules :
public function rules()
{
return [
'password' => 'required|min:6|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/',
'environment_roles' => 'array',
'environment_roles.*.role_id' => 'required_if:admin,false|exists:roles,role_id',
'environment_roles.*.environment_id' => 'required_if:admin,false|exists:environment,environment_id',
'admin' => 'sometimes'
];
}
But it is showing the following validation error if I give the above input, which has admin as true.
"validation": {
"environment_roles.0.role_id": [
"The selected environment_roles.0.role_id is invalid."
],
"environment_roles.0.environment_id": [
"The selected environment_roles.0.environment_id is invalid."
]
},
How can I fix this. I need to validate the environment_roles.*.role_id and environment_roles.*.environment_id when the value for admin is true.
If you are always sending the admin prop it would be more suitable to make it nullable not required. You could try with that:
public function rules()
{
return [
'password' => 'required|min:6|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/',
'environment_roles' => 'array',
'environment_roles.*.role_id' => 'nullable|required_if:admin,false|exists:roles,role_id',
'environment_roles.*.environment_id' => 'nullable|required_if:admin,false|exists:environment,environment_id',
'admin' => 'bool|sometimes'
];
}
But your error shows that the role and the environment id's does not exist in the database ( The exists rule ). Setting those two fields to nullable means it will not trigger the exists rule.

How to validate a JSON post with Dingo API in Laravel?

I'm trying to validate a json I'm getting via post, using the Dingo API library in Laravel. It seems that the validation is working incorrectly as I send a valid JSON according to the fields I am validating and it returns me the message saying:
The X field is required.
But I'm sending the X field in json, which I do not understand.
JSON:
[
{
"currency_id": 1,
"bills": [
{
"barcode": "99999.9999999.99999999.9999 9",
"due_date": "2018-09-14",
"value": 70.00
},
{
"barcode": "8888.888888.88888.8888 8",
"due_date": "2018-09-15",
"value": 32.00
}
]
}
]
I'm getting this error:
"message": "422 Unprocessable Entity",
"errors": {
"currency_id": [
"The currency id field is required."
],
"bills": [
"The bills field is required."
]
},
This is my custom FormRequest with validation rules, where I pass it as a parameter in the BillController store method.
namespace App\Http\Requests;
use App\Rules\Sum;
use Dingo\Api\Http\FormRequest;
class BillRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'currency_id' => 'required|integer|exists:currency,id',
'bills' => ['required', 'array', 'min:1', 'max:3', new Sum],
'bills.*.barcode' => 'required|string|min:10|max:255',
'bills.*.due_date' => 'date',
'bills.*.value' => 'required|numeric|between:10,30000',
];
}
}
To solve I had to put *. in front of each rule, as I'm getting an array the validator only understands this way.
public function rules()
{
return [
'*.currency_id' => 'required|integer|exists:currency,id',
'*.bills' => ['required', 'array', 'min:1', 'max:3', new Sum],
'*.bills.*.barcode' => 'required|string|min:10|max:255',
'*.bills.*.due_date' => 'date',
'*.bills.*.value' => 'required|numeric|between:10,30000',
];
}

Customizing The Flashed Error Format - Laravel 5.3

I want to customize the format of flashed error message, that is recieved after i do something like $this->validator($request->all())->validate();
For an ajax request, it responds with:
{
"name": [
"The name field is required."
],
"email": [
"The email field is required."
],
}
But i want it to look like
{
"status": "fail",
"errors": {
"name": [
"The name field is required."
],
"email": [
"The email field is required."
],
}
}
I read the documentation under Customizing The Flashed Error Format and added formatValidationErrors method in my Controller class, but it's not making any difference.
public function formatValidationErrors(Validator $validator)
{
return ['status' => 'fail', 'errors' => $validator->errors()->getMessages()];
}
It's not changing even if i change formatValidationErrors method in the original Illuminate\Foundation\Validation\ValidatesRequests trait
I'm forced have to build this format in all request calls and so there's code-duplication. It would be nice if i could just call $this->validator($request->all())->validate() and it automatically formats as per my requirement.
The way to change it is to create a class that extends Validator and override the addError method. Here's a sample code:
<?php
namespace App\Validators;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\Validator;
class RestValidator extends Validator {
/**
* Add an error message to the validator's collection of messages.
*
* #param string $attribute
* #param string $rule
* #param array $parameters
* #return void
*/
protected function addError($attribute, $rule, $parameters)
{
$message = $this->getMessage($attribute, $rule);
$message = $this->doReplacements($message, $attribute, $rule, $parameters);
$customMessage = new MessageBag();
$customMessage->merge(['code' => strtolower($rule.'_rule_error')]);
$customMessage->merge(['message' => $message]);
$this->messages->add($attribute, $customMessage);
}
}
Now you can structure the validation in your controllers like so:
$rules = [
// Your rules here
];
$attributes = [
// The attributes you're checking here
];
$validator = Validator::make($attributes, $rules);
if ($validator->fails()) {
$errorMessage = [
'status' => 'fail',
'errors' => $validator->errors()
];
return $errorMessage;
}
// The rest of your code goes here
Try this in your model,
public function response(array $errors)
{
if (($this->ajax() && !$this->pjax()) || $this->wantsJson()) {
$errors = array('status' => 'fail', 'errors' => $errors);
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
It will result as you expected,
{
"status": "fail",
"error": {
"name": [
"The name field is required."
],
"email": [
"The email field is required"
]
}
}

Categories