تذکر: این یک پست آموزشی برای عموم نیست! بلکه تنها جایی برای یادداشتهای من حین یادگیریه تا بهتر به خاطر بسپارم و در صورت لزوم به اونها مراجعه کنم.
تمرینهای مربوط به این دوره از طریق این لینک در دسترس هستند.
git clone "https://github.com/LinkedInLearning/php-techniques-pagination-2884225" git checkout your_branch
برای دسترسی به برنچها توی vscode:
در Pagination نیاز داریم که سه مورد اساسی رو بدونیم:
$current_page = (int) ($_GET['page'] ?? 1); $next_page = $current_page + 1; $previous_page = $current_page -1;
توی قطعه کد بالا از اپراتور null coalescing استفاده شده؛ این اپراتور اولین بار در PHP 7 معرفی شد و به این صورت عمل میکنه که اگر مقداری که سمت چپ قرار گرفته set نشده باشه، مقدار دیفالت سمت راست که در اینجا یک هست رو برمیگردونه.
همیشه مقداری که از متغیر GET میخونیم یک string هست؛ بنابراین نیاز داریم که به int تبدیلش کنیم.
یک مقدار ثابت هم برای تعداد رکورد به ازای هر صفحه داریم:
Define("PER_PAGE",20); $per_page = 20;
برای پیدا کردن نقطه شروع هر صفحه نیاز به یک offset داریم که می تونیم اینطوری به دستش بیاریم:
$offset = $per_page * ($current_page - 1)
مثلا اگر در صفحه اول باشیم، مقدار offset برابر صفر میشود و این یعنی اینکه نیازی نیست از تعدادی رکورد صرف نظر کنیم.
قطعه کد زیر یک حالت بسیار ساده و basic از pagination ارائه میده:
$per_page = 20; $current_page = (int) ($_GET['page'] ?? 1); $offset = $per_page * ($current_page - 1); $customers = array_slice($all_customers, $offset, $per_page);
توی قطعه کد زیر، مقدار کل رکوردها رو با استفاده از تابع count محاسبه کردیم؛ البته چون اینجا داده به شکل آرایه بود از این تابع استفاده شد؛ اگر قرار بود از db بخونیم، میتونستیم از دستور sql استفاده کنیم و... .
برای محاسبه $total_pages تعداد کل رکوردها رو بر تعداد رکورد در هر صفحه تقسیم کردیم و با تابع ceil به سمت بالا (سقف) گردش کردیم:
$per_page = 20; $total_count = count($all_customers); $total_pages = ceil($total_count / $per_page); $current_page = (int) ($_GET['page'] ?? 1); if($current_page < 1 || $current_page > $total_pages) { $current_page = 1; } $offset = $per_page * ($current_page - 1); $customers = array_slice($all_customers, $offset, $per_page);
دسترسی به دیتابیسهای MySQL از طریق command-line:
mysql -u user_name -p password db_name
دستورات کاربردی mysql:
CREATE TABLE customers ( id int(11) NOT NULL auto_increment, first_name varchar(255), last_name varchar(255), PRIMARY KEY (id) ); INSERT INTO customers (first_name, last_name) VALUES ('Tyler', 'Spradley');
اگر بخوایم یک db اکسپورت شده رو با CLI ایمپورت کنیم:
mysql -u user_name -p password db_name < "/your/path/to/file.sql" // to see the tables of a db: SHOW TABLES;
از تابع زیر برای کوئری زدن به db استفاده کردیم؛ در واقع یک wrapper خیلی ساده برای mysqli_query هست؛ که جواب یک کوئری ساده رو برمیگردونه، confirm_query هم چک میکنه که اگر مقدار برگشتی false بود؛ برنامه رو متوقف کنه:
function db_query($connection, $sql) { $result_set = mysqli_query($connection, $sql); if(substr($sql, 0, 7) == 'SELECT ') { confirm_query($result_set); } return $result_set; }
با تابع find_customers یک کوئری به db میزنیم و تمامی رکوردها رو میگیریم:
function find_customers() { global $db; $sql = "SELECT * FROM customers " $sql .= "ORDER BY last_name ASC, first_name ASC" $result = db_query($db, $sql); return $result; }
حالا با استفاده از قطعه کد زیر، مقادیر بازگشتی رو iterate میکنیم توی یک table، که db_fetch_assco یک wrapper ساده برای mysqli_fetch_assoc هست:
while($customer = db_fetch_assoc($customers)) { echo "<tr>" echo "<td>" . h($customer['first_name']) . "</td>" echo "<td>" . h($customer['last_name']) . "</td>" echo "</tr>" } // end while db_free_result($customers);
توی سناریوی اولیه، ما با استفاده از یک کوئری کل داده های موجود توی جدول رو خوندیم و بعد اونها رو iterate کردیم، اما این سناریو چندان بهینه نیست و ما میتونیم فیلترسازی تعداد و ست کردن offset رو در سطح mysql db انجام بدیم.
برای این کار باید با یک سری از مفاهیم مرتبط با MySQL آشنا باشیم:
LIMIT: Maximum records to return in an SQL query ex: SELECT * FROM table LIMIT num; OFFSET: records to skip over in an SQL query ex: SELECT * FROM table OFFSET num; COUNT: returns a count of records matching an SQL query does not return records faster and requires less memory ex: SELECT COUNT(*) FROM table;
ترکیب OFFSET و LIMIT، به ترتیب قرارگیری باید دقت داشت؛ LIMIT همیشه قبل از OFFSET قرار میگیرد:
SELECT * FROM table LIMIT num OFFSET num;
برای جلوگیری از SQL Injection بهترین راهبرد استفاده از عبارت های آماده یا Prepared statements هست. اما توی این دوره که دارم نگاه میکنم از Sanitization استفاده کرده، این تکنیک میاد و کاراکترهای خاص رو بیاثر میکنه، مثلا برای ضد عفونی یا بی اثر کردن یک رشته که از سمت کاربر میاد و قراره داخل دل یک کوئری قرار بگیره از mysqli_real_escape_string استفاده میشه.
تابعی که برای محاسبه تعداد رکودها استفاده کردیم:
function count_customers() { global $db; $sql = "SELECT COUNT(*) FROM customers " $result = db_query($db, $sql); $array = db_fetch_assoc($result); return $array['COUNT(*)']; }
نکتهٔ مهم در مورد تابع قبلی اینه که برای گرفتن مقدار برگشتی، باید مقدار:
COUNT(*)
رو به عنوان کلید از آرایه برگشتی بخوایم.
نحوهٔ اضافه کردن لینکهای Next و Previous برای رفتن به صفحات قبل و بعد:
<p class="pagination"> <?php if($current_page > 1) { echo "<a href=\"customers.php?page=" . ($current_page - 1) . "\">← Previous</a>" } ?> | <?php if($current_page < $total_pages) { echo "<a href=\"customers.php?page=" . ($current_page + 1) . "\">Next →</a>" } ?> </p>
قطعه کد بالا، دکمههای Next و Previous را به شکل کاملا ساده به صفحه HTML ما اضافه کرده است.
با استفاده از قطعه کد زیر میتونیم لیست شماره صفحه رو به سیستم Pagination خودمون توی صفحهٔ HTML اضافه کنیم:
for($i=1; $i <= $total_pages; $i++) { echo "<a href=\"customers.php?page={$i}\">{$i}</a> " }
توی قطعه کد بالا با توجه به تعداد صفحاتی که داریم iterate کردیم و شماره هر صفحه به علاوهٔ لینک مربوط به اون رو توی صفحه خودمون echo کردیم.
اگر بخوایم شمارهٔ صفحهای که الان داخلش هستیم رو متمایز کنیم؛ می تونیم از منطقی مثل این استفاده کنیم:
for($i=1; $i <= $total_pages; $i++) { if ($current_page == $i){ echo "<strong>{i}</strong> } else { echo "<a href=\"customers.php?page={$i}\">{$i}</a> " } }
در قسمت پایین، نمونه کامل شدهٔ قطعه کد مربوط به اضافه کردن gap بین سریهای اعداد رو میبینم:
<?php $win = 2; // window size $gap = false; for($i=1; $i <= $total_pages; $i++) { if($i > 1 + $win && $i < $total_pages - $win && abs($i - $current_page) > $win) { if(!$gap) { echo "... " $gap = true; } continue; } $gap = false; if($current_page == $i) { echo "<strong>{$i}</strong> " } else { echo "<a href=\"customers.php?page={$i}\">{$i}</a> " } } ?>
یکی از مشکلاتی که توی Pagination باید حل بشه و براش میتونیم چند راه حل ارائه بدیم، «به یاد سپاری صفحه حاضر» هست. وقتی ما هر دفعه روی یک لینک کلیک میکنیم و به صفحه دیگهای منتقل میشیم؛ صفحهای که توش بودیم از خاطر میره، مثلا توی صفحه زیر برای هر مشتری یک آیکن show در نظر گرفته شده، حالا اگر روی این show کلیک کنیم؛ به یک صفحه دیگه منتقل میشیم، ما باید یه مکانیزمی داشته باشیم تا بدونیم روی چه صفحهای بودیم:
احتمالا بهترین استراتژی استفاده از session و یا cookie برای این کار هست.
صفحهبندی بر مبنای oop:
خوبی استفاده از OOP برای صفحهبندی یا Pagination اینه که کل logic رو توی یک class کپسوله میکنیم و به هر عملکردی که نیاز داشتیم خیلی ساده، call ش میکنیم:
راهنمای ساده استفاده از کلاس Pagination:
$total_count = count_customers(); $page = $_GET['page'] ?? 1; $pagination = new Pagination($total_count, $page, 20); $customers = find_customers($pagination->per_page, $pagination->offset()); <?php echo $pagination->page_links('customers_oo.php'); ?>
در خط اول با استفاده از تابع count_customers تعداد کل رکورد ها را با استفاده از کوئری زدن به table با COUNT حساب کردیم.
توی خط دوم، شماره صفحه رو گرفتیم و اگر ست نشده بود با null coalescing برابر ۱ قرارش دادیم.
توی خط سوم، یک شی از کلاس Pagination ساختیم که ۳ تا آرگومان دریافت میکنه، اولی تعداد کل رکوردها که توی خط اول به دست آوردیم، دومی صفحه فعلی و آخری per_page.
در خط بعدی، با استفاده از find_customers که دو تا آرگومان per_page (به عنوان LIMIT) و offset (به عنوان OFFSET) رو دریافت میکنه، دادهها رو از دیتابیس واکشی کردیم و در نهایت با استفاده از متد page_links کل سیستم pagination رو لود کردیم.
البته کد لود شدن مقادیر واکشی شده find_customers توی سند html در بالا آورده شده.