PDO (PHP Data Objects) هي الطريقة المدمجة للتعامل مع قاعدة بيانات. اتصل بـ new PDO($dsn, $user, $password)، ثم شغّل أي استعلام يحتوي قيمة بـ prepare() وexecute()، فتُرسل القيمة منفصلة عن SQL:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'shop_user', 'secret', [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch(); // an array, or false if no row matched
تحتاج أمثلة هذه الصفحة إلى خادم قاعدة بيانات، لذا تُعرض شيفرةً عادية. أما مثال حقن SQL في الأسفل فيعمل.
الاتصال بـ MySQL باستخدام PDO
الوسيط الأول هو DSN: اسم المشغّل، ثم تفاصيل الاتصال. ضع الاتصال في مكان واحد وضمّنه حيث تحتاج إليه.
<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // the default since PHP 8.0
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // rows as ['id' => 1, ...]
PDO::ATTR_EMULATE_PREPARES => false, // real prepared statements, real int types
];
try {
$pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), $options);
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
exit('Database unavailable.');
}
return $pdo;
- مكان
charset=utf8mb4هو DSN. ودونه قد تصل الحروف ذات العلامات والرموز التعبيرية على شكل?أو بايتات مشوهة. - لا تطبع
$e->getMessage()للزوار أبدًا: قد تحتوي اسم المضيف واسم المستخدم. سجّلها. - أبقِ كلمة المرور خارج الشيفرة، في متغير بيئة أو ملف إعدادات خارج جذر الويب.
قواعد البيانات الأخرى لا تغيّر إلا DSN: pgsql:host=localhost;dbname=shop لـ PostgreSQL، وsqlite:/var/data/app.db لملف SQLite. ويحتاج كل منها إلى تثبيت مشغّل PDO الخاص به (pdo_mysql وpdo_pgsql وpdo_sqlite)؛ ويسرد php -m ما لديك منها.
الجمل المُعدّة مع ? و:name
يحدد العنصر النائب مكان القيمة. استخدم العناصر النائبة الموضعية ? مع قائمة، أو المسماة مع مصفوفة ترابطية:
<?php
// Positional
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = ? AND price < ?');
$stmt->execute(['books', 20]);
// Named: easier to read when there are many values
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = :cat AND price < :max');
$stmt->execute(['cat' => 'books', 'max' => 20]);
// bindValue when you want to set the type explicitly
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY id LIMIT :limit');
$stmt->bindValue('limit', 10, PDO::PARAM_INT);
$stmt->execute();
لا تضع علامات تنصيص حول العنصر النائب: تقارن WHERE name = '?' بالنص الحرفي ?. ويمكن أيضًا تنفيذ الجملة المُعدّة مرات كثيرة بقيم مختلفة، لذا أعدّها مرة واحدة قبل الحلقة، لا داخلها.
لماذا يكون ضم النصوص في SQL خطيرًا
هذه هي الكتلة الوحيدة في الصفحة التي تعمل: تبني الاستعلام بالطريقة الخاطئة والطريقة الصحيحة للمُدخل نفسه. غيّر $email وشغّلها مرة أخرى.
في الاستعلام المضموم، تغلق علامة التنصيص في المُدخل النص، ويجعل OR '1'='1' الشرط صحيحًا لكل صف: يعيد الاستعلام كل المستخدمين. وفي نموذج تسجيل الدخول يعني ذلك الدخول دون معرفة كلمة المرور، وإذا قبل المشغّل عدة جمل في استدعاء واحد، فإن مُدخلًا مثل '; DROP TABLE users; -- يشغّل جملة ثانية يختارها المهاجم. وفي النسخة المُعدّة تتلقى قاعدة البيانات SQL والقيمة منفصلتين، فتكون علامة التنصيص مجرد محرف داخل بريد لا يطابق أحدًا.
دوال التهريب مثل addslashes() ليست حلًا: فهي تفوّت حالات تعتمد على مجموعة محارف الاتصال. استخدم العناصر النائبة لكل قيمة، بما فيها الأعداد والقيم التي «تأتي من شيفرتك نفسها».
جلب الصفوف: fetch وfetchAll وfetchColumn
<?php
// One row
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch(); // ['id' => 42, 'name' => 'Ada', ...] or false
if ($user === false) {
http_response_code(404);
}
// All rows
$rows = $pdo->query('SELECT id, name FROM users ORDER BY name')->fetchAll();
// Loop row by row without building an array of all rows
$stmt = $pdo->query('SELECT id, name FROM users');
foreach ($stmt as $row) {
echo $row['name'], "\n";
}
// One value
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
// Two columns as key => value
$names = $pdo->query('SELECT id, name FROM users')->fetchAll(PDO::FETCH_KEY_PAIR); // [42 => 'Ada', ...]
لا بأس بـ query() حين لا يحتوي SQL قيمًا من الخارج. وحالما يدخل متغير، انتقل إلى prepare().
الإدراج والتحديث والحذف
<?php
$stmt = $pdo->prepare('INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$email, $name, password_hash($password, PASSWORD_DEFAULT)]);
$id = (int) $pdo->lastInsertId();
$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute([$newName, $id]);
echo $stmt->rowCount(); // rows changed
$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();
تعيد lastInsertId() نصًا، فحوّله. ومع MySQL، تعدّ rowCount() بعد UPDATE الصفوف التي تغيرت فعلًا، لذا فإن ضبط اسم على القيمة التي يحملها أصلًا يعطي 0. وخزّن كلمات المرور بـ password_hash، لا نصًا عاديًا أبدًا.
المعاملات
تجعل المعاملة عدة جمل تنجح أو تفشل معًا. وإذا رمى أي شيء، تراجع فلا يُحفظ شيء:
<?php
try {
$pdo->beginTransaction();
$pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([100, $from]);
$pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([100, $to]);
$pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')->execute([$from, $to, 100]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
يعتمد هذا على أن الأخطاء ترمي استثناءات (الافتراضي منذ PHP 8.0)، فتقفز الجملة الفاشلة إلى catch. ومع MySQL، يجب أن تستخدم الجداول InnoDB؛ فـ MyISAM يتجاهل المعاملات. وتشرح صفحة try/catch الواجهة Throwable وإعادة الرمي.
WHERE IN مع مصفوفة
العنصر النائب ? الواحد يحمل قيمة واحدة، لذا لا تعمل IN (?) مع مصفوفة. ابنِ عنصرًا نائبًا لكل قيمة:
<?php
$ids = [3, 8, 15];
if ($ids) {
$placeholders = implode(',', array_fill(0, count($ids), '?')); // "?,?,?"
$stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$products = $stmt->fetchAll();
} else {
$products = []; // IN () would be a syntax error
}
النص المُدرج في SQL ليس إلا علامات استفهام وفواصل، فيبقى الاستعلام آمنًا.
أخطاء شائعة مع العناصر النائبة
- أسماء الجداول والأعمدة. ترتّب
ORDER BY ?حسب نص ثابت، لا حسب العمود. اختر المعرّفات من قائمة مسموحة:$sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';، ثم ضع$sortفي SQL. LIMIT ?مع الإعداد المحاكى. معATTR_EMULATE_PREPARESالافتراضي في MySQL، ترسلexecute(['10'])العبارةLIMIT '10'، وهي خطأ في الصيغة. اربطها بـPDO::PARAM_INTأو أوقف المحاكاة.- العنصر النائب المسمى نفسه مرتين. لا تعمل
WHERE first = :q OR last = :qإلا مع الإعداد المحاكى. ومع الإعداد الحقيقي تحصل علىSQLSTATE[HY093]: Invalid parameter number؛ استخدم:q1و:q2. - نسيان أن
fetch()تعيدfalse. تكون$user['name']علىfalseتحذيرًا وnull؛ افحص النتيجة أولًا.
الأسئلة الشائعة
كيف أتصل بـ MySQL باستخدام PDO؟
أنشئ كائن PDO مع DSN ومستخدم وكلمة مرور: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. والاتصال الفاشل يرمي PDOException؛ التقطه، وسجّل الرسالة، واعرض على الزائر خطأً عامًا.
هل تمنع الجمل المُعدّة حقن SQL؟
نعم، للقيم: مع prepare('... WHERE id = ?') وexecute([$id]) تُرسل القيمة بيانات ولا يمكنها أبدًا تغيير الاستعلام. ولا يمكن للعناصر النائبة أن تمثل أسماء الجداول أو الأعمدة أو ASC/DESC، لذا افحص تلك مقابل قائمة بالأسماء المسموحة قبل وضعها في SQL.
ما الفرق بين fetch وfetchAll في PDO؟
تعيد fetch() الصف التالي (أو false حين لا يبقى المزيد)، فتناسب صفًا واحدًا أو حلقة على نتيجة كبيرة. وتعيد fetchAll() كل الصفوف في مصفوفة دفعة واحدة. وتعيد fetchColumn() قيمة واحدة من الصف التالي، وهي مفيدة لـ SELECT COUNT(*).
كيف أستخدم WHERE IN مع مصفوفة في PDO؟
ابنِ ? واحدة لكل قيمة ومرّر المصفوفة إلى execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. وتحقق من أن المصفوفة ليست فارغة أولًا، لأن IN () خطأ في الصيغة.
هل أستخدم PDO أم mysqli؟
استخدم PDO ما لم تحتج إلى ميزة خاصة بـ MySQL. واجهتها البرمجية واحدة لـ MySQL وPostgreSQL وSQLite وغيرها، وتدعم العناصر النائبة المسماة، ومنذ PHP 8.0 ترمي استثناءات عند الأخطاء افتراضيًا. أما mysqli فلا تعمل إلا مع MySQL وMariaDB.