ارتباط با دیتابیس در PHP از طریق لایبرری PDO

ارتباط با دیتابیس در PHP از طریق لایبرری PDO

در این پست به بررسی این موضوع خواهیم پرداخت که به چه شکل می‌شود از طریق لایبرری PDO در زبان برنامه‌نویسی PHP به ارتباط با دیتابیس پرداخت.

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

PDO مخفف واژگان PHP Data Object است و به منزله یکی از ای‌پی‌آی‌های زبان برنامه‌نویسی PHP برای ارتباط با دیتابیس است (برای آشنایی بیشتر با اصطلاح ای‌پی‌آی، به آموزش API چیست؟ مراجعه نمایید.) در‌ واقع، روش سُنتی یا همان استفاده از تابع ()mysql_connect کار ما را برای ارتباط با دیتابیس به خوبی راه می‌انداخت و این در حالی بود که با استفاده از این متد به سادگی می‌توانستیم تا به ابزاری همچون MySQL کوئری زده و نتیجه را هم بگیریم اما توجه داشته باشیم که این روش دارای نقاط ضعف خود است که از آن جمله می‌توان به موارد زیر اشاره کرد:

- این روش اصطلاحاً Deprecated است. به عبارت دیگر، استفاده از این متد زمانش به سر رسیده و کمتر برنامه‌نویسی که حرفه‌ای باشد از این روش استفاده می‌کند.

- زمانی که از این متد استفاده می‌کنیم، فرایند Escaping به عهدهٔ برنامه‌نویس گذاشته می‌شود (به طور کلی، منظور از اِسکیپ کردن دیتا این است که داده‌های ورودی، به‌‌خصوص داده‌های ورودی از طریق صفحات لاگین و غیره، باید عاری از هرگونه علائم خاصی شوند.)

- این روش خیلی انعطاف‌پذیر نیست و برخی مواقع برای انجام کاری خاص، برنامه‌نویس خیلی اذیت خواهد شد. در مقابل، زمانی که برای ارتباط با دیتابیس از PDO استفاده کنیم، این API با سیستم‌های مدیریت دیتابیس مختلفی ارتباط برقرار کرده، دارای یکسری متدهای دیفالت برای انجام تَسک‌هایی خاص مثل فراخوانی داده و … است و در نهایت هم خیلی نیازی نیست تا نگران حملات SQL Injection باشیم (برای آشنایی بیشتر با مفهوم SQL Injection به مقالهٔ آشنایی با مفهوم SQL Injection در زبان PHP مراجعه نمایید.)

زمانی که بخواهیم از روش استفاده از متد ()mysql_connect به پی‌دی‌او مهاجرت کنیم، ممکن است که در ابتدا این کار کمی دشوار به نظر برسد و این مسأله به خاطر این نیست که روش استفاده از پی‌دی‌او خیلی دشوار است بلکه این مسأله از آنجا ناشی می‌شود که متد ()mysql_connect بسیار ساده بوده است. در یک کلام، PDO بسیار انعطاف‌پذیر است و نیاز به توضیح نیست که این لایبرری از بیش از ۱۰ درایور مختلف پشتیبانی می‌کند که از آن جمله می‌توان به MySQL ،Oracle و … اشاره کرد و این در حالی است که ()mysql_connect صرفاً از سیستم مدیریت دیتابیس MySQL پشتیبانی می‌کرد (PDO از نسخهٔ PHP 5.1 به بعد به این زبان برنامه‌نویسی اضافه شد و از همین روی حتماً مطمئن شوید که نسخهٔ نصب‌شده روی سیستم لوکال یا لایو شما این پیش‌نیاز را دارا است.)

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

var_dump(PDO::getAvailableDrivers());

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

آموزش نحوهٔ استفاده از PDO
حال که سیستم آمادهٔ استفاده از PDO است، باید یک آبجکت از روی این کلاس بسازیم و برای این منظور متغیری تحت عنوان connection$ در نظر گرفته و با استفاده از کلیدواژهٔ new یک آبجکت ایجاد می‌کنیم:

$connection = new PDO('mysql:host=host;dbname=Database','username', 'password');

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

$connection = new PDO('mysql:host=localhost;dbname=sokanacademy','root', 'root');

همان‌طور که در کد فوق ملاحظه می‌شود، ابتدا نام سیستم مدیریت دیتابیس که mysql است را در نظر گرفته‌ایم سپس نام هاست را localhost در نظر گرفته و دلیل انتخاب لوکال‌هاست این است که هم دیتابیس و هم اسکریپت‌های ما قرار است که روی یک سرور اجرا شوند و در نهایت هم نام دیتابیس که در این مثال sokanacademy است را در نظر گرفته و پایان این پارامتر یک علامت کاما قرار داده‌ایم. پارامتر دوم مربوط به نام کاربری اتصال به مای‌اس‌کیوال است که در این مثال نام کاربری مربوط به PhpMyAdmin نصب شده روی سیستم root است و پارامتر آخر هم مربوط به پسورد است که آن هم root در نظر گرفته شده است.

توجه داشته باشیم که به جای Hard Coding یا بهتر بگوییم وارد کردن نام کاربری و رمزعبور به صورت دستی، می‌توان از متغیرها (Variable) و ثابت‌ها (Constant) نیز استفاده کرد که در آن صورت باید علامت‌های ' ' را برای نام کاربری و رمز‌عبور حذف کنیم:

$connection = new PDO('mysql:host=localhost;dbname=sokanacademy',$myUsername, $myPassword);

در زبان برنامه‌نویسی PHP زمانی که اقدام به استفاده از قابلیت شیئ‌گرایی این زبان می‌کنیم، یا بهتر بگوییم با کلاس‌ها و آبجکت‌های ساخته‌شده از روی آن‌ها سر و کار داریم، به سادگی می‌توانیم دست به مدیریت اِکسپشن‌ها بزنیم بدین شکل که اگر این آبجکت ساخته‌شده تحت عنوان connection$ مشکلی داشته باشد، این مشکل را متوجه نخواهیم شد مگر آنکه از قبل راه‌کاری اتخاذ کرده باشیم. استفاده از دستورات try و catch برای هندل کردن اِکسپشن‌ها راه‌کار رایجی است بین دولوپرهای این زبان است:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'root', 'root');
} catch(PDOException $e) {
    echo 'Opps, Something bad just happened!' . '<br>';
    echo $e->getMessage();
}

همان‌طور که در کد فوق ملاحظه می‌شود، دستور اصلی را داخل بلوک try نوشته‌ایم که اگر این دستور به درستی کار کند هیچ مشکلی پیش نخواهد آمد اما اگر به هر دلیلی با مشکلی مواجه شود، دستور داخل بلوک catch اجرا می‌شود که یک پارامتر ورودی می‌گیرد که آبجکتی از روی کلاس PDOException است تحت عنوان e$ (نام این آبجکت کاملاً دلخواه انتخاب می‌شود.) در ادامه، داخل کروشه‌های مرتبط با catch گفته‌ایم که ابتدا عبارت Opps, Something bad just happened به معنی «اوه، یک اتفاق بدی رخ داده» نمایش داده شود سپس در خط بعدی با استفاده از یکی از متدهای کلاس PDOException تحت عنوان ()getMessage پیغام خطا در معرض دید دولوپر قرار گیرد (توجه داشته باشیم که آبجکت e$ را به تنهایی و بدون استفاده از متد ()getMessage هم می‌توان پرینت کرد اما این در حالی است که متد ()getMessage ارور به مراتب خواناتری را در معرض دید ما برای فرایند دیباگینگ قرار می‌دهد.)

برای تست کردن این موضوع، اگر اسکریپت فوق را در یک فایل PHP قرار داده و آن‌ را روی لوکال‌هاست اجرا کنیم، هیچ چیزی نباید روی صفحه مرورگر بیاید (البته در صورتی که از قبل پایگاه داده‌ای تحت عنوان sokanacademy روی PhpMyAdmin ایجاد کرده و هم رمزعبور و هم نام کاربری برابر با root باشند.) اکنون قصد داریم تا عمداً نام کاربری را از root به ss تغییر دهیم:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'ss', 'root');
} catch(PDOException $e) {
    echo 'Opps, Something bad just happened!' . '<br>';
    echo $e->getMessage();
}

حال یک بار دیگر خروجی کدهای فوق را در مرورگر مشاهده می‌کنیم:

Opps, Something bad just happened!
SQLSTATE[28000] [1045] Access denied for user 'ss'@'localhost' (using password: YES)

می‌بینیم که اِکسپشنی همچون چیزی که در بالا مشاده می‌شود در معرض دیدمان قرار می‌گیرد. این ویژگی PDO برای فرایند دیباگینگ وب‌ اپلیکیشن بسیار کاربردی خواهد بود اما توجه داشته باشیم که وقتی خواستیم سایت را روی یک سرور لایو قرار دهیم، تحت هیچ عنوان کاربران سایت نباید با ارورهای احتمالی روبه‌رو شوند چرا که این کار امنیت سایت ما را پایین خواهد آورد (توجه داشته باشیم که وضعیت نمایش ارورهای کلاس PDO روی PDO::ERRMODE_SILENT گرفته و این در حالی است که این مُد را می‌توان بسته به نیاز خود تغییر داد.)

تا این مرحله از آموزش، توانسته‌ایم با موفقیت با دیتابیس مد نظر خود ارتباط برقرار سازیم و در ادامه اولین کاری که قصد داریم یاد بگیریم، فراخوانی داده‌ها از دیتابیس است که برای این منظور از دو روش مختلف می‌توان استفاده کرد که یکی استفاده از متد ()query و دیگری متد ()execute است. ابتدا متد ()query را بررسی می‌کنیم:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'root', 'root');
    $result = $connection->query('SELECT * FROM users');
    foreach ($result as $row) {
        print_r($row);
    }
} catch(PDOException $e) {
    echo 'Opps, Something bad just happened!' . '<br>';
    echo $e->getMessage();
}

همان‌طور که در کد فوق ملاحظه می‌شود، ابتدا یک متغیر جدید ساخته‌ایم تحت عنوان result$ و مقدار آن را برابر با آبجکت ساخته‌شده از روی کلاس PDO قرار داده‌ایم که متدی تحت عنوان ()query به آن ضمیمه شده است. داخل این متد هم یک دستور بسیار سادهٔ اس‌کیو‌ال قرار داده‌ایم به این صورت که این اسکریپت باید کلیهٔ داده‌های قرار گرفته در جدولی تحت عنوان users را در متغیر result$ ذخیره سازد. در ادامه، با استفاده از یک حلقه تمامی ردیف‌های قرار گرفته در جدول users را به متغیری تحت عنوان row$ اختصاص داده و با استفاده از متدی تحت عنوان ()print_r آن‌ها را نمایش می‌دهیم (توجه داشته باشیم که به جای این متد، از متد دیگری تحت عنوان ()var_dump هم می‌شود استفاده کرد.) خروجی کدهای فوق به صورت زیر خواهد بود:

Array ( [id] => 2 [0] => 2 [username] => user1 [1] => user1 [password] => 12dea96fec20593566ab75692c9949596833adc9 [2] => 12dea96fec20593566ab75692c9949596833adc9 ) Array ( [id] => 3 [0] => 3 [username] => user2 [1] => user2 [password] => a1881c06eec96db9901c7bbfe41c42a3f08e9cb4 [2] => a1881c06eec96db9901c7bbfe41c42a3f08e9cb4 )

می‌بینیم که خروجی جدول، یک آرایه حاوی دو ردیف است که مقادیر username ،id و password در آن قرار گرفته‌اند. این روش را با استفاده از حلقهٔ while نیز می‌توان به صورت زیر پیاده‌سازی کرد:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'root', 'root');
    $query = $connection->query('SELECT * FROM users');
    while ($row = $query->fetch()) {
        var_dump($row);
    }
} catch (PDOException $e) {
    echo $e->getMessage();
}

همان‌طور که در کد فوق ملاحظه می‌شود، شرط حلقه while را متغیری تحت عنوان row$ قرار داده‌ایم که مقدار آن برابر است با متغیر query$ که متدی تحت عنوان ()fetch به آن ضمیمه شده است. این حلقه آن‌قدر گردش می‌کند تا کلیهٔ ردیف‌های موجود در جدول به متغیر row$ اختصاص یابند. اگر بخواهیم تا خروجی حلقهٔ ما یک آرایه از جنس عددی باشد، باید پارامتری معادل با PDO::FETCH_NUM را برای متد ()fetch در نظر بگیریم و اگر هم بخواهیم یک آرایه با اندیس‌های غیرعددی دریافت کنیم، باید پارامتر PDO::FETCH_ASSOC را برای این متد در نظر بگیریم:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'root', 'root');
    $query = $connection->query('SELECT * FROM users');
    while ($row = $query->fetch(PDO::FETCH_ASSOC)) {
        var_dump($row);
    }
} catch (PDOException $e) {
    echo $e->getMessage();
}

توجه داشته باشیم که PDO متد دیگری در اختیار برنامه‌نویس می‌گذارد تحت عنوان ()fetchAll که این امکان را می‌دهد تا در قالب یک کوئری به دیتابیس، کلیهٔ مقادیر یک جدول به‌خصوص را دریافت کند اما توجه داشته باشیم که نسبت به ()fetch از میزان حافظه بیشتری استفاده خواهد کرد ولی به هر حال مزیت آن ارسال فقط یک کوئری به دیتابیس است:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'root', 'root');
    $query = $connection->query('SELECT * FROM users');
    $query = $query->fetchAll();
    foreach ($query as $row) {
        print_r($row);
    }
} catch (PDOException $e) {
    echo $e->getMessage();
}

آشنایی با مفهوم Prepared Statements
در این مرحله از آموزش باید با مفهومی تحت عنوان Prepared Statements (دستورات از پیش آماده شده) آشنا شویم. همواره یکی از دغدغه‌های برنامه‌نویسان برای کوئری زدن به دیتابیس و ذخیره‌سازی چیزی در آن حملات SQL Injection است که در روش‌های سنتی می‌توانستیم با استفاده از متدی تحت عنوان ()mysql_real_escape_string جلوی این دست حملات را بگیریم (برای کسب اطلاعات بیشتر، می‌توانید به مقالهٔ آشنایی با مفهوم SQL Injection در زبان PHP مراجعه نمایید.)

پس از مهاجرت از روش‌های سُنتی ارتباط با دیتابیس در زبان برنامه‌نویسی PHP به لایبرری PDO، اگر بدانیم که چگونه از این کلاس به درستی استفاده کنیم، به صورت خودکار جلوی حملات SQL Injection هم گرفته خواهد شد. سازوکار دستورات Prepared Statement به این شکل است که اِسترینگ‌ها را صرفاً به شکل یک متن در قالب دستوارت اس‌کیوال برای دیتابیس ارسال نمی‌کنند و از همین روی با خیال راحت می‌توان به ارتباط با دیتابیس و ذخیره‌سازی داده‌ها، فراخوانی داده و ... در آن پرداخت.

برای این منظور، باید از متدی تحت عنوان ()prepare در کنار آبجکتی که از روی کلاس پی‌دی‌او ساخته‌ایم بپردازیم و در ادامه داخل پرانتزهای این متد کدهای اس‌کیوال مد نظر خود را بنویسیم. در همین راستا، در ادامه قصد داریم تا کدهایی که پیش از این نوشتیم را با استفاده از متدهای ()prepare و ()execute ریفکتور نماییم:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'root', 'root');
    $prepare = $connection->prepare('SELECT * FROM users');
    $prepare->execute();
    $result = $prepare->fetchAll();
    foreach ($result as $row) {
        print_r($row);
    }
} catch(PDOException $e) {
    echo 'Opps, Something bad just happened!' . '<br>';
    echo $e->getMessage();
}

همان‌طور که در کد فوق ملاحظه می‌شود، ابتدا یک متغیر ساخته‌ایم تحت عنوان prepare$ و مقدار آن را برابر با آبجکت ساخته‌شده از روی کلاس پی‌دی‌او قرار داده‌ایم که متدی تحت عنوان ()prepare به آن ضمیمه شده و داخلش هم کدهای اس‌کیو‌ال مد نظر را نوشته‌ایم. در ادامه وقتی که کدهای اس‌کیوال اصطلاحاً Prepare (آماده) شدند، باید آن‌ها را اجرا کرد که برای این منظور متد ()execute را به متغیر prepare$ ضمیمه می‌کنیم و در نهایت یک متغیر جدید می‌سازیم تحت عنوان result$ و مقدار آن را برابر با متغیر prepare$ قرار می‌دهیم که متدی تحت عنوان ()fetchAll را به آن ضمیمه کرده‌ایم. از این پس متغیر result$ به عنوان یک آرایه حساب خواهد شد و از همین روی با استفاده از حلقه، همچون مثال قبل، کلیهٔ مقادیر آن را از یکدیگر مجزا کرده و در قالب متغیری تحت عنوان row$ ذخیره می‌سازیم.

همچنین ممکن است موقعیت‌هایی پیش آید که بخواهیم مقادیر ارسالی به دیتابیس را مجزا از کدهای اس‌کیوال ارسال کنیم که برای این منظور می‌توان کدهای فوق را به صورت زیر ریفکتور کرد:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'root', 'root');
    $statement = $connection->prepare('SELECT * FROM users WHERE username=?');
    $statement->bindValue(1, 'user1');
    $statement->execute();
    $result = $statement->fetch();
    foreach ($result as $row) {
        var_dump($row);
    }
} catch(PDOException $e) {
    echo $e->getMessage();
}

همان‌طور که در کد فوق ملاحظه می‌شود، در کدهای اس‌کیوال دستور داده‌ایم تا از جدول users فقط ردیفی که نام کاربری آن معادل با یوزرنیم خاصی است نمایش داده شود. برای این منظور، مقابل نام username یک ? قرار داده و در خط بعد با استفاده از متدی تحت عنوان ()bindValue آن علامت سؤال را مقداردهی کرده‌ایم.

توجه داشته باشیم با توجه به اینکه در کدهای اس‌کیوال خود صرفاً از یک علامت سؤال استفاده کرده‌ایم، پارامتر اول متد ()bindValue را عدد 1 انتخاب کرده سپس یک کاماً می‌گذاریم و مقدار مد نظر را وارد می‌کنیم و اگر در دیتابیس خود در جدول users کاربری با نام کاربری user1 داشته باشیم، دیتای مرتبط با این کاربر در معرض دیدمان قرار خواهد گرفت.

آشنایی با مفهوم Name Parameters
در ادامه، با مفهومی تحت عنوان Name Parameters (پارامترهای نامگذاری‌شده) آشنا خواهیم شد. همان‌طور که در کد فوق ملاحظه می‌شود، در دستورات اس‌کیوال از علامت سؤال به جای مقادیر یک فیلد استفاده کردیم اما گاهی در شرایطی قرار می‌گیریم که می‌خواهیم همان نامی که در دیتابیس برای ستون‌های مختلف در نظر گرفته‌ایم را برای کوئری زدن مورد استفاده قرار دهیم (فایدهٔ این کار این است که کدهای ما راحت‌تر خوانده می‌شوند و مابین جداول دیتابیس و کدهای ما هماهنگی اسمی به وجود خواهد آمد.) برای این منظور، کدهای فوق را به صورت زیر بازنویسی می‌کنیم:

try {
    $connection = new PDO('mysql:host=localhost;dbname=sokanacademy', 'root', 'root');
    $statement = $connection->prepare('SELECT * FROM users WHERE username=:username');
    $statement->bindValue(':username', 'user1');
    $statement->execute();
    $result = $statement->fetch();
    foreach ($result as $row) {
        var_dump($row);
    }
} catch(PDOException $e) {
    echo $e->getMessage();
}

می‌بینیم که به جای ? از علامت : و نام ستون مد نظر در دیتابیس استفاده کرده‌ایم سپس در متد ()bindValue هم علامت : و هم نام username را داخل علائم ' ' قرار داده و مقدار آن را هم به عنوان پارامتر بعدی مشخص کرده‌ایم. اگر صفحه را به‌روزرسانی کنیم، می‌بینیم که نتیجه‌ای مشابه کدهای قبل دریافت خواهیم کرد.

نتیجه‌گیری
به طور کلی، دولوپرهای PHP که قصد استفاده از فریمورک‌های مطرح این زبان همچون لاراول، سیمفونی، زند و غیره را دارند ‌‌باید به خوبی با نحوهٔ عملکرد PDO آشنایی داشته باشند و توصیه می‌شود خواه از فریمورک‌ها استفاده کنیم و خواه اصطلاحاً به صورت Pure PHP کدنویسی کنیم، برای ارتباط با دیتابیس حتماً از این لایبرری استفاده نماییم (چنانچه علاقمند به فراگیری گام به گام زبان برنامه‌نویسی PHP هستید، می‌توانید به دورهٔ آموزش PHP در سکان آکادمی مراجعه نمایید.)



بهزاد مرادی