ZendFramework Pager

Aus NOBAQ
Zur Navigation springenZur Suche springen
Zflogo.jpg

Ein oft wiederkehrendes Problem bei Webseiten ist ein sogenanntes "Paging": Von einer Tabelle mit 1000 Zeilen sollen nur 10 auf einer Seite dargestellt werden und der Rest durch "Blättern" mit "vor" und "zurück" zugänglich sein. Die normale Herangehensweise in PHP ist es zusätzlich zur normalen Abfrage (durch LIMIT begrenz), ein SQL Statement mit "SELECT COUNT(*) ... GROUP BY" zu erstellen. Der Nachteil dabei ist aber, dass zwei Abfragen benötigt werden, und die eigentliche Datenabfrage umgeändert werden muss. Ist die ursprüngliche Abfrage sehr komplex (z.B. Nested Sets, Verknüpfungen mit temporären Tabellen) ist diese Methode sehr komplex.

Meine Lösung für ZendFramework beschreibe ich hier.


Das neue mySQL Feature

In mySQL 5 gibt es ein neues Feature: Fügt man einem SELECT-Statement ein SQL_CALC_FOUND_ROWS zu, so berechnet mySQL im Hintergrund automatisch die Größe des Datensatzes ohne LIMIT-Statement. Dieses kann mit

SELECT FOUND_ROWS()

abgefragt werden.

Problem in ZendFramework

Die Zend_Db_Select Klasse im ZendFramework hat leider keine Unterstützung für dieses Feature (zumal die Klasse mit anderen RDBMS kompatibel sein soll). Leider lässt sich die Klasse auch schwer erweitern, da die Informationen über den Aufbau eines SELECT Statements in einem statischen Array stehen :-(

Folgende Klasse erweitert dennoch Zend_Db_Select - wenn auch auf nicht so schöne Art und Weise:

class Zend_Db_Select_Extended extends Zend_Db_Select
{
        const CALC_FOUND_ROWS = 'calc_found_rows';
        const SQL_CALC_FOUND_ROWS = 'SQL_CALC_FOUND_ROWS';

        public function __construct(Zend_Db_Adapter_Abstract $adapter)
        {
                parent::__construct($adapter);
                $this->_parts[self::CALC_FOUND_ROWS] = false;
        }

        public function calcFoundRows($flag = true)
        {
                $this->_parts[self::CALC_FOUND_ROWS] = (bool) $flag;
                return $this;
        }

        public function __toString()
        {
                $sql = parent::__toString();

                if($this->_parts[self::CALC_FOUND_ROWS])
                {
                        $sql = preg_replace('/^' . self::SQL_SELECT . '/', self::SQL_SELECT . ' '  . self::SQL_CALC_FOUND_ROWS, $sql);
                }

                return $sql;
        }
}

Das Feature kann nun ganz normal verwendet werden, z.B. mit:

$select = new Zend_Db_Select_Extended( Zend_Registry::get('db') );
// [...]
// erstelle kompliziertes SQL statement
//
$select->limitToPage($page, 10);

$sel2 = Zend_Registry::get('db')->select()->from('', new Zend_Db_Expr('FOUND_ROWS()'));

$pages_count = ceil($sel2->query()->fetchColumn() / 10);

ViewHelper

Fehlt nur mehr ein View-Helper, in dem man die Generierung des Pagers verpackt:

class View_Helper_Pager
{
        private $view;

        public function __construct()
        {
        }

        public function setView(Zend_View $view)
        {
                $this->view = $view;
        }

        public function Pager(Pager $pager)
        {
                if($pager->getNumPages() <= 1)
                {
                        return '';
                }
                $code = '<ul id="pager">';

                if($pager->getCurrentPage() > 1)
                {
                        $url = $this->view->url(array('page' => 1));
                        $code .= "<li><a href=\"$url\">&lt;&lt;</a></li>";

                        $url = $this->view->url(array('page' => $pager->getCurrentPage() - 1));
                        $code .= "<li><a href=\"$url\">&lt;</a></li>";
                }

                for($i = 1; $i <= $pager->getNumPages(); $i++)
                {
                        if($i == $pager->getCurrentPage())
                        {
                                $code .= "<li>$i</li>";
                        }
                        else
                        {
                                $url = $this->view->url(array('page' => $i));
                                $code .= "<li><a href=\"$url\">$i</a></li>";
                        }
                }

                if($pager->getCurrentPage() < $pager->getNumPages())
                {
                        $url = $this->view->url(array('page' => $pager->getCurrentPage() + 1));
                        $code .= "<li><a href=\"$url\">&gt;</a></li>";

                        $url = $this->view->url(array('page' => $pager->getNumPages()));
                        $code .= "<li><a href=\"$url\">&gt;&gt;</a></li>";
                }

                $code .= '</ul>';

                return $code;
        }
}

Alles zusammen

Für die Übergabe vom Controller an den Viewhelper hab ich (sinniger- oder unsinnigerweise) eine einfache Klasse Pager erstellt. Die Routen sind so eingerichtet, dass sie optional einen page-Parameter erhalten. Der Default-Wert ist dabei auf 1.

Die Anwendung des Zend_Db_Select_Extended Statements hab ich in mein Datenmodell verpackt, hier z.B. ProductListModel. Demensprechend schaut der Part im Controller aus:

// Parameter vom ZF holen
$page = $this->getRequest()->getExt('param');
// Das Datenmodell instanzieren. Für das LIMIT-Statement wird natürlich die aktuelle Seite benötigt
$product_list = new ProductListModel(..., $page);

// Dem VIEW einach die Pager Infos zuweisen:
$this->view->assign('pager', new Pager($page, $product_list->getPageCount()));

und alles, was im View noch geschenen muss, ist (z.B. unter der Ausgabetabelle):

<div id="pager_bar"><?=$this->pager($this->pager)?></div>

Kommentare

<comments />Diskussion:ZendFramework Pager