If you’ve been following my Laravel writing journey, you know where we started.

1. Laravel CRUD Like a Pro introduced the idea of a handle() method inside FormRequest classes — a simple way to collapse validation, authorization, and persistence into one clear action.

2. The End of Redundant Forms removed form redundancy with findOrNew()+ old() by pushing toward even more elegant CRUD.

3. Atomic Query Construction (AQC) Design Pattern a dedicated pattern for encapsulating business logic behind queries and data manipulations.

This post is about the next step in that evolution: replacing handle() from FormRequest with dedicated AQC classes. And more importantly, it’s about why — and how — this pattern unlocks serious architectural power, implementing onion architecture that handles web, API, and WebSocket requests seamlessly.

Why move from the handle() method of FormRequest to AQC?

I originally used handle() inside FormRequest. It was fine for a while. But it couples validation to persistence. And more importantly:

You can’t reuse that logic outside the HTTP layer.

I’m not okay with that. My app is not just web — it may be web, an API, a WebSocket, a CLI call, or scheduled jobs — it breaks. So we are bound to go through the HTTP layer to use it.

So I stopped doing that and pulled the logic out and moved it to AQC classes. Let’s see how.

What is AQC?

AQC stands for Atomic Query Construction. At its core, it’s a principle of isolating each query into its own dedicated class. There’s no installation. No binding. It’s just a pattern I follow. Here’s how it works:

Folder structure:

                                app/
└── AQC/
    └── Product/
        └── CreateProduct.php
        └── UpdateProduct.php
        └── GetAllProducts.php
        └── GetProduct.php           
        └── DeleteProduct.php
                            

GetAllProducts.php

                                <?php

namespace App\AQC\Product;
use App\Models\Product;

class GetAllProducts
{
    public static function handle($params = [], $paginate = true, $scenario = 'default')
    {
        $productObj = Product::latest('id');

        if (isset($params['category_id']) && $params['category_id'] > 0) {
            $productObj->where('category_id', $params['category_id']);
        }

        if (isset($params['brand_id']) && $params['brand_id'] > 0) {
            $productObj->where('brand_id', $params['brand_id']);
        }

        // add more conditions for different use cases

        switch ($scenario) {
            case 'minimal':
                $productObj->select(['id', 'name']);
                break;
            case 'compact':
                $productObj->select(['id', 'name', 'price', 'image']);
                break;
            case 'admin':
                $productObj->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
                break;
            default:
                $productObj->select('*');
        }

        return $paginate
            ? $productObj->paginate(Product::PAGINATE)
            : $productObj->get();
    }
}
                            

The class is dead simple. You pass in filters, and it gives you results. That’s all. Since the class is detached from validation logic, we now call it separately like this.

                                <?php

use App\Http\Requests\Product\GetAllProductsRequest;
use App\AQC\Product\GetAllProducts;

class ProductController extends Controller
{
    public function index(GetAllProductsRequest $request)
    {
        $params = $request->all();
        $product = GetAllProducts::handle($params);
        return ResponseHelper::handle('index', $data);
    }
}
                            

HTTP route

                                // web.php or api.php
Route::get('/products', [ProductController::class, 'index']);
                            

API route

Same controller, no change needed.

WebSocket

                                use App\AQC\Product\GetAllPosts;

WebSocketsRouter::webSocket('/socket/products', GetProductsHandler::class);
                            
                                class GetProductsHandler implements MessageComponentInterface
{
    public function onMessage(ConnectionInterface $from, MessageInterface $message)
    {
        $filters = json_decode($message->getPayload(), true);
        $products = GetAllProducts::handle($filters);
        $from->send(json_encode($products));
    }
}
                            

Now you can see AQC is the center. Everything else is just an entry point.

This gives me a clean onion architecture, where

  • UI layers (web, API, WebSocket) are outer layers
  • Controllers/Handlers are thin intermediaries
  • AQC is the center: solid, reusable, testable

Why Not Use Service Class Instead?

You could call them Services. But most Laravel “services” are just vague bags of static methods. AQC forces discipline:

  • Atomic: one job per class
  • Query-centric: focused on data actions
  • Reusable: not tied to HTTP

This naming convention also helps me organize actions better and avoid bloated service classes.

To see AQC classes in action, take a look at this post of mine.

https://raheelshan.com/posts/atomic-query-construction-aqc-design-pattern-explained

Summary

By moving logic to the AQC class and calling the AQC class in any layer, we have now achieved onion architecture in the Laravel application. We pass different parameters based on our requirements to the AQC class, and based on the parameters, it dynamically generates the query and hands over the result. This saves us from writing the same queries again and again. Whenever a new requirement comes in, we know exactly where we need to see.

Final Words

  • AQC keeps your core logic in one place.
  • Keep controllers ultra-thin
  • Works with web routes, APIs, WebSockets, whatever.
  • Everything plays nicely with Laravel’s structure.
  • Follows Onion Architecture principles without overengineering.
  • No packages. Just Laravel.
  • Easy Testing

I’m not saying this is the only way — but it’s the cleanest way I’ve found to scale Laravel apps without drowning in glue code. I hope you enjoyed this structure. I would appreciate your take on this.


If you think this article was useful to you, you can consider supporting me. It encourages me to share my experience with you even more.

 Raheel Shan | Support my work
Raheel Shan | Support my work
Support my work
raheelshan.com
Comments


Comment created and will be displayed once approved.