0
\$\begingroup\$

After reviewing my previous question I came up with this pagination function. On my first approach I wrote a class thinking about IDE's code assist and the review wasn't positive. In this function I decided to return a stdClass object with necessary properties. I have my concerns about the output approach. I want one function that does the full job. Thanks in advance.

So here's the code:

/**
 * @author Julio
 * @date 11/MAY/23
 *
 *
 * This function splits the dataset to be shown across multiple pages
 * @param array $data_source Simple or multidimensional array
 * @param int $results_per_page
 * @param int $page_number
 * @param string $get_var The name of the $_GET variable page number name. Default is 'page_number'
 * @throws LengthException
 * @throws Exception
 * @return stdClass
 *
 * Notes about the function code:
 *
 * You will notice that some variables could have been ommited. I decided to
 * declare them (redundant) to make the code easier to read and have the
 * output assignment ($output) set all together at the end. Also, there are a
 * couple of $output properties that are unnecessary: results_per_page and
 * page_number. This data is known by the developer.
 *
 *
 * Parameters explain by themselves. $get_var parameter takes the name of
 * the $_GET variable desired name to hold selected page value. Its default
 * value is "page_number".
 *
 * When page_number is out of range, function triggers a E_USER_LEVEL warning
 * and sets the page_number accordingly.
 * That is to say that when $page_number < 1, page_number will be set to 1.
 * Or if $page_number > $total_pages, will be set to $total_pages.
 *
 *
 * It returns a Standard Class (stdClass) object with the following properties:
 *
 *      record_count: total number of records
 *      results_per_page: number of records per page (unnecessary)
 *      total_pages: total number of pages
 *      page_number: selected page (unnecessary)
 *      resultset: selected records according to parameters
 *      url_previous: previous navigator URL
 *      url_next: next navigator URL
 *      url_first: first navigator URL
 *      url_last: last navigator URL
 *      navigator: simple HTML code por navigating through pages
 *      css: simple CSS3 code example for styling
 *
 *
 * TO-DO:
 * - when many many pages are available limit the number of navigator links to a
 * predefined number (i.e. 10). That is to say, if you are on page 34 of 98, links in
 * navigator should be between 30 and 38 or so. Something like SO's pagination.
 *
 * - allow the developer to set HTML id and class
 *
 * - add support for any HTML attribute
 *
 *
 * Usage:


// $data can be 'anything' you want:
$data = $result->fetch_all(MYSQLI_ASSOC);
// or
$data = [
    ["data11", "data12", "data13"],
    ["data21", "data22", "data23"]
];
// or
$data = [];
for ($i = 0; $i < 84; $i++) {
    $data[] = $i;
}

$pagination = pagination($data, 10, $_GET['page'] ?? 1, 'page');
$html .= "<br>Results: " . $pagination->record_count; // 84 (using last $data)
$html .= "<br>Results per page: " . $pagination->results_per_page; // 10
$html .= "<br>Total pages: " . $pagination->total_pages; // 9
$html .= "<br>Page number: " . $pagination->page_number; // 3
$html .= "<br>First URL: " . $pagination->url_first; // http://url/script.php?var=value&page=1
$html .= "<br>Last URL: " . $pagination->url_last; // http://url/script.php?var=value&page=9
$html .= "<br>Previous URL: " . $pagination->url_previous; // http://url/script.php?var=value&page=2
$html .= "<br>Next URL: " . $pagination->url_next; // http://url/script.php?var=value&page=4
$html .= "<br>Resultset: " . print_r($pagination->resultset, true); // [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
$html .= $pagination->navigator; // will render HTML div navigation links
//$html .= "<pre><code>" . $pagination->css . "</code></pre>"; // styling example
$html .= "<br><br>";
echo $html;


 *
 *
 *
 */
function pagination(
    array $data_source,
    int $results_per_page = 10,
    int $page_number = 1,
    string $get_var = "page_number"
): stdClass {

    $output = new stdClass();
    $record_count = count($data_source);
    $total_pages = 0;
    $resultset = [];

    /**
     * URL for resultset navigator
     * @var string $url
     */
    $url = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] . "?";
    $next_page = 0;
    $previous_page = 0;

    $navigator = "";
    $navigator_container = "\n<div id='paginator' class='paginator'>%links\n</div>\n";
    $navigator_link_tpl = "\n<a href='%url'%selected>%text</a>";
    $navigator_links = "";

    if ($record_count < 1) {
        throw new LengthException("Data source is empty");
    }

    if ($results_per_page < 1) {
        throw new Exception("Results per page must be equal to or greater than 1");
    }

    $total_pages = ceil($record_count / $results_per_page);

    if ($page_number < 1) { // set it to 1
        trigger_error("Page number '$page_number' is not valid; set to '1'", E_USER_WARNING);
        $page_number = 1;
    } elseif ($page_number > $total_pages) { // set it to max pages
        trigger_error("Page number '$page_number' is not valid; set to '$total_pages'", E_USER_WARNING);
        $page_number = $total_pages;
    }

    if (strlen($get_var) === 0) {
        $get_var = "page_number";
    }
    if (strlen($_SERVER['QUERY_STRING']) !== 0) {
        foreach ($_GET as $variable => $value) {
            if ($variable !== $get_var) {
                $url .= "$variable=$value&";
            }
        }
    }
    $url .= "$get_var=";

    $offset = ($page_number - 1) * $results_per_page;
    $resultset = array_slice($data_source, $offset, $results_per_page);

    $previous_page = $page_number - 1;
    if ($previous_page === 0) {
        $previous_page = 1;
    }

    $next_page = $page_number + 1;
    if ($next_page > $total_pages) {
        $next_page = $total_pages;
    }

    // prepare navigator
    if ($total_pages > 1) {
        if ($page_number !== 1) {
            $navigator_links .= str_replace(
                ["%url", "%selected", "%text"],
                [$url . $previous_page, "", "<"],
                $navigator_link_tpl
            );
        }

        for ($counter = 1; $counter <= $total_pages; $counter++) {
            if ($page_number !== $counter) {
                $navigator_links .= str_replace(
                    ["%url", "%selected", "%text"],
                    [$url . $counter, "", $counter],
                    $navigator_link_tpl
                );
            } else { // here is set the selected link styling
                $navigator_links .= str_replace(
                    ["%url", "%selected", "%text"],
                    ["#", " class='selected'", $counter],
                    $navigator_link_tpl
                );
            }
        }

        if ($page_number < $total_pages) {
            $navigator_links .= str_replace(
                ["%url", "%selected", "%text"],
                [$url . $next_page, "", ">"],
                $navigator_link_tpl
            );
        }

        $navigator = str_replace("%links", $navigator_links, $navigator_container);
    }

    // output assignment
    $output->record_count = $record_count;
    $output->results_per_page = $results_per_page; // unnecessary
    $output->total_pages = $total_pages;
    $output->page_number = $page_number; // unnecessary
    $output->resultset = $resultset;
    $output->url_previous = $url . $previous_page;
    $output->url_next = $url . $next_page;
    $output->url_first = $url . "1";
    $output->url_last = $url . $total_pages;
    $output->navigator = $navigator;

    $output->css = <<<EOT
    #paginador a, #paginator a {
        display:inline-block;
        padding:10px;
        border:1px solid blue;
        margin-top:20px;
    }
    #paginador a.selected, #paginator a.selected {
        background-color: blue !important;
        color: white !important;
        font-weight: bold !important;
    }
EOT;

    return $output;
}
\$\endgroup\$
3
  • \$\begingroup\$ This code ignores the single responsibility principle, and its readability / maintainability suffer as a result. There's a simple rule that helps one adhere to SRP: "Can I read the whole function on 1 page without scrolling?" So it's ironic that a Pagination function requires so much scrolling from the Gentle Reader. Here's another simple rule: "If I felt it needed a // comment, maybe that's because it needed a helper." So extract function prepare_navigator(...) \$\endgroup\$
    – J_H
    May 14 at 18:55
  • \$\begingroup\$ @J_H Extracting navigator will save around 40 lines. Maybe you can point me in the right direction so I don't overwhelm the Gentle Reader. If extract navigator maybe I should re-think the class approach. I don't see much comments in my code other than the docblock where I explain why I did something and its usage... \$\endgroup\$
    – julio
    May 15 at 11:09
  • \$\begingroup\$ Here's the big thing that will help with SRP. Always write a one-sentence comment at top of each function explaining what it does. If you find it's hard to write, because you need two sentences, or you write that it does "x AND y", well, that's a hint that you could decompose a bit further. Do this and it's nearly guaranteed that you will get "short" single-screen chunks of code. Also, even if you haven't been writing TDD unit tests as you go along, all those helpers will make it easy when you or someone else wants to write tests. \$\endgroup\$
    – J_H
    May 15 at 15:40

1 Answer 1

2
\$\begingroup\$

This code doesn't make for happy reading.

I also saw your previous question, and it seems you followed some of the advice there. I wouldn't have said that you shouldn't use classes, although I understand why it was said. Regrettably you didn't quite understand why it was said: You need to split your code up into multiple functional units, classes or functions, that can stand on their own and be tested. This is the same as the SRP mentioned by J_H in his comments.

I guess the only way to really explain what is meant is to show you. I like classes, because they encapsulate all the helper methods that I write, so I'll be using classes again.

class Pagination
{
    private $row_count;
    private $current_page_no;
    private $rows_per_page;

    public function __construct($row_count, $current_page_no, $rows_per_page = 10)
    // page numbering starts at zero
    {
        $this->setRowCount($row_count)
             ->setCurrentPageNo($current_page_no)
             ->setRowsPerPage($rows_per_page);
    }

    public function setRowCount($row_count)
    {
        $this->row_count = $row_count;
        return $this;
    }

    public function getRowCount()
    {
        return $this->row_count;
    }

    public function setCurrentPageNo($current_page_no)
    {
        $this->current_page_no = $current_page_no;
        return $this;
    }

    public function getCurrentPageNo()
    {
        return $this->current_page_no;
    }

    public function setRowsPerPage($rows_per_page)
    {
        $this->rows_per_page = $rows_per_page;
        return $this;
    }

    public function getRowsPerPage()
    {
        return $this->rows_per_page;
    }

    public function getTotalPageCount()
    {
        return (int)ceil($this->getRowCount() / $this->getRowsPerPage());
    }

    public function getFirstRowNo()
    // row numbering starts at zero
    {
        return $this->getCurrentPageNo() * $this->getRowsPerPage();
    }

    public function getLastRecordNo()
    // row numbering starts at zero
    {
        return min($this->getRowCount(), $this->getFirstRowNo() + $this->getRowsPerPage());
    }

    public function validateSettings()
    {
        if ($this->getRowCount() < 1) {
            triggerError("Row count is smaller than 1", E_USER_WARNING);
        }
        if ($this->getRowsPerPage() < 1) {
            triggerError("Rows per page is smaller than 1", E_USER_WARNING);
        }
        if ($this->getCurrentPageNo() < 0) {
            $this->setCurrentPageNo(0);
        }
        if ($this->getCurrentPageNo() >= $this->getTotalPageCount()) {
            $this->setCurrentPageNo($this->getTotalPageCount() - 1);
        }
        return $this;
    }

    public function getPageLinkHtml($title, $page_no, $is_current_page_no = false)
    {
        return '<a href="?page=' . $page_no . '"' .
               ($is_current_page_no ? ' class="selected"' : '') .
               '>' . htmlentities($title) . '</a>';
    }

    public function getNavigatorHtml($id = 'paginator')
    {
        $this->validateSettings();
        $total_page_count = $this->getTotalPageCount();
        $current_page_no  = $this->getCurrentPageNo();
        $html = '<div id="' . $id . '" class="paginator">';
        if ($total_page_count > 1) {
            if ($current_page_no > 0) {
                // previous page
                $html .= $this->getPageLinkHtml("<", $current_page_no - 1);
            }
            for ($page_no = 0; $page_no < $total_page_count; $page_no++) {
                $html .= $this->getPageLinkHtml($page_no + 1, $page_no,
                                                $page_no == $current_page_no);
            }
            if ($current_page_no < $total_page_count - 1) {
                // next page
                $html .= $this->getPageLinkHtml(">", $current_page_no + 1);
            }
        }
        return $html . '</div>';
    }

}

My sole aim with this class is to create the HTML for the navigator. Notice how all methods have been kept reasonably short.

I did intentionally not include the CSS, use the $_SERVER super global, or process table data inside this class. Also notice how I use the getPageLinkHtml() method to prevent repetition of code.

I hope this example shows you how to write simpler and easier to read code. Is this perfect? Probably not. But it is better than having to look through 133 lines of code to understand a single function.

You can test the class with this code:

$pagination = new Pagination(count($data), $_GET['page'] ?? 0, 10);
echo "<br><br>" .
    "getRowCount() => " . $pagination->getRowCount() . "<br>" .
    "getCurrentPageNo() => " . $pagination->getCurrentPageNo() . "<br>" .
    "getRowsPerPage() => " . $pagination->getRowsPerPage() . "<br>" .
    "getTotalPageCount() => " . $pagination->getTotalPageCount() . "<br>" .
    "getFirstRowNo() => " . $pagination->getFirstRowNo() . "<br>" .
    "getLastRecordNo() => " . $pagination->getLastRecordNo() . "<br><br>" .
    $pagination->getNavigatorHtml();

This code runs, but is not fully tested, I do not exclude the possibility that something isn't quite right.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.