Author Topic: [PHP] Tackling searching, ordering, pagination and feedback  (Read 954 times)

0 Members and 1 Guest are viewing this topic.

Offline ande

  • Owner
  • Titan
  • *
  • Posts: 2664
  • Cookies: 256
    • View Profile
[PHP] Tackling searching, ordering, pagination and feedback
« on: December 10, 2014, 06:35:56 am »
Hello EZ,

I have recently with the new Alpha project development come to realize that I need a better way to do searching, ordering, pagination and feedback. Alone and in combination with each other. It gets especially tricky when you need to combine them.

The problems are:
- URL building with many GET/POST variables. (Without having a gazillion isset() and is_string()'s etc)
- Doing clean pagination code for multiple pages and scenarios.
- A good way to set and display user feedback.

All of the problems above are solved in the following code package. They are solved individually so you can easily only solve two of them without needing any additional/unnecessary code.

I don't KNOW if this is the best way to do it. I am sure there are plenty of other ways to do it, but after doing searching, ordering, pagination and feedbacks for some years I think this is the best solution to a server-side only approach I have come across so far. If anyone knows of better ways to do this, please do share!

The code package consists of 4 files:
  • index.php
  • database.php
  • feedback.php
  • pagination.php

I am using PDO in this code, but it should be easily adaptable to any sort of database framework/class.

The only thing you need is a database and a table. The table's name is "test", and it's fields are: ID, name, email.

Download ZIP: http://upload.evilzone.org?page=download&file=fLtZcU3DDWERO1CokxbQDAhWNTjGdQOKvbRPVyZltsQttD8PgQ

Live DEMO: http://upload.evilzone.org/ande/searchsortpagination/index.php


Simple feedback usage:
Code: (php) [Select]
<?php
// Simple feedback
Feedback::setFeedback('Results: '.$totalNumRes);
?>



// Then place this somewhere in your HTML:
<?php Feedback::printFeedback(); ?>

Simple pagination usage:
Code: (php) [Select]
<?php
// Pagination example
$myPagination = new Pagination($totalNumRes);
$myPagination->currentPage  $startPage;
?>



// Then place this somewhere in your HTML:
<?php $myPagination->printPagination(); ?>

How I solved searching and sorting is in index.php

In-forum code:

index.php
Code: (php) [Select]
<?php

/*

Coded by ande@evilzone.org
Use as you wish.

*/

// Includes
include('database.php');
include('pagination.php');
include('feedback.php');

// Few basic functions
// Validates a GET parameter as string. Has a default value if error.
function getGetString($name$default) {
if(isset($_GET[$name]) && is_string($_GET[$name]))
return $_GET[$name];
return $default;
}
// Validates a GET paramenter as int. Has a default value if error.
function getGetInt($name$default) {
if(isset($_GET[$name]) && validateInt($_GET[$name]))
return intval($_GET[$name]);
return $default;
}
// Validates that a numeric number is within the int scope.
function validateInt($i) {
if(is_numeric($i) && (int)$i==$i)
return TRUE;
return FALSE;
}

/////////////////////////////////////////////////////////////////////////////////////////////

// Set up the database interface
$DBInterface DB::getInstance();

// Grab the various inputs
$searchPattern  getGetString('q' NULL);
$startPage  getGetInt   ('start'0);
$sort getGetString('sort' 'id');
$sortDir getGetString('dir' 'asc');
// Not part of URL, used as part of the limiting, also used in pagination
$numRows 5;

// Sanetising $sort and $sortDir
if(!in_array($sort, array('id','name','email')))
$sort 'id';
if(!in_array($sortDir, array('asc','desc')))
$sortDir 'asc';


/////// Searching and ordering
// First we see how many results we got without any ordering and limiting. This is needed for pagination later.
if($searchPattern != NULL) {
$statement $DBInterface->prepare('SELECT COUNT(*) FROM test WHERE name LIKE :searchPattern OR email LIKE :searchPattern');
$searchPatternTmp '%'.$searchPattern.'%';
$statement->bindParam(':searchPattern'$searchPatternTmpPDO::PARAM_STR);
} else {
$statement $DBInterface->prepare('SELECT COUNT(*) FROM test');
}
$statement->execute();
$totalNumRes $statement->fetchColumn();


// Then we do the same query with ordering and limiting
if($searchPattern != NULL) {
$statement $DBInterface->prepare('SELECT * FROM test WHERE name LIKE :searchPattern OR email LIKE :searchPattern ORDER BY '.$sort.' '.$sortDir.' LIMIT :limitStart, :numRes');
$searchPatternTmp '%'.$searchPattern.'%';
$statement->bindParam(':searchPattern'$searchPatternTmpPDO::PARAM_STR);
} else {
$statement $DBInterface->prepare('SELECT * FROM test ORDER BY '.$sort.' '.$sortDir.' LIMIT :limitStart, :numRes');
}
$startPageTmp $startPage*$numRows;
$statement->bindParam(':limitStart'$startPageTmpPDO::PARAM_INT);
$statement->bindParam(':numRes'  $numRows PDO::PARAM_INT);
$statement->execute();

// Fetch the results
$results '';
while($result $statement->fetch()) {
$results .= '<tr><td>'.$result['id'].'</td><td>'.htmlentities($result['name'], ENT_QUOTES).'</td><td>'.htmlentities($result['email'], ENT_QUOTES).'</td></tr>';
}




/////// Feedback
// Simple feedback
Feedback::setFeedback('Results: '.$totalNumRes.'<br />Listing: '.$startPageTmp.'-'.$numRows);




/////// Pagination
// Pagination example
$myPagination = new Pagination($totalNumRes);
$myPagination->pageMultiplier  $numRows;
$myPagination->currentPage  $startPage;
// Insert additional unknown variables to the suffix to preserve search query and ordering
$urlData = array('q' => $searchPattern'sort' => $sort'dir' => $sortDir);
$myPagination->URLSuffix '&'.http_build_query($urlData);


// Used for table sort links
$urlData = array('q' => $searchPattern'dir' => (($sortDir=='asc')?'desc':'asc'), 'start' => $startPage);
$existingURL http_build_query($urlData);

?>

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Seaching, sorting, pagination and user feedback demo</title>

<style type="text/css">
#searchDiv, #feedbackDiv, #resultsDiv, #paginationDiv {
margin: 10px;
padding: 10px;
border: solid #505050 1px;
}


#resultsDiv table {
width: 100%;
}
#resultsDiv table tr:nth-child(odd) td {
background-color: #ffffff;
}
#resultsDiv table tr:nth-child(even) td {
background-color: #eeeeee;
}
#resultsDiv table tr:first-child td {
background-color: #dddddd;
}
#resultsDiv table tr td:nth-child(1) { /* ID */
width: 30px;
}


</style>

</head>

<body>

<div id="searchDiv">
<form method="get" action="index.php">
<input type="text" name="q" placeholder="Search" value="<?php if(isset($searchPattern)) echo(htmlentities($searchPatternENT_QUOTES)); ?>" />
<input type="submit" value="Search" />
</form>
</div>

<?php Feedback::printFeedback(); ?>

<div id="resultsDiv">
<table>
<tr>
<td><a href="index.php?sort=id&<?php print $existingURL?>">ID</a></td>
<td><a href="index.php?sort=name&<?php print $existingURL?>">Name</a></td>
<td><a href="index.php?sort=email&<?php print $existingURL?>">Email</a></td>
</tr>

<?php print $results?>
</table>
</div>

<?php $myPagination->printPagination(); ?>

</body>
</html>


database.php
Code: (php) [Select]
<?php

/*

Coded by ande@evilzone.org
Use as you wish.

*/


// DB structure:
// Table: test
//  ID | name | email


abstract class DB
{
public static function getInstance($newInstance false)
{
static $connection NULL;
if($connection == NULL || $newInstance === true)
{
try {
$connection = new PDO('mysql:host=localhost;dbname=searchsortpagination''root''');
} catch(Exception $e) {
die('Could not connect to the database. Forgot to initialize?');
}
}
return $connection;
}
}


?>



feedback.php
Code: (php) [Select]
<?php

/*

Coded by ande@evilzone.org
Use as you wish.

*/

abstract class Feedback {

private static $feedback '';

public static function computeFeedback() {
if(self::$feedback != '')
return '<div id="feedbackDiv">'.self::$feedback.'</div>';
return '';
}

public static function printFeedback() {
echo(self::computeFeedback());
}

public static function setFeedback($feedback) {
self::$feedback $feedback;
}

}

?>



pagination.php
Code: (php) [Select]
<?php

/*

Coded by ande@evilzone.org
Use as you wish.

*/

class Pagination {

public $displayOuterArrows TRUE; // Show the [<<] and [>>] buttons?
public $displayInnerArrows TRUE; // Show the [<] and [>] buttons?
public $numButtons  5; // Number of buttons to display. [1] [2] [3] [4] [5]
public $numItems  0; // Total number of items being browser (MAX)
public $pageMultiplier  5; // How many items per page do you want to display?
public $currentPage  0; // What page are we currently on?
public $URLPrefix 'index.php'; // URL start (part Typically index.php)
public $URLInfix '?start='; // URL middle part (Pagination part ?start=)
public $URLSuffix ''; // URL end part (Got additional data? &herp=derp&derp=herp)


// Class constructor
function __construct($numItems$numButtons=5$pageMultiplier=5$currentPage=0) {
$this->numItems  $numItems;
$this->numButtons $numButtons;
$this->pageMultiplier  $pageMultiplier;
$this->currentPage  $currentPage;
}


// Return output of pagination
public function computePagination() {
// Are there even enough data for pagination?
if($this->numItems $this->pageMultiplier)
return '';

// Init data
$startPrintPage  max(0$this->currentPage floor($this->numButtons 2));
$maxPages  ceil($this->numItems $this->pageMultiplier);
$numPrintedButtons  0;
$currentPage  $this->currentPage;
$URLPrefix  $this->URLPrefix;
$URLInfix  $this->URLInfix;
$URLSuffix  $this->URLSuffix;


// This does so that when you come to the end of the page display, it will still display $this->numButtons buttons
while(($startPrintPage floor($this->numButtons 2)*>= $maxPages) && $startPrintPage 0)
$startPrintPage--;


// Outer pagination div
$output '<div id="paginationDiv">';


// [<<] and [<] buttons
if($this->displayOuterArrows && $currentPage 0) {
$output .= '<a href="'.$URLPrefix.$URLInfix.'0'.$URLSuffix.'">&lt;&lt;</a>';
}
if($this->displayInnerArrows && $currentPage 0) {
$output .= ' <a href="'.$URLPrefix.$URLInfix.($currentPage-1).$URLSuffix.'">&lt</a>';
}


// [1] [2] [3] [4] [5] buttons
$currentPrintPage $startPrintPage;
while($currentPrintPage $maxPages && $numPrintedButtons $this->numButtons) {
if($currentPrintPage == $currentPage)
$output .= ' <a href="'.$URLPrefix.$URLInfix.($currentPrintPage).$URLSuffix.'">[_'.($currentPrintPage+1).'_]</a>';
else
$output .= ' <a href="'.$URLPrefix.$URLInfix.($currentPrintPage).$URLSuffix.'">['.($currentPrintPage+1).']</a>';
$currentPrintPage++;
$numPrintedButtons++;
}


// [>] and [>>] buttons
if($this->displayInnerArrows && $currentPage $maxPages-1) {
$output .= ' <a href="'.$URLPrefix.$URLInfix.($currentPage+1).$URLSuffix.'">&gt;</a>';
}
if($this->displayOuterArrows && $currentPage $maxPages-1) {
$output .= ' <a href="'.$URLPrefix.$URLInfix.($maxPages-1).$URLSuffix.'">&gt;&gt;</a>';
}


// End of outer pagination div
$output .= '</div>';


return $output;
}


// Print output of pagination
public function printPagination() {
echo $this->computePagination();
}

}

?>

« Last Edit: December 11, 2014, 11:28:23 am by ande »
if($statement) { unless(!$statement) { // Very sure } }
https://evilzone.org/?hack=true

Offline Stackprotector

  • Administrator
  • Titan
  • *
  • Posts: 2515
  • Cookies: 205
    • View Profile
Re: [PHP] Tackling searching, ordering, pagination and feedback
« Reply #1 on: December 10, 2014, 02:50:48 pm »
Wooh dynamic php programming :D
~Factionwars

Offline Schalla

  • VIP
  • Peasant
  • *
  • Posts: 81
  • Cookies: 29
    • View Profile
Re: [PHP] Tackling searching, ordering, pagination and feedback
« Reply #2 on: December 10, 2014, 03:09:30 pm »

Quote
<ande> You might want to add some context to your psot ^^
<ande> post*
<ande> Like "Talked to ande on IRC and did some changes to the pagination class: "

Code: (php) [Select]
<?php

/*

    Coded by ande@evilzone.org
    Use as you wish.

*/

class Pagination {

    public 
$displayOuterArrows TRUE;         // Show the [<<] and [>>] buttons?
    
public $displayInnerArrows TRUE;         // Show the [<] and [>] buttons?
    
public $numButtons          5;         // Number of buttons to display. [1] [2] [3] [4] [5]
    
public $numItems          0;         // Total number of items being browser (MAX)
    
public $pageMultiplier       5;         // How many items per page do you want to display?
    
public $currentPage       0;         // What page are we currently on?
    
public $URLPrefix         'index.php';   // URL start (part Typically index.php)
    
public $URLInfix         '?start=';   // URL middle part (Pagination part ?start=)
    
public $URLSuffix         '';         // URL end part (Got additional data? &herp=derp&derp=herp)

    /**
     * Default Constructor for Pagination
     *
     * @param $numItems
     * @param int $numButtons
     * @param int $pageMultiplier
     * @param int $currentPage
     */

    
function __construct($numItems$numButtons=5$pageMultiplier=5$currentPage=0) {
        
$this->numItems       $numItems;
        
$this->numButtons      $numButtons;
        
$this->pageMultiplier    $pageMultiplier;
        
$this->currentPage       $currentPage;
    }


    
/**
     * Creates Output for Pagination
     *
     * @return string
     */

    
public function computePagination() {

        
// Are there even enough data for pagination?
        
if($this->numItems $this->pageMultiplier){
            return 
'';
        }

        
//Schalla:
        //Don't use calculations in loop when they are not changed in there,
        //also you used that value in $startPrintPage, do not calculate it twice.
        //Wording might be bad...

        
$halfedNumButtons=floor($this->numButtons 2);

        
// Init data
        
$startPrintPage    max(0$this->currentPage $halfedNumButtons);
        
$maxPages          ceil($this->numItems $this->pageMultiplier);
        
$numPrintedButtons    0;

        
// This does so that when you come to the end of the page display, it will still display $this->numButtons buttons
        
while(($startPrintPage $halfedNumButtons*>= $maxPages) && $startPrintPage 0){
            
$startPrintPage--;
        }

        
// Outer pagination div
        
$output '<div id="paginationDiv">';

        
//Schalla: Seperate Functions if possible, that way its way less cluttered.
        
$output=$this->createBackButton($output);
        
$output=$this->createNumberButton($output$startPrintPage$maxPages$numPrintedButtons);
        
$output=$this->createForwardButton($output$maxPages);

        
// End of outer pagination div
        
$output .= '<>';

        return 
$output;
    }

    
/**
     * Creates Backwards Navigation for Pagination
     *
     * @param $output   string  Current Output for Pagination
     * @return string
     */

    
private function createBackButton($output)
    {

        
//Could also be > 1, since > 0 is just &lt;
        
if($this->displayOuterArrows && $this->currentPage 0) {
            
$output .= $this->createLink('0',"&lt;&lt;");
        }

        if(
$this->displayInnerArrows && $this->currentPage 0) {
            
$output .= $this->createLink($this->currentPage-1,'&lt;');
        }

        return 
$output;
    }

    
/**
     * Creates Number Navigation for Pagination
     *
     * @param string    $output            Current Output for Pagination
     * @param int       $currentPrintPage  Number of current page
     * @param int       $maxPages          Maximum of Page Count
     * @param int       $numPrintedButtons Number of printed buttons
     * @return string
     */

    
private function createNumberButton($output$currentPrintPage$maxPages$numPrintedButtons)
    {

        while(
$currentPrintPage $maxPages && $numPrintedButtons $this->numButtons) {

            if(
$currentPrintPage == $this->currentPage){
                
$output .= $this->createLink($currentPrintPage,'[_'.($currentPrintPage+1).'_]');
            }else{
                
$output .= $this->createLink($currentPrintPage,'['.($currentPrintPage+1).']');
            }

            
$currentPrintPage++;
            
$numPrintedButtons++;
        }

        return 
$output;
    }

    
/**
     * Creates Forward Navigation for Pagination
     *
     * @param string    $output   Current Output for Pagination
     * @param int       $maxPages Maximum of Page Count
     * @return string
     */

    
private function createForwardButton($output$maxPages)
    {
        
// [>] and [>>] buttons
        
if($this->displayInnerArrows && $this->currentPage $maxPages-1) {
            
$output .= $this->createLink($this->currentPage+1,'&gt;');
        }

        if(
$this->displayOuterArrows && $this->currentPage $maxPages-1) {
            
$output .= $this->createLink($maxPages-1,'&gt;&gt;');
        }

        return 
$output;
    }

    
/**
     * Creates Link for Pagination
     *
     * @param string $href   Target Addition for Link
     * @param string $value  Value of the Link
     * @return string
     */

    
private function createLink($href$value)
    {
        return 
' <a href="'.$this->URLPrefix.$this->URLInfix.$href.$this->URLSuffix.'">'.$value.'</a>';
    }

    
/**
     * Computes the Navigation for Output
     */

    
public function printPagination() {
        echo 
$this->computePagination();
    }

}
« Last Edit: December 10, 2014, 03:55:01 pm by Schalla »

Offline ande

  • Owner
  • Titan
  • *
  • Posts: 2664
  • Cookies: 256
    • View Profile
Re: [PHP] Tackling searching, ordering, pagination and feedback
« Reply #3 on: December 10, 2014, 08:11:30 pm »
Here is a version of pagination.php more function separated like Schalla's version but optimized slightly. Does the same job but is more flexible and readable I guess.

Code: (php) [Select]
<?php

/*

Coded by ande@evilzone.org
Use as you wish.

*/

class Pagination {

public $displayOuterArrows TRUE; // Show the [<<] and [>>] buttons?
public $displayInnerArrows TRUE; // Show the [<] and [>] buttons?
public $numButtons  5; // Number of buttons to display. [1] [2] [3] [4] [5]
public $numItems  0; // Total number of items being browser (MAX)
public $pageMultiplier  5; // How many items per page do you want to display?
public $currentPage  0; // What page are we currently on?
public $URLPrefix 'index.php'; // URL start (part Typically index.php)
public $URLInfix '?start='; // URL middle part (Pagination part ?start=)
public $URLSuffix ''; // URL end part (Got additional data? &herp=derp&derp=herp)


// Class constructor
function __construct($numItems$numButtons=5$pageMultiplier=5$currentPage=0) {
$this->numItems  $numItems;
$this->numButtons $numButtons;
$this->pageMultiplier  $pageMultiplier;
$this->currentPage  $currentPage;
}


// Return output of pagination
public function computePagination() {
// Are there even enough data for pagination?
if($this->numItems $this->pageMultiplier)
return '';


// Output buffer
$output '';

// [<<] and [<] buttons
$output .= $this->makePreviousButtons();

// [1] [2] [3] [4] [5] buttons
$output .= $this->makeNumberButtons();

// [>] and [>>] buttons
$output .= $this->makeNextButtons();


// Encapsulate result to make styling easier and return output
return '<div id="paginationDiv">'.$output.'</div>';
}


// Print output of pagination
public function printPagination() {
echo $this->computePagination();
}


// Creates the the "previous" buttons [<<] and [<]
private function makePreviousButtons() {
$output '';
if($this->displayOuterArrows && $this->currentPage 0)
$output .= $this->makeButton('0''&lt;&lt;');
if($this->displayInnerArrows && $this->currentPage 0)
$output .= $this->makeButton($this->currentPage-1'&lt;');
return $output;
}


// Creates the number buttons [1] [2] [3] [4] [5]
private function makeNumberButtons() {
$currentPage  $this->currentPage;
$numButtonsSides  floor($this->numButtons 2);
$startPrintPage  max(0$currentPage $numButtonsSides);
$maxPages  ceil($this->numItems $this->pageMultiplier);
$numPrintedButtons  0;
$output '';

// This does so that when you come to the end of the page display, it will still display $this->numButtons buttons instead of only displaying the last one and $numButtonsSides before it:
// [<<] [<] [11] [12] [_13_]
// VS
// [<<] [<] [9] [10] [11] [12] [_13_]
// When you do intend to have 5 buttons all the time
while(($startPrintPage $numButtonsSides*>= $maxPages) && $startPrintPage 0)
$startPrintPage--;

while($startPrintPage $maxPages && $numPrintedButtons $this->numButtons) {
$output .= $this->makeButton($startPrintPage$startPrintPage+1, ($startPrintPage == $currentPage));
$startPrintPage++;
$numPrintedButtons++;
}
return $output;
}


// Creates the "next" buttons [>] and [>>]
private function makeNextButtons() {
$currentPage  $this->currentPage;
$maxPages  ceil($this->numItems $this->pageMultiplier);
$output '';
if($this->displayInnerArrows && $currentPage $maxPages-1)
$output .= $this->makeButton($currentPage+1'&gt;');
if($this->displayOuterArrows && $currentPage $maxPages-1)
$output .= $this->makeButton($maxPages-1'&gt;&gt;');
return $output;
}


// Creates the HTML for a button
private function makeButton($URLAfterInfix$Text$Highlighted=FALSE) {
if($Highlighted)
return '<a href="'.$this->URLPrefix.$this->URLInfix.$URLAfterInfix.$this->URLSuffix.'">[_'.$Text.'_]</a>';
else
return '<a href="'.$this->URLPrefix.$this->URLInfix.$URLAfterInfix.$this->URLSuffix.'">['.$Text.']</a>';
}

}

?>

if($statement) { unless(!$statement) { // Very sure } }
https://evilzone.org/?hack=true

Offline ande

  • Owner
  • Titan
  • *
  • Posts: 2664
  • Cookies: 256
    • View Profile
Re: [PHP] Tackling searching, ordering, pagination and feedback
« Reply #4 on: December 11, 2014, 11:28:57 am »
I am allowed to double post. Stfu.

Added a link to live demo: http://upload.evilzone.org/ande/searchsortpagination/index.php
if($statement) { unless(!$statement) { // Very sure } }
https://evilzone.org/?hack=true