📖 درس سوم: مدیریت دادهها و پایگاهداده در API
🎯 هدف این درس: اتصال حرفهای به پایگاهداده MySQL با PDO، پیادهسازی عملیات CRUD کامل، مدیریت تراکنشها، صفحهبندی (Pagination)، فیلترگذاری و مرتبسازی دادهها در API.
مهمان عزیز! 👋
در درس قبل، API خود را با دادههای ساختگی (آرایه) پیادهسازی کردیم. اما در دنیای واقعی، دادهها در پایگاهداده ذخیره میشوند. در این درس، API خود را به یک دیتابیس واقعی MySQL متصل میکنیم و تمام عملیات CRUD را به صورت دائمی پیادهسازی میکنیم.
همچنین یاد میگیریم چگونه دادهها را صفحهبندی کنیم، فیلترهای مختلف اعمال کنیم و از تراکنشها برای حفظ یکپارچگی دادهها استفاده کنیم.
💡 چرا PDO؟
PDO (PHP Data Objects) یک لایه انتزاعی برای دسترسی به پایگاهداده است که:
- ✅ از Prepared Statements پشتیبانی میکند (امنیت بالا).
- ✅ با چندین نوع دیتابیس کار میکند (MySQL, PostgreSQL, SQLite, …).
- ✅ مدیریت خطا و استثناها را به خوبی انجام میدهد.
🗄️ ساختار دیتابیس
ابتدا یک دیتابیس ساده برای مدیریت کاربران طراحی میکنیم:
database.sql
SQL
MySQL 8.x
-- ایجاد دیتابیس
CREATE DATABASE IF NOT EXISTS api_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE api_db;
-- جدول کاربران
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('admin', 'user', 'guest') DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- درج دادههای نمونه
INSERT INTO users (name, email, password, role) VALUES
('یونس', 'younes@example.com', '$2y$10$...', 'admin'),
('مریم', 'maryam@example.com', '$2y$10$...', 'user'),
('علی', 'ali@example.com', '$2y$10$...', 'user');
🔗 اتصال به دیتابیس (Database.php)
کلاس Database مسئول ایجاد و مدیریت اتصال به دیتابیس با استفاده از PDO است.
Database.php
PHP
8.2
<?php
class Database
{
private static $instance = null;
private $pdo;
private function __construct()
{
$host = 'localhost';
$dbname = 'api_db';
$username = 'root';
$password = '';
try {
$this->pdo = new PDO(
"mysql:host={$host};dbname={$dbname};charset=utf8mb4",
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
// در محیط تولید، خطا را لاگ کن و پیام عمومی نمایش بده
die('Database connection failed: ' . $e->getMessage());
}
}
// Singleton Pattern - فقط یک اتصال در کل برنامه
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection()
{
return $this->pdo;
}
// متد کمکی برای شروع تراکنش
public function beginTransaction()
{
return $this->pdo->beginTransaction();
}
// متد کمکی برای commit تراکنش
public function commit()
{
return $this->pdo->commit();
}
// متد کمکی برای rollback تراکنش
public function rollback()
{
return $this->pdo->rollback();
}
}
💡 نکته مهم – Singleton Pattern:
با استفاده از الگوی Singleton، اطمینان حاصل میکنیم که فقط یک اتصال به دیتابیس در کل برنامه وجود دارد. این کار باعث کاهش مصرف منابع و افزایش عملکرد میشود.
💾 مدل کاربران با دیتابیس (User Model)
حالا مدل User را بهروزرسانی میکنیم تا با دیتابیس واقعی کار کند:
User.php
PHP
8.2
<?php
require_once __DIR__ . '/../Config/Database.php';
class User
{
private $db;
public function __construct()
{
$this->db = Database::getInstance()->getConnection();
}
// دریافت لیست کاربران با صفحهبندی و فیلتر
public function getAll($page = 1, $limit = 10, $filters = [])
{
$offset = ($page - 1) * $limit;
$sql = "SELECT id, name, email, role, created_at, updated_at FROM users WHERE 1=1";
$params = [];
// فیلتر بر اساس نام
if (!empty($filters['name'])) {
$sql .= " AND name LIKE :name";
$params[':name'] = '%' . $filters['name'] . '%';
}
// فیلتر بر اساس ایمیل
if (!empty($filters['email'])) {
$sql .= " AND email LIKE :email";
$params[':email'] = '%' . $filters['email'] . '%';
}
// فیلتر بر اساس نقش
if (!empty($filters['role'])) {
$sql .= " AND role = :role";
$params[':role'] = $filters['role'];
}
// مرتبسازی
$sql .= " ORDER BY id DESC LIMIT :limit OFFSET :offset";
$params[':limit'] = $limit;
$params[':offset'] = $offset;
$stmt = $this->db->prepare($sql);
// اتصال پارامترها با نوع داده مناسب
foreach ($params as $key => $value) {
if ($key === ':limit' || $key === ':offset') {
$stmt->bindValue($key, $value, PDO::PARAM_INT);
} else {
$stmt->bindValue($key, $value, PDO::PARAM_STR);
}
}
$stmt->execute();
return $stmt->fetchAll();
}
// دریافت تعداد کل کاربران (برای صفحهبندی)
public function getTotalCount($filters = [])
{
$sql = "SELECT COUNT(*) as total FROM users WHERE 1=1";
$params = [];
if (!empty($filters['name'])) {
$sql .= " AND name LIKE :name";
$params[':name'] = '%' . $filters['name'] . '%';
}
if (!empty($filters['email'])) {
$sql .= " AND email LIKE :email";
$params[':email'] = '%' . $filters['email'] . '%';
}
if (!empty($filters['role'])) {
$sql .= " AND role = :role";
$params[':role'] = $filters['role'];
}
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetch()['total'];
}
// دریافت یک کاربر با ID
public function find($id)
{
$stmt = $this->db->prepare(
"SELECT id, name, email, role, created_at, updated_at FROM users WHERE id = :id"
);
$stmt->execute([':id' => $id]);
return $stmt->fetch() ?: null;
}
// ایجاد کاربر جدید
public function create($data)
{
// هش کردن رمز عبور
$hashedPassword = password_hash($data['password'], PASSWORD_DEFAULT);
$stmt = $this->db->prepare(
"INSERT INTO users (name, email, password, role) VALUES (:name, :email, :password, :role)"
);
$result = $stmt->execute([
':name' => $data['name'],
':email' => $data['email'],
':password' => $hashedPassword,
':role' => $data['role'] ?? 'user'
]);
if ($result) {
$id = $this->db->lastInsertId();
return $this->find($id);
}
return null;
}
// بروزرسانی کاربر
public function update($id, $data)
{
$fields = [];
$params = [':id' => $id];
if (isset($data['name'])) {
$fields[] = "name = :name";
$params[':name'] = $data['name'];
}
if (isset($data['email'])) {
$fields[] = "email = :email";
$params[':email'] = $data['email'];
}
if (isset($data['password'])) {
$fields[] = "password = :password";
$params[':password'] = password_hash($data['password'], PASSWORD_DEFAULT);
}
if (isset($data['role'])) {
$fields[] = "role = :role";
$params[':role'] = $data['role'];
}
if (empty($fields)) {
return $this->find($id);
}
$sql = "UPDATE users SET " . implode(", ", $fields) . ", updated_at = CURRENT_TIMESTAMP WHERE id = :id";
$stmt = $this->db->prepare($sql);
$result = $stmt->execute($params);
if ($result) {
return $this->find($id);
}
return null;
}
// حذف کاربر
public function delete($id)
{
$stmt = $this->db->prepare("DELETE FROM users WHERE id = :id");
return $stmt->execute([':id' => $id]);
}
// بررسی وجود ایمیل تکراری
public function emailExists($email, $excludeId = null)
{
$sql = "SELECT COUNT(*) as count FROM users WHERE email = :email";
$params = [':email' => $email];
if ($excludeId) {
$sql .= " AND id != :id";
$params[':id'] = $excludeId;
}
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetch()['count'] > 0;
}
}
🎮 کنترلر بهروزرسانی شده (UserController)
کنترلر را با قابلیتهای جدید صفحهبندی، فیلتر و مدیریت خطا بهروز میکنیم:
UserController.php
PHP
8.2
<?php
class UserController
{
private $userModel;
public function __construct()
{
require_once __DIR__ . '/../Models/User.php';
$this->userModel = new User();
}
// GET /users - دریافت لیست کاربران با صفحهبندی و فیلتر
public function index($request)
{
// دریافت پارامترهای صفحهبندی
$page = $_GET['page'] ?? 1;
$limit = $_GET['limit'] ?? 10;
// محدود کردن limit برای جلوگیری از حملات
if ($limit > 100) {
$limit = 100;
}
// دریافت فیلترها
$filters = [];
if (isset($_GET['name'])) {
$filters['name'] = $_GET['name'];
}
if (isset($_GET['email'])) {
$filters['email'] = $_GET['email'];
}
if (isset($_GET['role'])) {
$filters['role'] = $_GET['role'];
}
$users = $this->userModel->getAll($page, $limit, $filters);
$total = $this->userModel->getTotalCount($filters);
return Response::success([
'data' => $users,
'pagination' => [
'current_page' => $page,
'per_page' => $limit,
'total' => $total,
'total_pages' => ceil($total / $limit)
]
]);
}
// GET /users/{id} - دریافت یک کاربر
public function show($request, $params)
{
$id = $params[0] ?? null;
if (!$id || !is_numeric($id)) {
return Response::error('Valid user ID is required', 400);
}
$user = $this->userModel->find($id);
if (!$user) {
return Response::notFound('User not found');
}
return Response::success($user);
}
// POST /users - ایجاد کاربر جدید
public function store($request)
{
$data = $request->getBody();
// اعتبارسنجی
$validation = $request->validate([
'name' => 'required|min:3',
'email' => 'required|email',
'password' => 'required|min:6'
]);
if ($validation !== true) {
return Response::error('Validation failed', 422, $validation);
}
// بررسی تکراری نبودن ایمیل
if ($this->userModel->emailExists($data['email'])) {
return Response::error('Email already exists', 422);
}
$user = $this->userModel->create($data);
if (!$user) {
return Response::error('Failed to create user', 500);
}
return Response::created($user);
}
// PUT /users/{id} - بروزرسانی کاربر
public function update($request, $params)
{
$id = $params[0] ?? null;
if (!$id || !is_numeric($id)) {
return Response::error('Valid user ID is required', 400);
}
$existingUser = $this->userModel->find($id);
if (!$existingUser) {
return Response::notFound('User not found');
}
$data = $request->getBody();
// اعتبارسنجی
$validation = $request->validate([
'name' => 'min:3',
'email' => 'email',
'password' => 'min:6'
]);
if ($validation !== true) {
return Response::error('Validation failed', 422, $validation);
}
// بررسی تکراری نبودن ایمیل (به جز خود کاربر)
if (isset($data['email']) && $this->userModel->emailExists($data['email'], $id)) {
return Response::error('Email already exists', 422);
}
$user = $this->userModel->update($id, $data);
if (!$user) {
return Response::error('Failed to update user', 500);
}
return Response::success($user, 'User updated successfully');
}
// DELETE /users/{id} - حذف کاربر
public function delete($request, $params)
{
$id = $params[0] ?? null;
if (!$id || !is_numeric($id)) {
return Response::error('Valid user ID is required', 400);
}
$existingUser = $this->userModel->find($id);
if (!$existingUser) {
return Response::notFound('User not found');
}
$deleted = $this->userModel->delete($id);
if (!$deleted) {
return Response::error('Failed to delete user', 500);
}
return Response::noContent();
}
}
🔐 مدیریت تراکنشها در دیتابیس
تراکنشها برای عملیاتهایی که شامل چندین دستور SQL هستند، ضروری هستند. مثلاً وقتی کاربر ثبتنام میکند، باید هم اطلاعات کاربر ذخیره شود و هم یک ورودی در جدول لاگ ایجاد شود. اگر یکی از این عملیاتها با خطا مواجه شود، باید کل عملیات برگردانده شود (Rollback).
Transaction Example
PHP
8.2
<?php
// مثال: ثبتنام کاربر با ایجاد لاگ
public function registerWithLog($data)
{
$db = Database::getInstance();
$conn = $db->getConnection();
try {
// شروع تراکنش
$db->beginTransaction();
// ۱. ایجاد کاربر
$user = $this->create($data);
if (!$user) {
throw new Exception('Failed to create user');
}
// ۲. ایجاد لاگ ثبتنام
$logStmt = $conn->prepare(
"INSERT INTO logs (user_id, action, ip_address) VALUES (:user_id, :action, :ip)"
);
$logResult = $logStmt->execute([
':user_id' => $user['id'],
':action' => 'register',
':ip' => $_SERVER['REMOTE_ADDR']
]);
if (!$logResult) {
throw new Exception('Failed to create log');
}
// تأیید همه عملیاتها
$db->commit();
return $user;
} catch (Exception $e) {
// در صورت بروز خطا، همه تغییرات برگردانده میشوند
$db->rollback();
// لاگ کردن خطا
error_log($e->getMessage());
return null;
}
}
⚠️ هشدار مهم:
برای جلوگیری از حملات SQL Injection، همیشه از Prepared Statements استفاده کنید. هرگز مقادیر را مستقیماً در SQL وارد نکنید. PDO با Prepared Statements از شما در برابر حملات محافظت میکند.
🔴 خطاهای رایج در کار با دیتابیس
- ❌ اشتباه: اتصال به دیتابیس در هر درخواست (اتصالهای متعدد).
- ✅ درست: از Singleton Pattern برای یک اتصال استفاده کن.
- ❌ اشتباه: فراموش کردن مدیریت استثناها (Try-Catch).
- ✅ درست: همیشه خطاهای دیتابیس را با Try-Catch مدیریت کن.
- ❌ اشتباه: نمایش پیامهای خطای دیتابیس به کاربر نهایی.
- ✅ درست: خطاها را لاگ کن و پیامهای عمومی برگردان.
- ❌ اشتباه: عدم استفاده از ایندکس در دیتابیس.
- ✅ درست: روی ستونهایی که مرتباً جستجو میشوند ایندکس بزن.
💎 نکات کلیدی درس
- ✅ PDO بهترین روش برای اتصال به دیتابیس در PHP است.
- ✅ Prepared Statements از حملات SQL Injection جلوگیری میکند.
- ✅ Singleton Pattern باعث ایجاد یک اتصال در کل برنامه میشود.
- ✅ صفحهبندی (Pagination) برای APIهایی با دادههای زیاد ضروری است.
- ✅ فیلترها و جستجو باید با دقت پیادهسازی شوند.
- ✅ تراکنشها یکپارچگی دادهها را تضمین میکنند.
- ✅ رمز عبور باید با
password_hash()هش شود.
🛠️ پروژه عملی درس سوم
مهمان عزیز، زمان آن رسیده که مهارتهای خود را در کار با دیتابیس به کار بگیری!
مسئله:
یک API برای مدیریت محصولات (Products) با دیتابیس واقعی پیادهسازی کن. این API باید شامل موارد زیر باشد:
- ✅ دریافت لیست محصولات با صفحهبندی (GET /products)
- ✅ دریافت یک محصول خاص (GET /products/{id})
- ✅ ایجاد محصول جدید (POST /products)
- ✅ بروزرسانی محصول (PUT /products/{id})
- ✅ حذف محصول (DELETE /products/{id})
- ✅ فیلتر بر اساس دستهبندی (category) و قیمت (min_price, max_price)
- ✅ جستجو در نام محصولات (search)
جدول محصولات شامل: id، name، price (DECIMAL)، category (VARCHAR)، stock (INT)، description (TEXT) و created_at، updated_at است.
🧪 راهحل پروژه (پاسخ)
برای پیادهسازی این پروژه، این مراحل را دنبال کن:
- 1️⃣ جدول
productsرا در دیتابیس ایجاد کن. - 2️⃣ کلاس
Productرا با متدهایgetAll()،find()،create()،update()وdelete()بساز. - 3️⃣ کلاس
ProductControllerرا برای مدیریت Endpointها ایجاد کن. - 4️⃣ مسیرهای جدید را در
index.phpثبت کن. - 5️⃣ از Postman برای تست Endpointها استفاده کن.
📝 تمرینهای عملی
🧪 تمرین ۱ (ساده):
با استفاده از Postman، API کاربران را با دیتابیس واقعی تست کن. یک کاربر جدید ایجاد کن، سپس آن را بروزرسانی و بعد حذف کن. تمام مراحل را مستند کن.
🧪 تمرین ۲ (متوسط):
یک جدول logs برای ثبت تمام عملیات CRUD روی کاربران ایجاد کن. هر بار که کاربر ایجاد، بروزرسانی یا حذف میشود، یک رکورد در جدول لاگ ذخیره کن. از تراکنشها برای اطمینان از یکپارچگی دادهها استفاده کن.
🧪 تمرین ۳ (چالشی):
یک سیستم Soft Delete پیادهسازی کن. به جای حذف واقعی کاربران، یک ستون deleted_at به جدول اضافه کن. متد delete() فقط این ستون را با زمان فعلی پر کند. متد getAll() فقط کاربرانی را برگرداند که deleted_at IS NULL دارند.
🏁 جمعبندی درس
مهمان عزیز، در این درس یاد گرفتی:
- ✅ چگونه با PDO به دیتابیس متصل شوی.
- ✅ چگونه عملیات CRUD را با Prepared Statements پیادهسازی کنی.
- ✅ چگونه صفحهبندی و فیلتر را در API پیادهسازی کنی.
- ✅ چگونه از تراکنشها برای یکپارچگی دادهها استفاده کنی.
- ✅ چگونه رمز عبور را با
password_hash()هش کنی. - ✅ چگونه از Singleton Pattern برای مدیریت اتصال استفاده کنی.
حالا API شما به یک دیتابیس واقعی متصل است و دادهها به صورت دائمی ذخیره میشوند. در درس بعدی، امنیت API را با JWT و احراز هویت پیشرفته میپوشانیم. آمادهای؟ 🚀
🗺️ نقشه راه دوره: شما اینجا هستید!
برای اینکه بدانی دقیقاً کجای مسیر هستی و چه درسهایی در انتظار توست، به جدول زیر نگاه کن. درس فعلی با رنگ متفاوت مشخص شده است.
| درس | عنوان درس | آنچه یاد میگیرید |
|---|---|---|
| ۱ | مفاهیم پایه API | HTTP، REST، معماری Client-Server |
| ۲ | پیادهسازی RESTful API با PHP خام | مسیریابی، متدها، پارامترها، پاسخهای JSON |
| ۳ | مدیریت دادهها و پایگاهداده | اتصال PDO، CRUD، تراکنشها، Pagination |
| ۴ | اعتبارسنجی و احراز هویت | JWT، Roles/Permissions، Middleware |
| ۵ | امنیت API پیشرفته | OWASP Top 10، SQLi، XSS، CSRF، Rate Limiting |
| ۶ | مستندسازی API با OpenAPI | Swagger، OpenAPI Specification، مستندات تعاملی |
| ۷ | Caching و افزایش عملکرد | Redis، ETag، Cache-Control، Query Optimization |
| ۸ | API Versioning و مدیریت چرخه حیات | استراتژیهای نسخهبندی، Deprecation، Backward Compatibility |
| ۹ | تست و دیباگ API حرفهای | PHPUnit، Mocking، Logging، Postman Collection |
| ۱۰ | پروژه نهایی: API فروشگاهی | پروژه کامل فروشگاه اینترنتی با تمام قابلیتها |




نظر خود را بنویسید
با ثبت نظر، به بهبود محتوای ما کمک کنید