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.
