Datenbank unter Zend

In fast allen Anwendungen wird eine Datenbank für die Datenhaltung benötigt. In diesem Artikel möchte ich einen Weg beschreiben, um eine Anbindung an eine Datenbank unter einer Zend-Anwendung zu realisieren.

Voraussetzung für diese Artikel ist eine bereits eingerichtete Anwendung. Dies habe ich bereits im dem Artikel Zend Framework – Ein Startversuch beschrieben.

Verbindung konfigurieren

Im ersten Schritt benötigen wir einen korrekt konfigurierten Adapter. Das setzt z. B. eine lokal eingerichtete Datenbank voraus. Die Konfiguration kann direkt über die Konsole vorgenommen werden:

zf configure db-adapter "adapter=PDO_MYSQL&host=localhost&dbname=test&
username=testuser&password=testpasswort&charset=utf8" production

Das Zend-Tool passt durch diesen Aufruf die application.ini im Projekt an:

resources.db.adapter = "PDO_MYSQL"
resources.db.params.host = "localhost"
resources.db.params.dbname = "test"
resources.db.params.username = "testuser"
resources.db.params.password = "testpasswort"
resources.db.params.charset = "utf8"

Für den Microsoft Server könnte der Aufruf z. B. folgendermaßen aussehen:

zf configure db-adapter "adapter=Sqlsrv&host=mydbhost&dbname=mydbname&
username=myusername&password=mypassword&charset=utf-8"

Damit unter Mssql der Zeichensatz korrekt gesetzt ist, mußte ich die application.ini um eine Zeile ergänzen:

resources.db.adapter = "Sqlsrv"
resources.db.params.host = "mydbhost"
resources.db.params.dbname = "mydbname"
resources.db.params.username = "myusername"
resources.db.params.password = "mypassword"
resources.db.params.charset = "utf-8"
; Zeichensatz über die Driver-Options setzen
resources.db.params.driver_options.CharacterSet = "UTF-8"

Datenbank konfigurien und Tabellen anlegen

Da ich hier nicht die Datenbank-Seite, sondern die Zend-Seite betrachten möchte, gehe ich an dieser Stelle nicht weiter auf das Anlegen einer Tabelle ein. Ich möchte aber auf meinen Artikel Zend Framework und Liquibase verweisen.

Datenbankzugriff über ein Table Data Gateway

Der Zugriff auf eine Datenbank wird über ein sogenanntes Table Data Gateway ermöglicht. Das ist einfach gesagt eine PHP-Klasse, über die alle Zugriff auf die Tabelle abstrahiert werden. Die Klasse leitet von Zend_Db_Table_Abstract ab, die bereits Methoden für den Zugriff mitbringt (find, fetchAll, insert, update, delete). Über das zf-Tool kann diese einfach erstellt werden:

zf create db-table Project my_project_table_name

Im Ordner application/models/DbTable wird eine neue Klasse Project.php angelegt, die sehr überschaubar ist:

class Application_Model_DbTable_Protocol
  extends Zend_Db_Table_Abstract
{
  // Name der Datenbanktabelle
  protected $_name = 'my_project_table_name';
}

Erstellen einer Mapper-Klasse

Die Mapper-Klasse ist für die Übersetzung zwischen den PHP-Objekten und dem Table Data Gateway verantwortlich. Beispielsweise übergeben wir der Mapper-Klasse eine Instanz eines Models, damit dieses im Datenbank persistent gespeichert wird.

Im Ordner application/models erstellen wir einen neuen Ordner Mappers und legen dort eine neue Klasse Base.php an, die für alle künftigen Mapper-Klassen als Basisklasse dient. Dort können wir gemeinsam genützte Funktionalität implementieren.

Beispiel:

<?php
abstract class Application_Model_Mapper_Base
{
  // accessors/mutators
  public function setDbTable($dbTable)
  {
    if($this->_dbTable)
      return $this;
    if(is_string($dbTable))
      $dbTable=new $dbTable();
    if(!$dbTable instanceof Zend_Db_Table_Abstract)
      throw new Exception('Invalid table data gateway');
    $this->_dbTable=$dbTable;
    return $this;
  }
  /**
   * @return Zend_Db_Table_Abstract
   */
  public function getDbTable()
  {
    if(null === $this->_dbTable)
      $this->setDbTable($this->getDbTableClass());
    return $this->_dbTable;
  }
  //------------------------------------------------------------------
 
  // IMPLEMENTATION
  /**
   * @var Zend_Db_Table_Abstract
   */
  protected $_dbTable;
  //-----
 
  protected function getDbTableClass()
  {
    $path=explode('_', get_class($this));
    $name=array_pop($path);
    return 'Application_Model_DbTable_'.$name;
  }
}

Eine konkrete Implementierung könnte dann beispielsweise folgendermaßen aussehen:

<?php
class Application_Model_Mapper_Protocol
  extends Application_Model_Mapper_Base
{
  public function findAll()
  {
    $resultSet=$this->getDbTable()->fetchAll();
    $entries=array();
    foreach($resultSet as $row)
    {
      $entry=new Application_Model_Protocol();
      // TODO: Eigenschaften des Models setzen
      $entries[]=$entry;
    }
    return $entries;
  }
}

Wir haben hier eine Methode findAll implementiert und sehen dort sehr schön, wie in der Schleife die Model-Objekte erzeugt werden und anschließend zurückgegeben werden.

Ein Model anlegen

Das bereits im vorherigen Abschnitt erwähnte Model muß noch erzeugt werden. Wir können das wiederum mit dem zf-Tool vornehmen:

zf create model Project

Ich empfehle auch an dieser Stelle eine Basisklasse anzulegen, in der gemeinsam genützte Funktionalität implementiert wird (z. B. Setter/Getter für die id-Eigentschaft). Nachfolgend eine exemplarische Implementierung:

<?php
abstract class Application_Model_Base
{
  // properties
  /**
   * @var Zend_Db_Table_Row
   */
  public $dbentry;
  //------------------------------------------------------------------
 
  // construction
  public function __construct(array $options=null)
  {
    if (is_array($options))
      $this->setOptions($options);
  }
  //------------------------------------------------------------------
 
  // setters/getters
  public function __set($name, $value)
  {
    $method='set' . $name;
    if (('mapper'==$name) || !method_exists($this, $method)) {
      throw new Exception('Invalid '.__CLASS__.' property');
    }
    $this->$method($value);
  }
  public function __get($name)
  {
    $method='get' . $name;
    if (('mapper'==$name) || !method_exists($this, $method)) {
      throw new Exception('Invalid '.__CLASS__.' property');
    }
    return $this->$method();
  }
 
  public function setOptions(array $options)
  {
    $methods=get_class_methods($this);
    foreach ($options as $key => $value) {
      $method='set' . ucfirst($key);
      if (in_array($method, $methods)) {
        $this->$method($value);
      }
    }
    return $this;
  }
 
  public function setId($id)
  {
    $this->_id=$id;
    return $this;
  }
 
  public function getId()
  {
    return $this->_id;
  } 
  //------------------------------------------------------------------
 
  // IMPLEMENTATION
  protected $_id;
}
class Application_Model_Project extends Application_Model_Base
{
  // weitere Eigenschaften und Methoden
}

Tests

Wir sollten die erstellten Klassen frühzeitig testen. Wenn wir eine Zend Server Installation haben und PEAR und PhpUnit korrekt installiert ist, dann können wir im Ordner tests/application/models eine neue PHP-Datei anlegen und grundlegende Funktionalität testen. Die Einrichtung beinhaltet einige Fallstricke und ich werde bei Gelegenheit einen separaten Blog-Eintrag dazu verfassen. Nachfolgend nur ein Auszug, wie eine einfache Test-Klasse aussehen kann:

class ProjectTest extends Zend_Test_PHPUnit_ControllerTestCase
{
  public function setUp()
  {
    $this->bootstrap = new Zend_Application(APPLICATION_ENV, 
      APPLICATION_PATH . '/configs/application.ini');
    parent::setUp();
  }
 
  public function testProjectModel()
  {
    $model=new Application_Model_Project();
    $model->setId(1);
    $this->assertTrue($model->getId()===1);    
  }
}

Fazit

Es ist einiges an Vorarbeit notwendig, um Daten in einer Zend-Anwendung persistent zu bekommen. Es kommt auch entscheidend auf die Umsetzung an, da der Entwickler sehr frei in der Implementierung ist. Bei großen Datenmengen ist ein Objektmapping meistens nicht geeignet, da dies Zulasten der Performance und des Speicherverbrauchs geht – ein direkter Zugriff läßt sich jedoch auch direkt über den Adapter realisieren. In den meisten Szenarien ist das Arbeiten mit Modellen und Mappern jedoch zu empfehlen, weil es in der Verwendung einfacher zu Handhaben ist.

 

Kommentar verfassen