Kodokon kodokon.com

PDO: العبارات المُحضَّرة والمعاملات

اضبط PDO كما يُضبَط في الإنتاج، وتخلّص بنيويًا من حقن SQL، واجعل عمليات الكتابة لديك موثوقة بالمعاملات.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

إن PDO هي الواجهة الموحّدة للوصول إلى قواعد البيانات: فسواء كانت MySQL أو PostgreSQL أو SQLite، تُدار جميعها بالـ API نفسها. وثلاثة إعدادات تفصل الاتصال الساذج عن اتصال الإنتاج: ERRMODE_EXCEPTION كي يُطلق كل خطأ SQL استثناءً بدلًا من الفشل بصمت، وFETCH_ASSOC افتراضيًا للحصول على نتائج نظيفة، وEMULATE_PREPARES مضبوطًا على false لعبارات مُحضَّرة يُنفّذها الخادم فعليًا. وأضِف charset=utf8mb4 مباشرةً في الـ DSN - فهو المكان الوحيد الموثوق للتصريح به.

PHP
<?php

declare(strict_types=1);

$dsn = 'mysql:host=localhost;dbname=shop'
    . ';charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

$pdo = new PDO($dsn, 'app_user', 'secret', $options);
اتصال إنتاجي: استثناءات، جلب ترابطي (associative)، تحضير أصلي.

العبارة المُحضَّرة هي دفاعك البنيوي الوحيد ضد حقن SQL. والمبدأ: تُرسَل بنية الاستعلام أولًا إلى الخادم، بما فيها العناصر النائبة :email أو ?؛ ثم تتبعها البيانات بشكل منفصل، ولا تُفسَّر أبدًا على أنها SQL. ويبقى ربط متغير بالاستعلام بالتسلسل (concatenation)، ولو كان "مُهرَّبًا"، خطأً مهنيًا. والعناصر النائبة المُسمّاة تجعل الكود مقروءًا؛ أما الموضعية فتكفي للاستعلامات القصيرة.

PHP
<?php

declare(strict_types=1);

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION,
);

$pdo->exec(
    'CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        email TEXT NOT NULL
    )'
);

$insert = $pdo->prepare(
    'INSERT INTO users (email) VALUES (:email)'
);
$insert->execute(['email' => 'lea@example.com']);

$query = $pdo->prepare(
    'SELECT id, email FROM users
     WHERE email = :email'
);
$query->execute(['email' => 'lea@example.com']);

var_dump($query->fetch(PDO::FETCH_ASSOC));
مثال SQLite قائم بذاته: التحضير، والربط، والتنفيذ.

تضمن المعاملة الذرّية: فإما أن تنجح جميع عمليات الكتابة، أو لا ينجح أيّ منها. والنمط المعياري: beginTransaction()، ثم العمل داخل try، ثم commit() في نهاية الكتلة، وrollBack() في catch - ثم تعيد إطلاق الاستثناء، لأن إخفاء الفشل أسوأ من الفشل نفسه. وأبقِ المعاملات قصيرة: فكل قفل يُحتجز أثناء نداء شبكي أو عملية طويلة يُضعِف التزامن (concurrency) عبر التطبيق بأكمله.

PHP
<?php

declare(strict_types=1);

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION,
);

$pdo->exec(
    'CREATE TABLE accounts (
        id INTEGER PRIMARY KEY,
        balance INTEGER NOT NULL
    )'
);
$pdo->exec(
    'INSERT INTO accounts (balance)
     VALUES (100), (20)'
);

$pdo->beginTransaction();

try {
    $debit = $pdo->prepare(
        'UPDATE accounts
         SET balance = balance - :n
         WHERE id = :id'
    );
    $debit->execute(['n' => 50, 'id' => 1]);

    $credit = $pdo->prepare(
        'UPDATE accounts
         SET balance = balance + :n
         WHERE id = :id'
    );
    $credit->execute(['n' => 50, 'id' => 2]);

    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

echo 'Transfer done';
تحويل ذرّي: إما أن ينجح كل شيء، أو يُتراجَع عن كل شيء.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. لماذا تمنع العبارة المُحضَّرة حقن SQL؟
    • تُهرِّب علامات الاقتباس المفردة في السلاسل تلقائيًا
    • تنتقل بنية SQL والبيانات بشكل منفصل: فلا تُفسَّر البيانات أبدًا على أنها كود SQL
    • تُشفِّر الاستعلام بين PHP والخادم
  2. ما الذي يمكنك تمريره عبر عنصر نائب :param في عبارة مُحضَّرة؟
    • قيمة: سلسلة، أو عدد، أو null
    • اسم جدول أو عمود
    • جملة ORDER BY كاملة
  3. في نمط المعاملة المعياري، ماذا تفعل في كتلة catch؟
    • commit() لحفظ ما نجح
    • rollBack()، ثم إعادة إطلاق الاستثناء
    • لا شيء: تنتهي صلاحية المعاملة من تلقاء نفسها