الاتصال بقاعدة البيانات عبر PDO
ما تعلّمته في 121 يتصل الآن بالواجهة الخلفية. ⏱ نحو 35 دقيقة.
- تشرح دور
PDOوتكتبDSNلـMySQLوSQLite. - تجهّز قاعدة بيانات في
phpMyAdminعلىXAMPP. - تتصل بالقاعدة من ملف واحد وتقرأ الصفوف بـ
fetchAllوfetchوfetchColumn. - تعرض بيانات القاعدة في قالب
HTML، وتتعامل مع أخطاء الاتصال بأمان.
١من الملفات إلى قاعدة البيانات
في مقرر 121 كتبت استعلامات SQL على قاعدة «متجر الواحة». والآن نربطها بالواجهة الخلفية: كود PHP يرسل استعلامًا، ويستقبل الصفوف، ويبني منها الصفحة أو JSON. هذا هو العمل اليومي لمطوّر الواجهات الخلفية.
وفي PHP طريقتان للاتصال بـ MySQL: mysqli الخاصة بـ MySQL، وPDO (PHP Data Objects) التي تعمل بالأسلوب نفسه مع MySQL و SQLite و PostgreSQL وغيرها. نستخدم PDO لأنها الأحدث والأوسع، ولأن الكود نفسه يعمل في المختبر (SQLite) وعلى XAMPP (MySQL) بتغيير سطر الاتصال فقط.
// MySQL على XAMPP$pdo = new PDO('mysql:host=localhost;dbname=store;charset=utf8mb4', 'root', ''); // SQLite (ملف واحد، يستخدمه المختبر)$pdo = new PDO('sqlite:' . __DIR__ . '/store.sqlite');
النص الأول يسمى DSN: نوع القاعدة ومكانها واسمها وترميزها. و utf8mb4 ضروري للعربية والرموز التعبيرية في MySQL.
٢جهّز القاعدة على XAMPP
- شغّل MySQL من XAMPP Control Panel بجانب Apache.
- افتح
http://localhost/phpmyadmin← New وأنشئ قاعدة اسمهاstoreبترميزutf8mb4_unicode_ci. - من تبويب SQL ألصق أوامر إنشاء الجداول والبيانات من مقرر 121 ونفّذها (في MySQL استخدم
VARCHAR(100)بدل TEXT للأعمدة التي عليها UNIQUE). - مستخدم XAMPP الافتراضي
rootبلا كلمة مرور. هذا للتطوير فقط؛ على الخادم الحقيقي لكل تطبيق مستخدم بكلمة مرور قوية وصلاحيات محدودة.
٣ملف الاتصال وأول استعلام
نضع الاتصال في db.php ونضمّنه في كل صفحة تحتاج القاعدة. والإعدادان فيه مهمان: الأخطاء تصير استثناءات (الدرس 12)، والصفوف تعود مصفوفات ترابطية مفاتيحها أسماء الأعمدة:
<?phprequire __DIR__ . '/db.php'; $rows = $pdo->query('SELECT id, name, city FROM customers ORDER BY name')->fetchAll(); echo "<p>عدد العملاء: " . count($rows) . "</p><ul>";foreach ($rows as $r) { echo "<li>{$r['id']}. {$r['name']} — {$r['city']}</li>";}echo "</ul>";
<?php// db.php: الاتصال بقاعدة البيانات في مكان واحد// في المختبر نستخدم SQLite (ملف). وعلى XAMPP بدّل السطر بسطر MySQL الذي تحته$pdo = new PDO('sqlite:' . __DIR__ . '/store.sqlite');// $pdo = new PDO('mysql:host=localhost;dbname=store;charset=utf8mb4', 'root', ''); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // الأخطاء استثناءات$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // الصفوف مصفوفات ترابطية
| الطريقة | تعيد |
|---|---|
$pdo->query($sql) | كائن «نتيجة» Statement (للاستعلامات الثابتة بلا مدخلات من المستخدم). |
->fetchAll() | كل الصفوف: مصفوفة من المصفوفات الترابطية. |
->fetch() | صفًّا واحدًا، أو false إن انتهت الصفوف. |
->fetchColumn() | قيمة أول عمود في الصف التالي، مثالية لـ COUNT و SUM. |
٤القالب نفسه، والبيانات من القاعدة
تذكّر جدول المنتجات في الدرس 3: كانت المصفوفة مكتوبة باليد. الآن تأتي من القاعدة، والقالب لم يتغيّر حرفًا:
<?phprequire __DIR__ . '/db.php';$products = $pdo->query('SELECT name, category, price, stock FROM products ORDER BY category, price')->fetchAll();$total = $pdo->query('SELECT SUM(stock) FROM products')->fetchColumn();?><table border="1" cellpadding="5"> <tr><th>المنتج</th><th>التصنيف</th><th>السعر</th><th>الحالة</th></tr> <?php foreach ($products as $p): ?> <tr> <td><?= htmlspecialchars($p['name']) ?></td> <td><?= $p['category'] ?></td> <td><?= $p['price'] ?> ريال</td> <td><?= $p['stock'] > 0 ? 'متوفر' : 'نفد' ?></td> </tr> <?php endforeach; ?></table><p>القطع في المخزون: <?= $total ?></p>
<?php// db.php: الاتصال بقاعدة البيانات في مكان واحد// في المختبر نستخدم SQLite (ملف). وعلى XAMPP بدّل السطر بسطر MySQL الذي تحته$pdo = new PDO('sqlite:' . __DIR__ . '/store.sqlite');// $pdo = new PDO('mysql:host=localhost;dbname=store;charset=utf8mb4', 'root', ''); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // الأخطاء استثناءات$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // الصفوف مصفوفات ترابطية
جرّب في المختبر: عدّل الاستعلام ليعرض المتوفر فقط بـ WHERE stock > 0، أو صنفًا واحدًا. كل ما تعلّمته في 121 يعمل هنا.
٥صف واحد، وقيمة واحدة، والتجميع
<?phprequire __DIR__ . '/db.php'; // صف واحد$top = $pdo->query('SELECT name, price FROM products ORDER BY price DESC LIMIT 1')->fetch();echo "الأغلى: {$top['name']} ({$top['price']})\n"; // قيمة واحدةecho "عدد الطلبات: " . $pdo->query('SELECT COUNT(*) FROM orders')->fetchColumn() . "\n"; // تجميع من جدولين$sql = 'SELECT c.city, COUNT(*) AS n FROM orders o JOIN customers c ON c.id = o.customer_id GROUP BY c.city ORDER BY n DESC';foreach ($pdo->query($sql) as $r) { // يمكن المرور على النتيجة مباشرة echo "{$r['city']}: {$r['n']}\n";}
<?php// db.php: الاتصال بقاعدة البيانات في مكان واحد// في المختبر نستخدم SQLite (ملف). وعلى XAMPP بدّل السطر بسطر MySQL الذي تحته$pdo = new PDO('sqlite:' . __DIR__ . '/store.sqlite');// $pdo = new PDO('mysql:host=localhost;dbname=store;charset=utf8mb4', 'root', ''); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // الأخطاء استثناءات$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // الصفوف مصفوفات ترابطية
٦حين يفشل الاتصال
كلمة مرور خاطئة، أو خادم MySQL متوقف، أو جدول غير موجود: كلها تصير PDOException. ورسالتها قد تحوي اسم المستخدم ومسار الملف، فلا تعرضها للزائر:
<?phptry { $pdo = new PDO('mysql:host=localhost;dbname=store;charset=utf8mb4', 'root', '');} catch (PDOException $e) { error_log('DB: ' . $e->getMessage()); // التفاصيل للمطوّر في السجل http_response_code(503); // Service Unavailable exit('الخدمة غير متاحة مؤقتًا'); // رسالة عامة للزائر}
توقّع وتحقّق
ما ميزة PDO على mysqli؟
MySQL و SQLite و PostgreSQL…لعدد الصفوف من COUNT(*) نستخدم:
FETCH_ASSOC تجعل الصف:
r['name']ERRMODE_EXCEPTION تعني:
try/catch.الخلاصة
new PDO(DSN، مستخدم، كلمة مرور). وDSNلـMySQL: mysql:host=…;dbname=…;charset=utf8mb4db.phpواحد بإعدادين:ERRMODE_EXCEPTIONوFETCH_ASSOC.queryللاستعلامات الثابتة، ثمfetchAllأوfetchأوfetchColumn.- القالب لا يتغير؛ يتغير مصدر المصفوفة فقط.
- أخطاء الاتصال: سجّلها، وأعطِ الزائر رسالة عامة و503.
تحدٍّ برمجي: لوحة إحصاءات المتجر
متوسطاكتب stats.php التي تقرأ من قاعدة «متجر الواحة» وتعرض لوحة إحصاءات. db.php جاهز.
- اطبع
<p>المنتجات: 12</p>بعدد المنتجات من القاعدة (لا تكتبه بيدك). - اطبع
<p>النافد: …</p>بعدد المنتجات التي stock فيها 0. - ثم قائمة
<ul>بعدد المنتجات في كل تصنيف، مرتبة أبجديًّا بالتصنيف:<li>التصنيف: العدد</li> - استخدم db.php و $pdo.
👀 اعرض الاستجابة المطلوبة
GET /stats.php
- الكود يعمل بلا أخطاء أو تحذيرات
- عدد المنتجات من القاعدة
- النافد والقائمة كما في المطلوب تمامًا
- تستخدم GROUP BY
- لا تكتب العدد 12 بيدك
💡 تلميح 1
$pdo->query('SELECT COUNT(*) FROM products')->fetchColumn()💡 تلميح 2
SELECT category, COUNT(*) AS n FROM products GROUP BY category ORDER BY categoryحاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
<?phprequire __DIR__ . '/db.php'; $count = $pdo->query('SELECT COUNT(*) FROM products')->fetchColumn();$out = $pdo->query('SELECT COUNT(*) FROM products WHERE stock = 0')->fetchColumn();echo "<p>المنتجات: $count</p>";echo "<p>النافد: $out</p>"; echo "<ul>";foreach ($pdo->query('SELECT category, COUNT(*) AS n FROM products GROUP BY category ORDER BY category') as $r) { echo "<li>{$r['category']}: {$r['n']}</li>";}echo "</ul>";
<?php// db.php: الاتصال بقاعدة البيانات في مكان واحد// في المختبر نستخدم SQLite (ملف). وعلى XAMPP بدّل السطر بسطر MySQL الذي تحته$pdo = new PDO('sqlite:' . __DIR__ . '/store.sqlite');// $pdo = new PDO('mysql:host=localhost;dbname=store;charset=utf8mb4', 'root', ''); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // الأخطاء استثناءات$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // الصفوف مصفوفات ترابطية