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.

 

Contao 2.11.1 ist da

Nach relativ kurzer Zeit wurde am 09.03. ein Bugfix-Release veröffentlicht, das die Stabilität des 2.11-Zweigs verbessert. Die Nachrichtenliste und andere dynamische Seiten mit Seitenmenü wurden bisher nicht gecacht oder für die Suche indiziert und wurde nun mit dem neuen Release nachgebessert. Einige kleine Bugs wie z. B. falsch konvertiere absolute URLs durch den CSS-Combiner oder Fehler in der Handhabung des neuen URL-Formats (domain.com/en/page.html) wurden behoben. Eine Liste aller Änderungen findet sich wie immer im Contao-Changelog.

PHP Optimierungen

Auf der Seite PHP Benchmark gibt es eine kleine Auflistung von PHP Optimierungen, die ganz gelungen sind. Natürlich ist die Optimierung von Code bis auf die letzte Zeile übertrieben, aber es schadet nicht ein paar Regeln zu beachten.

Referenzierungsoperators &

Im ersten Test wird der Einsatz des Referenzierungsoperators & geprüft. Als Ergebnis kommt heraus, dass bei eindimensionalen Arrays der Zugriff schneller erfolgt als bei mehrdimensionalen Arrays.

$value = &$simpleArray[$i]; // Laufzeit schneller
$value = $simpleArray[$i]; // als diese Variante

Im Gegenzug zu mehrdimensionalen Arrays:

$value = &$bigArray[$a][0][0][0] // Laufzeit langsamer
$value = $bigArray[$a][0][0][0] // als diese Variante

IF und Switch

Zwischen den Varianten gibt es keinen nennenswerten Laufzeitunterschied. Lediglich bei den Vergleichen mit Typisierung (===) gab es einen kleinen Vorteil.

Einfache vs. doppelte Anführungszeichen

Bei reine Strings macht es schienbar keinen Unterschied, ob einfache oder doppelte Anführungszeichen verwendet werden. Leider stimmt das nur, solange keine Variablen innerhalb des Strings ersetzt werden müssen. Ich habe das mal ausprobiert:

"aa $x aaaa $x aaaa $x a"; // 0.000581
'aa $x aaaa $x aaaa $x a'; // 0.000160
'aa '. $x. ' aaaa '. $x. ' aaaa '. $x. ' a'; // 0.000565
echo 'aa ', $x, ' aaaa ', $x, ' aaaa ', $x, ' a'; // 0.000378

Funktionen ISSET und EMPTY UND IN_ARRAY

Auch hier gab es keinen nenneswerten Sieger. Die Funktion in_array ist teurer als die anderen zwei und sollte nur auf existierenden Variablen aufgerufen werden.

is_array($gibtsNicht); // langsam (und PHP Notice: Undefined variable)
isset($gibsNicht) && is_array($gibtsNicht) // besser

 Schleifen

Ein Funktionsaufruf im Schleifenkopf ist keine gute Idee. Die vielen Aufrufe kosten sehr viel Laufzeit.

<code>for ($i=0; $i<count($x); $i++);</code> // ganz schlecht
$count=count($x);
for ($i=0; $i<$count; $i++); // super

Außerdem kostet der Befehl list ebenfalls sehr viel Laufzeit. Meiner Meinung nach sollte man diese Funktion nicht verwenden, da der Code dadurch schlecht lesbar wird:

while(list($key) = each($hash))... // langsam und schlecht lesbar
foreach($hash as $key=>$value)... // schnell

Gegen Ende des Beitrags wurde nochmal die Schleifen bzw. die Modifikation des Array betrachtet, über das iteriert wird. Das die for-Schleife in folgender Variante so schlecht abschneidet, hat mich doch sehr verwundert:

// 425%
foreach($aHash as $key=>$val)
  $aHash[$key] .= "a"; 
 
// 100%
$key = array_keys($aHash);
$size = sizeOf($key);
for ($i=0; $i<$size; $i++)
  $aHash[$key[$i]] .= "a";

Das schlechte Ergebnis wird aber durch die Operation .= verursacht, die den bestehenden Wert mit “a” konkateniert. Mit einer reinen Zuweisung läuft die foreach-Schleife sehr performant. Ich habe zusätzlich folgendes ausprobiert:

foreach($aHash as $key=>$val);  // 0.00001
foreach($aHash as $val);        // 0.000007
foreach($aHash as $key=>&$val); // 0.00023
foreach($aHash as &$val);       // 0.00024
foreach($aHash as $key=>$val)
  $aHash[$key] .= "a";          // 0.00024 (ursprüngliche Variante)
 
while(list($key) = each($aHash))
  $aHash[$key] .= "a";          // 0.00008 (ursprüngliche Variante)

Anmerkung: Das Array bestand ebenfalls aus 100 Elemente mit einer Schlüssellänge von 24byte und einem Wert mit 10k pro Eintrag.

Ich schätze, dass es damit zusammenhängt, dass die foreach-Schleife intern mit einer Kopie des Array arbeitet. Notiz an mich: Diesen Artikel mal genauer lesen: PHP internals: When does foreach copy?

Fazit

Ich glaube, dass die wenigsten Ergebnisse praxisrelevant sind. Bester Tip ist, dass man in Schleifenkopfen keine Funktionen aufrufen soll bzw. in Schleifen allgemein auf Funktionsaufrufe achten soll. Ein super Artikel fand ich auf ircmaxell’s blog im Artikel On Optimization in PHP. Code soll in erster Linie lesbar und somit besser warten sein. Außerdem wird dort die 90/10-Regel von R. Pattis erwähnt: “90% of a program’s execution time is spent in only 10% of its code.”. Die Kunst des Optimieres ist es also, die 10% des Codes in angemessener Zeit ausfindig zu machen…

 

 

 

Contao 2.11.0 verfügbar

Am 15.02.2012 wurde das Contao 2.11.0 Release veröffentlicht. Das Release ist das erste für die ein “Long-Term-Support” gilt, also mindestens 18 Monate gepflegt wird. Es wurde kräftig an Contao gearbeitet. Die wichtigsten Änderungen sind nachfolgend beschrieben:

  • Stylesheets können nun in die kombinierte CSS-Datei eingefügt werden:
    $GLOBAL['TL_CSS'][] = 'mystyle.css|screen|static';
  • Es gibt einige neue Hooks:
    • sqlCompileCommands
    • sqlGetFromFile
    • sqlGetFromDB
    • getCookie
    • getRootPageFromUrl
  • File-/Folder-Klasse: Die Klassen “File” und “Folder” haben die neuen Funktionen “chmod()” und “copyTo()” erhalten.
  • Die Anonymisierung von IP-Adressen kann im Backend kon­fi­gu­rie­rt werden.
  • Google-Webfonts können über ein neues Feld im Seitenlayout eingebunden werden, die dann als externes Stlyesheet geladen werden.
  • Erweiterte Optionen zur Bildverkleinerung
  • Bei den Backend-Passwörtern kann die Änderung des Passworts erzwungen werden.
  • Es wurden alle externen Plugins aktualisiert.
  • Die URLs der Seiten können nun auch in der Form domain.tld/de/ bzw. domain.tld/[Ländercode]/ dargestellt werden. Dazu gibts auch einen neuen Sprachenwechsler.
  • Globale Stylesheet-Variablen: In den Theme-Einstellungen können globale Variablen definiert werden, die in den CSS-Definitionen verwendet werdne können.
  • Verbessertes FAQ-Modul, insbesondere das Rechtesystem.
  • News-Archiv und News-Leser auf derselben Seite: Das ist eine super Sachen. Es müssen nun nicht zwei Seiten mit einem Archiv und einem Leser erstellt werden (man kann auch Überssicht und Detailseite dazu sagen), sondern es kann eine Seite für beides verwendet werden… klasse.
  • Das CSS-Framework kann nun auch im Backend in den Einstellungen deaktiviert werden.
  • Request-Token-System: Es gab immer wieder Probleme mit dem Request-System, wenn der Nutzer die Browser-Funktion “vor” bzw. “zurück” verwendet hat. Das Token ist nun für eine Session und nicht für ein Request gültig.
  • Einen Safemode für Contao: Third-Party-Er­wei­te­run­gen werden nicht geladen, wenn der Mode aktiv ist. Wird auch bei einer Contao-Aktualisierung verwendet, damit eventuell fehlerhafte Erweiterungen die Stabilität des Systems nicht beeinträchtigen.
  • Systemnachrichten: Es gibt ein paar neue Funktionen, um Systemmeldungen im Backend anzuzeigen.
  • Einen neuen Inserttag {{page}}

Ein Update von Contao 2.10 auf 2.11 funktionierte problemlos.

Einen Cron Manager unter Windows?

Heute war ich auf der Suche nach eine Lösung für einen Cron Manager unter Windows. Der ein oder andere kann jetzt mit dem Argument kommen, dass man ja mit den “geplanten Tasks”, “Aufgabenplanung”, “Task Scheduler” oder “Scheduled Tasks” (je nach Windows Version) gemacht werden kann – das ist natürlich richtig. Aber wie siehts mit einer Ausführungshistorie aus? – Ja, auch möglich, wenn man das aktiviert. Geht auch eine Email, falls der Job fehlschlägt? – Ja, auch das ist möglich, sofern der SMTP-Server korrekt konfiguriert ist. Jetzt die Preisfrage: Können auch andere Nutzer (Nicht-Administratoren) Jobs/Crons anlegen? …mhh… schwer? Oder wie wäre es mit einer Statusanzeige fürs Intranet? Ganz nützlich wäre auch das Ausführen von Cronjobs auf verschiedenen Servern, jedoch mit zentraler Administration. Wir sehen, an diesem Punkt stoßen wir mit den Windows Bordmitteln an Grenzen.

Continue reading

Robots Meta Tag und X-Robot Tags

Der Titel klingt ja schon kriptisch, aber mal von vorne… über die Robots-Anweisungen lassen sich die “guten” Suchmaschinen bzw. dessen Crawler beeinflussen. Beispielsweise kann man bestimmte Seiten von der Indizierung ausschließen indem man im Wurzelverzeichnis einfach eine robots.txt anlegt und dort ein paar Regeln reinschreibt.

Beispiel:

disallow: [path]

Eine Auflistung aller Möglichkeiten findet sich auf der Seite Controlling Crawling and Indexing.

Außerdem gibts das ganze auch als Meta-Tag-Anweisung:

<meta name="robots" content="noindex" />

Schließlich kann man das ganze auch als HTTP-Header-Anweisung definieren:

X-Robots-Tag: noindex

In PHP kann dies einfach realisiert werden:

header('X-Robots-Tag: noindex');

Da der Google Crawler immer besser wird und mittlerweise auch URLs in Javascript folgt, ist diese Anweisung ggf. bei einem AJAX-Aufruf sinnvoll.

Conditional Commands mit Typo3 und TemplaVoila

Leider funktionieren Conditional Commands mit TemplaVoila nicht, da das abschließende HTML-Kommentar nicht ausgegeben wird. Beispiel:

<!--[if lt IE 9]><script src="..."></script><![endif]-->

Bis das funktioniert, löse ich das über eine TypoScript Anweisung:

page.headerData.100 = HTML
page.headerData.100.value = <!--[if lt IE 9]><script src="..."></script><![endif]-->

PHP Curl mit Proxy

Hier ein Snippet, wie man sich an einem Proxy über CURL authentifizieren kann:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://www.foo.bar');
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 
curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_NTLM);
curl_setopt($ch, CURLOPT_PROXY, 'proxy:8080');    
curl_setopt($ch, CURLOPT_PROXYPORT, 8080);    
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'DOMÄNE\benutzer:password');    
 
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($ch, CURLOPT_REFERER, 'http://myreferer.de');
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla...');
 
$result = curl_exec($ch);
$info = curl_getinfo($ch);
$error = curl_error($ch);
 
curl_close($ch);