Manchmal ist es frustrierend, wenn man mit dem Zend Framework arbeitet. Es kann zwar so ziemlich alles, aber die Dokumentation ist nicht besonders zu empfehlen. Darin wird zwar jedes leidige Thema angesprochen, jedoch nie in einem globaleren Kontext. Beispiel: PHP kann mit einer speziellen __autoload Funktion Klassen dynamisch aus Dateien laden, sofern sie benoetigt werden. Darauf basierend arbeiten Autoloader von Zend, die es verschiedene Aufgabenbereiche gibt. Die Dokumentation sagt nun, wie man Autoloader erzeugt. Es fehlen aber einige wichtige Informationen: Wo wird das gemacht, wieso wird das gemacht, welchen Nebeneffekte hat das und wie sieht ein Standard-Use Case aus? Es gibt natuerlich eine Begruendung dafuer, wieso die Dokumentation so anscheinend wichtige Dinge weglaesst: Das Zend Framework ist so konzipiert, dass man es auf beliebige Weise anwenden kann: Ob es als Model-View-Controller Framework oder fuer noch komplexeren Spaghetticode genutzt wird ist so ziemlich egal und diese Flexibilitaet ist durchaus erwuenscht. Best Practice sieht aber anders aus.
Best Practice
Zur Best Practise hat sich mittlerweile die auch von Zend Studio automatisch generierbare Model-View-Controller-Struktur gemausert. Dazu wird zunaechst eine Verzeichnisstruktur in folgender oder aehnlicher Weise mit einigen Standarddateien genutzt:
application
configs
application.ini
controller
ErrorController.php
IndexController.php
models
views
scripts
error
error.phtml
index
index.phtml
Bootstrap.php
public
index.php
Hierbei wird der Webserver so konfiguriert, dass die Domain auf das public-Verzeichnis zeigt. Dadurch wird vor allem die Sicherheit der Applikation erhoeht, da der Zugriff auf die einzelnen Dateien nicht mehr moeglich ist. Die Datei application.ini enthaelt die wichtigsten Informationen, die fuer das Deployment benoetigt werden. Das controller-Verzeichnis enthaelt alle Controller und das models-Verzeichnis enthaelt analog die Modellabstraktionen. Jeder Controller besitzt mindestens eine Action in Form einer Methode, mit der Namenskonvention actionnameAction. Zu jedem Controller gibt es im views-Unterverzeichnis scripts ein Verzeichnis, in dem fuer jede Action eine Datei mit der Namenskonvention actionname.phtml vorliegt. Diese Dateien definieren die an den Client zurueckzusendenen Darstellungsinformationen.
Soweit so gut. Wenn Projekte wachsen wird man irgendwann anfangen das Gesamtsystem in kleine Haeppchen zu zerhacken. Divide and Conquer – Teile und herrsche. Wir (angehenden) Software Systems Engineers nennen sowas auch gerne Modularisierung. Das Zend Framework ist fuer diesen Fall natuerlich auch gewappnet und ermoeglicht es Module zu definieren, wobei jedes Modul eine Verzeichnisstruktur aufweist, die identisch zum application-Verzeichnis ist. Es muessen natuerlich nicht alle Verzeichnisse existieren, aber Minimum ist in den meisten Faellen controller, models und views. Fuer jedes Modul gibt es ein Verzeichnis im Verzeichnis modules und das ganze sieht dann in etwa so aus:
application
configs
application.ini
modules
default
controller
ErrorController.php
IndexController.php
models
views
scripts
error
error.phtml
index
index.phtml
Bootstrap.php
foobarmodule
controller
ErrorController.php
IndexController.php
models
views
scripts
error
error.phtml
index
index.phtml
Bootstrap.php
Bootstrap.php
public
index.php
Hierbei hat default eine spezielle Rolle, denn es wird von Zend als Standard verwendet, sofern kein anderes Modul explizit angegeben wurde.
Models
Wie oben bereits mehrfach erwaehnt kann jedes Modul Models besitzen. Und hierbei gehen die Geister auseinander. Lange Zeit wurde im Quick Start Tutorial vom Zend Framework darauf gepocht, dass man die Klasse Zend_Db_Table_Abstract erweitert. Diese abstrakte Klasse ermoeglicht es sehr einfach eine Datenbanktabellenabstraktion zu erstellen, indem man einfach von ihr erbt und lediglich den Tabellennamen angibt, auf der operiert werden soll. Da selbst das einigen Menschen wohl zu viel Schreibarbeit war, wurde in frueheren Versionen des Zend Frameworks Inflection benutzt (hier steht ein bisschen dazu und hier ganz unten auch). Das funktioniert wie folgt: Man erweitert Zend_Db_Table (nicht die abstrakte Klasse!) und benennt sie nach einem bestimmten Schema (meist Default_Model_DatenbankTabellenName, je nach Autoloader-Konfiguration mit oder ohne Default_-Prefix). Der Name der Klasse wird dann zum Namen der zugehoerigen Tabelle transformiert (hier waere es datenbank_tabellen_name).
Ich sehe in diesem Ansatz jedoch einen Widerspruch zum Begriff Datenbankabstraktion. Man abstrahiert, um vor allem Komplexitaet zu verringern. Zusaetzlich – und nicht zu vernachlaessigen – abstrahiert man aber auch, um eine Entkopplung von tieferliegender Implementierung zu hoeheren Komponenten zu erreichen. Stellt man nun eine direkte Abhaengigkeit zwischen dem Klassennamen und den Tabellennamen her, so ist man gezwungen jede Namensaenderung der Datenbanktabelle auf alle Verwendungen des Models anzuwenden. Epic fail!
Ich habe bereits gesagt, dass dies so alles mal in der offiziellen Dokumentation vom Zend Framework stand bzw. noch steht. Dem gewiefte Leser mag nun aufgefallen, dass ich im vorherigen Absatz erwaehnt habe, dass jede Verwendung des Models anzupassen waere, wenn Inflection genutzt wird und sich der Tabellenname in der Datenbank aendert. Wenn es jedoch nur eine einzige Verwendung gibt: Who cares? Aber hier geht es weiter: Die Dokumentation geht an vielen Stellen davon aus, dass man direkt auf Instanzen von Zend_Db_Table oder Instanzen von Unterklassen von Zend_Db_Table_Abstract arbeitet. Das hat natuerlich zur Folge, dass lose Kopplung zum Fremdwort wird.
Is-A vs. Has-A
Plump unterscheidet man haeufig (unter anderem) zwischen einer Ist- (Is-A) und einer Hat-Beziehung (Has-A) zwischen Entitaeten. Was hat das nun mit Models zu tun? Die Weiterfuehrung des Gedankens nicht direkt auf Datenbankabstraktionen zu arbeiten hat zur Folge, dass man Datenbankabstraktionen zu Modelabstraktionen abstrahiert. Beispiel: Es gibt eine Benutzerverwaltungssystem, also gibt es eine Tabelle von Benutzern in der Datenbank (users). Dafuer gibt es nun eine Tabellenabstraktion. Diese Klasse heisse nun Default_Model_Users. Wenn wir nun fuer einzelne Datensaetze, also fuer einzelne Benutzer auch eine Abstraktion verwenden, so gibt es die Wahl zwischen den beiden Beziehungsarten. Fuer eine Ist-Beziehung haetten wir in diesem Beispiel eine Klasse Default_Model_User und wir wuerden Instanzen davon erhalten, wenn wir ueber Default_Model_Users (mit Oberklasse Zend_Db_Table_Row_Abstract) bestimmte Datensaetze auswaehlen. Da man nun die Datenbank wegabstrahieren moechte hat man zunaechst Gateways eingefuehrt.
Gateways besitzen eine Instanz von Zend_Db_Table (bzw. eine Instanz einer Unterklasse von Zend_Db_Table_Abstract), auf der Operationen ausgefuehrt werden, um einzelne Datensaetze zu erhalten. Diese Datensaetze sollen nun nicht in der Form einer Unterklasse von Zend_Db_Table_Row (bzw. Zend_Db_Table_Row_Abstract) vorliegen, da wir ja die Datenbank verschleiern moechten. Haeufige Herangehensweise dabei ist es, fuer jedes Feld einer Tabelle ein Feld in einem Objekt zu generieren. Wir sind endlich bei der Hat-Beziehung angelangt.
Mehr Informationen zu den Beziehungsarten in diesem Kontext (viel besser erklaert, als ich es in aller Kuerze machen koennte) findet ihr bei Rob Allen’s Dev Notes: On models in a Zend Framework application.
Service Layer
Hab ich schonmal erwaehnt, dass ich Zwiebeln jeder Art sehr gerne esse? Nicht? Ist auch nicht wichtig. Wichtig ist jedoch, dass wir es in der Software Entwicklung haeufig mit Zwiebeln zu tun haben. Heisst soviel wie: Wir bauen etwas um etwas anderes herum, um u.A. (1) ein gewuenschtes Abstraktionsniveau zu erhalten, (2) lose gekoppelte Systeme schreiben zu koennen und (3) damit wir auf Aenderungen besser reagieren koennen. Es gibt natuerlich auch die Gefahr, dass diese Zwiebelmethode angewendet wird, weil das Wissen ueber die Komponente nicht ausreichend ist oder weil man mit der Komponente nicht zufrieden ist. Dann wird das ganze schnell zu dem Anti-Pattern, das natuerlich auch Onion heisst.
Wenn wir mit Persistenzschichten arbeiten, wollen wir haeufig eine Schnittstelle fuer verschiedene Persistenzsysteme. Wir wollen also zum Beispiel ohne grossen Aufwand ein auf CSV oder XML basiertes Persistenzsystem auf eine Datenbank umaendern. Das ist einfach, wenn wir in unserer Business Logic auf Repraesentationen von den eigentlich betrachteten Objekten arbeiten und nicht direkt auf den Persistenzsystemen. Gateways ermoeglichen das bereits im Ansatz, jedoch besteht noch immer eine direkte Verbindung zwischen den Feldern eine Tabelle in einer Datenbank und den Attributen eines Business Logic Objects.
Und jetzt kommt der Clue: Statt Bottom-Up zunaechst an die Implementierung der Datenbankanbindung zu denken, besinnen wir uns zunaechst daran, dass wir ja auf bestimmten fiktiven oder realen Objekten wie z.B. einem Benutzer arbeiten moechten. Top-Down: Wir definieren uns als erstes die Objekte als Schnittstellen, um zu gewaehrleisen, dass alle Operationen existieren, die wir benoetigen. Da wir Attribute nicht in Schnittstellen definieren koennen, nutzen wir Getter und Setter. Alternativ koennen wir auch mit abstrakten Klassen arbeiten, dann ist es auch moeglich Attribute vorzugeben, es ist jedoch schwieriger sicherzustellen, dass die Attribute auch allen Vorbedingungen, Nachbedingungen und Invarianten des Systems folgen bzw. ueberhaupt genutzt werden. Vorteil von abstrakten Klassen ist jedoch, dass wir Standardimplementierungen vorgeben koennen.
Nachdem wir alle Objekte zumindest strukturell spezifiziert haben, koennen wir nun beginnen sie zu implementieren. Da ich jetzt schon viel zu viel geschrieben habe ohne ein Beispiel zu nennen kommt erstmal eins, bei dem ich mich auf die grundlegenden Dinge beschraenke:
class Default_Model_User
{
protected $_id;
protected $_name;
public function getId()
{
return $this->_id;
}
public function setId($id)
{
$this->_id = (int) $id;
return $this;
}
public function getName()
{
return $this->_name;
}
public function setName($name)
{
$this->_name = (string) $name;
return $this;
}
}
Weiter geht’s: Jetzt wo wir alle Schnittstellen definiert haben, auf denen wir unabhaengig von der Persistenzschicht arbeiten moechten, faellt auf: Wir haben einen Schicht gebaut, die uns zugleich als Service dienst, daher auch Service Layer genannt.

Komponentendiagramm
Data Mapper Pattern
Top Down: Vom Model zu den Daten, oder: Von Modelspezifikation zur Anbindung an die Persistenzschicht. Es ist nun moeglich auf Repraesentationen von unseren Business Logic Objects zu arbeiten. Allerdings kommen die Daten bisher anscheinend aus dem Nirvana. Die Idee ist nun, einen Mapper zu konstruieren, der die Daten auf das Model projizieren kann und der das Model auf die Daten projizieren kann. Wenn wir nun unser echtes Objekt, mit der Datenentsprechung und einem Mapper als Strukturmuster definieren, dann erhalten wir das Data Mapper Pattern, das dank Matthew Weier O’Phinney nun auch im Quick Start Tutorial von Zend verwendet wird (er hat da laut dieser Quelle Diskussionen mit dem Autor des Tutorials gefuehrt).
Wenn ihr mehr Informationen zum Data Mapper Pattern haben moechtet, schaut auch mal das Webinar Play-Doh: Modelling Your Objects von Matthew Weier O’Phinney an oder lest euch die Slides dazu durch.
Ich bau mal an dem Beispiel weiter und hoffentlich wird dann auch klar, wie der Mapper funktioniert:
class Default_Model_DbTable_User extends Zend_Db_Table_Abstract
{
protected $_name = 'users';
}
class Default_Model_Mapper_User
{
protected $_dbTable;
public function setDbTable($dbTable)
{
if (is_string($dbTable)) {
$dbTable = new $dbTable();
}
if (!$dbTable instanceof Zend_Db_Table_Abstract) {
throw new Exception('Invalid table data gateway provided');
}
$this->_dbTable = $dbTable;
return $this;
}
public function getDbTable()
{
if (null === $this->_dbTable) {
$this->setDbTable('Default_Model_DbTable_User');
}
return $this->_dbTable;
}
public function save(Default_Model_User $user)
{
$data = array(
'name' => $user->getName()
);
if (null === ($id = $user->getId())) {
$this->getDbTable()->insert($data);
} else {
$this->getDbTable()->update($data, array('id = ?' => $id));
}
}
public function delete($where)
{
return $this->getDbTable()->delete($where);
}
public function find($id, $user)
{
$result = $this->getDbTable()->find($id);
if (0 == count($result)) {
return;
}
$row = $result->current();
$user->setId($row->id)
->setName($row->name);
}
public function fetchAll()
{
$resultSet = $this->getDbTable()->fetchAll();
$entries = array();
foreach ($resultSet as $row) {
$entry = new Default_Model_User();
$entry->setId($row->id)
->setName($row->name)
->setMapper($this);
$entries[] = $entry;
}
return $entries;
}
public function getAdapter()
{
return $this->getDbTable()->getAdapter();
}
}
Und am User muessen wir auch noch etwas aendern, sodass es wie folgt aussieht:
class Default_Model_User
{
protected $_id;
protected $_name;
protected $_mapper;
public function __construct(array $options = null)
{
if (is_array($options)) {
$this->setOptions($options);
}
}
public function __set($name, $value)
{
$method = 'set' . $name;
if (('mapper' == $name) || !method_exists($this, $method)) {
throw new Exception('Invalid model property');
}
$this->$method($value);
}
public function __get($name)
{
$method = 'get' . $name;
if (('mapper' == $name) || !method_exists($this, $method)) {
throw new Exception('Invalid model 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 setMapper($mapper)
{
if (is_string($mapper)) {
$mapper = new $mapper();
}
if (!$mapper instanceof Default_Model_Mapper_Abstract) {
throw new Exception('Invalid data mapper provided');
}
$this->_mapper = $mapper;
return $this;
}
public function getMapper()
{
if (null === $this->_mapper) {
$this->setMapper($this->_mapperClass);
}
return $this->_mapper;
}
public function getAdapter()
{
return $this->getMapper()->getAdapter();
}
public function save()
{
return $this->getMapper()->save($this);
}
public function delete($where)
{
return $this->getMapper()->delete($where);
}
public function find($id)
{
$this->getMapper()->find($id, $this);
return $this;
}
public function fetchAll()
{
return $this->getMapper()->fetchAll();
}
public function getId()
{
return $this->_id;
}
public function setId($id)
{
$this->_id = (int) $id;
return $this;
}
public function getName()
{
return $this->_name;
}
public function setName($name)
{
$this->_name = (string) $name;
return $this;
}
}
Endlich! Wir haben das Data Mapper Pattern komplett. Hier mal ein Usage Example:
/* somewhere in your controller */
$user = new Default_Model_User();
$users = $user->fetchAll();
foreach ($users as $u) {
if ($u->getName() == 'unknown') {
$u->setName('unbekannter Nutzername')
->save();
}
}
Pretty easy, huh? Aber stopp! Im Titel steht doch noch irgendwas von Abstract. Was soll denn der Quark? …
Reusability: Abstract Data Mapper Pattern
Wenn wir das Data Mapper Pattern jetzt mehrfach verwenden, stellen wir sehr sehr schnell fest, dass wir unglaublich viel Code doppelt und dreifach haben. Jetzt kommt mein Ansatz: Wir packen alles bekannte in abstrakte Oberklassen und erben davon, setzen die benoetigten Meta-Informationen fuer das Pattern und sind fertig. Ohne grosse Umschweife (just kidding, das war ja jetzt wohl genug Umschweife):
class Default_Model_DbTable_User extends Zend_Db_Table_Abstract
{
protected $_name = 'users';
}
abstract class Default_Model_Abstract
{
protected $_mapper;
protected $_mapperClass;
public function __construct(array $options = null)
{
if (is_array($options)) {
$this->setOptions($options);
}
}
public function __set($name, $value)
{
$method = 'set' . $name;
if (('mapper' == $name) || !method_exists($this, $method)) {
throw new Exception('Invalid model property');
}
$this->$method($value);
}
public function __get($name)
{
$method = 'get' . $name;
if (('mapper' == $name) || !method_exists($this, $method)) {
throw new Exception('Invalid model property');
}
return $this->$method();
}
public function __call($method, array $arguments)
{
if (!method_exists($this->_mapper, $method)) {
throw new Exception('Inaccessible method called');
}
return call_user_func_array(array($this->_mapper, $method), $arguments);
}
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 setMapper($mapper)
{
if (is_string($mapper)) {
$mapper = new $mapper();
}
if (!$mapper instanceof Default_Model_Mapper_Abstract) {
throw new Exception('Invalid data mapper provided');
}
$this->_mapper = $mapper;
return $this;
}
public function getMapper()
{
if (null === $this->_mapper) {
$this->setMapper($this->_mapperClass);
}
return $this->_mapper;
}
public function getAdapter()
{
return $this->getMapper()->getAdapter();
}
public function save()
{
return $this->getMapper()->save($this);
}
public function delete($where)
{
return $this->getMapper()->delete($where);
}
public function find($id)
{
$this->getMapper()->find($id, $this);
return $this;
}
public function fetchAll()
{
return $this->getMapper()->fetchAll();
}
}
class Default_Model_User extends Default_Model_Abstract
{
protected $_mapperClass = 'Default_Model_Mapper_User';
protected $_id;
protected $_name;
public function getId()
{
return $this->_id;
}
public function setId($id)
{
$this->_id = (int) $id;
return $this;
}
public function getName()
{
return $this->_name;
}
public function setName($name)
{
$this->_name = (string) $name;
return $this;
}
}
abstract class Default_Model_Mapper_Abstract
{
protected $_dbTable;
protected $_dbTableClass;
public function setDbTable($dbTable)
{
if (is_string($dbTable)) {
$dbTable = new $dbTable();
}
if (!$dbTable instanceof Zend_Db_Table_Abstract) {
throw new Exception('Invalid table data gateway provided');
}
$this->_dbTable = $dbTable;
return $this;
}
public function getDbTable()
{
if (null === $this->_dbTable) {
$this->setDbTable($this->_dbTableClass);
}
return $this->_dbTable;
}
public function getAdapter()
{
return $this->getDbTable()->getAdapter();
}
abstract public function save($entry);
abstract public function delete($where);
abstract function find($id, $entry);
abstract public function fetchAll();
}
class Default_Model_Mapper_User extends Default_Model_Mapper_Abstract
{
protected $_dbTableClass = 'Default_Model_DbTable_User';
public function save($user)
{
$data = array(
'name' => $user->getUsername()
);
if (null === ($id = $user->getId())) {
return $this->getDbTable()->insert($data);
} else {
$this->getDbTable()->update($data, array('id = ?' => $id));
return $id;
}
}
public function delete($where)
{
return $this->getDbTable()->delete($where);
}
function find($id, $user)
{
$result = $this->getDbTable()->find($id);
if (0 == count($result)) {
return false;
}
$row = $result->current();
$user->setId($row->id)
->setName($row->name);
return true;
}
public function fetchAll()
{
$resultSet = $this->getDbTable()->fetchAll();
$entries = array();
foreach ($resultSet as $row) {
$entry = new Default_Model_User();
$entry->setId($row->id)
->setName($row->name)
->setMapper($this);
$entries[] = $entry;
}
return $entries;
}
}
Das Pattern nenn ich jetzt einfach mal wie oben genannt und schenke es der Welt.
Update
Ich habe mal ein Demo-Projekt fuer Zend Studio zusammengebaut. In etwa so sollte es aussehen (Modulstruktur und kleine Anpassungen inklusive): AbstractDataMapperPatternDemo-1.2.zip.