3. Zend Tutorial

Im ersten und zweiten Tutorial haben wir die Grundlagen geschaffen, nun will ich mit der Planung bzw. Architektur zu der Beispiel-Anwendung beginnen. Ich habe mir eine Turnier-Verwaltung ausgedacht, da man sich darin gleich reindenken kann und die Anforderungen zu Beginn nicht zu hoch sind. Also was brauchen wir auf Datenbank-Ebene? Eine Tabelle für die Mannschaften und eine für die Turniere. Außerdem möchte ich die Mannschaften einer Gruppe zuordnen können (Gruppierung). Somit sieht das erste Datenbank-Diagramm folgendermaßen aus:

Erstes Datenbankschema der Beispielanwendung

Erstes Datenbankschema der Beispielanwendung

Zur Tabelle Competition: Auch ein Turnier hat einen Namen und ein geplanten Startermin. Für das Turnier wird auch festgelegt, wie lange ein Spiel gehen soll (single_duration) und wieviele Sätze pro Spiel gemacht werden (number_of_sets). Das wären z. B. beim Volleyball die einzelnen Sätze eines Spiels.

Zur Tabelle Team: Ein Team besteht aus einem Namen und muß am Tag des Turniers auch da sein (available). Außerdem muß vorher eine Gebühr bezahlt werden (fee_paid). Diese zwei Spalten speichern lediglich Bool-Werte ab.

Zur Tabelle Group: Eine Gruppe hat lediglich einen Namen. Dadurch kann z. B. gekennzeichnet werden, dass ein Verein mehrere Mannschaften stellen kann. Dies kann dann später im Spielplan berücksichtigt werden.

Der Zend-Server kann gleich mit einer mySql-Datenbank installiert werden. Deswegen werde ich diese Datenbank zugrunde legen.

Datenbankverbindung

Eine Datenbankverbindung kann über das zf-Tool eingerichtet werden. Ich habe das schon in dem Artikel Datenbank unter Zend ausführlich beschrieben.

ORM

ORM steht für Object-Relational Mapping und ist nichts anderes, als dass rationale Datenbanken auf Objekte abgebildet werden. Im offiziellen Zend-Tutorial werden dazu Model-, Mapper-, und Table-Klassen verwendet. Nachfolgende Skizze soll das verdeutlichen:

Zend Datenbank Abbildungen

Prinzip des Zend Datenbank ORM

Im Pseudocode der Funktion fetchAll sehen wir, dass im Mapper eine Instanz der Datenbank-Abstraktion geholt wird (z. B. eine MySql-Adapter-Klasse) über die Daten von der Datenbank geholt wird. Anschließend werden die Daten auf das Model abgebildet und zurückgegeben.

zf-Tool

Den Adapter konfigurieren und die ersten Klassen anlegen können wir einfach mit dem zf-Tool erledigen:

zf configure dbadapter "adapter=Pdo_Mysql&username=test&
                        password=test&dbname=zend_volleyball"
zf create db-table Team team
zf create db-table Competition competition
zf create db-table Group group
zf create model Team
zf create model Competition
zf create model Group

 

Lokale Datenbank mit JavaScript – LocalStorage

Mit jStorage steht eine Javascript-Bibliothek, die den lokalen Speichermechanismus von HTML5 abstrahiert. Es werden sogar ältere Browser unterstützt, jedoch muß dann mit einer Speicherplatzbeschänkung gerechnet werden.

Browserunterstützung von jStorage

Browserunterstützung von jStorage - Quelle: http://www.jstorage.info

Es gibt einige Vorteile die für diese Technologie sprechen:

  • Cookies können nur eine sehr begrenzte Datenmenge aufnehmen (max. 80 kB).
  • Gespeicherten Daten werden nicht wie bei den Cookies bei einem Request mitgeschickt.
  • Lokal gespeicherte Daten laufen nicht ab (sofern kein Gültigkeitszeitraum definiert wurde).
  • Cookies können (leicht) deaktiviert werden und haben zum Teil einen schlechten Ruf.

Funktionsumfang von jStorage

Die wichtigsten Funktionen sind:

$.jStorage.set(key, value)

Mit der Set-Funktion können Schlüssel-Werte-Paare gespeichert werden. Der Schlüssel muß ein String sein. Der Wert kann außer einem skalaren Wert auch ein JSON-Objekt, ein Array oder ein XML-Node sein.

$.jStorage.get(key)

Mit der Get-Funktion werden die Werte abgerufen.

$.jStorage.storageAvailable()

Mit dieser Funktion wird abgefragt, ob die Funktion im Browser überhaupt zur Verfügung steht.

Ich habe mir diesmal die Mühe gemacht eine kleine Demo zu erstellen:

Demo im neuen Fenster öffnen.

Demo

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.

 

Zugriff auf mehrere Datenbanken unter Zend

Wenn man innerhalb einer Zend Anwendung auf verschiedenen Datenbanken (mysql, mssql, etc.) zugreifen möchte, kann die Verbindungen in der application.ini hinterlegen:

resources.db.adapter = "pdo_mysql"
resources.db.params.host = "localhost"
resources.db.params.username = "xxx"
resources.db.params.password = "yyy"
resources.db.params.dbname = "zzz"
resources.db.isDefaultTableAdapter = true
 
resources.multidb.db1.adapter = "Sqlsrv"
resources.multidb.db1.host = "server-1"
resources.multidb.db1.dbname = "xxx"
resources.multidb.db1.username = "yyy"
resources.multidb.db1.password = "zzz"
resources.multidb.db1.charset = utf8
...

Anschließend kann die Instanz des Adapters geholt werden:

$front=Zend_Controller_Front::getInstance();
$bootstrap=$front->getParam('bootstrap');
$resource=$bootstrap->getPluginResource('multidb');
/**
 * @var Zend_Db_Adapter_Sqlsrv
 */
$db=$resource->getDb('db1');

Super, oder?

Interoperabilität zwischen Typo3 und MSSQL

Wer sich schon einmal den Wiki-Eintrag zu MSSQL durchgelesen hat, wird schnell merken, dass Typo3 an der einen oder anderen Stelle nicht wirklich rund mit der Microsoft Datenbank zusammenarbeiten wird. Ob diese Info aus dem Wiki noch aktuell ist, steht auf einem anderen Blatt… dort aufgeführte Bugmeldungen wurden jedoch bereits behoben. Ich habe jedoch einige Blog-Einträge (siehe Links unten),  gefunden, die von “großem” Aufwand sprechen, ein Typo3 unter MSSQL zu installieren. Die Unterstützung scheint jedoch besser zu werden. Zumindest stellt eine PHP Installation unter Windows kein Problem mehr da. Wer den Typo-Source gern wie unter Linux mittels symbolischen Link in sein Projekt einbinden will, kann das auch unter Windows mit einem kleinen Tool Junction von Microsoft ermöglichen.

Für die Bildgenerierung muß ImageMagick und Ghostscript installiert werden. Auch dies ist für Windows verfügbar.

Das “Rewrite” von URLs erfordert Zusatzaufwand ist jedoch auch in einem Typo3-Wiki-Eintrag beschrieben.

Das größtes Problem, das ich sehe, sind jedoch die Erweiterungen, die nicht unbedingt mit MSSQL kompatibel sein müssen. Diese sind dann entsprechend zu testen.

Wer seine Webseite unter Typo3 betreiben will und Daten von einem Microsoft SQL Server benötigt, könnte auch das Typo3 auf MySQL betreiben. MySQL läuft problemlos auf einem Windows-Server und ist (mitterweile) auch dort nicht wesentlich langsamer als unter Linux. Die Daten des MSSQL-Servers können in einer eigenen (Extbase-)Extension verarbeitet und dargestellt werden. Ich denke da z. B. an ein Produktmodul, dass die Produkteinformationen aus dem ERP-System erhält. Leider habe ich die Vermutung, dass Extbase keine komfortables Möglichkeit für den Zugriff auf MSSQL bietet, zumindest habe ich dazu nichts gefunden. Mag sein, dass das mit Typo5 mit Flow besser wird, da dort wohl Doctrine als Abstraktionsschicht verwendet wird, aber bis dahin wird noch etwas Zeit vergehen.

Aber was spricht dagegen den Zugriff auf die Datenbank mit etwas anderem durchzuführen? Nix (naja, ein paar Argumente gibts sicherlich). Und wie? Beispielsweise mit dem Zend Framework. Ich habe dazu eine Extension geschrieben, die den Autoloader des ZF einbindet (Extension an erster Stelle laden “top”). In jeder anderen Extension kann mittels passendem Zend Adapter eine Verbindung aufgebaut werden. Natürlich gibt es auch damit das eine oder andere Problem z. B. funktioniert der für den Microsoft SQL Server benötigte Adapter nur mit der PHP Erweiterung sqlsrv und wird erst aber Microsoft SQL Server 2005 oder höher unterstützt (darunter funktionierte bei mir die limit(Anzahl, Offset) Funktion nicht korrekt… ich hab den Adapter abgeleitet und diese Funktion überschrieben). Ansonst kann man nicht das ORM von Extbase verwenden, aber zumindest kann man sich daran “orientieren”, indem man die Zugriffe auf MSSQL-Daten in einer Repository-Klasse kapselt.

Fazit:

Wenn man ein Typo3-System auf MSSQL betreiben will, kann dies sicherlich mit etwas Aufwand hinbekommen. Wenn man jedoch die Möglichkeit hat MySQL unter Windows zu betreiben, würde ich das auch tun. Meist liegen die Daten im Business-Umfeld auf Windows-Servern und stammen dann wohl von anderen Anwendungen, die sowieso durch eine spezielle Implementierung aufbereitet werden müssen.

Links:

Zend Framework und Liquibase

Wer ein größeres Projekt inklusive verschiedenen Lebenszyklen realisieren will, der muß sich auch um das Datenbankmanagement kümmern. Das Database Change Management Tool Liquibase bietet sich dafür an und ergänzt die fehlende Funktion des Zend-Frameworks. Ich möchte hier beschreiben, wie dies in ein Projekt integriert werden kann. Ich möchte hierbei auf den bereits veröffentlichen Artikel “Das Zend Framework – Ein Startversuch” aufbauen. Desweiteren habe ich eine grobe Beschreibung der Funktionalität von Liquibase in meinem Artikel “Datenbankmigration mit Liquibase” vorgenommen. Continue reading

Datenbankmigration mit Liquibase

Wer kennt folgendes Problem nicht: Man entwickelt ein System, das eine Datenbank nutzt und entwickelt zwar wunderbar unter Versionskontrolle, jedoch läuft die Datenbankentwicklung “nebenher”. Das Datenbankschema wird direkt z. B. unter phpmyadmin entwickelt. Das funktioniert bei einem Entwickler möglicherweise noch ganz wunderbar, sobald aber mehrere daran arbeiten, verliert man schnell den Überblick. Für die Implementierung einer eigenen Lösung fehlt natürlich die Zeit und das verwendete Framework (z. B. Zend) hat natürlich von Haus aus auch keine Lösung parat. Mit dem Datenbankmigrationstool Liquibase kann man solchen Problemen aus dem Weg gehen. Continue reading

Contao und InnoDB

Irgendwo im Internet habe ich gelesen, dass das Installationstool nicht mit InnoDB funktioniere. Jedoch hat das Anlegen einer neuen Tabelle mit der Contao Version 2.9.4 ohne Probleme funktioniert. Was nicht ging, ist der Wechsel der Engine einer bestehenden Tabelle von MyISAM zu InnoDB. Dies ist aber auch besser so und sollte immer manuell durchgeführt werden.

Fakten über MyISAM

- Ist die MySQL Standard-Speicher-Engine
- Jede MyISAM-Tabelle wird in drei Dateien auf der Festplatte gespeichert (.frm, .MYD, .MYI).
- Alle Daten werden mit dem niederwertigen Byte zuerst gespeichert. Somit können die Datendateien zwischen verschiedenen Plattformen kopiert werden. Für Embedded-Systeme gilt das nicht immer.
- Numerischen Schlüsselwerte werden mit dem höchstwertigen Byte zuerst gespeichert (bessere Indexkompression).
- Große Dateien werden unterstützt, sofern es keine Betriebssystembedingte Einschränkung gibt, sind das bis zu 256TB
- Keine Transaktionen
- Standardmäßig kann eine MyISAM-Tabelle maximal 64 Indizes haben. Jeder Indize kann bis zu 16 Spalten besitzen.
- Die Höchstlänge für Schlüssel beträgt standardmäßig 1000 Bytes
- Standardmäßig maximal 2^32 (~4.295E+09) Zeilen pro Tabelle.
- Eine AUTO_INCREMENT Spalte pro Tabelle
- Indizierung von BLOB und TEXT-Spalten (Full-text search index)
- Jede Zeichenspalte kann einen anderen Zeichensatz haben
- Die Summe der Längen der VARCHAR- und CHAR-Spalten in einer Tabelle kann bis zu 64KB betragen.
- Kein Clustering
- Locking ist auf Tabellenebene möglich
- Daten- und Indexdateien können in unterschiedliche Verzeichnisse/Festplatten gelegt werden, um mehr Geschwindigkeit zu erzielen.

Quelle: MySQL 5.1 Reference Manual :: 13 Storage Engines :: 13.5 The MyISAM Storage Engine

Eine Konvertierung zu InnoDB ist unter bestimmten Voraussetzungen möglich.