Laravel Core

Request Validation within Laravel Controllers

Saturday, January 11, 2025

// 5 min read

Laravel 11 Controller Validation
Request Validation within Laravel Controllers

Inside your application, it's always important to properly validate any incoming data, especially input values sent from a form request or JSON body sent by an API request. In Laravel, you can easily validate the request data with its built-in validator.

Request validation with the validate method

Let's see an example of how to do it in your controller. On my webshop's admin dashboard, I have an HTML form to create a new Product.

My form has the following inputs:

  • name string input

  • description textarea

  • price number input

  • stock number input

  • image file upload input

  • category selector

  • brand selector

  • active checkbox

Product Form

In your controller, you can use the validate method to validate your request data.

$request->validate([
    'name' => 'required|string|unique:products|max:100',
    'description' => 'required|string|max:1500',
    'price' => 'required|numeric|min:0',
    'stock' => 'required|integer|min:0',
    'image' => 'required|image',
    'category_id' => 'required|exists:categories,id',
    'brand_id' => 'required|exists:brands,id',
    'active' => 'nullable|boolean',
]);

There are many built-in Laravel validation rules, but here I just use a couple.

  • required for mandatory input values

  • numeric for expecting a numeric value

  • string for expecting a string

  • max to verify that the field must be less than or equal to a maximum value.
    E.g.: The Product name cannot be more than 100 characters long

  • min to verify that the field must have a minimum value.
    Both the price and stock fields have 0 as a minimum value, so either cannot be a negative number.

  • exists which checks if the field under validation must exist in a given database table.
    E.g.: The given category_id already exists in the database.

  • unique which checks if the field under validation must not exist within the given database table.
    E.g: Cannot create a Product with the same name that already exists in the database.

  • image to verify if the file under validation must be an image (jpg, jpeg, png, bmp, gif, svg, or webp).

If any validation rules fail, an error response will be returned to the user. If it passes, the code keeps executing normally.

For example, in our case, the name input receives an error because I already have a product with the same name.

Unique name validation error

The request's validate method is useful for quick, small-scale validations. But for more scalable and maintainable validation logics, you should use a FormRequest class.

Form Request Validation

In my controller, I also have a product update method, where I would like to use almost the same validation rules. Instead of copy-paste the rules, I can create a Form Request class and use it for both the store and update method.

Let's generate the class with the artisan command:

php artisan make:request StoreProductRequest

With a bit of modification, we can use the same rules.

<?php

namespace App\Http\Requests;

use App\Models\Product;
use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        if ($this->isMethod('POST')) {
            return $this->user()->isAdmin() || $this->user()->isEditor();
        }

        return $this->user()->isAdmin() ||
            ($this->user()->isEditor() && $this->user()->id === $this->product->user_id);
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $productId = $this->route('product') ? $this->route('product')->id : null;

        return [
            'name' => 'required|string|max:100|unique:products,name,' . $productId,
            'description' => 'required|string|max:1500',
            'price' => 'required|numeric|min:0',
            'stock' => 'required|integer|min:0',
            'image' => [
                $this->isMethod('POST') ? 'required' : 'nullable',
                'image',
            ],
            'category_id' => 'required|exists:categories,id',
            'brand_id' => 'required|exists:brands,id',
            'active' => 'nullable|boolean',
        ];
    }
}

To check the HTTP method, use the Form Request class isMethod function. To retrieve the parameter you can use the route method: $this->route('product') or if you use route model binding, which we do, just simply access the model: $this->product.

Notice that in the case of the update method, I added one more parameter to the unique rule, this means the rule is going to ignore the actual product's name, and during the update in case you don't change its name it won't be in the list of names to verify against, so it is not going to throw a validation error.

You can also take advantage of the authorize method which determines if the user is allowed to proceed with the request. In our example, only users with an admin or a editor role can create or update posts. Moreover, an editor user can only update his/her own blog post.

Customizing Validation Messages

You can customize the validation messages by listing each rule and its custom message inside the messages method. For example, to change the validation message if the product name is not unique, you can add 'name.unique' => 'My custom message' to the messages array.

public function messages(): array
{
    return [
        'name.unique' => 'The product name must be unique.',
        'description.max' => 'The product description must not be greater than :max characters.',
        'image.required' => 'The product image is required.',
        'brand_id.exists' => 'The selected brand is invalid.',
    ];
}

Custom validation messages

It is possible to change the validation messages globally as well. For this, you need to edit the desired rule in the lang/en/validation.php file. If you do not have a lang directory, you can create it using the lang:publish Artisan command.

php artisan lang:publish

Global validation rule messages

Validating Arrays and Nested Data

Let's assume I can have different sizes and colors for each product. And how many I have in stock for each size-color combination. I want to validate an array of product attributes where each attribute has a size, color, and quantity value. Here is an example of how you can validate an array in a form request.

public function rules(): array
{
    return [
        'attributes' => 'required|array',
        'attributes.*.size' => [
            'nullable',
            'string',
            'max:3',
            Rule::in(['XS', 'S', 'M', 'L', 'XL', 'XXL']),
        ],
        'attributes.*.color' => [
            'required',
            'string',
            'max:20',
            Rule::in(['Black', 'White', 'Red', 'Green', 'Blue', 'Yellow', 'Purple', 'Orange', 'Pink', 'Brown']),
        ],
        'attributes.*.quantity' => 'required|integer|min:0',
    ];
}

public function messages(): array
{
    return [
        'attributes.required' => 'The product attributes are required.',
        'attributes.*.size.string' => 'The size must be a string.',
        'attributes.*.size.max' => 'The size must not be greater than :max characters.',
        'attributes.*.size.in' => 'The selected size is invalid.',
        'attributes.*.color.required' => 'The color is required',
        'attributes.*.color.string' => 'The color must be a string.',
        'attributes.*.color.max' => 'The color must not be greater than :max characters.',
        'attributes.*.color.in' => 'The selected color is invalid.',
        'attributes.*.quantity.required' => 'The quantity is required',
        'attributes.*.quantity.integer' => 'The quantity must be an integer',
        'attributes.*.quantity.min' => 'The quantity must be at least :min',
    ];
}

Validating arrays

Using Conditional Validation Rules

Conditional validation rules allow you to apply validation rules based on certain criteria.

You might want to apply validation rules based on the value of another field.
For example, if the product I am creating is active, I want to require the stock field to be greater than zero:

'stock' => [
    'required',
    'integer',
    'min:0',
    function ($attribute, $value, $fail) {
        if ($this->input('active') && $value == 0) {
            $fail('The stock must be greater than 0 when the product is active.');
        }
    }
],

You can use the required_if rule to make a field required based on another field's value.
For example, I want to set a special offer every time I select the Holiday Special category:

'special_offer' => 'required_if:category_id,1|nullable|numeric|min:0|max:100',

In case you want to list multiple categories, you can do so by using a closure:

'special_offer' => [
    'nullable',
    'numeric',
    'min:0',
    'max:100',
    Rule::requiredIf(function () {
        $specialOfferCategoryIds = Category::whereIn('name', ['Holiday Specials', 'Clearance', 'Seasonal Offers'])
            ->pluck('id')
            ->toArray();
        return in_array($this->input('category_id'), $specialOfferCategoryIds);
    }),
]

It is possible to use the inverse of the previous rule with the required_unless method, which is used to make a field required unless another field has a specific value.  
So in case you need to apply the special offer on almost all the categories except a few, you need to list the exceptions:

'special_offer' => 'required_unless:category_id,3|nullable|numeric|min:0|max:100',

Custom Validation Rules

You can easily create your custom validation rule with the make:rule artisan command:

php artisan make:rule ValidSKU

Here I am creating a rule that validates Stock Keeping Unit. Which is a unique code assigned to each product or item in a store's inventory. 

<?php

namespace App\Rules;

use Closure;
use App\Models\Product;
use Illuminate\Contracts\Validation\ValidationRule;

class ValidSKU implements ValidationRule
{
    /**
     * Run the validation rule.
     *
     * @param  \Closure(string, ?string = null): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Check if the value is in the format of XX999999
        if (! preg_match('/^[A-Z]{2}\d{6}$/', $value)) {
            $fail('The :attribute must be in the format of XX999999.');
        }

        // Check if the SKU is unique
        if (
            Product::where('sku', $value)
            ->when(
                request()->route('product'),
                fn($query) => $query->whereNot('id', request()->route('product')->id)
            )
            ->exists()
        ) {
            $fail('The :attribute is already in use.');
        }
    }
}

The advantage of creating a custom validation rule is to combine multiple conditions into one class.

To use the rule, you can just simply add the rule to the desired key:

'sku' => [
    'required',
    new \App\Rules\ValidSKU,
],

Handling Validation Errors

By default, Laravel handles failed validation by redirecting the user back to the previous page with the validation errors and old input data.

You can change this behavior by overriding the failedValidation method in your form request class.

Here, the Form request class returns validation errors in a JSON format:

protected function failedValidation(Validator $validator)
{
    $response = response()->json([
        'message' => 'The given data was invalid.',
        'errors' => $validator->errors(),
    ], 422);

    throw new HttpResponseException($response);
}
{
  "message": "The given data was invalid.",
  "errors": {
    "sku": [
      "The sku must be in the format of XX999999."
    ],
    "attributes.0.color": [
      "The color is required"
    ],
    "attributes.0.quantity": [
      "The quantity is required"
    ]
  }
}

I hope after reading through this post you're more familiar with validations in Laravel. Just remember to keep validation logic out of controllers whenever possible, and use reusable form requests for consistency.

If you want to support my work, you can donate using the button below. Thank you!