When I started my career as a developer, one of my senior team members used to construct queries in core PHP using concatenation based on different parameters. Once the construction was complete, he would echo it for different use cases and run it in PHPMyAdmin or SQLyog. Once satisfied, he’d move on. I always liked the way he built those queries.
Later, when I moved on to CodeIgniter and eventually Laravel, I noticed something: Developers often write SQL or ORM-based queries buried deep inside controllers, services, views, event listeners — even helpers. This scattered approach leads to redundancy and painful maintenance. We’ve all seen this.
This always bugged me. Why repeat the same logic scattered across the application when it can be centralized?
Being an OOP geek, I finally settled on a pattern that works cleanly and elegantly. To counter this problem, I am introducing a new design principle: Atomic Query Construction — or AQC. It’s a design approach where each query lives in its own class. You write it once and use it everywhere. It’s structured, predictable, and aligned with better software design principles.
What is AQC?
AQC stands for Atomic Query Construction. At its core, it’s a principle of isolating each query into its own class. Each of these classes:
- Accepts Parameters
- Constructs Query based on those parameters for different use cases
- Returns Results
You only call this class whenever that specific query is needed — nothing else. It becomes the single source of truth for that operation.
Rules of the Pattern
1. One Class, One Responsibility
Every class should focus on a single, well-defined query. Stick to the Single Responsibility Principle (SRP).
2. Accept Parameters
Each class should accept parameters — filters, identifiers, flags — whatever’s needed to shape the query dynamically.
3. Construct the Query Internally
Based on the parameters, the class should build and return a query.
4. Single Source of Truth
Anywhere in the app where you need this data — whether in a controller, event, or service — call this class. Do NOT rewrite the query logic elsewhere.
5. Handle Method
Each class should expose a single public method, handle(). It can have private helpers internally, but the interface stays clean.
6. Consistent Naming
Class names must be clear and consistent. For a resource like Product
, your AQC classes might be:
- GetAllProducts
- GetProduct
- StoreProduct
- UpdateProduct
- DeleteProduct
Benefits
- Modular: Every query is isolated and self-contained. You know exactly where to look when changes are needed.
- Reusable: Once written, the query class can be reused across the app with different params or use cases.
- Flexible: It supports multiple scenarios (admin view, frontend listing, etc.) using dynamic input.
- Clean Separation: Keeps your controllers, services, and views clean. No query logic there — just calls to your AQC classes.
- Testable: Because logic is in isolated classes, it’s easy to write unit tests without involving controllers or database state.
Implementation Example
Let’s say we’re working with a Product
model in a typical eCommerce app. First, create a folder:
app/AQC/Product/
GetAllProducts.php
GetProduct.php
StoreProduct.php
UpdateProduct.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();
}
}
Usage:
// Admin listing
$products = GetAllProducts::handle();
// return paginated product order id latest
// Frontend product listing
$products = GetAllProducts::handle($request->all(), true, 'compact');
GetProduct.php
<?php
namespace App\AQC\Product;
use App\Models\Product;
class GetProduct
{
public static function handle($id, $params = [], $scenario = 'default')
{
$productObj = Product::where('id', $id);
if (isset($params['active']) && $params['active']) {
$productObj->where('active', $params['active']);
}
// 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 $productObj->first();
}
}
Usage:
// Admin edit screen
$product = GetProduct::handle($productID, [], 'admin');
// Frontend product detail
$product = GetProduct::handle($productID, [], 'compact');
StoreProduct.php
<?php
namespace App\AQC\Product;
use App\Models\Product;
class StoreProduct
{
public static function handle($params)
{
$product = new Product();
$product->fill($params);
if (isset($params['type']) && $params['type'] === 'combo') {
$product->is_combo = true;
}
if (!isset($params['sku'])) {
$product->sku = self::generateSku($params);
}
$product->save();
return $product;
}
private static function generateSku($params)
{
// SKU generation logic
return 'SKU-' . rand(1000, 9999);
}
}
UpdateProduct.php
<?php
namespace App\AQC\Product;
use App\Models\Product;
class UpdateProduct
{
public static function handle($id, $params)
{
$product = Product::find($id);
if (!$product) return null;
if (isset($params['cost'])) $product->cost = $params['cost'];
if (isset($params['price'])) $product->price = $params['price'];
if (isset($params['stock'])) $product->stock = $params['stock'];
// add more fields as needed
$product->save();
return $product;
}
}
DeleteProduct.php
<?php
namespace App\AQC\Product;
use App\Models\Product;
class DeleteProduct
{
public static function handle($params = [])
{
$productsObj = Product::query();
if (!empty($params['category_id'])) {
$productsObj->where('category_id', $params['category_id']);
}
if (!empty($params['brand_id'])) {
$productsObj->where('brand_id', $params['brand_id']);
}
if (!empty($params['product_id'])) {
$productsObj->where('id', $params['product_id']);
}
if (!$product) return false;
$productObj->delete();
return true;
}
}
Final Thoughts
Each class builds the query, covers various scenarios, and cleanly separates logic. You can now reuse queries across your entire app — controllers, API endpoints, and admin panels — without rewriting anything.
When a new requirement comes in, you know exactly where to go. No more chasing query logic across 20 files.
If you try AQC in your projects, I’d love to hear how it works for you — or what you’d improve.
I’ll be sharing more Laravel architecture patterns soon. Follow along if you’re into clean, scalable code.
If you found this post helpful, consider supporting my work — it means a lot.
