DEV Community

Raheel Shan
Raheel Shan

Posted on

Laravel Business Logic: A Cleaner, Smarter Approach

You can also read this story (https://raheelshan.com/posts/laravel-business-logic-a-cleaner-smarter-approach).

While working with Laravel, I often found myself wondering where exactly to put my business logic. If you’ve felt the same way, you’re not alone! Laravel’s documentation doesn’t provide a clear answer, leaving it up to developers to decide. For business logic many people turn to the Repository Pattern or use controller method making controllers thick, but I am not a fan of these approaches. So, I went looking for a better solution, and I found one that makes perfect sense. Let me share it with you!

The power of Form Request Validation

Laravel has this wonderful feature called Form Request Validation. You’ve probably used it to authorize request, define validation rules and customize error messages. Here is a sample of this class.

namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body' => 'required',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Then it is called it in the controller like this:

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Http\Controllers\Controller;

class StoreController extends Controller
{
    /**
     * Store a new blog post.
     */
    public function store(StorePostRequest $request): RedirectResponse
    {
        // The incoming request is valid...

        // Retrieve the validated input data...
        $validated = $request->validated();

        // your business logic here

        return redirect('/posts');
    }
}
Enter fullscreen mode Exit fullscreen mode

Pretty handy, right? But here’s where I had a lightbulb moment. Instead of just calling the validated() method in the controller, why not take it a step further and let the Form Request handle some business logic?

The Handle Method Trick

What I did was simple: I added a handle method in my Form Request class. In this method, I called the validated() function and performed the business logic. Here's modified code for request class:

namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body' => 'required',
        ];
    }

    public function handle()
    {
        $data = $this->validated();

        // Your business logic here

        return $data;        
    }
}
Enter fullscreen mode Exit fullscreen mode

Now call this method in controller

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Http\Controllers\Controller;

class StoreController extends Controller
{
    /**
     * Store a new blog post.
     */
    public function store(StorePostRequest $request): RedirectResponse
    {
        $request->handle();

        return redirect('/posts');
    }
}
Enter fullscreen mode Exit fullscreen mode

How Does It Work?

At this point, you might be wondering how we access request data in the handle method. We’re not passing the request object! The answer is simple — $this is your friend here. Since we’re already inside the context of the BaseRequest, you can use $this to access any data from the request.

public function handle()
{
    $data = $this->validated();

    $params = $this->all();

    // Your business logic here

    return $data;        
}
Enter fullscreen mode Exit fullscreen mode

This approach keeps your controllers lean and lets your Form Request take care of both validation and related business logic. Pretty neat, right? Here you must rest assured this method will function correctly. If validation fails it will redirect back with validation errors. But if validation passes handle method will be executed and it will redirect to posts route.

Finally my controller is totally lean now. Here is a sample.

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

use App\Http\Requests\Post\GetAllPostsRequest;
use App\Http\Requests\Post\GetPostRequest;
use App\Http\Requests\Post\CreatePostRequest;
use App\Http\Requests\Post\UpdatePostRequest;
use App\Http\Requests\Post\DeletePostRequest;

class PostController extends Controller
{
    public function index(GetAllPostsRequest $request)
    {
        $posts = $request->handle();

        return view('post.index', compact('posts') );
    }

    public function create()
    {           
        return view('post.create');  
    }

    public function store(CreatePostRequest $request)
    {
        $request->handle();

        return redirect()->route('posts.index');
    }

    public function show(GetPostRequest $request, $id)
    {
        $post = $request->handle($id);

        return view('post.show', compact('post') );
    }

    public function edit($id, GetPostRequest $request)
    {
        $post = $request->handle($id);

        return view('post.edit', compact('post') );
    }

    public function update(UpdatePostRequest $request, $id)
    {
        $request->handle($id);

        return redirect()->route('posts.index');
    }

    public function destroy(DeletePostRequest $request, $id)
    {
        $request->handle($id);

        return redirect()->route('posts.index');
    }
}
Enter fullscreen mode Exit fullscreen mode

All my request classes have now handle method containing my business logic.

I hope this little trick helps someone out there struggling with the same dilemma. Happy coding! 😊

Top comments (3)

Collapse
 
xwero profile image
david duymelinck

The FormRequest class function is to do validation.
If you add business logic to it the reusability will suffer, because it is hard-coded. For example if the same data is produced by a html form and an API call the structure of the output data can be different.

A better solution would be to extend the FormRequest class with a function that formats the output. A minimal example;

abstract class MyFormRequest extends FormRequest {
      public function getOutput(string $class) {
           if(method_exists($class, 'handle')) {
                 return new $class()->handle($this->validated());
           }

          return $this->validated();
     }
}
Enter fullscreen mode Exit fullscreen mode

The argument could be an invokable class or even a function. But I wanted to match it with the code of the post.

This way you have the opportunity to use the same output class more than once or switch it for another class. And the output classes can be grouped in their own directory like the FormRequest classes.

Collapse
 
raheelshan profile image
Raheel Shan • Edited

Hi @xwero, thanks for your feedback! You bring up a solid point about reusability, but my approach is intentionally pragmatic.

In most Laravel applications, FormRequests are tightly coupled with specific request flows—for example, an API request or a form submission. The output format can often be handled at the response layer for example using API Resources for JSON responses.

Moreover, the proposed abstraction adds an extra layer of indirection. Instead of defining and wiring up an additional handler for every FormRequest, my approach keeps everything self-contained while maintaining a clear structure.

Finally, Laravel already embraces single responsibility with flexibility, but it also encourages developer-friendly conventions. In my experience, embedding some logic in FormRequest does not hinder maintainability in most real-world applications—especially when scoped appropriately.

That said, your idea works well for those who prefer a stricter separation of concerns. It’s a matter of balancing reusability with simplicity based on project needs. Thanks again for sharing your thoughts!

Collapse
 
xwero profile image
david duymelinck

Giving the output as an example might not be the best, you are right that can be handled another way.

I took inspiration from the after method in the FormRequest class. So for me it is just that function with another goal.

I understand your pragmatism. I like my controller methods fat and not reusable, until the time has come the abstractions have added value.