در مسیر یادگیری PHP، لحظه‌ای می‌رسد که دیگر صفحات استاتیک کافی نیستند. می‌خواهید داده‌ها را ذخیره کنید، بعدا بازیابی کنید، و سایتتان را به یک برنامه‌ی واقعی تبدیل کنید. اینجاست که اتصال PHP به MySQL وارد می‌شود. تجربه‌ی من این است که این لحظه، یکی از بزرگ‌ترین جهش‌ها در یادگیری هر توسعه‌دهنده‌ی وب است — و در عین حال، یکی از پرخطرترین لحظه‌ها، چون امنیت در همین نقطه تعیین می‌شود.

در این مقاله، همان مسیری را باز می‌کنم که در پروژه‌های واقعی برای اتصال PHP به MySQL طی می‌کنم: از انتخاب بین mysqli و PDO، تا نوشتن کد امن با Prepared Statements، تا مدیریت خطا و بهینه‌سازی. اگر با مبانی PHP آشنایی ندارید، پیشنهاد می‌کنم پیش از این مقاله، آموزش PHP از صفر برای مبتدیان را بخوانید. و اگر با مبانی MySQL هم آشنا نیستید، آموزش MySQL از صفر مکمل دقیق این مقاله است.

چرا اتصال به دیتابیس، نقطه‌ی عطف است؟

تا قبل از اتصال به دیتابیس، تمام کد PHP شما در حافظه‌ی موقت اجرا می‌شود. صفحه باز می‌شود، کد اجرا می‌شود، کاربر نتیجه را می‌بیند، و بعد همه چیز تمام می‌شود. هیچ‌چیز ماندگار نیست. دیتابیس، این محدودیت را می‌شکند: داده‌ها ذخیره می‌شوند، در درخواست بعدی قابل بازیابی هستند، و سایت شما از یک «مجموعه صفحه» به یک «برنامه‌ی واقعی» تبدیل می‌شود.

اما این تبدیل، مسئولیت جدیدی هم می‌آورد: امنیت. تا وقتی کد شما فقط در حافظه اجرا می‌شود، بزرگ‌ترین ریسک، یک خطای کد است. وقتی به دیتابیس وصل می‌شوید، ریسک‌ها چند برابر می‌شوند: SQL Injection، افشای داده، خرابی داده، و از دست رفتن اطلاعات. تجربه‌ی من این است که در پروژه‌های واقعی، بیش از نیمی از آسیب‌پذیری‌های امنیتی، از نقطه‌ی اتصال به دیتابیس می‌آید. برای درک عمیق این ریسک‌ها، امنیت وب چیست و چه اصولی دارد نقطه‌ی شروع دقیقی است.

سه لایه‌ای که در این مقاله به آن‌ها می‌پردازم، همان سه لایه‌ای هستند که هر اتصال حرفه‌ای باید داشته باشد:

  • لایه‌ی اتصال: برقراری ارتباط امن با دیتابیس، مدیریت خطاها، و بستن اتصال.
  • لایه‌ی کوئری: نوشتن کوئری‌های امن با Prepared Statements، جلوگیری از SQL Injection، و مدیریت داده‌های ورودی.
  • لایه‌ی کارایی: بهینه‌سازی کوئری‌ها، مدیریت اتصال پایدار، و کاهش بار روی سرور.

در ادامه، هر لایه را با مثال‌های واقعی باز می‌کنم. اگر تا انتهای این مقاله همراه باشید، قادر خواهید بود یک سیستم اتصال حرفه‌ای و امن بسازید که در پروژه‌های واقعی جواب می‌دهد.

در PHP، تا وقتی فقط با متغیرها و آرایه‌ها کار می‌کنید، در زمین تمرین هستید. لحظه‌ای که به دیتابیس وصل می‌شوید، وارد زمین بازی واقعی می‌شوید — و قواعدش جدی‌تر است.

انتخاب بین mysqli و PDO

PHP دو روش اصلی برای اتصال به MySQL دارد: mysqli (MySQL Improved) و PDO (PHP Data Objects). هر دو روی سرورهای امروزی به‌طور پیش‌فرض نصب هستند، هر دو از Prepared Statements پشتیبانی می‌کنند، و هر دو در پروژه‌های واقعی استفاده می‌شوند. اما تفاوت‌های مهمی دارند:

معیارmysqliPDO
دیتابیس‌های پشتیبانی‌شدهفقط MySQLچندین دیتابیس (MySQL، PostgreSQL، SQLite و…)
سینتکسرویه‌ای یا شی‌گرافقط شی‌گرا
Prepared Statementsپشتیبانی کاملپشتیبانی کامل، در همه‌ی درایورها یکسان
Named Parametersندارد (فقط ?)دارد (:name)
قابلیت انتقال کدپایین (فقط MySQL)بالا (با تغییر DSN، به دیتابیس دیگر سوئیچ می‌کنید)
توصیه در پروژه‌های جدیدنادراستاندارد رایج

تجربه‌ی من این است که در پروژه‌های جدید، PDO انتخاب درست است. دلایلش ساده است: Named Parameters خوانایی کد را بالا می‌برد، قابلیت انتقال کد باعث می‌شود در پروژه‌های بزرگ‌تر بدون تغییر ساختار بتوانید دیتابیس را عوض کنید، و شی‌گرا بودنش با OOP در PHP هم‌خوانی طبیعی دارد. اگر با مفهوم OOP در PHP آشنایی ندارید، آموزش شی‌گرایی در PHP پیش‌نیاز ضروری این تصمیم است.

mysqli کجا مناسب است؟ در پروژه‌های بسیار کوچک، یا زمانی که کد قدیمی نگه‌داری می‌کنید و تغییری نمی‌خواهید بدهید. در هر حالت دیگری، PDO انتخاب بهتری است. در ادامه‌ی این مقاله، تمرکز اصلی روی PDO است، چون همان چیزی است که در پروژه‌های حرفه‌ای امروز استفاده می‌شود.

پیش‌نیازها و آماده‌سازی

قبل از شروع، باید مطمئن شوید چند چیز روی محیط شما فراهم است:

  1. PHP نسخه‌ی ۷.۴ یا بالاتر: نسخه‌های قدیمی‌تر پشتیبانی امنیتی ندارند. اگر روی نسخه‌ی قدیمی هستید، تفاوت PHP ۷ و PHP ۸ راهنمای دقیق مهاجرت را در اختیار شما می‌گذارد.
  2. افزونه‌ی PDO و PDO_MYSQL فعال: در اکثر محیط‌ها به‌طور پیش‌فرض فعال است، اما در بعضی هاست‌ها باید از پنل فعالش کنید.
  3. یک دیتابیس MySQL و یک کاربر با دسترسی: اگر با محیط لوکال کار می‌کنید، ابزارهایی مثل XAMPP یا MAMP دیتابیس پیش‌فرض دارند.
  4. آشنایی با دستورات پایه‌ی SQL: حداقل باید بدانید SELECT، INSERT، UPDATE و DELETE چه کاری می‌کنند. اگر این‌ها را نمی‌دانید، دستورات پرکاربرد MySQL نقطه‌ی شروع دقیقی است.

برای اینکه کد این مقاله قابل اجرا باشد، فرض می‌کنم یک دیتابیس ساده به نام shop_db داریم که در آن جدولی به نام products با ساختار زیر وجود دارد:

CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INT DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

اگر با مفهوم utf8mb4 یا InnoDB آشنا نیستید، تفاوت InnoDB و MyISAM و آموزش MySQL از صفر این دو را به‌تفصیل باز کرده‌اند. نکته‌ی مهم: همیشه از utf8mb4 استفاده کنید، نه utf8 ساده؛ چون utf8mb4 از کاراکترهای چهاربایتی (مثل ایموجی) هم پشتیبانی می‌کند.

اتصال با PDO: گام به گام

برای اتصال به دیتابیس با PDO، سه اطلاعات اصلی نیاز دارید: نام دیتابیس، نام کاربری، و رمز عبور. علاوه بر این، یک DSN (Data Source Name) هم لازم است که مشخص می‌کند به چه نوع دیتابیسی و روی چه سروری وصل می‌شوید. ساختار کلی اتصال:

<?php
$dsn = 'mysql:host=localhost;dbname=shop_db;charset=utf8mb4';
$user = 'your_username';
$pass = 'your_password';

try {
    $pdo = new PDO( $dsn, $user, $pass );
    $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
    $pdo->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
    $pdo->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );
    echo 'اتصال با موفقیت برقرار شد.';
} catch ( PDOException $e ) {
    die( 'خطا در اتصال به دیتابیس: ' . $e->getMessage() );
}

سه تنظیمات مهم در این کد:

  • ATTR_ERRMODE = ERRMODE_EXCEPTION: وقتی خطایی رخ دهد، PDO یک استثنا (Exception) پرتاب می‌کند. این رفتار باعث می‌شود که خطاها مخفی نشوند و بتوانید آن‌ها را مدیریت کنید.
  • ATTR_DEFAULT_FETCH_MODE = FETCH_ASSOC: نتیجه‌ی کوئری‌ها به‌صورت آرایه‌ی انجمنی برمی‌گردد، نه دوگانه (هم عددی هم کلید). این باعث خوانایی بیشتر کد می‌شود.
  • ATTR_EMULATE_PREPARES = false: باعث می‌شود Prepared Statements به‌صورت واقعی در سطح MySQL اجرا شوند، نه شبیه‌سازی در PHP. این تنظیم، امنیت را بالا می‌برد و خطاهای دقیق‌تری می‌دهد.

نکته‌ی ظریف درباره‌ی ATTR_EMULATE_PREPARES: در پیش‌فرض قدیمی PHP، این مقدار true بود و بعضی از باگ‌ها و نشت‌های امنیتی از همین‌جا می‌آمد. تجربه‌ی من این است که همیشه آن را false بگذارید.

در اتصال به دیتابیس، سه تنظیمات کوچک، سه تفاوت بزرگ می‌سازند: مدیریت خطا، خوانایی نتیجه، و امنیت. هر سه را باید صریح تنظیم کنید — حتی اگر کد کوتاه‌تر می‌شد با پیش‌فرض.

مدیریت خطاهای اتصال

خطاهای اتصال در پروژه‌های واقعی اجتناب‌ناپذیرند: سرور دیتابیس ممکن است خوابیده باشد، کاربر ممکن است حذف شده باشد، یا رمز عبور تغییر کرده باشد. نکته‌ی مهم: خطاها را هرگز نباید بدون مدیریت رها کنید. اگر خطای خام به کاربر نشان داده شود، اطلاعات حساسی مثل نام کاربری و رمز عبور ممکن است افشا شوند.

روش درست، سه لایه دارد:

try {
    $pdo = new PDO( $dsn, $user, $pass );
    $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
} catch ( PDOException $e ) {
    // لایه‌ی اول: ثبت خطا در لاگ
    error_log( 'DB Connection Error: ' . $e->getMessage() );

    // لایه‌ی دوم: نمایش پیام عمومی به کاربر
    if ( $is_development ) {
        die( 'خطا در اتصال: ' . $e->getMessage() );
    } else {
        die( 'سرویس موقتا در دسترس نیست. لطفا بعدا تلاش کنید.' );
    }
}

سه نکته‌ی مهم در این کد:

  1. لاگ خطا: پیام دقیق خطا در error_log ثبت می‌شود. این کار، تشخیص را در آینده ساده می‌کند.
  2. تفکیک محیط توسعه از تولید: در محیط توسعه، پیام دقیق نمایش داده می‌شود؛ در محیط تولید، پیام عمومی.
  3. عدم افشای اطلاعات: در محیط تولید، هرگز نام کاربری، رمز، یا جزئیات فنی را به کاربر نشان ندهید.

موضوع مشابهی را در خطای ۵۰۰ وردپرس چیست و چگونه رفع می‌شود هم به عنوان یکی از ریشه‌های رایج این خطا مطرح کرده‌ام؛ در آنجا هم افشای اطلاعات در پیام خطا یک اشتباه رایج است.

خواندن داده از دیتابیس

بعد از برقراری اتصال، اولین کاری که معمولا می‌کنید، خواندن داده از دیتابیس است. سه روش اصلی برای خواندن وجود دارد که هر کدام مناسب سناریوی متفاوتی است:

روش اول — خواندن یک رکورد با fetch():

$stmt = $pdo->prepare( 'SELECT * FROM products WHERE id = :id' );
$stmt->execute( [ ':id' => 5 ] );
$product = $stmt->fetch();

if ( $product ) {
    echo $product['name'] . ' - ' . $product['price'];
} else {
    echo 'محصول یافت نشد.';
}

روش دوم — خواندن همه‌ی رکوردها با fetchAll():

$stmt = $pdo->query( 'SELECT * FROM products ORDER BY created_at DESC LIMIT 20' );
$products = $stmt->fetchAll();

foreach ( $products as $product ) {
    echo $product['name'] . ' - ' . $product['price'] . '<br>';
}

روش سوم — خواندن یک مقدار تک با fetchColumn():

$stmt = $pdo->query( 'SELECT COUNT(*) FROM products' );
$count = $stmt->fetchColumn();

echo 'تعداد محصولات: ' . $count;

نکته‌ی مهم: در روش دوم، اگر جدول شما هزاران رکورد داشته باشد، استفاده از fetchAll() بدون LIMIT می‌تواند حافظه‌ی سرور را پر کند و سایت را کند. همیشه یک محدودیت منطقی بگذارید. تجربه‌ی من: در پروژه‌های واقعی، بی‌توجهی به این نکته چندین بار باعث خطای Memory Limit شده است — موضوعی که در خطای Memory Limit در وردپرس به تفصیل باز کرده‌ام.

درج داده در دیتابیس

درج داده با PDO، با Prepared Statement انجام می‌شود. الگوی ساده:

$stmt = $pdo->prepare(
    'INSERT INTO products (name, price, stock) VALUES (:name, :price, :stock)'
);

$stmt->execute( [
    ':name'  => 'هدفون سونی'،
    ':price' => 1250000,
    ':stock' => 15,
] );

$new_id = $pdo->lastInsertId();
echo 'محصول با شناسه ' . $new_id . ' اضافه شد.';

نکته‌ی مهم در این کد: lastInsertId() شناسه‌ی رکورد تازه درج‌شده را برمی‌گرداند. این تابع، در سناریوهایی که بعد از درج یک رکورد، به شناسه‌ی آن نیاز دارید (مثل اضافه کردن جزئیات مرتبط در جدول دیگر)، حیاتی است.

یک تذکر مهم: هرگز مقادیر را مستقیم در کوئری قرار ندهید. مثلا این کد خطرناک است:

// این کد را هرگز ننویسید:
$pdo->query( "INSERT INTO products (name) VALUES ('$name')" );

چرا؟ چون اگر $name چیزی مثل '); DROP TABLE products; -- باشد، جدول شما حذف می‌شود. این همان حمله‌ی SQL Injection است که در بخش بعدی به تفصیل باز می‌کنم.

به‌روزرسانی و حذف داده

الگوی UPDATE و DELETE مشابه INSERT است، با این تفاوت که معمولا با یک شرط WHERE همراه هستند:

// به‌روزرسانی
$stmt = $pdo->prepare(
    'UPDATE products SET price = :price, stock = :stock WHERE id = :id'
);

$stmt->execute( [
    ':price' => 1350000,
    ':stock' => 10,
    ':id'    => 5,
] );

echo $stmt->rowCount() . ' رکورد به‌روزرسانی شد.';

// حذف
$stmt = $pdo->prepare( 'DELETE FROM products WHERE id = :id' );
$stmt->execute( [ ':id' => 5 ] );

echo $stmt->rowCount() . ' رکورد حذف شد.';

تابع rowCount() تعداد رکوردهای تحت تأثیر را برمی‌گرداند. یک هشدار مهم: هرگز کوئری UPDATE یا DELETE بدون WHERE ننویسید — این کار تمام رکوردهای جدول را تغییر می‌دهد یا حذف می‌کند. تجربه‌ی من: چندین فاجعه‌ی واقعی را دیده‌ام که از یک WHERE فراموش‌شده شروع شده است.

در کار با دیتابیس، دو دستور وجود دارند که باید با احتیاط بیشتری نوشته شوند: UPDATE و DELETE. هر دو، پتانسیل از بین بردن حجم بزرگی از داده را دارند. یک لحظه صبر قبل از اجرا، ساعت‌ها پشیمانی را نجات می‌دهد.

Prepared Statements: قلب امنیت

Prepared Statements، ابزار اصلی جلوگیری از SQL Injection هستند. ایده‌ی اصلی ساده است: به جای اینکه مقادیر را داخل کوئری بچسبانید، ابتدا کوئری را با جای‌نگهدارها (placeholder) آماده می‌کنید، سپس مقادیر را جداگانه ارسال می‌کنید. سرور MySQL، مقادیر را به عنوان «داده» پردازش می‌کند، نه به عنوان «کد».

تفاوت عملی را در این دو مثال ببینید:

// روش ناامن (هرگز استفاده نکنید)
$name = $_POST['name'];
$query = "SELECT * FROM products WHERE name = '$name'";
$result = $pdo->query( $query );

// روش امن (Prepared Statement)
$stmt = $pdo->prepare( 'SELECT * FROM products WHERE name = :name' );
$stmt->execute( [ ':name' => $_POST['name'] ] );
$result = $stmt->fetchAll();

در روش اول، اگر کاربر مقدار ' OR '1'='1 را وارد کند، کوئری نهایی به این شکل می‌شود:

SELECT * FROM products WHERE name = '' OR '1'='1'

و این کوئری، تمام رکوردهای جدول را برمی‌گرداند — بدون اینکه کلمه‌ی عبوری وجود داشته باشد. بدتر، اگر کاربر مقدار '; DROP TABLE products; -- را وارد کند، جدول کاملا حذف می‌شود.

در روش دوم (Prepared Statement)، همان ورودی، به عنوان یک رشته‌ی ساده‌ی «داده» پردازش می‌شود و هیچ‌وقت به کد تبدیل نمی‌شود. این تفاوت، همان چیزی است که Prepared Statements را به استاندارد طلایی امنیت در PHP و MySQL تبدیل می‌کند.

نکته‌ی مهم: Prepared Statements را باید در تمام کوئری‌هایی که ورودی کاربر دارند استفاده کنید، نه فقط در SELECT. در INSERT، UPDATE و DELETE هم همین قانون است. تجربه‌ی من: توسعه‌دهندگانی که فقط در SELECT از Prepared Statements استفاده می‌کنند، در معرض خطر جدی هستند. برای مطالعه‌ی بیشتر درباره‌ی این حمله، SQL Injection چیست و چگونه از آن جلوگیری کنیم را ببینید.

SQL Injection: چطور جلوی آن را بگیریم

SQL Injection یکی از قدیمی‌ترین و در عین حال پرتکرارترین آسیب‌پذیری‌های وب است. علتش ساده است: اگر کد شما ورودی کاربر را مستقیم در کوئری قرار دهد، مهاجم می‌تواند کد SQL خودش را وارد کند. سه لایه‌ی دفاعی برای جلوگیری:

لایه‌ی اول — همیشه Prepared Statements: این لایه، اصلی‌ترین و موثرترین لایه‌ی دفاعی است. تمام کوئری‌هایی که ورودی کاربر دارند، باید از Prepared Statements استفاده کنند.

لایه‌ی دوم — اعتبارسنجی ورودی: قبل از ارسال به دیتابیس، ورودی را اعتبارسنجی کنید. مثلا اگر انتظار دارید یک عدد باشد، مطمئن شوید که واقعا عدد است:

$id = filter_input( INPUT_GET, 'id', FILTER_VALIDATE_INT );

if ( $id === false || $id === null ) {
    die( 'شناسه نامعتبر است.' );
}

$stmt = $pdo->prepare( 'SELECT * FROM products WHERE id = :id' );
$stmt->execute( [ ':id' => $id ] );

لایه‌ی سوم — محدودسازی دسترسی کاربر دیتابیس: کاربر دیتابیس شما نباید دسترسی DROP یا ALTER داشته باشد، مگر اینکه واقعا لازم باشد. اگر سایت شما فقط به SELECT، INSERT، UPDATE و DELETE نیاز دارد، همین‌ها را بدهید و بس. این اصل، در بحث امنیت وب چیست و چه اصولی دارد هم به عنوان یکی از پایه‌های معماری امن مطرح شده است.

تراکنش‌ها: عملیات اتمیک

گاهی نیاز دارید چند عملیات دیتابیس را به‌صورت «همه یا هیچ» انجام دهید. مثلا در یک فروشگاه، وقتی مشتری خرید می‌کند، باید هم موجودی محصول کم شود، هم سفارش ثبت شود، هم تراکنش مالی ذخیره شود. اگر یکی از این‌ها شکست بخورد، بقیه هم باید لغو شوند. اینجاست که تراکنش‌ها (Transactions) وارد می‌شوند:

try {
    $pdo->beginTransaction();

    // مرحله‌ی اول: کم کردن موجودی
    $stmt = $pdo->prepare( 'UPDATE products SET stock = stock - :qty WHERE id = :id AND stock >= :qty' );
    $stmt->execute( [ ':qty' => 2, ':id' => 5 ] );

    if ( $stmt->rowCount() === 0 ) {
        throw new Exception( 'موجودی کافی نیست' );
    }

    // مرحله‌ی دوم: ثبت سفارش
    $stmt = $pdo->prepare( 'INSERT INTO orders (product_id, qty) VALUES (:pid, :qty)' );
    $stmt->execute( [ ':pid' => 5, ':qty' => 2 ] );

    $pdo->commit();
    echo 'سفارش با موفقیت ثبت شد.';
} catch ( Exception $e ) {
    $pdo->rollBack();
    echo 'خطا: ' . $e->getMessage();
}

سه متد اصلی در تراکنش‌ها:

  • beginTransaction(): شروع تراکنش. از این لحظه، تمام تغییرات «موقتی» هستند.
  • commit(): تأیید تراکنش. تغییرات برای همیشه ثبت می‌شوند.
  • rollBack(): لغو تراکنش. تمام تغییرات از زمان beginTransaction لغو می‌شوند.

نکته‌ی حیاتی: تراکنش‌ها فقط با موتور InnoDB کار می‌کنند، نه MyISAM. اگر جدول شما موتور MyISAM دارد، تراکنش‌ها هیچ اثری ندارند و شما با یک توهم امنیت کار می‌کنید. برای درک تفاوت این دو موتور، تفاوت InnoDB و MyISAM را ببینید. تجربه‌ی من: در پروژه‌های فروشگاهی، همیشه از InnoDB استفاده کنید، حتی اگر به تراکنش نیاز نداشته باشید.

اتصال در قالب شی‌گرا

در پروژه‌های واقعی، اتصال به دیتابیس معمولا در قالب یک کلاس اختصاصی مدیریت می‌شود. مزایای این رویکرد: متمرکزسازی تنظیمات، استفاده‌ی مجدد، و جداسازی منطق از نمایش. یک الگوی ساده:

class Database {
    private static ?PDO $instance = null;

    public static function getConnection(): PDO {
        if ( self::$instance === null ) {
            $dsn = 'mysql:host=localhost;dbname=shop_db;charset=utf8mb4';

            try {
                self::$instance = new PDO( $dsn, DB_USER, DB_PASS );
                self::$instance->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
                self::$instance->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
                self::$instance->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );
            } catch ( PDOException $e ) {
                error_log( 'DB Error: ' . $e->getMessage() );
                throw new RuntimeException( 'اتصال به دیتابیس برقرار نشد.' );
            }
        }

        return self::$instance;
    }
}

این الگو، Singleton نام دارد: اتصال فقط یک بار ساخته می‌شود و در طول اجرای اسکریپت استفاده می‌شود. در PHP، که هر درخواست در یک پروسه‌ی مستقل اجرا می‌شود، این الگو به کاهش سربار کمک می‌کند — چون ساخت اتصال جدید در هر تابع، هزینه‌ی زیادی دارد. اگر با مفهوم Singleton و سایر الگوهای طراحی آشنا نیستید، آموزش شی‌گرایی در PHP مسیر کاملی از الگوهای طراحی را نشان می‌دهد.

مدیریت اطلاعات حساس اتصال

اطلاعات اتصال (نام کاربری و رمز عبور) اطلاعات حساس هستند و نباید مستقیم در کد قرار بگیرند. سه راه‌حل رایج:

راه اول — استفاده از متغیر محیطی (Environment Variables): استاندارد امروز. اطلاعات در فایل .env ذخیره می‌شوند و از کد جدا هستند. این فایل، در مخزن Git قرار نمی‌گیرد:

DB_HOST=localhost
DB_NAME=shop_db
DB_USER=my_user
DB_PASS=my_secret_password

و در کد:

$dsn = 'mysql:host=' . getenv( 'DB_HOST' ) . ';dbname=' . getenv( 'DB_NAME' ) . ';charset=utf8mb4';
$pdo = new PDO( $dsn, getenv( 'DB_USER' ), getenv( 'DB_PASS' ) );

راه دوم — فایل تنظیمات جداگانه: یک فایل PHP که اطلاعات را برمی‌گرداند و در مخزن Git قرار نمی‌گیرد:

// config.php (خارج از مخزن Git)
return [
    'host' => 'localhost'،
    'name' => 'shop_db'،
    'user' => 'my_user'،
    'pass' => 'my_secret_password'،
];

راه سوم — استفاده از ابزار مدیریت رمز: در پروژه‌های بزرگ، از سرویس‌هایی مثل AWS Secrets Manager یا HashiCorp Vault استفاده می‌شود که اطلاعات را در زمان اجرا و به‌صورت امن می‌رسانند.

نکته‌ی حیاتی: هرگز اطلاعات اتصال را در کد public (مثل مخزن GitHub باز) قرار ندهید. متاسفانه این یکی از رایج‌ترین اشتباهات تازه‌کارها است و باعث افشای دیتابیس‌های واقعی می‌شود.

در کد حرفه‌ای، رمز عبور دیتابیس هرگز در متن کد نوشته نمی‌شود. تفاوت بین یک پروژه‌ی آماتور و یک پروژه‌ی حرفه‌ای، در همین جزئیات کوچک است که در روز اول شاید بی‌اهمیت به نظر برسند.

بهینه‌سازی و اتصال پایدار

در پروژه‌های با ترافیک بالا، دو مفهوم مهم درباره‌ی بهینه‌سازی اتصال وجود دارد: اتصال پایدار (Persistent Connection) و Pool اتصال.

اتصال پایدار: در PHP، به‌طور پیش‌فرض، اتصال دیتابیس در انتهای هر درخواست بسته می‌شود. با اتصال پایدار، اتصال بین درخواست‌ها باقی می‌ماند و در درخواست بعدی استفاده می‌شود. مزیت: کاهش زمان ساخت اتصال. معایب: مصرف حافظه‌ی بیشتر روی سرور، و پتانسیل مشکلات در محیط‌های multi-tenant. تجربه‌ی من: در سایت‌های معمولی، فعال کردن اتصال پایدار تفاوت محسوسی نمی‌سازد؛ در سایت‌های بسیار پربازدید (میلیون‌ها درخواست در روز)، می‌تواند تفاوت مهمی بسازد.

برای فعال کردن اتصال پایدار در PDO، گزینه‌ی ATTR_PERSISTENT را در زمان اتصال تنظیم کنید:

$options = [
    PDO::ATTR_PERSISTENT => true,
    PDO::ATTR_ERRMODE    => PDO::ERRMODE_EXCEPTION,
];

$pdo = new PDO( $dsn, $user, $pass, $options );

Pool اتصال: در معماری‌های پیشرفته، Pool اتصال (اتصال از یک مجموعه‌ی آماده) جایگزین اتصال مستقیم می‌شود. ابزارهایی مثل ProxySQL یا MySQL Router این لایه را مدیریت می‌کنند. این رویکرد، در سایت‌های بسیار پربازدید و محیط‌های میکروسرویس رایج است.

موضوع مکمل: بهینه‌سازی کوئری‌های MySQL — چون حتی اتصال سریع، اگر کوئری کندی داشته باشد، بی‌فایده است. و برای درک عمیق‌تر، تاثیر دیتابیس بر سرعت سایت تصویر کاملی از لایه‌ی دیتابیس ارائه می‌دهد.

اتصال در بستر وردپرس

اگر در بستر وردپرس کار می‌کنید، اتصال به دیتابیس را نباید مستقیم با PDO بسازید. وردپرس یک لایه‌ی انتزاعی به نام wpdb دارد که تمام کارهای مربوط به دیتابیس را انجام می‌دهد. مزایای استفاده از wpdb در بستر وردپرس:

  • مدیریت خودکار پیشوند جدول‌ها: در وردپرس، جدول‌ها پیشوند دارند (مثل wp_posts) و این پیشوند ممکن است در نصب‌های مختلف متفاوت باشد.
  • امنیت داخلی: متدهای wpdb به‌طور پیش‌فرض از Prepared Statements پشتیبانی می‌کنند.
  • هم‌خوانی با هسته: استفاده از wpdb باعث می‌شود کد شما با هسته‌ی وردپرس و افزونه‌های دیگر هم‌خوان باشد.

نمونه‌ی ساده‌ی اتصال از طریق wpdb:

global $wpdb;

$table = $wpdb->prefix . 'custom_products';

$results = $wpdb->get_results(
    $wpdb->prepare( "SELECT * FROM $table WHERE price < %d", 1000000 )
);

نکته‌ی مهم: متد prepare() در wpdb، همان کار Prepared Statement را انجام می‌دهد اما با سینتکس متفاوت (%d برای عدد، %s برای رشته). استفاده‌ی درست از این متد، یکی از پایه‌های امنیت در توسعه‌ی وردپرس است. موضوع مشابه در کدنویسی وردپرس چیست و از کجا شروع کنیم به‌عنوان یکی از اصول کد حرفه‌ای وردپرس مطرح شده است.

اگر می‌خواهید در وردپرس جدول اختصاصی بسازید (که گاهی لازم است)، روند ساخت و مدیریت آن در آموزش مدیریت دیتابیس وردپرس به‌تفصیل آمده است. و اگر می‌خواهید از دیتابیس وردپرس در یک اپلیکیشن بیرونی استفاده کنید، مسیر API مسیر مدرن‌تری است — موضوعی که در API چیست و چه کاربردی دارد و آموزش REST API باز شده است.

اشتباهات رایج و پرهزینه

در سال‌ها کار با اتصالات PHP و MySQL، الگوهای مشخصی از اشتباهات را دیده‌ام. جدول زیر، این اشتباهات را با پیامد و راه‌حل نشان می‌دهد:

اشتباهپیامدروش درست
نوشتن کوئری بدون Prepared StatementSQL Injection و افشای دادههمیشه Prepared Statements
قرار دادن رمز در کد publicافشای دیتابیس در مخزن بازمتغیر محیطی یا فایل جداگانه
نمایش خطای خام به کاربرافشای اطلاعات حساسلاگ خطا و پیام عمومی
استفاده از utf8 به‌جای utf8mb4مشکل با کاراکترهای خاصهمیشه utf8mb4
موتور MyISAM برای داده‌های تراکنشیتراکنش‌ها کار نمی‌کنندهمیشه InnoDB
خواندن تمام رکوردها بدون LIMITخطای Memory Limitهمیشه LIMIT منطقی
نمایش خطای دقیق در محیط تولیدافشای ساختار دیتابیستفکیک محیط توسعه و تولید
اتصال در هر تابع به‌صورت مستقلهزینه‌ی زیاد، مصرف منابعالگوی Singleton
فراموش کردن WHERE در UPDATE یا DELETEتغییر یا حذف تمام داده‌هاهمیشه WHERE صریح
نادیده گرفتن خطاهای اتصالخطاهای پنهان، دیباگ دشوارtry/catch و لاگ

یک اشتباه ظریف که در دیدگاه‌ها زیاد می‌بینم: کاربران فرض می‌کنند که چون سایتشان کوچک است و ترافیک پایینی دارد، امنیت مهم نیست. این فرض، از پایه اشتباه است. ابزارهای خودکار، هر روزه تمام وب را برای آسیب‌پذیری اسکن می‌کنند و سایت‌های کوچک هم قربانی می‌شوند. امنیت، هزینه‌ی بلندمدت نیست؛ سرمایه‌گذاری بلندمدت است.

نقشه‌ی ادامه‌ی مسیر

بعد از تسلط بر مبانی اتصال PHP به MySQL، سه مسیر اصلی برای ادامه وجود دارد:

مسیر اول — لایه‌ی انتزاعی: در پروژه‌های بزرگ، معمولا از یک لایه‌ی انتزاعی بین PHP و دیتابیس استفاده می‌شود. Doctrine ORM و Eloquent در لاراول، دو نمونه‌ی رایج هستند. این ابزارها، کار با دیتابیس را به سطح مدل‌ها و آبجکت‌ها می‌برند و کد را بسیار تمیزتر می‌کنند. شروع این مسیر نیازمند تسلط بر OOP است — همان چیزی که در آموزش شی‌گرایی در PHP باز کرده‌ام.

مسیر دوم — بهینه‌سازی کوئری: حتی اگر لایه‌ی انتزاعی استفاده کنید، درک عمیق کوئری‌های MySQL ضروری است. موضوعات مهم: Index گذاری، Join بهینه، و تحلیل کوئری‌های کند. Index گذاری در MySQL و بهینه‌سازی کوئری‌های MySQL دو منبع کلیدی این مسیر هستند.

مسیر سوم — کار با API: در معماری‌های مدرن، اپلیکیشن‌ها به‌جای اتصال مستقیم به دیتابیس، از API استفاده می‌کنند. این رویکرد، جداسازی منطق را ممکن می‌کند و امنیت را بالا می‌برد. مسیر کامل در API چیست و چه کاربردی دارد و آموزش REST API آمده است. اگر با زبان دیگری هم کار می‌کنید، مقایسه‌ی اتصال PHP با اتصال Python به MySQL می‌تواند دید جالبی بدهد.

در هر سه مسیر، یک توصیه‌ی مشترک دارم: پروژه‌ی واقعی بسازید. اتصال به دیتابیس، بدون پروژه، شبیه یادگیری آشپزی از روی کتاب است. یک پروژه‌ی کوچک انتخاب کنید — مثلا یک سیستم مدیریت مخاطبین، یک انبار ساده، یا یک دفترچه یادداشت آنلاین — و آن را با PDO بسازید. در جریان ساخت، تمام مفاهیمی که در این مقاله خواندید، خودشان جا می‌افتند. برای یک پروژه‌ی محکم‌تر، پروژه‌ی وردپرسی با جدول اختصاصی انتخاب کنید — مسیر کاملش در آموزش مدیریت دیتابیس وردپرس آمده است.

و حالا یک تمرین عملی که می‌تواند در همین امشب، مسیر یادگیری‌تان را روشن کند: یک فایل PHP بسازید که به دیتابیس shop_db وصل شود و جدول products را با سه محصول تستی پر کند. اما نه با INSERT مستقیم — با یک فرم HTML ساده که کاربر نام، قیمت و موجودی را وارد کند. حالا همان فرم را طوری تغییر دهید که اگر کاربر در فیلد قیمت به‌جای عدد، متن وارد کرد، سایت به‌جای خطای خام، پیام دوستانه‌ای نشان دهد. اگر این دو مرحله را انجام دهید، سه چیز را با هم تمرین کرده‌اید: اتصال، Prepared Statements، و اعتبارسنجی ورودی — سه ستون اول امنیت دیتابیس.

و اگر در حین این تمرین به یک رفتار غیرمنتظره رسیدید — مثلا اینکه چرا rowCount() در UPDATE گاهی صفر برمی‌گرداند در حالی که تغییرات اعمال شده، یا چرا یک کوئری با LIMIT بزرگ، سرور را کند می‌کند — همان مشاهدات را در دیدگاه‌ها بنویسید. دیتابیس، از آن دسته موضوعاتی است که هر مشکل عملی، یک درس عمیق در خودش دارد؛ و آن درس وقتی با چند نفر به اشتراک گذاشته شود، خیلی سریع‌تر جا می‌افتد. اگر هم در حین کار با خطای اتصال یا خطای کوئری روبه‌رو شدید، خطاهای رایج MySQL نقطه‌ی شروع دقیقی است — بسیاری از آن خطاها، ریشه‌شان در همان جزئیاتی است که در این مقاله باز کرده‌ام. 🗄️