Tìm hiểu thuật toán phân trang trong php

Từ khóa phân trang thì có lẽ không xa lạ gì với các bạn nữa đúng không nhỉ? Và cũng rất nhiều bạn gặp khó khăn trong việc xử lý nó. Nên tôi xin phép được mở bài này giới thiệu một lớp phân trang có cách hoạt động giống như freetuts.net vậy.

1. Giới thiệu thuật toán phân trang

Vì là văn viết nên việc diễn giải thuật toán sẽ gặp khó khăn rất nhiều để người đọc có thể hiểu được, nên trong bài này nếu bạn không hiểu thì có thể tham khảo một số video trên youtube, có rất nhiều video cho bạn lựa chọn.

Thuật toán phân trang thực chất nó cũng giống như việc bạn chia kẹo ở lớp một vậy, giả sử tôi có 100 cái kẹo, bây giờ tôi muốn mỗi bạn nhận 5 cái kẹo, hỏi có bao nhiêu bạn sẽ nhận được? Đơn giản phải không nào, ta lấy 100/5 = 20 bạn. Và trước khi đi vào vấn đề tôi xin đưa ra 3 bài toán như sau:

Bài toán 1: Giả sử trong CSDL của tôi có 1000 sản phẩm, bây giờ tôi muốn chia ra mỗi kho sẽ chứa 50 sản phẩm. Hỏi cần bao nhiêu kho chứa? Câu trả lời cũng đơn giản, 1000/50 = 20 kho chứa. Trong bài toán này số nhà kho cần để chứa hết sản phẩm chính là số trang mà ta tính được, 1000 sản phẩm chính là tổng số record trong CSDL, 50 chính là giới hạn một trang sẽ hiển thị bao nhiêu record.

Bài toán 2. Giả sử CSDL có 1990 sản phẩm,  bây giờ tôi muốn chia mỗi kho chứa 50 sản phẩm. Hỏi bao cần bao nhiêu kho chứa? Ta thấy nếu lấy 1990/50 = 19 (nếu làm tròn). nhưng nếu cần 19 kho thôi thì 40 sản phẩm bị dư đó đặt ở đâu? À đơn giản ta phải lấy thêm 1 kho nữa để chứa nó, vậy ta có tổng cộng 20 kho. Trong bài toán này giải thích cũng giống như bài toán 1, nhưng có một điều khác là trường hợp tổng số record khi chia cho các trang sẽ dư một vài record, như thế ta bắt buộc phải kiếm thêm một kho nữa để chứa.

Bài toán 3: Tiếp tục bài toán 1, giả sử mỗi sản phẩm sẽ được đánh dấu từ 1 -> 1000, và được cất vào kho theo thứ tự tăng dần. Bây giờ tôi muốn các bạn tìm xem ở kho thứ 5 được bắt đầu từ sản phẩm thứ mấy? Và kết thúc ở sản phẩm thứ mấy? để giải bài này ta phải tính toán một chút. Ta thấy mỗi kho sẽ chứa 50 sản phẩm và:

  • Kho thứ nhất bắt đầu từ 1 -> 50
  • Kho thứ hai bắt đầu từ 51 -> 100
  • Kho thứ ba bắt đầu từ 101 -> 150
  • Kho thứ tư bắt đầu từ 151 -> 200
  • Kho thứ năm bắt đầu từ 201 -> 250

Quá đơn giản :D. Bây giờ tôi muốn tìm ở kho thứ 15 bắt đầu từ số nào và kết thúc ở số nào ? Hì hì hơi căng, các bạn rãnh tính đi chứ tôi hơi bận :D. Nhưng bạn có nhận thấy rằng có một quy luật nào đó không? Tôi thì nhận thấy rằng ta có một quy luật để tính như sau:

product_start = ((kho_hien_tai - 1) * so_san_pham_trong_mot_kho) + 1. Không tin bạn tính nhé

  • Tính kho thứ 5: product_start = (5-1)*50 + 1 = 201
  • Tính kho thứ 10: product_start = (10-1)*50 + 1 = 451
  • ...

Với bài toán 3 này liên hệ trong thuật toán phân trang như thế nào? Như bạn biết trong câu lệnh select sẽ có hai tham số là limit và start, limit chính là số record trên một trang và start chính là số thứ tự record bắt đầu, liên hệ với bài toán 3 thì chính là số thứ tự của sản phẩm ở kho thứ x.

Nhưng trong MYSQL thì số record sẽ được tính bắt đầu từ số 0 nên với công thức tính trên ta viết lại như sau:

product_start = ((kho_hien_tai - 1) * so_san_pham_trong_mot_kho) + 1.

Kết luận: Các bước phân trang như sau:

  • Bước 1: Tính tổng số record trong CSDL
  • Bước 2: Xác định số record trên một trang
  • Bước 3: Tính tổng số trang dưa vào tổng số record và số record trên một trang
  • Bước 4: Sau khi có tổng số record thì in ra trình duyệt

2. Thuật toán phân trang đơn giản

Để cho các bạn dễ hình dung tôi sẽ viết một lớp phân trang đơn giản có sử dụng OOP. Các bạn tạo file index.php và điền nội dung này vào:

class Pagination
{
    protected $_config = array(
        'current_page'  => 1, // Trang hiện tại
        'total_record'  => 1, // Tổng số record
        'total_page'    => 1, // Tổng số trang
        'limit'         => 10,// limit
        'start'         => 0, // start
        'link_full'     => '',// Link full có dạng như sau: domain/com/page/{page}
        'link_first'    => '',// Link trang đầu tiên
    );
    
    /*
     * Hàm khởi tạo ban đầu để sử dụng phân trang
     */
    function init($config = array())
    {
        /*
         * Lặp qua từng phần tử config truyền vào và gán vào config của đối tượng
         * trước khi gán vào thì phải kiểm tra thông số config truyền vào có nằm
         * trong hệ thống config không, nếu có thì mới gán
         */
        foreach ($config as $key => $val){
            if (isset($this->_config[$key])){
                $this->_config[$key] = $val;
            }
        }
        
        /*
         * Kiểm tra thông số limit truyền vào có nhỏ hơn 0 hay không?
         * Nếu nhỏ hơn thì gán cho limit = 0, vì trong mysql không cho limit bé hơn 0
         */
        if ($this->_config['limit'] < 0){
            $this->_config['limit'] = 0;
        }
        
        /*
         * Tính total page, công tức tính tổng số trang như sau: 
         * total_page = ciel(total_record/limit).
         * Tại sao lại như vậy? Đây là công thức tính trung bình thôi, ví
         * dụ tôi có 1000 record và tôi muốn mỗi trang là 100 record thì 
         * đương nhiên sẽ lấy 1000/100 = 10 trang đúng không nào :D
         */
        $this->_config['total_page'] = ceil($this->_config['total_record'] / $this->_config['limit']);
        
        /*
         * Sau khi có tổng số trang ta kiểm tra xem nó có nhỏ hơn 0 hay không
         * nếu nhỏ hơn 0 thì gán nó băng 1 ngay. Vì mặc định tổng số trang luôn bằng 1
         */
        if (!$this->_config['total_page']){
            $this->_config['total_page'] = 1;
        }
        
        /*
         * Trang hiện tại sẽ rơi vào một trong các trường hợp sau:
         *  - Nếu người dùng truyền vào số trang nhỏ hơn 1 thì ta sẽ gán nó = 1 
         *  - Nếu trang hiện tại người dùng truyền vào lớn hơn tổng số trang
         *    thì ta gán nó bằng tổng số trang
         * Đây là vấn đề giúp web chạy trơn tru hơn, vì đôi khi người dùng cố ý
         * thay đổi tham số trên url nhằm kiểm tra lỗi web của chúng ta
         */
        if ($this->_config['current_page'] < 1){
            $this->_config['current_page'] = 1;
        }
        
        if ($this->_config['current_page'] > $this->_config['total_page']){
            $this->_config['current_page'] = $this->_config['total_page'];
        }
        
        /* 
         * Tính start, Như bạn biết trong mysql truy vấn sẽ có limit và start
         * Muốn tính start ta phải dựa vào số trang hiện tại và số limit trên mỗi trang
         * và áp dụng công tức start = (current_page - 1)*limit
        */
        $this->_config['start'] = ($this->_config['current_page'] - 1) * $this->_config['limit'];
    }
    
    /*
     * Hàm lấy link theo trang
     */
    private function __link($page)
    {
        // Nếu trang < 1 thì ta sẽ lấy link first
        if ($page <= 1 && $this->_config['link_first']){
            return $this->_config['link_first'];
        }
        // Ngược lại ta lấy link_full
        // Như tôi comment ở trên, link full có dạng domain.com/page/{page}.
        // Trong đó {page} là nơi bạn muốn số trang sẽ thay thế vào
        return str_replace('{page}', $page, $this->_config['link_full']);
    }
    
    /*
     * Hàm lấy mã html
     * Hàm này ban tạo giống theo giao diện của bạn
     * tôi không có config nhiều vì rất rối
     * Bạn thay đổi theo giao diện của bạn nhé
     */
    function html()
    {   
        $p = '';
        // Kiểm tra tổng số trang lớn hơn 1 mới phân trang
        if ($this->_config['total_record'] > $this->_config['limit'])
        {
            $p = '<ul>';
            
            // Nút prev và first
            if ($this->_config['current_page'] > 1)
            {
                $p .= '<li><a href="'.$this->__link('1').'">First</a></li>';
                $p .= '<li><a href="'.$this->__link($this->_config['current_page']-1).'">Prev</a></li>';
            }
            
            // lặp trong khoảng cách giữa min và max để hiển thị các nút
            for ($i = 1; $i <= $this->_config['total_page']; $i++)
            {
                // Trang hiện tại
                if ($this->_config['current_page'] == $i){
                    $p .= '<li><span>'.$i.'</span></li>';
                }
                else{
                    $p .= '<li><a href="'.$this->__link($i).'">'.$i.'</a></li>';
                }
            }

            // Nút last và next
            if ($this->_config['current_page'] < $this->_config['total_page'])
            {
                $p .= '<li><a href="'.$this->__link($this->_config['current_page'] + 1).'">Next</a></li>';
                $p .= '<li><a href="'.$this->__link($this->_config['total_page']).'">Last</a></li>';
            }
            
            $p .= '</ul>';
        }
        return $p;
    }
}
Qua phần comment tôi có giải thích rồi. Và các bạn lưu ý ở 4 bước trên tôi có tính một số đoạn code giúp hệ thống chạy đúng an toàn và bảo mật hơn.

Cách sử dụng:

$config = array(
    'current_page'  => isset($_GET['page']) ? $_GET['page'] : 1, // Trang hiện tại
    'total_record'  => 1900, // Tổng số record
    'limit'         => 10,// limit
    'link_full'     => 'index.php?page={page}',// Link full có dạng như sau: domain/com/page/{page}
    'link_first'    => 'index.php',// Link trang đầu tiên
);

$paging = new Pagination();

$paging->init($config);

echo $paging->html();

Và đây là tổng hợp code cho bài này:

<?php 
class Pagination
{
    protected $_config = array(
        'current_page'  => 1, // Trang hiện tại
        'total_record'  => 1, // Tổng số record
        'total_page'    => 1, // Tổng số trang
        'limit'         => 10,// limit
        'start'         => 0, // start
        'link_full'     => '',// Link full có dạng như sau: domain/com/page/{page}
        'link_first'    => '',// Link trang đầu tiên
    );
    
    /*
     * Hàm khởi tạo ban đầu để sử dụng phân trang
     */
    function init($config = array())
    {
        /*
         * Lặp qua từng phần tử config truyền vào và gán vào config của đối tượng
         * trước khi gán vào thì phải kiểm tra thông số config truyền vào có nằm
         * trong hệ thống config không, nếu có thì mới gán
         */
        foreach ($config as $key => $val){
            if (isset($this->_config[$key])){
                $this->_config[$key] = $val;
            }
        }
        
        /*
         * Kiểm tra thông số limit truyền vào có nhỏ hơn 0 hay không?
         * Nếu nhỏ hơn thì gán cho limit = 0, vì trong mysql không cho limit bé hơn 0
         */
        if ($this->_config['limit'] < 0){
            $this->_config['limit'] = 0;
        }
        
        /*
         * Tính total page, công tức tính tổng số trang như sau: 
         * total_page = ciel(total_record/limit).
         * Tại sao lại như vậy? Đây là công thức tính trung bình thôi, ví
         * dụ tôi có 1000 record và tôi muốn mỗi trang là 100 record thì 
         * đương nhiên sẽ lấy 1000/100 = 10 trang đúng không nào :D
         */
        $this->_config['total_page'] = ceil($this->_config['total_record'] / $this->_config['limit']);
        
        /*
         * Sau khi có tổng số trang ta kiểm tra xem nó có nhỏ hơn 0 hay không
         * nếu nhỏ hơn 0 thì gán nó băng 1 ngay. Vì mặc định tổng số trang luôn bằng 1
         */
        if (!$this->_config['total_page']){
            $this->_config['total_page'] = 1;
        }
        
        /*
         * Trang hiện tại sẽ rơi vào một trong các trường hợp sau:
         *  - Nếu người dùng truyền vào số trang nhỏ hơn 1 thì ta sẽ gán nó = 1 
         *  - Nếu trang hiện tại người dùng truyền vào lớn hơn tổng số trang
         *    thì ta gán nó bằng tổng số trang
         * Đây là vấn đề giúp web chạy trơn tru hơn, vì đôi khi người dùng cố ý
         * thay đổi tham số trên url nhằm kiểm tra lỗi web của chúng ta
         */
        if ($this->_config['current_page'] < 1){
            $this->_config['current_page'] = 1;
        }
        
        if ($this->_config['current_page'] > $this->_config['total_page']){
            $this->_config['current_page'] = $this->_config['total_page'];
        }
        
        /* 
         * Tính start, Như bạn biết trong mysql truy vấn sẽ có limit và start
         * Muốn tính start ta phải dựa vào số trang hiện tại và số limit trên mỗi trang
         * và áp dụng công tức start = (current_page - 1)*limit
        */
        $this->_config['start'] = ($this->_config['current_page'] - 1) * $this->_config['limit'];
    }
    
    /*
     * Hàm lấy link theo trang
     */
    private function __link($page)
    {
        // Nếu trang < 1 thì ta sẽ lấy link first
        if ($page <= 1 && $this->_config['link_first']){
            return $this->_config['link_first'];
        }
        // Ngược lại ta lấy link_full
        // Như tôi comment ở trên, link full có dạng domain.com/page/{page}.
        // Trong đó {page} là nơi bạn muốn số trang sẽ thay thế vào
        return str_replace('{page}', $page, $this->_config['link_full']);
    }
    
    /*
     * Hàm lấy mã html
     * Hàm này ban tạo giống theo giao diện của bạn
     * tôi không có config nhiều vì rất rối
     * Bạn thay đổi theo giao diện của bạn nhé
     */
    function html()
    {   
        $p = '';
        // Kiểm tra tổng số trang lớn hơn 1 mới phân trang
        if ($this->_config['total_record'] > $this->_config['limit'])
        {
            $p = '<ul>';
            
            // Nút prev và first
            if ($this->_config['current_page'] > 1)
            {
                $p .= '<li><a href="'.$this->__link('1').'">First</a></li>';
                $p .= '<li><a href="'.$this->__link($this->_config['current_page']-1).'">Prev</a></li>';
            }
            
            // lặp trong khoảng cách giữa min và max để hiển thị các nút
            for ($i = 1; $i <= $this->_config['total_page']; $i++)
            {
                // Trang hiện tại
                if ($this->_config['current_page'] == $i){
                    $p .= '<li><span>'.$i.'</span></li>';
                }
                else{
                    $p .= '<li><a href="'.$this->__link($i).'">'.$i.'</a></li>';
                }
            }

            // Nút last và next
            if ($this->_config['current_page'] < $this->_config['total_page'])
            {
                $p .= '<li><a href="'.$this->__link($this->_config['current_page'] + 1).'">Next</a></li>';
                $p .= '<li><a href="'.$this->__link($this->_config['total_page']).'">Last</a></li>';
            }
            
            $p .= '</ul>';
        }
        return $p;
    }
}


$config = array(
    'current_page'  => isset($_GET['page']) ? $_GET['page'] : 1, // Trang hiện tại
    'total_record'  => 1900, // Tổng số record
    'limit'         => 10,// limit
    'link_full'     => 'index.php?page={page}',// Link full có dạng như sau: domain/com/page/{page}
    'link_first'    => 'index.php',// Link trang đầu tiên
);

$paging = new Pagination();

$paging->init($config);

echo $paging->html();

?>

<style>
    li{float:left; margin: 3px; border: solid 1px gray;}
    a{padding: 5px;}
    span{display:inline-block; padding: 0px 3px; background: blue; color:white }
</style>

3. Lời kết

Ở phần 2 tôi cũng không nói nhiều vì qua những đoạn code tôi đã có comment rõ ràng rồi. Sẽ hơi khó với những bạn chưa rành với thuật toán phân trang này. Ở bài tiếp theo tôi sẽ viết lại thuật toán nâng cao hơn chút xíu, tức là sẽ hiển thị một số trang nhất định thôi chứ không hiển thị tất  cả các trang như ở bài này. Mời các bạn đón xem.

Hãy để lại link bài viết gốc khi chia sẻ bài viết này, mình sẽ report DMCA với những website lấy nội dung mà không để nguồn hoặc copy bài với số lượng lớn.

Nguồn: freetuts.net

Profile photo of adminTheHalfHeart

TheHalfHeart

Có sở thích viết tuts nên đã từng tham gia viết ở một số diễn đàn, đến năm 2014 mới có điều kiện sáng lập ra freetuts.net. Sinh năm 90 và có 1 vợ 2 con, thích ca hát và lập trình.

ĐĂNG BÌNH LUẬN: Đăng câu hỏi trên Group Facebook để được hỗ trợ nhanh nhất.