API Setup: Folders, PDO और JSON
हमारी API कैसी दिखती है
REST API URLs का एक set है जो web pages की जगह data लौटाता है। App request भेजती है, PHP चलता है, और हम JSON में जवाब देते हैं:
| Method | किसके लिए | Example |
|---|---|---|
| GET | data पढ़ना | products.php?q=watch |
| POST | data बनाना / बदलना (body JSON में) | cart/add.php {"product_id":7,"qty":1} |
हम इसे simple रखते हैं: सिर्फ GET और POST, हर endpoint के लिए एक PHP file। यह बिना किसी special server rule के हर hosting पर चलता है।
Folder structure
📁 backend/ .htaccess database.sql 📁 includes/ ← shared code, blocked from the browser .htaccess config.php DB password, BASE_URL, delivery fee db.php one PDO connection helpers.php json_out(), input(), require_auth() … cart.php cart totals + coupons (later lesson) 📁 api/ ← one PHP file = one endpoint index.php health check home.php products.php product.php categories.php banners.php 📁 auth/ cart/ wishlist/ address/ coupons/ orders/ reviews/ 📁 admin/ ← admin panel (later lesson) 📁 uploads/ ← images (PHP disabled here)
Step 1: config.php
सारी settings एक file में। हर value environment variable (getenv) से भी आ सकती है, जो local testing में काम आता है — hosting पर आप बस default values edit कर दें।
<?php
// ------------------------------------------------------------------
// KartEasy configuration. Edit these values after uploading.
// On Hostinger: hPanel -> Databases -> MySQL Databases shows the
// database name, user and host (usually "localhost").
// ------------------------------------------------------------------
define('DB_HOST', getenv('KE_DB_HOST') ?: 'localhost');
define('DB_PORT', getenv('KE_DB_PORT') ?: '3306');
define('DB_NAME', getenv('KE_DB_NAME') ?: 'karteasy');
define('DB_USER', getenv('KE_DB_USER') ?: 'karteasy_user');
define('DB_PASS', getenv('KE_DB_PASS') ?: 'change-this-password');
// Full URL of the backend folder, used to turn "uploads/x.png" into a full image URL.
// Example: https://yourdomain.com/karteasy
define('BASE_URL', rtrim(getenv('KE_BASE_URL') ?: 'https://yourdomain.com/karteasy', '/'));
define('TOKEN_DAYS', 30); // login token validity
define('DELIVERY_FEE', 40); // charged when subtotal is below FREE_DELIVERY_ABOVE
define('FREE_DELIVERY_ABOVE', 499);
define('DEBUG', getenv('KE_DEBUG') === '1'); // never true on a live site
date_default_timezone_set('Asia/Kolkata');DEBUG on होने पर असली error messages (table names, SQL) app को भेजे जाते हैं। अपने computer पर काम के, online ख़तरनाक।
Step 2: db.php (PDO)
PDO PHP की database layer है। हम हर request में connection सिर्फ एक बार बनाते हैं (static $pdo) और तीन ज़रूरी options on करते हैं:
<?php
require_once __DIR__ . '/config.php';
/**
* One shared PDO connection per request.
* Prepared statements are real (not emulated) and errors throw exceptions.
*/
function db()
{
static $pdo = null;
if ($pdo === null) {
$dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=utf8mb4';
$pdo = new PDO($dsn, DB_USER, DB_PASS, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
));
}
return $pdo;
}| Option | क्या करता है |
|---|---|
| ERRMODE_EXCEPTION | SQL errors चुपचाप false लौटाने की जगह exception throw करते हैं |
| FETCH_ASSOC | rows array('name' => 'Smart Watch Pro', …) की तरह आती हैं |
| EMULATE_PREPARES false | असली prepared statements — SQL injection से मुख्य बचाव |
Step 3: helpers.php
हर endpoint require_once __DIR__ . '/../includes/helpers.php'; से शुरू होता है। यह file JSON headers set करती है, सारे errors पकड़ती है और छोटे helper functions देती है। इसे एक बार ध्यान से पढ़ें — बाकी पूरी API इन्हीं टुकड़ों से बनी है।
<?php
require_once __DIR__ . '/db.php';
// Every API response is JSON, and the app may call us from any origin.
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// Any uncaught error becomes a clean JSON 500 instead of an HTML page.
set_exception_handler(function ($e) {
error_log('[KartEasy] ' . $e->getMessage());
$msg = DEBUG ? $e->getMessage() : 'Something went wrong. Please try again.';
json_out(array('success' => false, 'message' => $msg), 500);
});
/** Send a JSON response and stop. */
function json_out($data, $code = 200)
{
http_response_code($code);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function ok($data = null, $message = 'OK')
{
json_out(array('success' => true, 'message' => $message, 'data' => $data));
}
function fail($message, $code = 400)
{
json_out(array('success' => false, 'message' => $message, 'data' => null), $code);
}
/** Only allow the given HTTP method. */
function require_method($method)
{
if ($_SERVER['REQUEST_METHOD'] !== $method) {
fail('Method not allowed. Use ' . $method . '.', 405);
}
}
/**
* Read the request body. Works for JSON bodies (Retrofit @Body)
* and for normal form posts (@FormUrlEncoded).
*/
function input()
{
static $data = null;
if ($data === null) {
$raw = file_get_contents('php://input');
$json = json_decode($raw, true);
$data = is_array($json) ? $json : $_POST;
}
return $data;
}
function in_str($key, $default = '')
{
$d = input();
return isset($d[$key]) ? trim((string) $d[$key]) : $default;
}
function in_int($key, $default = 0)
{
$d = input();
return isset($d[$key]) ? (int) $d[$key] : $default;
}
function q_int($key, $default = 0)
{
return isset($_GET[$key]) ? (int) $_GET[$key] : $default;
}
function q_str($key, $default = '')
{
return isset($_GET[$key]) ? trim((string) $_GET[$key]) : $default;
}
/** Return "Name is required" style errors for missing fields. */
function require_fields($fields)
{
foreach ($fields as $key => $label) {
if (in_str($key) === '') {
fail($label . ' is required.', 422);
}
}
}
/** "uploads/p1.png" -> "https://site.com/karteasy/uploads/p1.png" */
function img_url($path)
{
if ($path === null || $path === '') {
return null;
}
if (preg_match('#^https?://#', $path)) {
return $path;
}
return BASE_URL . '/' . ltrim($path, '/');
}
/** Read the Bearer token from the Authorization header. */
function bearer_token()
{
$h = '';
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
$h = $_SERVER['HTTP_AUTHORIZATION'];
} elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$h = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (function_exists('apache_request_headers')) {
$all = apache_request_headers();
foreach ($all as $k => $v) {
if (strtolower($k) === 'authorization') {
$h = $v;
}
}
}
if (preg_match('/Bearer\s+([a-f0-9]{64})/i', $h, $m)) {
return strtolower($m[1]);
}
return null;
}
/** Create a new login token for a user. Only its sha256 hash is stored. */
function create_token($userId)
{
$token = bin2hex(random_bytes(32));
$stmt = db()->prepare('INSERT INTO user_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)');
$stmt->execute(array($userId, hash('sha256', $token), date('Y-m-d H:i:s', time() + TOKEN_DAYS * 86400)));
return $token;
}
/** Returns the logged-in user row, or stops with 401. */
function require_auth()
{
$token = bearer_token();
if ($token === null) {
fail('Please login to continue.', 401);
}
$stmt = db()->prepare(
'SELECT u.id, u.name, u.email, u.phone, u.created_at FROM user_tokens t
JOIN users u ON u.id = t.user_id
WHERE t.token_hash = ? AND t.expires_at > NOW()'
);
$stmt->execute(array(hash('sha256', $token)));
$user = $stmt->fetch();
if (!$user) {
fail('Session expired. Please login again.', 401);
}
$user['id'] = (int) $user['id'];
return $user;
}
/** Shape one product row for the app (numbers as numbers, full image URL). */
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'],
);
}
/** Product ids in the user's wishlist, as a lookup array [id => true]. */
function wishlist_ids($userId)
{
$stmt = db()->prepare('SELECT product_id FROM wishlist WHERE user_id = ?');
$stmt->execute(array($userId));
$ids = array();
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $id) {
$ids[(int) $id] = true;
}
return $ids;
}
/** Optional login: returns the user id if a valid token was sent, else 0. */
function optional_user_id()
{
$token = bearer_token();
if ($token === null) {
return 0;
}
$stmt = db()->prepare('SELECT user_id FROM user_tokens WHERE token_hash = ? AND expires_at > NOW()');
$stmt->execute(array(hash('sha256', $token)));
$id = $stmt->fetchColumn();
return $id ? (int) $id : 0;
}
function money($n)
{
return round((float) $n, 2);
}
/** Money for messages: ₹1,899 or ₹224.80 */
function rs($n)
{
$n = round((float) $n, 2);
return '₹' . number_format($n, floor($n) == $n ? 0 : 2);
}| Helper | काम |
|---|---|
| json_out / ok / fail | सही HTTP status के साथ JSON भेजें और रुक जाएँ |
| require_method('POST') | गलत method पर 405 |
| input(), in_str(), in_int() | JSON body (या normal form post) पढ़ें |
| q_str(), q_int() | ?query parameters पढ़ें |
| require_fields() | "Email is required." जैसे 422 errors |
| img_url() | uploads/p7.png को Glide के लिए पूरे https URL में बदलें |
| require_auth(), create_token() | login tokens (अगला lesson) |
| product_out() | हर जगह product का एक जैसा JSON shape |
<?php
// Without helpers – every endpoint would repeat this:
header('Content-Type: application/json; charset=utf-8');
http_response_code(404);
echo json_encode(array('success' => false, 'message' => 'Product not found.', 'data' => null));
exit;
// With helpers – one line:
fail('Product not found.', 404);हर चीज़ के लिए एक response format
हर endpoint — success हो या error — वही तीन keys लौटाता है। तब Android app सारी calls के लिए एक Kotlin class (ApiResponse<T>) use कर सकती है।
Success
- HTTP 200
- "success": true
- "message": "Added to cart."
- "data": { … }
Error
- HTTP 401 / 404 / 409 / 422
- "success": false
- "message": "Only 12 left in stock."
- "data": null
| Status | KartEasy में मतलब |
|---|---|
| 200 | OK |
| 401 | login नहीं / token expire |
| 403 | allowed नहीं (जैसे delivery से पहले review) |
| 404 | नहीं मिला |
| 405 | गलत method (POST की जगह GET) |
| 409 | conflict: stock ख़त्म, email पहले से use |
| 422 | validation error: गलत phone, field missing |
| 500 | server bug (details सिर्फ error log में) |
Step 4: Private files बचाएँ
तीन छोटी .htaccess files (Apache / LiteSpeed — Hostinger और cPanel पर यही होते हैं):
Options -Indexes
# Pass the "Authorization: Bearer ..." header to PHP (some Apache/LiteSpeed setups drop it).
RewriteEngine On
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# The SQL file must never be downloadable.
<Files "database.sql">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
</Files># Nobody may open these files from a browser (they contain the DB password). <IfModule mod_authz_core.c> Require all denied </IfModule> <IfModule !mod_authz_core.c> Deny from all </IfModule>
# Uploaded files are images only: never run PHP from this folder.
<FilesMatch "\.(php|phtml|php3|php4|php5|php7|phar)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
</FilesMatch>
Options -Indexesकुछ shared hosts पर PHP को Authorization: Bearer … header दिखता ही नहीं। RewriteRule उसे $_SERVER['HTTP_AUTHORIZATION'] में copy करता है, और bearer_token() तीनों जगह check करता है।
Step 5: पहला endpoint + चलाएँ
"Health check" endpoint एक नज़र में बता देता है कि PHP, config और database ठीक हैं:
<?php
// Open https://yourdomain.com/karteasy/api/ in a browser to check that the API and database work.
require_once __DIR__ . '/../includes/helpers.php';
$products = (int) db()->query('SELECT COUNT(*) FROM products')->fetchColumn();
ok(array(
'name' => 'KartEasy API',
'version' => '1.0',
'database' => 'connected',
'products' => $products,
'php' => PHP_VERSION,
'endpoints' => array(
'GET home.php', 'GET categories.php', 'GET banners.php', 'GET products.php', 'GET product.php?id=',
'POST auth/register.php', 'POST auth/login.php', 'POST auth/logout.php', 'GET auth/me.php', 'POST auth/update.php',
'GET cart/list.php', 'POST cart/add.php', 'POST cart/update.php', 'POST cart/remove.php',
'GET wishlist/list.php', 'POST wishlist/toggle.php',
'GET address/list.php', 'POST address/save.php', 'POST address/delete.php',
'GET coupons/list.php', 'POST coupons/apply.php',
'POST orders/place.php', 'GET orders/list.php', 'GET orders/detail.php?id=', 'POST orders/cancel.php',
'GET reviews/list.php?product_id=', 'POST reviews/add.php',
),
), 'API is running');Backend को PHP के built-in server से चलाएँ (XAMPP Apache की ज़रूरत नहीं):
cd backend # Use your own local DB user. The env vars override config.php (no file edit needed) KE_DB_USER=root KE_DB_PASS= KE_BASE_URL=http://10.0.2.2:8000 KE_DEBUG=1 php -S 0.0.0.0:8000 # New terminal window: curl http://localhost:8000/api/
HTTP 500 के साथ "Something went wrong" का मतलब ज़्यादातर गलत DB_USER / DB_PASS / DB_NAME है। असली message देखने के लिए KE_DEBUG=1 से शुरू करें। Hostinger पर host localhost होता है और names में u123456_ जैसा prefix।
PHP 7.2 friendly code
बहुत-सी shared hosting पर अभी भी पुराना PHP है। पूरे backend को PHPCompatibility से PHP 7.2+ के लिए check किया गया है, इसलिए यह 7.2, 7.4, 8.0 … 8.3 पर चलता है। हम नया syntax avoid करते हैं:
Avoid किया (PHP 7.4 / 8 चाहिए)
- fn($x) => $x * 2
- private int $count;
- match ($status) { … }
- $user?->name
- str_contains()
इसकी जगह (PHP 7.2 OK)
- function ($x) { return $x * 2; }
- private $count;
- if / elseif या array map
- isset($user) ? $user->name : null
- strpos($a, $b) !== false
