PDO چیست و چرا باید از آن استفاده کنیم؟

01 فروردین 1396
pdo-main

با سلام و احترام و ضمن تبریک به مناسبت سال ۱۳۹۶ و آرزوی سلامت و تندرستی خدمت شما عزیزان. در این بخش قصد داریم مبحث PDO را برای آن دسته از عزیزانی که تمایل دارند یک کد واحد برای تمام دیتابیس ها ایجاد کنند، تشریح کنیم.

در پاسخ به این سوال که PDO چیست باید نوشت:

PDO مخفف (PHP Data Object) ابزار و یا یک افزونه (Extention) برای PHP ورژن ۵ به بالاست که به شما اجازه می دهد یک کد واحد برای تمام دیتابیس ها ایجاد کنید.

یعنی چی؟ یعنی شما با استفاده از این ابزار می توانید بگونه ای دیتابیس را طراحی کنید که هر لحظه دوست داشتید نوع آن را تغییر دهید. مثلا سایت شما در حال حاضر با دیتابیس MySQL طراحی شده و به یکباره و بنا به دلایل مختلف می خواهید این پایگاه داده را به نوع SQL Server تغییر دهید. در صورتیکه از افزونه ی PDO برای کدنویسی خودتان استفاده نکرده باشید باید تمام کدهای مربوط به پایگاه داده و بعضا کوئری ها را تغییر دهید. ولی اگر کدهای شما با ابزار PDO طراحی شده باشد می توانید به راحتی هرچه تمام تر به و تنها با اعمال چندین دستور ساده پایگاه داده ی خود را نوع دلخواه تغییر دهید. انواع مختلف دیتابیسی که این افزونه پشتیبانی می‌کند عبارت است از:

  • FreeTDS / Microsoft SQL Server / Sybase - PDO_DBIB
  • Firebird/interbase 6 - PDO_FIREBIRD
  • IBM DB2 - PDO_IBM
  • IBM Informix Dynamic Server - PDO_INFORMIX
  • MySQL 3.x/4.x/5.x - PDO_MYSQL
  • Oracle Call Interface - PDO_OCL
  • ODBC v3 - IBM DB2, unixODBC and win32 ODBC PDO_OBC
  • PostgreSQL - PDO_PGSQL
  • SQLite 3 and SQLite 2 - PDO_SQLITE
  • 4D - PDO_4D

همچنین معادل تمام این دیتابیس ها یک سری درایورها یا راه‌اندازها وجود دارند که باید روی سرور شما نصب شوند. (در ادامه‌ ی نام هر دیتابیس، نام درایور آن نیز نوشته شده است)

توجه داشته باشید که نصب تمام این درایورها روی سیستم شما ضروری نیست برای اینکه بدانید چه درایوری در سیستم شما نصب شده است می توانید از دستور زیر استفاده کنید:

print_r(PDO::getAvailableDrivers());

فرآیند کاری PDO

متصل شدن به دیتابیس

همانطور که در جریان هستید هر پایگاه داده یک روش برای اتصال دارد. اما با استفاده از PDO می‌توان یک کد واحد برای اتصال به تمام پایگاه‌های داده ایجاد کرد که زیر مشاهده می‌کنید:

$DBH = new PDO("mysql:host=$host;dbname:$dbname", $user, $pass);

متغییر DBH$ به عنوان یک متغییر برای ذخیره‌ی اطلاعات دیتابیس و کنترل کردن ‌آنها به حساب می‌آید که همواره نام آن می‌تواند ثابت باشد.

عبارت mysql نوع پایگاه داده را مشخص کرده است که برای هر پایگاه داده، منحصر به فرد است. مثلا sqlserver, oracle و ...

دستورهای host = $host; dbname = $dbname, $user, $pass به عنوان یک رشته برای اتصال به پایگاه داده استفاده می‌شوند.

در ادامه چند مثال از پایگاه داده‌های مختلف خدمت شما عزیزان ارائه می‌دهیم:

try {
  # MS SQL Server and Sybase with PDO_DBLIB
  $DBH = new PDO("mssql:host=$host;dbname=$dbname, $user, $pass");
  $DBH = new PDO("sybase:host=$host;dbname=$dbname, $user, $pass");
 
  # MySQL with PDO_MYSQL
  $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
 
  # SQLite Database
  $DBH = new PDO("sqlite:my/database/path/database.db");
}
catch(PDOException $e) {
    echo $e->getMessage();
}

همچنین برای قطع اتصال با پایگاه داده می‌توان از دستور زیر استفاده کرد:

# close the connection
$DBH = null;

خطاها در PDO

برای کنترل خطاها در PDO افزونه‌هایی در اختیار شما قرار می‌گیرد که با استفاده از آن می‌توانید وضعیت نمایش خطاها را مشخص کنید. معمولا این خطاها زمانیکه از بلاک try/catch استفاده می‌شود، در بخش catch نمایش داده خواهند شد. در حالت کلی ۳ نوع مود خطا داریم که با استفاده از عبارت ATTR_ERRMODE تنظیم می‌شوند:

$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

PDO::ERRMODE_SILENT

همواره به عنوان خطای پیش‌فرض می‌باشد. اگر شما تنظیمات را روی این نوع خطا قرار دهید تنها خطاهایی که در افزونه‌های MYSQL یا MYSQLI می‌باشد را در اختیار دارید. دو راه دیگر برای برنامه‌نویسی بدون تکرار یا DRY Programming وجود دارد که بهتر است از آنها استفاده شود:

PDO::ERRMODE_WARNING

این مود هشدارها و اخطارهای PHP را نمایش می‌دهد و برنامه پس از دریافت هشدار به اجرا شدن خود ادامه می‌دهد. این حالت برای خطایابی مورد استفاده قرار می‌گیرد.

PDO::ERRMODE_EXCEPTION

این مود در بیشتر مواقع مورد استفاده قرار می‌گیرد زیرا با اعمال آن یک exception یا استثناء مشخص می‌دهد که می‌توان از طریق آن خطاها را بررسی کرده و داده‌هایی که برای تخریب سایت شما ممکن است خطرناک باشند را پنهان می‌کند. در ادامه یک مثال از این سیستم تنظیم خطا ارائه می‌دهیم:

# connect to the database
try {
  $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
  $DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
 
  # UH-OH! Typed DELECT instead of SELECT!
  $DBH->prepare('DELECT name FROM people');
}
catch(PDOException $e) {
    echo "I'm sorry, Dave. I'm afraid I can't do that.";
    file_put_contents('PDOErrors.txt', $e->getMessage(), FILE_APPEND);
}

هماینطور که ملاحظه می‌کنید در صورت وجود خطا از طریق PDOException متن آن خطا نمایش داده خواهد شد.

ایجاد و بروزرسانی داده

بدیهی‌ست که ایجاد و یا آپدیت داده در دیتابیس‌ها هموراه به عنوان یکی از مهم‌ترین اصول می‌شاد. با استفاده از PDO این عملیات در طی دو مرحله پردازش و ارسال می‌شود.

ارسال اطلاعات

همانطور که در تصویر فوق مشاهده می‌کنید با اعمال هر دستور PDO در تابع و متد prepare ابتدا پردازش انجام می‌شود و سپس اطلاعات بایند شده به مرحله اجرا در می‌آید.

به این مثال توجه کنید:

# STH means "Statement Handle"
$STH = $DBH->prepare("INSERT INTO folks ( first_name ) values ( 'Cathy' )");
$STH->execute();

همانطور که ملاحظه می‌کنید دستورهای مربوط به ایجاد یک رکورد به متد prepare ارسال شده است و آن را درون متغییر STH قرار داده‌ایم. سپس با اعمال تابع execute عملیات پزدارش شروع می‌شود.

جملات Prepared یا آماده شده

استفاده از جملات و عبارات تنظیم شده یا Prepared به شما کمک می‌کند تا وب سایت و پایگاه داده شما از حملات SQL Injection یا تزریق SQL‌ در امان باشد. برای آشنایی کامل از SQL Injection حتما مقاله‌ی زیر را مطالعه بفرمایید:

حال که با مفهوم دقیق SQL Injection آشنا شدید راه‌های مقابله با آن را نیز باید بیاموزید.

چگونه از حملات SQL Injection جلوگیری کنم؟

برای مقابله با این نوع حملات باید ورودی‌های پایگاه داده را پارامتری کنیم. یعنی اطلاعات را در غالب متغییرها و پارامترها به کوئری ارسال کرده و از قرارگیری مستقیم عبارات در کوئری ها خودداری کنیم. پارامتری کردن داده‌های دریافتی از طریق یک نگه‌دارنده‌ی داده یا Placeholder اتفاق می‌افتد و هنگامیکه شما عبارت prepared را بکار می‌برید در واقع دارید این placeholder را به دستورات SQL خود اضافه می‌کنید! در ادامه با سه مثال از دستور بدون placeholder، دستور همراه با placeholder نامگذاری نشده و دستور همراه با placeholder نامگذاری شده، این مفهوم را برای شما شفاف‌سازی می‌کنیم:

//بدون استفاده از placeholder
// افزایش خطر حملات تزریق SQL
$STH = $DBH->("INSERT INTO folks (name, addr, city) values ($name, $addr, $city)");
 
// استفاده از placeholder
// بدون نام
# unnamed placeholders
$STH = $DBH->("INSERT INTO folks (name, addr, city) values (?, ?, ?);
 
// استفاده از placeholder
// به همراه نام متغییر
# named placeholders
$STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");

همواره باید از اجرای متد ۱ خودداری کنید چون قطعا وب سایت شما در معرض حملات SQL خواهد بود. اما استفاده از هر یک از دو عبارت بدون نام یا همراه با نام متغییر مشکلی ندارد. بنابراین به توضیح متدهای ۲ و ۳ می‌پردازیم:

نگه‌دارنده‌ی بدون نام

# assign variables to each place holder, indexed 1-3
$STH->bindParam(1, $name);
$STH->bindParam(2, $addr);
$STH->bindParam(3, $city);
 
# insert one row
$name = "Daniel"
$addr = "1 Wicked Way";
$city = "Arlington Heights";
$STH->execute();
 
# insert another row with different values
$name = "Steve"
$addr = "5 Circle Drive";
$city = "Schaumburg";
$STH->execute();

این روش دو مرحله دارد. مرحله اول در خطوط ۲ تا ۴ رخ می‌دهد و شامل متغییرهایی‌ست که تعریف کرده و آن را به مقادیر placeholder به ترتیب ارجاع داده‌ایم. مرحله دوم ارسال مجموعه‌ی داده‌ها به این متغییرها و در نتیجه جایگذاری آنها در placeholder است. بنابراین برای هر اجرای مجدد تنها کافیست مقادیر را تغییر دهیم.

آیا این شکل نمایش زمانیکه پارامترهای شما در دیتابیس زیاد باشند کمی بی‌نظم و گمراه کننده نیست؟

قطعا همینطوره! بنابراین بهتره دستورات را به صورت آرایه ای از مقادیر به ورودی تابع execute ارسال کرده تا دقیقا مقادیر متناسب با ایندکس ۰ و ۱ و ۲ در دستور SQL به جای علامت سوال اول دوم و سوم جایگزین شود:

# the data we want to insert
$data = array('Cathy', '9 Dark and Twisty Road', 'Cardiff');
 
$STH = $DBH->("INSERT INTO folks (name, addr, city) values (?, ?, ?);
$STH->execute($data);

این شیوه نمایش بهتر شد همچنین شما می‌توانید با استفاده از دستور data[1]$ به داده‌ی دوم دست پیدا کنید.

نگه‌دارنده‌ی همراه با نام

روش دیگری برای ارسال اطلاعات وجود دارد که کدنویسی شما را تمیز تر می‌کند. نام این روش نگه‌دارنده‌ی با نام است:

# the first argument is the named placeholder name - notice named
# placeholders always start with a colon.
$STH->bindParam(':name', $name);

این نگه‌دارنده همواره به صورت یک اسم در اولین آرگومان تعریف می‌شود و برای تعریف آن حتما و حتما باید از یک علامت : استفاده کرد.

حال برای ارسال داده‌ها به یک دستور sql می‌توان به سادگی هرچه تمام آرایه‌ای از رشته‌ها را به همراه انتساب هر کلید که نمایانگر placeholder است، متغییرها را جایگزین کرد:

# the data we want to insert
$data = array( 'name' => 'Cathy', 'addr' => '9 Dark and Twisty', 'city' => 'Cardiff' );
 
# the shortcut!
$STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
$STH->execute($data);

یک روش دیگر که خیلی کلاسیک محسوب می‌شود استفاده از کلاس‌ها و سازنده‌ها برای تعریف فیلدهای یک جدول می‌باشد که به صورت زیر خواهد بود:

# a simple object
class person {
    public $name;
    public $addr;
    public $city;
 
    function __construct($n,$a,$c) {
        $this->name = $n;
        $this->addr = $a;
        $this->city = $c;
    }
    # etc ...
}
 
$cathy = new person('Cathy','9 Dark and Twisty','Cardiff');
 
# here's the fun part:
$STH = $DBH->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
$STH->execute((array)$cathy);

در این حالت با ساختن یک شیء جدید از کلاس می‌توان مقادیر را به سازنده ارسال کرده و در query ذخیره کنیم.

انتخاب داده‌ها یا دستور SELECT

انواع دریافت داده در PDO

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

PDO:FETCH_ASSOC : این گزینه آرایه‌ی ایندکس شده توسط نام ستون را باز می‌گرداند.

PDO::FETCH_BOTH : این گزینه آرایه‌‌ای ایندکس شده توسط ستون و اعداد آن را باز می‌گرداند. و به صورت پیشفرض فعال است.

PDO::FETCH_BOUND : این گزینه مقادیر ستون‌ها را به متغییرهایی با استفاده از متد ()bindColumn نسبت می‌دهد.

PDO::FETCH_CLASS : این گزینه مقادیر ستون‌ها را به ویژگی‌ها و نام کلاس نسبت می‌دهد. با این گزینه اگر یک ویژگی وجود نداشته باشد آن را ایجاد می‌کند.

PDO::FETCH_INTO : این گزینه یک نمونه از کلاس ایجاد شده را آپدیت می‌کند.

PDO::FETCH_LAZY :‌ این گزینه روش‌های PDO::FETCH_BOTH و PDO::FETCH_OBJ را با هم ترکیب کرده و یک شیء متغییر ایجاد می‌کند.

PDO::FETCH_NUM :‌این گزینه آرایه‌ی ایندکس شده توسط شماره ستون را باز می‌گرداند.

PDO::FETCH_OBJ :‌این گزینه یک شیء بی‌نام به همراه ویژگی‌های نامگذاری شده را باز می‌گرداند که با ستون موردنظر در ارتباط است.

در حالت کلی برای تنظیم این گزینه‌ها از دستور زیر باید استفاده کنید:

$STH->setFetchMode(PDO::FETCH_ASSOC);

در ادامه به توضیح سه گزینه که پرکاربردترین هستند می‌پردازیم:

PDO::FETCH_ASSOC

این گزینه آرایه‌ای مجتمع را برای شما ایجاد می‌کند که توسط نام ستون آن ایندکس شده است:

# using the shortcut ->query() method here since there are no variable
# values in the select statement.
$STH = $DBH->query('SELECT name, addr, city from folks');
 
# setting the fetch mode
$STH->setFetchMode(PDO::FETCH_ASSOC);
 
while($row = $STH->fetch()) {
    echo $row['name'] . "\n";
    echo $row['addr'] . "\n";
    echo $row['city'] . "\n";
}

همانطور که ملاحظه می‌کنید ابتدا با استفاده از متد ()query دستور را ارسال کرده و مقادیر را درون متغییر STH ذخیره کرده‌ایم. حال برای نمایش آنها از مود FETCH_ASSOC استفاده کرده و در نهایت درون حلقه‌ی while برای نمایش هر یک از فلیدها درون یک سطر بهره برده‌ایم.

PDO::FETCH_OBJ

این نوع fetch یا بازیابی اطلاعات یک شیء از کلاس خالی یا std class برای هر سطر از اطلاعات بازیابی شده ایجاد می‌کند:

# creating the statement
$STH = $DBH->query('SELECT name, addr, city from folks');
 
# setting the fetch mode
$STH->setFetchMode(PDO::FETCH_OBJ);
 
# showing the results
while($row = $STH->fetch()) {
    echo $row->name . "\n";
    echo $row->addr . "\n";
    echo $row->city . "\n";
}

FETCH_CLASS

این روش به شما اجازه می‌دهد که اطلاعات و داده‌ها را به صورت مستقیم از کلاس انتخابی بازیابی کنید. هنگامیکه از FETCH_CLASS استفاده می‌شود ویژگی‌های شیء شما قبل از اینکه سازنده فراخوانی شود، تنظیم (set) می‌شوند. این جمله را مجددا بخوانید چون بسیار مهم است. اگر یکی از ویژگی‌هایی با نام ستون برابر نباشد آن را از نو (به صورت public) ایجاد می‌کند. به مثال زیر توجه کنید:

class secret_person {
    public $name;
    public $addr;
    public $city;
    public $other_data;
 
    function __construct($other = '') {
        $this->address = preg_replace('/[a-z]/', 'x', $this->address);
        $this->other_data = $other;
    }
}

حال می‌خواهم دیتاها را از این کلاس استخراج کنیم:

$STH = $DBH->query('SELECT name, addr, city from folks');
$STH->setFetchMode(PDO::FETCH_CLASS, 'secret_person');
 
while($obj = $STH->fetch()) {
    echo $obj->addr;
}

در صورتیکه بخواهیم سازنده پیشفرض قبل از تنظیم شدن ویژگی‌ها فراخوانی شود باید از دستور زیر استفاده کرد:

$stmt->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, 'secret_person'); 

دوستان عزیز توجه داشته باشید که برای دستیابی به مثال‌ها و نمونه‌های بیشتر متدهای FETCH در PDO به مقاله موجود در وب سایت مرجع PHP Manual مراجعه کنید.

نویسنده شوید

دیدگاه‌های شما (8 دیدگاه)

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

Yaghoub
31 شهریور 1399
عالی بود دمتون گرم

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

شکوفه
14 فروردین 1399
من صفر بود اطلاعاتم اما کامل فهمیدم عالی بود

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

نوید
02 بهمن 1397
سلام لطفا برای update ,delete هم آموزش قرار بدید. ممنون

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

مجتبی
09 مرداد 1397
سلام خیلی خوب بود. مرسی

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

صبغت الله
22 اردیبهشت 1397
واقعا عالی بود ممنون

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

سایت کوتاه کننده لینک
12 اسفند 1396
سلام ممنونم مطلب بسیار عالی بود

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

میرزا
26 آبان 1396
سلام . ممنون از توضیحات عالیتون یه سوال داشتم اگه قرار باشه چند تا کلاینت همزمان در یه جدول بنویسند، خود pdo این موضوع رو مدیریت می کنه یا مجبوریم خودمون جدول ها رو به ترتیب قفل کنیم و بعد نوشتن باز کنیم؟

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

طراحی سایت
17 آبان 1396
سلام و احترام. واقعا عالی بود. مرسی

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