Willkommen auf der privaten Homepage von Johannes Jarolim, Salzburg, Österreich. Welcome to the private homepage of Johannes Jarolim, Salzburg, Austria, Europe.

Saubere Paginierung von Daten ist ein Thema, über das man als Webentwickler öfter stolpert. Als eifriger Nutzer der Komponenten des Zend Frameworks nutze ich natürlich gerne die Zend_Paginator Komponente. Diese separiert mittels Adaptern die Paginierung von der Datenquelle und stellt für mich durchaus eine der besten Implementierungen in diesem Themengebiet dar.

In Kombination mit der Zend_Db – Komponentengruppe und den entsprechenden Adaptern erlaubt der Paginator auch noch die Verbesserung der Performance, da das Objekt direkt in die Abfrage der Datenbank eingreift und somit auch schon beim Holen der Datensätze entsprechend eingreifen kann.

Da ich mich derzeit tiefer in das Zend Application Framework einarbeite und hier auch der empfohlenen Trennung des Models von seiner Persistierung mittels “Mappern” folge, stellt sich jetzt natürlich sofort die Frage, wo der Paginator in diesem Konzept zu sehen ist.

Einerseits ist der Paginator eine Frontend-Komponente, andererseits soll er im Backend für performante Abfragen von Datensätzen sorgen. Dazwischen liegt das Model und soll von dem ganzen Vorgang möglichst unberührt bleiben. Da ich hier eine Zeit lang Best Practices gesucht und studiert habe, möchte ich einen Ansatz von Rob Allen, Author des Buches Zend Framework in Action, der mir persönlich gut gefällt, ausführlicher Untersuchen:

Das Beispiel

Wir nehmen an, dass wir im Rahmen einer Applikation agieren, die der Erfassung von Arbeitsstunden dient.

Eine zentrale Aufgabe erfüllt hier die “Entry” Klasse:

class Application_Model_Entry {

  protected $_id;
  protected $_date;
  protected $_rate;
  protected $_minutes;
  protected $_description;

  public __construct($options = array()) {
    ...
  }

  // Getters und Setters

  ...

}

Eine Zend_Db_Table_Abstract – Klasse dient zur Verortung in der genutzten Datenbank:

class Application_Model_DbTable_Entry extends Zend_Db_Table_Abstract {
  protected $_name = 'entry';
}

Dies Verbindung zwischen Model und seiner Persisitierung erfolgt mittels der Klasse EntryStore (Ich persönlich bevorzuge hier die Namenskonventionen aus Eric Evans Buch Domain-Driven Design statt zB. EntryMapper – Aber das ist wohl Geschmackssache):

class Application_Model_Store_EntryStore {

  protected $_dbTable;

  public function __construct() {
    $this->setDbTable(new Application_Model_DbTable_Entry());
  }

  /**
   * @return /Application_Model_Project
   */
  public function getLatestEntries() {

    $rows = $this->getDbTable()->fetchAll(
      $this->getDbTable()
           ->select()
           ->order('date DESC')
           ->order('id DESC')
    );

    $entries = array();
    if (!empty($rows)) {
      foreach ($rows as $row) {
        $entries[] = new Application_Model_Entry($row->toArray());
      }
    }

    return $entries;

  }

}

Soweit, so Quickstart: Das ist ziemlicher Standard für einen DataMapper. Wir holen die Daten aus der Datenbank und wandeln die als Zend_Db_Table_Row zurückkommenden Daten in Entry-Instanzen um, die wir in Folge im Model nutzen können. Damit der Paginator seine Arbeit erfüllen kann, müssen wir ihm das Select-Objekt übergeben, sodass er ein limit() darauf setzen kann – Wir refaktorieren den Code dazu etwas:

  /**
   * @return /Application_Model_Project
   */
  public function getLatestEntries() {

    $select = $this->getDbTable()
                   ->select()
                   ->order('date DESC')
                   ->order('id DESC');

    $adapter = new Zend_Paginator_Adapter_DbSelect($select);
    $paginator = new Zend_Paginator($adapter);
    return $paginator;

  }

Wie man sehen kann, erstellen wir eine Instanz der Klasse Zend_Paginator_Adapter_DbSelect, welche das $select Objekt übergeben bekommt. Dieses übergeben wir an einen Zend_Paginator, der in unserem Fall schlussendlich an den Controller zurückgegeben wird.

Das einzige Problem an der Geschichte ist, dass der Controller eigentlich ein Array mit Instanzen der Klasse “Entry” erwartet, der Adapter allerdings nur Zend_Db_Table_Rows zurückgibt.

Eine saubere Lösung

Eine nette Lösung dieses Henne-Ei-Problems liegt in der Erstellung eines eigenen Adapters, der die Klasse Zend_Paginator_Adapter_DbSelect erweitert: Wir wollen einen Adapter, der die richtigen Objekte für das Model zurückgibt:

class Application_Model_Paginator_Adapter_EntryAdapter extends Zend_Paginator_Adapter_DbSelect {

  /**
   * Gibt ein Array von Entry-Objekten zurück
   *
   * @param  integer $offset Page offset
   * @param  integer $itemCountPerPage Number of items per page
   * @return array
   */
  public function getItems($offset, $itemCountPerPage) {

    $rows = parent::getItems($offset, $itemCountPerPage);

    $entries = array();
    foreach ($rows as $row) {
      $entries[] = new Application_Model_Entry($row->toArray());
    }
    return $entries;
  }

}

Dieser Adapter tut nun genau, was wir brauchen – Wir müssen somit nur den Adapter im Store von

$adapter = new Zend_Paginator_Adapter_DbSelect($select);

auf

$adapter = new Application_Model_Paginator_Adapter_EntryAdapter($select)

ändern, und fertig: Der Paginator liefert die benötigten Instanzen des Modells zurück.

Den Paginator nutzen

Um das Beispiel abzurunden, wenden wir uns noch der Nutzung des Paginators zu: Wir müssen ihm noch mitteilen, auf welcher Seite wir uns befinden und wie viele Einträge auf einer Seite dargestellt werden sollen. Da das eindeutig ein Frontend-Thema ist, begeben wir uns in den Controller:

class Application_Controller_EntryController extends Zend_Controller_Action {

  ...

  public function listEntriesAction() {

    $request = $this->getRequest();

    $entryStore = new Application_Model_Store_EntryStore();
    $entries = $entryStore->getLatestEntries();
    $entries->setCurrentPageNumber($request->getParam('page', 1));
    $entries->setItemCountPerPage(20);

    $this->view->entries = $entries;

  }

  ...

}

Da der Paginator das Interface Traversable implementiert, iterieren wir im View wie gewohnt über die Elemente und freuen uns, dass wir hier nichts ändern müssen:

<?php if (!empty($this->entries)): ?>
  <table class="entrylist">
    <thead>
      <tr>
        <th>Date</th>
        <th>Rate</th>
        <th>Minutes</th>
        <th>Description</th>
      </tr>
    </thead>
    <tbody>
      <?php foreach ($this->entries as $entry): ?>
        <tr>
          <td><?php $this->escape($entry->getDate()) ?></td>
          <td><?php $this->escape($entry->getRate()->getName()) ?></td>
          <td><?php $this->escape($entry->getMinutes()) ?></td>
          <td><?php $this->escape($entry->getDescription()) ?></td>
        </tr>
      <?php endforeach ?>
    </tbody>
  </table>
<?php endif ?>

Mit einem netten Paginator-Template runden wir die Anzeige ab und haben jetzt eine performante Paginierung, die sauber in einem schön abstrahierten Modell eingebettet ist.

Geht alles viel einfacher?

Ich bin jederzeit an eleganten Lösungen interessiert und warte gespannt auf Antworten ;-)

Eine Antwort

  1. 15. January 2013, 21:08
    Comment by HW
    Hi!

    viel herzlichen Dank! So ein schönes Tut hab ich noch nie gesehen! WOW.. ich liebe dich!!! du hast mir nen Tag Arbeit erspart!

    THX!

Hier können Sie eine Antwort hinterlassen

CAPTCHA Image
Reload Image