Skip to content

Instantly share code, notes, and snippets.

@a-h-abid
Last active March 17, 2023 08:19
Show Gist options
  • Save a-h-abid/dd6220717c078061f16afb7b399c1e11 to your computer and use it in GitHub Desktop.
Save a-h-abid/dd6220717c078061f16afb7b399c1e11 to your computer and use it in GitHub Desktop.
Laravel Validation Response for API with Rule Key

Laravel Custom Validation Response for API with Rule Key

This is an extension to the Laravel Validator to support for Rule Name on API Response.

Tested with Laravel v8.36+

You will get a validation response for API as something like below:

{
  "message": "The given data was invalid.",
  "errors": {
    "name" : [
      [
        "rule": "required",
        "message": "The name field is required."
      ]
    ],
    "age" : [
      [
        "rule": "numeric",
        "message": "The age must be a number."
      ]
    ]
  }
}

Why ?

Because some of my clients needed this requirement for the APIs they were consuming.

Getting started

Use the middleware class ValidationResponseWithRuleName.php on the route you want and add the Validator.php file in app/Validation folder.

By default,

  • Rule names with just strings are taken as is.
  • For class based rules, we take the short class name and snake-dash-case it for rule name.
  • For Closures, the rule name 'closure-validation-rule' is returned because that is the name of the closure class in Laravel validator.
  • For custom rule name, turn your $rules array to associative array and provide key as your custom rule name. This is also highly prioritised over other options. Example:
$rules = [
    'name' => [
        'my-custom-required' => 'required',
        'test-rule-name' => function($attr, $value, $fail) {},
        // ...
    ], 
];

Limitations

  • Currently only tested with Laravel v8.36.
  • The implementation will work as long as the Laravel's Validator and the MessageBag classes are not modified to break it.
  • I only tested for API. So, this may not work well in blade view files.

My Wish

If something like this is integrated by the core team. :(

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ValidationResponseWithRuleName
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
app()->get('validator')
->resolver(function ($translator, $data, $rules, $messages, $customAttributes) {
return new \App\Validation\Validator($translator, $data, $rules, $messages, $customAttributes);
});
return $next($request);
}
}
<?php
namespace App\Validation;
use Illuminate\Validation\Validator as LaravelValidator;
use Illuminate\Contracts\Validation\Rule as RuleContract;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationRuleParser;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class Validator extends LaravelValidator
{
/**
* Determine if the data passes the validation rules.
*
* @return bool
*/
public function passes()
{
$this->messages = new MessageBag();
[$this->distinctValues, $this->failedRules] = [[], []];
// We'll spin through each rule, validating the attributes attached to that
// rule. Any error messages will be added to the containers with each of
// the other error messages, returning true if we don't have messages.
foreach ($this->rules as $attribute => $rules) {
if ($this->shouldBeExcluded($attribute)) {
$this->removeAttribute($attribute);
continue;
}
if ($this->stopOnFirstFailure && $this->messages->isNotEmpty()) {
break;
}
foreach ($rules as $ruleKey => $rule) {
$this->validateAttributeWithRuleKey($attribute, $rule, $ruleKey);
if ($this->shouldBeExcluded($attribute)) {
$this->removeAttribute($attribute);
break;
}
if ($this->shouldStopValidating($attribute)) {
break;
}
}
}
// Here we will spin through all of the "after" hooks on this validator and
// fire them off. This gives the callbacks a chance to perform all kinds
// of other validation that needs to get wrapped up in this operation.
foreach ($this->after as $after) {
$after();
}
return $this->messages->isEmpty();
}
/**
* Validate a given attribute against a rule.
*
* @param string $attribute
* @param string $rule
* @param string $ruleKey
* @return void
*/
protected function validateAttributeWithRuleKey($attribute, $rule, $ruleKey)
{
$this->currentRule = $rule;
[$rule, $parameters] = ValidationRuleParser::parse($rule);
if ($rule === '') {
return;
}
// First we will get the correct keys for the given attribute in case the field is nested in
// an array. Then we determine if the given rule accepts other field names as parameters.
// If so, we will replace any asterisks found in the parameters with the correct keys.
if (
($keys = $this->getExplicitKeys($attribute))
&& $this->dependsOnOtherFields($rule)
) {
$parameters = $this->replaceAsterisksInParameters($parameters, $keys);
}
$value = $this->getValue($attribute);
// If the attribute is a file, we will verify that the file upload was actually successful
// and if it wasn't we will add a failure for the attribute. Files may not successfully
// upload if they are too large based on PHP's settings so we will bail in this case.
if (
$value instanceof UploadedFile
&& ! $value->isValid()
&& $this->hasRule($attribute, array_merge($this->fileRules, $this->implicitRules))
) {
return $this->addFailureWithKey($attribute, 'uploaded', $ruleKey, []);
}
// If we have made it this far we will make sure the attribute is validatable and if it is
// we will call the validation method with the attribute. If a method returns false the
// attribute is invalid and we will add a failure message for this failing attribute.
$validatable = $this->isValidatable($rule, $attribute, $value);
if ($rule instanceof RuleContract) {
return $validatable
? $this->validateUsingCustomRuleWithKey($attribute, $value, $rule, $ruleKey)
: null;
}
$method = "validate{$rule}";
if ($validatable && ! $this->$method($attribute, $value, $parameters, $this)) {
$this->addFailureWithKey($attribute, $rule, $ruleKey, $parameters);
}
}
/**
* Validate an attribute using a custom rule object.
*
* @param string $attribute
* @param mixed $value
* @param \Illuminate\Contracts\Validation\Rule $rule
* @param string $ruleKey
* @return void
*/
protected function validateUsingCustomRuleWithKey($attribute, $value, $rule, $ruleKey = '')
{
$attribute = $this->replacePlaceholderInString($attribute);
$value = is_array($value) ? $this->replacePlaceholders($value) : $value;
if (! $rule->passes($attribute, $value)) {
$this->failedRules[$attribute][get_class($rule)] = [];
$messages = $rule->message();
$messages = $messages ? (array) $messages : [get_class($rule)];
foreach ($messages as $message) {
$formatedMessage = $this->makeReplacements(
$message,
$attribute,
get_class($rule),
[]
);
$this->messages->add($attribute, [
'rule' => $this->getRuleName(get_class($rule), $ruleKey),
'message' => $formatedMessage,
]);
}
}
}
/**
* Add a failed rule with key and error message to the collection.
*
* @param string $attribute
* @param string $rule
* @param string $ruleKey
* @param array $parameters
* @return void
*/
public function addFailureWithKey($attribute, $rule, $ruleKey = '', $parameters = [])
{
if (! $this->messages) {
$this->passes();
}
$attribute = str_replace(
[$this->dotPlaceholder, '__asterisk__'],
['.', '*'],
$attribute
);
if (in_array($rule, $this->excludeRules)) {
return $this->excludeAttribute($attribute);
}
$formatedMessage = $this->makeReplacements(
$this->getMessage($attribute, $rule),
$attribute,
$rule,
$parameters
);
$this->messages->add($attribute, [
'rule' => $this->getRuleName($rule, $ruleKey),
'message' => $formatedMessage,
]);
$this->failedRules[$attribute][$rule] = $parameters;
}
/**
* Get Rule Name
*
* @param string $rule
* @param string $ruleKey
* @return string
*/
protected function getRuleName($rule, $ruleKey)
{
if (!empty($ruleKey) && !is_numeric($ruleKey)) {
return $ruleKey;
}
if (class_exists($rule)) {
return Str::snake(substr($rule, strrpos($rule, '\\') + 1), '-');
}
return strtolower($rule);
}
}
@MauroB45
Copy link

MauroB45 commented Jun 6, 2021

Awesome! Thanks for sharing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment