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

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

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

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

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

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

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

- و بالاخره اینکه PDO اصطلاحاً خیلی Flexible (انعطاف‌پذیر) است (نیازی به توضیح نیست که PDO از بیش از ۱۲ درایور مختلف پشتیبانی می‌کند که از آن جمله می‌توان به MySQL ،Oracle و … اشاره کرد و این در حالی است که ()mysql_connect صرفاً از سیستم مدیریت دیتابیس MySQL پشتیبانی می‌کند).

زمانی که بخواهیم از روش استفاده از ()mysql_connect به PDO مهاجرت کنیم، ممکن است که در ابتدا این کار کمی دشوار به نظر برسد و این مسأله به خاطر این نیست که روش استفاده از پی‌دی‌او خیلی دشوار است بلکه این نگرانی از آنجا ناشی می‌شود که متد ()mysql_connect بسیار ساده و قابل‌فهم بوده است.

به هر حال بایستی گفت که استفاده از PDO API شتری است که درب منزل هر برنامه‌نویس PHP که بخواهد پایش را یک گام فراتر گذاشته و وارد دنیای برنامه‌نویسان حرفه‌ای شود خوابیده است! پیش از این مهاجرت ضروری، می‌بایست یک نکته را پس ذهن داشته باشیم که پرفرومنس ()mysqli_connect تا حدودی از PDO بیشتر است (جالب است بدانیم که سرعت ()mysql_connect از ()mysqli_connect هم بیشتر است) اما اگر بخواهیم به صورت کلی این ۳ رویکرد ارتباط با دیتابیس را مقایسه کنیم، مسلماً برندهٔ بازی PDO خواهد بود.

PDO از نسخه PHP 5.1 به بعد به این زبان برنامه‌نویسی محبوب اضافه شد؛ پس حتماً مطمئن شوید که نسخهٔ PHP نسب شده روی سیستم لوکال شما این پیش‌نیاز را تأمین می‌کند. پیش از این گفتیم که پی‌دی‌او از دیتابیس‌های مختلفی پشتیبانی می‌کند؛ برای آن که متوجه شویم که روی سیستمی که با استفاده از آن به توسعهٔ وب اپلیکیشن می‌پردازیم از چه پایگاه داده‌هایی پشتیبانی می‌شود، نیاز است تا دستور زیر را در یک فایل PHP قرار داده و آن را در لوکال‌هاست روی مرورگر اجرا کنیم:

var_dump(PDO::getAvailableDrivers());

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

array(1) { [0]=> string(5) "mysql" }

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

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

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

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

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

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

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

در زبان برنامه‌نویسی PHP زمانی که اقدام به استفاده از قابلیت شیئ‌گرایی این زبان می‌کنیم، یا بهتر بگوییم با کلاس‌ها و آبجکت‌های ساخته شده از روی آنها سر و کار داریم، اصطلاحاً با Exception سر و کار خواهیم داشت بدین شکل که اگر این آبجکت ساخته شده تحت عنوان 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 اجرا می‌شود. catch یک پارامتر ورودی می‌گیرد که آبجکتی از روی کلاس PDOException است تحت عنوان e (نام این آبجکت کاملاً دلخواه انتخاب می‌شود).

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

برای تست کردن این موضوع، اگر کدهای فوق را در یک فایل PHP قرار داده و آنها را روی لوکال‌هاست اجرا کنیم (البته در صورتی که از قبل پایگاه داده‌ای تحت عنوان sokanacademy روی phpmyadmin ایجاد کرده و هم رمزعبور و هم نام کاربری 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 قرار گرفته است و این در حالی است که این Mode را می‌توان بسته به نیاز خود تغییر داد.)

تا این مرحله از آموزش، توانسته‌ایم با موفقیت با دیتابیس مد نظر خود ارتباط برقرار سازیم؛ حال اولین کاری که قصد داریم یاد بگیریم، فراخوانی داده‌ها از دیتابیس است. برای این منظور از ۲ روش مختلف می‌توان استفاده کرد که یکی استفاده از متد ()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 به آن ضمیمه شده است. داخل این متد هم یک دستور بسیار سادهٔ SQL قرار داده‌ایم به این صورت که این اسکریپت می‌بایست کلیهٔ داده‌های قرار گرفته در جدولی تحت عنوان users را در متغیر result$ ذخیره سازد.

در ادامه، با استفاده از یک حلقهٔ foreach تمامی ردیف‌های قرار گرفته در جدول 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 )

می‌بینیم که خروجی جدول، یک آرایه حاوی ۲ ردیف است که مقادیر id و username و 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 در نظر بگیریم و اگر هم بخواهیم یک آرایه از جنس Associative (آرایه‌ای با اندیس‌های غیرعددی) دریافت کنیم، می‌بایست پارامتر 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 جلوی این دست حملات را بگیریم.

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

برای این منظور، می‌بایست از متدی تحت عنوان ()prepare در کنار آبجکتی که از روی کلاس PDO ساخته‌ایم بپردازیم؛ سپس داخل پرانتزهای این متد، کدهای SQL مد نظر خود را قرار می‌دهیم. اکنون قصد داریم تا نتیجهٔ فوق را با استفاده از متدهای ()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$ و مقدار آن را برابر با آبجکت ساخته شده از روی کلاس PDO قرار داده‌ایم که متدی تحت عنوان ()prepare به آن ضمیمه شده و داخل آن کدهای SQL مد نظر را نوشته‌ایم. در ادامه وقتی که کدهای SQL اصطلاحاً Prepare (آماده) شدند، می‌بایست آنها را اجرا کرد.

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

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

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();
}

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

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

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

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 آشنایی داشته باشند و توصیهٔ ما این است که خواه از فریمورک‌های PHP استفاده کنیم و خواه با Pure PHP کدنویسی کنیم، برای ارتباط با دیتابیس حتماً از این لایبرری استفاده نماییم.



بهزاد مرادی