Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
🛠️ PHP + MySQL REST API  ·  Lesson 05

Products API: Search, Filter, Pages

App को क्या चाहिए

एक endpoint, products.php, search screen, हर category page और हर "See all" button चलाता है। App बस query parameters बदलती है:

9:41▮▮ 85%←Products⋮🔍 watchPrice: low to highNewest1 result⌚Smart Watch Pro₹1,8999:41▮▮ 85%←Electronics⋮Price: low to high3 results · Filter: ₹0 – ₹2,000🎧Earbuds X2₹1,099🔋Power Bank₹1,499⌚Smart Watch₹1,899
products.php?q=watch&sort=price_asc और products.php?category_id=3&max_price=2000

Query parameters

ParameterExampleमतलब
qq=watchname और short description में search
category_idcategory_id=3सिर्फ एक category
min_price / max_pricemax_price=2000price range (filter bottom sheet से)
sortnewest, price_asc, price_desc, rating, popular, discountresults का order
page / limitpage=2&limit=20कौन-सा page, हर page पर कितने (max 50)

Step 1: WHERE हिस्सा safely बनाना

हम छोटी conditions एक array में और उनकी values दूसरे array में जमा करते हैं। सिर्फ वही conditions जुड़ती हैं जो user ने माँगी। Values हमेशा ? placeholders से जाती हैं।

PHP – idea
<?php
$where = array('p.is_active = 1');   // always: only visible products
$params = array();

if ($q !== '') {                     // ?q=watch
    $where[] = '(p.name LIKE ? OR p.short_desc LIKE ?)';
    $params[] = '%' . $q . '%';
    $params[] = '%' . $q . '%';
}
if ($categoryId > 0) {               // ?category_id=3
    $where[] = 'p.category_id = ?';
    $params[] = $categoryId;
}
// … min_price, max_price the same way

$whereSql = implode(' AND ', $where);
// "p.is_active = 1 AND (p.name LIKE ? OR p.short_desc LIKE ?) AND p.category_id = ?"

Step 2: SQL injection के बिना sorting

Placeholders सिर्फ values के लिए चलते हैं, column names या ASC/DESC के लिए नहीं। इसलिए sorting के लिए हम whitelist use करते हैं:

PHP
<?php
// ❌ NEVER: user text inside SQL
$sql = "SELECT * FROM products ORDER BY " . $_GET['sort'];
// ?sort=price;DROP TABLE users  → disaster

// ✅ Whitelist: the user picks a KEY, we choose the SQL
$sorts = array(
    'price_asc'  => 'p.price ASC',
    'price_desc' => 'p.price DESC',
);
$orderBy = isset($sorts[$sort]) ? $sorts[$sort] : 'p.created_at DESC';

Step 3: Pagination

App 20 products दिखाती है, और user आख़िर के पास scroll करे तो अगले 20 माँगती है। Server has_more लौटाता है ताकि app को पता हो कब रुकना है।

page=1OFFSET 0 → items 1–20page=2OFFSET 20 → items 21–40page=3OFFSET 40 → items 41–45has_more = falseloading बंद
OFFSET = (page − 1) × limit

हम दो queries चलाते हैं: total के लिए COUNT(*) ("45 results" दिखाने को) और असली SELECT … LIMIT 20 OFFSET 20।

पूरा products.php

PHP – backend/api/products.php
<?php
// Product list with search, category filter, price filter, sorting and paging.
// GET api/products.php?q=watch&category_id=3&min_price=500&max_price=3000&sort=price_asc&page=1&limit=20
require_once __DIR__ . '/../includes/helpers.php';
require_method('GET');

$q = q_str('q');
$categoryId = q_int('category_id');
$minPrice = q_int('min_price');
$maxPrice = q_int('max_price');
$sort = q_str('sort', 'newest');
$page = max(1, q_int('page', 1));
$limit = min(50, max(1, q_int('limit', 20)));
$offset = ($page - 1) * $limit;

$where = array('p.is_active = 1');
$params = array();
if ($q !== '') {
    $where[] = '(p.name LIKE ? OR p.short_desc LIKE ?)';
    $like = '%' . $q . '%';
    $params[] = $like;
    $params[] = $like;
}
if ($categoryId > 0) {
    $where[] = 'p.category_id = ?';
    $params[] = $categoryId;
}
if ($minPrice > 0) {
    $where[] = 'p.price >= ?';
    $params[] = $minPrice;
}
if ($maxPrice > 0) {
    $where[] = 'p.price <= ?';
    $params[] = $maxPrice;
}

// Only whitelisted ORDER BY values: user text never goes into SQL directly.
$sorts = array(
    'newest'     => 'p.created_at DESC, p.id DESC',
    'price_asc'  => 'p.price ASC',
    'price_desc' => 'p.price DESC',
    'rating'     => 'p.rating_avg DESC, p.rating_count DESC',
    'popular'    => 'p.rating_count DESC',
    'discount'   => '(p.mrp - p.price) / p.mrp DESC',
);
$orderBy = isset($sorts[$sort]) ? $sorts[$sort] : $sorts['newest'];
$whereSql = implode(' AND ', $where);

$count = db()->prepare("SELECT COUNT(*) FROM products p WHERE $whereSql");
$count->execute($params);
$total = (int) $count->fetchColumn();

// LIMIT/OFFSET are ints we built ourselves, so they are safe to place in the SQL.
$stmt = db()->prepare("SELECT p.* FROM products p WHERE $whereSql ORDER BY $orderBy LIMIT $limit OFFSET $offset");
$stmt->execute($params);

$userId = optional_user_id();
$wish = $userId ? wishlist_ids($userId) : array();
$items = array();
foreach ($stmt->fetchAll() as $r) {
    $p = product_out($r);
    $p['in_wishlist'] = isset($wish[$p['id']]);
    $items[] = $p;
}

ok(array(
    'items'    => $items,
    'page'     => $page,
    'limit'    => $limit,
    'total'    => $total,
    'has_more' => $offset + count($items) < $total,
));

Step 4: product.php (detail)

Detail screen को card से ज़्यादा चाहिए: description, category name, gallery, नए 3 reviews, उसी category के similar products, wishlist में है या नहीं, और cart में पहले से कितने हैं। सब एक response में, ताकि screen एक ही request में खुले।

PHP – backend/api/product.php
<?php
// One product with gallery, category, latest reviews and similar products.
// GET api/product.php?id=7
require_once __DIR__ . '/../includes/helpers.php';
require_method('GET');

$id = q_int('id');
$stmt = db()->prepare(
    'SELECT p.*, c.name AS category_name FROM products p
     JOIN categories c ON c.id = p.category_id
     WHERE p.id = ? AND p.is_active = 1'
);
$stmt->execute(array($id));
$row = $stmt->fetch();
if (!$row) {
    fail('Product not found.', 404);
}

$p = product_out($row);
$p['description'] = $row['description'];
$p['category_name'] = $row['category_name'];

$imgs = db()->prepare('SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order, id');
$imgs->execute(array($id));
$gallery = array();
foreach ($imgs->fetchAll(PDO::FETCH_COLUMN) as $img) {
    $gallery[] = img_url($img);
}
if (count($gallery) === 0 && $p['image'] !== null) {
    $gallery[] = $p['image'];
}
$p['images'] = $gallery;

$rev = db()->prepare(
    'SELECT r.rating, r.comment, r.created_at, u.name FROM reviews r
     JOIN users u ON u.id = r.user_id WHERE r.product_id = ? ORDER BY r.id DESC LIMIT 3'
);
$rev->execute(array($id));
$reviews = array();
foreach ($rev->fetchAll() as $r) {
    $reviews[] = array('name' => $r['name'], 'rating' => (int) $r['rating'], 'comment' => $r['comment'], 'date' => substr($r['created_at'], 0, 10));
}
$p['reviews'] = $reviews;

$sim = db()->prepare('SELECT * FROM products WHERE category_id = ? AND id <> ? AND is_active = 1 ORDER BY rating_avg DESC LIMIT 6');
$sim->execute(array($row['category_id'], $id));

$userId = optional_user_id();
$wish = $userId ? wishlist_ids($userId) : array();
$p['in_wishlist'] = isset($wish[$id]);
$p['cart_qty'] = 0;
if ($userId) {
    $c = db()->prepare('SELECT qty FROM cart_items WHERE user_id = ? AND product_id = ?');
    $c->execute(array($userId, $id));
    $p['cart_qty'] = (int) $c->fetchColumn();
}

$similar = array();
foreach ($sim->fetchAll() as $s) {
    $sp = product_out($s);
    $sp['in_wishlist'] = isset($wish[$sp['id']]);
    $similar[] = $sp;
}
$p['similar'] = $similar;

ok($p);
product + categoryJOIN categories, hidden हो तो 404galleryproduct_images (या main image)reviewsuser name के साथ नए 3wishlist / cart_qtyसिर्फ token भेजा हो तोsimilarsame category, best rated, 6 items
product.php क्या-क्या जमा करता है

हर जगह एक product shape

product_out() database row को उस JSON में बदलता है जो app चाहती है: numbers numbers की तरह, पूरा image URL, और discount % server पर calculate। Home, search, wishlist और similar products सब इसे use करते हैं, इसलिए Android app को सिर्फ एक Product class और एक ProductAdapter चाहिए।

PHP – includes/helpers.php
<?php
// From includes/helpers.php – used by home, products, product, wishlist
function product_out($p)
{
    $price = (float) $p['price'];
    $mrp = (float) $p['mrp'];
    return array(
        'id'           => (int) $p['id'],
        'category_id'  => (int) $p['category_id'],
        'name'         => $p['name'],
        'short_desc'   => $p['short_desc'],
        'price'        => $price,
        'mrp'          => $mrp,
        'discount_pct' => $mrp > $price ? (int) round(($mrp - $price) * 100 / $mrp) : 0,
        'stock'        => (int) $p['stock'],
        'image'        => img_url($p['image']),
        'rating'       => (float) $p['rating_avg'],
        'rating_count' => (int) $p['rating_count'],
    );
}

असली responses

Sort के साथ search
GET /karteasy/api/products.php?q=watch&sort=price_asc HTTP 200 { "success": true, "message": "OK", "data": { "items": [ { "id": 7, "category_id": 3, "name": "Smart Watch Pro", "short_desc": "AMOLED, SpO2, 7-day battery", "price": 1899, "mrp": 4999, "discount_pct": 62, "stock": 35, "image": "https://yourdomain.com/karteasy/uploads/products/p7.png", "rating": 4.5, "rating_count": 128, "in_wishlist": false } ], "page": 1, "limit": 20, "total": 1, "has_more": false } }
Category, हर page पर 2
GET /karteasy/api/products.php?category_id=3&sort=price_asc&limit=2&page=1 HTTP 200 { "success": true, "message": "OK", "data": { "items": [ { "id": 8, "category_id": 3, "name": "Wireless Earbuds X2", "short_desc": "30 hrs playback, ENC mic", "price": 1099, "mrp": 2999, "discount_pct": 63, "stock": 80, "image": "https://yourdomain.com/karteasy/uploads/products/p8.png", "rating": 4.2, "rating_count": 640, "in_wishlist": false }, { "id": 10, "category_id": 3, "name": "Power Bank 20000 mAh", "short_desc": "Dual USB + Type-C, 22.5W", "price": 1499, "mrp": 2499, "discount_pct": 40, "stock": 50, "image": "https://yourdomain.com/karteasy/uploads/products/p10.png", "rating": 4.3, "rating_count": 512, "in_wishlist": false } ], "page": 1, "limit": 2, "total": 4, "has_more": true } }
Product detail (logged in)
GET /karteasy/api/product.php?id=7 Authorization: Bearer 3f9c…e1a7 HTTP 200 { "success": true, "message": "OK", "data": { "id": 7, "category_id": 3, "name": "Smart Watch Pro", "short_desc": "AMOLED, SpO2, 7-day battery", "price": 1899, "mrp": 4999, "discount_pct": 62, "stock": 35, "image": "https://yourdomain.com/karteasy/uploads/products/p7.png", "rating": 4.5, "rating_count": 128, "description": "1.43 inch AMOLED screen, heart-rate and SpO2 tracking, 100+ sports modes and Bluetooth calling.", "category_name": "Electronics", "images": [ "https://yourdomain.com/karteasy/uploads/products/p7.png", "… 1 more" ], "reviews": [ { "name": "Riya Sharma", "rating": 5, "comment": "Battery easily lasts a week. Great value!", "date": "2026-10-19" } ], "in_wishlist": false, "cart_qty": 0, "similar": [ { "id": 9, "category_id": 3, "name": "Bluetooth Speaker Boom", "short_desc": "20W, waterproof IPX7", "price": 2499, "mrp": 3999, "discount_pct": 38, "stock": 22, "image": "https://yourdomain.com/karteasy/uploads/products/p9.png", "rating": 4.4, "rating_count": 205, "in_wishlist": false }, "… 2 more" ] } }
Unknown product
GET /karteasy/api/product.php?id=999 HTTP 404 { "success": false, "message": "Product not found.", "data": null }

अक्सर पूछे जाने वाले प्रश्न (FAQ)

LIMIT और OFFSET सीधे SQL में क्यों डाल सकते हैं?
ये integers हमने खुद (int) और min/max से बनाए हैं, इनमें कभी SQL नहीं आ सकता। User से आने वाली हर value (search text, ids, prices) ? placeholders से जाती है।
बड़ी tables पर LIKE %text% slow है — तब क्या?
कुछ हज़ार products के लिए ठीक है। ज़्यादा के लिए name और short_desc पर FULLTEXT index लगाकर MATCH … AGAINST use करें, या search service। API response वही रहता है, इसलिए app नहीं बदलती।
Product list in_wishlist क्यों भेजती है?
ताकि दूसरी request के बिना हर heart icon सही भरा दिखे। API optional token पढ़ती है; guests को बस false मिलता है।
← E-commerce App Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

अगला topicCategories और Banners API आपकी प्रगति: 5 / 28 (18%) · Lesson 6 / 28 आगे बढ़ें →

यह lesson कैसा लगा?

आपकी राय से हम content और बेहतर बनाते हैं।

अपना review और सुझाव लिखें (वैकल्पिक)

Review approve होने के बाद आपका नाम और शहर इसके साथ public दिख सकते हैं। Email या phone न लिखें।

▶ CodingEasily on YouTube

Video से सीखें – आसान Hindi में

चुनी हुई video: RDBMS भाग 3 – Class XI Commerce IT

▶ Channel subscribe करें