Lumen provide a code for validation errors - php

Currently in lumen when you use the $this->validate($request, $rules) function inside of a controller it will throw a ValidationException with error for your validation rules(if any fail of course).
However, I need to have a code for every validation rule. We can set custom messages for rules, but I need to add a unique code.
I know there's a "formatErrorsUsing" function, where you can pass a formatter. But the data returned by the passed argument to it, has already dropped the names of the rules that failed, and replaced them with their messages. I of course don't want to string check the message to determine the code that should go there.
I considered setting the message of all rules to be "CODE|This is the message" and parsing out the code, but this feels like a very hacked solution. There has to be a cleaner way right?

I've solved this for now with the following solution:
private function ruleToCode($rule) {
$map = [
'Required' => 1001,
];
if(isset($map[$rule])) {
return $map[$rule];
}
return $rule;
}
public function formatValidationErrors(Validator $validator) {
$errors = [];
foreach($validator->failed() as $field => $failed) {
foreach($failed as $rule => $params) {
$errors[] = [
'code' => $this->ruleToCode($rule),
'field' => $field,
];
}
}
return $errors;
}

Related

Laravel 9 API return error codes instead of error messages on validation

I am trying to make Laravel return error CODES instead of messages along with some extra data depending on the validation error.
Quick examples:
If I have a "unique" validation rule I would expect the result for that field to be:
<field>: {
"code": "unique_rule_error",
"message": "The username has already been taken."
}
If I have, let's say a min rule:
<field>: {
"code": "min_rule_error",
"value": "100",
"message": "The username must be at least 100 characters."
}
In both examples, the "message" field is unnecessary, I am just keeping it there for the time being.
What I have currently working and almost returning the desired results:
App\Validators\RestValidator (custom validator, extending the base Validator class)
class RestValidator extends Validator
{
public function addFailure($attribute, $rule, $parameters = [])
{
if (!$this->messages) {
$this->passes();
}
$attribute = str_replace(
[$this->dotPlaceholder, '__asterisk__'],
['.', '*'],
$attribute
);
if (in_array($rule, $this->excludeRules)) {
return $this->exclude`enter code here`Attribute($attribute);
}
$message = $this->getMessage($attribute, $rule);
$message = $this->makeReplacements($message, $attribute, $rule, $parameters);
$customMessage = new MessageBag();
$customMessage->merge(['code' => strtolower($rule . '_rule_error')]);
if ($rule !== 'Unique') {
$parts = explode(':', $this->currentRule);
if (count($parts) >= 2) {
$boundaries = explode(',', $parts[1]);
$boundaries_count = count($boundaries);
if ($boundaries_count == 1) {
$customMessage->merge(['value' => $boundaries[0]]);
}
if ($boundaries_count > 1) {
$customMessage->merge(['lower' => $boundaries[0]]);
}
if ($boundaries_count >= 2) {
$customMessage->merge(['upper' => $boundaries[1]]);
}
}
}
$customMessage->merge(['message' => $message]);
$this->messages->add($attribute, $customMessage);
}
}
AppServiceProvider
public function boot()
{
//
Validator::resolver(function ($trnslator, $data, $rules, $messages) {
return new RestValidator($trnslator, $data, $rules, $messages);
});
}
This gives me the results I want with all rules I have currently tried, except when I use the Password validation:
Password::min(8)->mixedCase()->numbers()->symbols()
This causes a rather strange behavior that I can't explain, the result is the following:
"password": [
{
"code": "confirmed_rule_error",
"message": "The password confirmation does not match."
},
"{\"code\":\"min_rule_error\",\"value\":\"8\",\"message\":\"The password must be at least 8 characters.\"}",
"The password must contain at least one uppercase and one lowercase letter.",
"The password must contain at least one symbol."
]
The complete password validation code is:
'password' => ['required', 'confirmed', Password::min(8)->mixedCase()->numbers()->symbols()]
I want to return error codes like "unique_rule_error" because I will be translating the messages in the front-end with Vue and I don't want to keep track of a locale on the API side.
The problem seems to be that Laravel handles responses for strings and classed defined rules in different sections of code.
Laravel 9.34.0 has this chunk of code in Illuminate\Validation\Validator#603-607.
In your code, the "confirmed" rule is defined as string, hence in #603, the if statement is false, and eventually in #612 the addFailure method is called in RestValidator.
The next rule would be Password::... which is indeed an instance of RuleContract thus an early return is executed preventing the #612 addFailure method in RestValidator to be executed.
Checking the validateUsingCustomRule method in this same Illuminate\Validation\Validator class we find that in #829 an add method is called to populate error messages, you may want to add your logic there as well.

Create rule to make request only contain certain keys

I am using the Lumen Framework, which utilizes the Laravel Validation
I wanted to create a Validator Rule to make the Request->input() json only contain specific keys at the root like "domain" and "nameservers". Not more and not less.
Example passing the rule:
{
"domain":"domain.tld",
"nameservers":
{...}
}
Example not passing the rule:
{
"domain":"domain.tld",
"nameservers":
{...},
"Hack":"executeSomething()"
}
I tried to use to use several default validation rules to achieve this but wasnt successful.
My approach was now to put the request in another array like this
$checkInput['input'] = $request->all();
to make the validator validate the "root" keys.
Now this is my Approach:
create the validator
$checkInput['input'] = $request->all();
$validator = Validator::make($checkInput, [
'input' => [
'onlyContains:domain,nameservers'
],
]);
creating the rule
Validator::extend('onlyContains', function($attribute, $value, $parameters, $validator){
$input = $validator->getData();
$ok = 0;
foreach ($parameters as $key => $value) {
if (Arr::has($input, $attribute . '.' . $value)) {
$ok++;
}
}
if (sizeof(Arr::get($input, $attribute)) - $ok > 0) {
return false;
}
return true;
});
It seems i got the desired result, but i am asking if there is maybe smarter solution to this with the default rules provided by Laravel/Lumen.
You are trying to do a blacklisting approach blocking out fields that are not intended. A simple approach, that is utilized a lot, is to only fetch out the validated. Also you are trying to do logic, that goes against normal validation logic, to do it a field at a time.
This is also a good time, to learn about FormRequest and how you can get that logic, into a place where it makes more sense.
public function route(MyRequest $request) {
$input = $request->validated();
}
With this approach, you will only ever have the validated fields in the $input variable. As an extra bonus, this approach will make your code way easier to pick up by other Laravel developers. Example form request below.
public class MyRequest extends FormRequest
{
public function rules()
{
return [
'domain' => ['required', 'string'],
'nameservers' => ['required', 'array'],
];
}
}
You should use prohibited rule.
For eg:
$allowedKeys = ['domain', 'nameservers'];
$inputData = $request->all();
$inputKeys = array_keys($inputData);
$diffKeys = array_diff($inputKeys, $allowedKeys);
$rules = [];
foreach($diffKeys as $value) {
$rules[$value] = ['prohibited'];
}

Laravel validation - comma separated string input as array

How can I validate one input with multiple values? I'm using bootstrap tagsinput plugin. It returns all tags in one field. I need to validate this tags - unique.
First I'm trying to place this tags into array and then validate it in request but still no luck.
Here is my code in request:
public function all()
{
$postData = parent::all();
// checkbox status
if(array_key_exists('keywords', $postData)) {
// put keywords into array
$keywords = explode(',', $postData['keywords']);
$test = [];
$i = 0;
foreach($keywords as $keyword)
{
$test[$i] = $keyword;
$i++;
}
$postData['keywords'] = $test;
}
return $postData;
}
public function rules()
{
$rules = [
'title' => 'required|min:3|unique:subdomain_categories,title|unique:subdomain_keywords,keyword',
'description' => '',
'image' => 'required|image',
'keywords.*' => 'min:3'
];
return $rules;
}
But as soon as keyword becomes invalid I get this error:
ErrorException in helpers.php line 531:
htmlentities() expects parameter 1 to be string, array given.
Any ideas what's wrong?
I was running into a similar problem with comma separated emails on 5.4. Here's how I solved it:
In your request class, override the prepareForValidation() method (which does nothing by default, btw), and explode your comma separated string.
/**
* #inheritDoc
*/
protected function prepareForValidation()
{
$this->replace(['keywords' => explode(',', $this->keywords)]);
}
Now you can just use Laravel's normal array validation!
Since my case needed to be a bit more explicit on the messages, too, I added in some attribute and message customizations as well. I made a gist, if that's of interest to you as well
Instead of mutating your input, for Laravel 6+ there is a package to apply these validations to a comma separated string of values:
https://github.com/spatie/laravel-validation-rules#delimited
You need to install it:
composer require spatie/laravel-validation-rules
Then, you can use it as a rule (using a FormRequest is recommended).
For example, to validate all items are emails, not long of 20 characters and there are at least 3 of them, you can use:
use Spatie\ValidationRules\Rules\Delimited;
// ...
public function rules()
{
return [
'emails' => [(new Delimited('email|max:20'))->min(3)],
];
}
The constructor of the validator accepts a validation rule string, a validation instance, or an array. That rules are used to validate all separate values; in this case, 'email|max:20'.
It's not a good practice to override the all() method to validate a field.
If you receive a comma separated String and not an array that you want to validate, but there is no common Method available, write your own.
in your App/Providers/ValidatorServiceProvider just add a new Validation:
public function boot()
{
Validator::extend('keywords', function ($attribute, $value, $parameters, $validator)
{
// put keywords into array
$keywords = explode(',', $value);
foreach($keywords as $keyword)
{
// do validation logic
if(strlen($keyword) < 3)
{
return false;
}
}
return true;
}
}
Now you can use this Validation Rule on any request if needed, it is reusable.
public function rules()
{
$rules = [
'title' => 'required|min:3|unique:subdomain_categories,title|unique:subdomain_keywords,keyword',
'description' => 'nullable',
'image' => 'required|image',
'keywords' => 'keywords',
];
return $rules;
}
If the validation pass and you want to make the keywords be accessible in your request as array, write your own method:
public function keywords ()
{
return explode(',', $this->get('keywords'));
}

How to pass variable to Laravel Validator Rule

The Issue
Consider this:
protected $rules = array(
'mobile_number' => 'phone:AUTO,mobile,:country_code'
);
In the above example, the value of country_code needs to change dependant on a variable defined prior to the validation taking place.
With this in mind, is it possible to pass a variable into a Laravel Validation rule?
Please bear in mind, this is how I call my validation:
if(!$this->some_validator->with($data)->passes()){
// Get the validation errors and throw the exception
$error_info = $this->some_validator->formatErrorMessages();
throw new ExampleException(ExampleExceptionType::$VALIDATION_ERROR,$error_info);
}
You are allowed to create custom validation rules.
It looks like this:
Validator::extend('country_code', function($attribute, $value, $parameters, $validator) {
if ($value === 'US') {
return false;
}
return true;
});
And then just use it like this:
protected $rules = array(
'mobile_number' => 'phone:AUTO,mobile|country_code'
);
After re-reading your message I realized that you meant a bit different thing. But take a look at the $parameters argument.
And read documentation. It's quite well covered there https://laravel.com/docs/5.3/validation#custom-validation-rules
With the new way of doing this, if you extend a new rule and you have a variable that you want to pass to the closure, you can do it like this:
Validator::extend(nameOfTheRule, function ($attribute, $value, $parameters, $validator) use ($priorVariable) {
//code
}

Error when trying to extend validator in Laravel

I have been using the following validation for my form in Laravel:
public function isValid($data, $rules)
{
$validation = Validator::make($data, $rules);
if($validation->passes()){
return true;
}
$this->messages = $validation->messages();
return false;
}
The rules passed to it are simple:
$rules = [
'name' => 'required',
'type' => 'required'
];
And $data is the input post data. Now I need to add a custom validation extension to this, specifically to make sure that the value of input field round2 is greater than the value of input field round1. Looking at the docs, I have tried the following syntax which I think should be correct, but I keep getting an error.
$validation->extend('manual_capture', function($attribute, $value, $parameters)
{
return $value > $parameters[0];
});
Then I could call this with $attribute = 'round1', $value = $data['round1'] and $parameters = [$data['round2']].
The error is Method [extend] does not exist. - I'm not sure if my understanding of this whole concept is correct, so can someone tell me how to make it work? The docs only have about 2 paragraphs about this.
Put the following in your route.php
Validator::extend('manual_capture', function($attribute, $value, $parameters)
{
return $value > $parameters[0];
});
Additional documentation here
Then use it like so:
$rules = [ 'foo' => 'manual_capture:30'];

Categories