Doctrine 2 SQL Profiler in Debugleiste

21. Dezember 2011
by Daniel Leeb

Auch wenn es sich um ein sehr spezielles Thema handelt, möchte ich hier kurz zusammenfassen, wie man einen Doctrine 2 SQL Logger/Profiler in einer Zend Framework Umgebung umsetzen kann. Da die Kombination aus ZF und Doctrine 2 durchaus beliebt zu sein scheint, zumindest wenn man von Blogeinträgen und Tutorials im Internet ausgeht, könnte dieser Beitrag für manch einen hilfreich sein. Vorweg möchte ich auch noch auf eine praktische Lösung hinweisen, um Doctrine 2.x mit dem Zend Framework 1.x zu verbinden: Und zwar Bisna von Guilherme Blanco, zu finden in seinem GitHub Repository. Danach lässt sich Doctrine mittels Zend_Config (ini, xml, etc.) konfigurieren.

SQL Queries in der ZFDebug-Leiste

Nach der Integration von Doctrine in unsere neue Webapplikation basierend auf dem Zend Framework fehlte uns noch eine einfache Möglichkeit SQL Queries anzuzeigen. Da wir bereits ZFDebug verwenden, lag ein zusätzliches Plugin für die Ausgabe in der Debugleiste für uns nahe. ZFDebug wurde leider seit Mitte 2009 nicht mehr aktualisiert, funktioniert jedoch immer noch sehr gut, liefert hilfreiche Informationen rund um die Webapplikation und lässt sich sehr einfach erweitern. Daher werde ich hier zeigen, wie man den Doctrine 2 Profiler in einem ZFDebug Plugin verwenden kann.

Profiler

Doctrine bietet mit Doctrine\DBAL\Logging\SQLLogger bereits ein Interface um einen Logger mit einer Datenbank-Verbindung zu verknüpfen. Wir implementieren dieses Interface mit den beiden Methoden startQuery() und stopQuery() und speichern einfach alle Queries in ein Array das wir später in der Debugleiste ausgeben werden.

DoctrineExtensions/Profiler/ZFDebugProfiler.php:
[cc escaped="true" lang="php"]
namespace DoctrineExtensions\Profiler;

class ZFDebugProfiler implements \Doctrine\DBAL\Logging\SQLLogger
{
public $totalTime = 0.0;
public $queries = array();

protected $_curQuery = null;

public function startQuery($sql, array $params = null, array $types = null)
{
$this->_curQuery = new \stdClass();
$this->_curQuery->sql = $sql;
$this->_curQuery->params = $params;
$this->_curQuery->time = \microtime(true);
}

public function stopQuery()
{
$executionTime = \microtime(true) – $this->_curQuery->time;
$this->totalTime += $executionTime;
$this->queries[] = array(
‘time’ => $executionTime,
‘sql’ => $this->_curQuery->sql,
‘params’ => $this->_curQuery->params
);
}
}
[/cc]

Nun müssen wir Doctrine noch mitteilen, dass unser Profiler verwendet werden soll. Wird Bisna eingesetzt, kann dies ganz einfach in der Konfiguration (hier beispielsweise in einer ini-Datei) erledigt werden:
[cc escaped="true" lang="php"]
resources.doctrine.dbal.connections.default.sqlLoggerClass = “DoctrineExtensions\Profiler\ZFDebugProfiler”
[/cc]

Im Hintergrund wird der Konfiguration der DBAL-Verbindung eine neue Instanz unseres SQL Loggers übergeben:
[cc escaped="true" lang="php"]
$configuration = new Doctrine\DBAL\Configuration();
$configuration->setSQLLogger(new DoctrineExtensions\Profiler\ZFDebugProfiler());
$connection = Doctrine\DBAL\DriverManager::getConnection($connectionParams, $configuration);
[/cc]

Natürlich lässt sich statt des ZFDebugProfilers auch einfach ein LogProfiler oder FirebugProfiler implementieren, welche die Queries direkt in eine Datei schreiben bzw. im Firebug ausgeben. Wir verwenden unseren Profiler hingegen, um die SQL Queries in einem ZFDebug Plugin aufzulisten.

ZFDebug Plugin

Nachdem der SQL Logger an Doctrine übergeben wurde, werden alle SQL Queries, deren Parameter und die Dauer der einzelnen Queries gespeichert. Ausgeben wollen wir das Ganze in unserer ZFDebug Leiste, wir erstellen also zunächst ein Plugin, indem wir das Interface ZFDebug_Controller_Plugin_Debug_Plugin_Interface implementieren:

ZFDebug/Controller/Plugin/Debug/Plugin/Doctrine.php:
[cc escaped="true" lang="php"]
class ZFDebug_Controller_Plugin_Debug_Plugin_Doctrine implements ZFDebug_Controller_Plugin_Debug_Plugin_Interface
{
protected $_identifier = ‘doctrine’;
protected $_profilers = array();

public function __construct(array $options = array())
{
if (isset($options['profiler'])) {
$this->_profilers = $options['profiler'];
}
if (!is_array($this->_profilers)) {
$this->_profilers = array($this->_profilers);
}
}

public function getIdentifier()
{
return $this->_identifier;
}

public function getTab()
{
if (!count($this->_profilers)) {
return ‘No Profiler’;
}
foreach ($this->_profilers as $profiler) {
$profilerInfo[] = count($profiler->queries) . ‘ in ‘ . round($profiler->totalTime * 1000, 2) . ‘ ms’;
}
return implode(‘ / ‘, $profilerInfo);
}

public function getPanel()
{
$html = ”;
if (!count($this->_profilers)) {
return $html . ‘No Profiler’;
}
foreach ($this->_profilers as $index => $profiler) {
$html .= ‘

Profiler ['.$index.']

    ‘;
    foreach ($profiler->queries as $query) {
    $html .= ‘
  1. [' . round($query['time'] * 1000, 2) . ‘ ms] ‘;
    $html .= $this->highlightSql(htmlspecialchars($query['sql']));

    if (!empty($query['params'])) {
    $html .= ‘
    bindings:‘ . $this->listBindings($query['params']);
    }
    $html .= ‘

  2. ‘;
    }
    $html .= ‘

‘;
}
return $html;
}

private function listBindings($bindings)
{
$html = ‘

    ‘;
    foreach ($bindings as $binding) {
    $html .= ‘
  1. ‘;
    if (is_array($binding)) {
    $html .= $this->listBindings($binding);
    } else {
    $html .= $binding;
    }
    $html .= ‘
  2. ‘;
    }
    return $html . ‘

‘;
}

private function highlightSql($sql)
{
$statements = array(‘SELECT’, ‘UPDATE’, ‘INSERT’, ‘FROM’, ‘WHERE’,
‘LEFT JOIN’, ‘JOIN’, ‘ORDER BY’, ‘GROUP BY’,
‘OFFSET’, ‘LIMIT’, ‘SET’, ‘VALUES’);
return preg_replace(‘/(‘.implode(‘|’, $statements).’)/’, ‘\1‘, $sql);
}
}
[/cc]

Da wir auch ein wenig Wert auf die Formatierung legen, ist der Code des Plugins etwas umfangreicher ausgefallen. Wichtig sind die getTab() und getPanel() Methoden, welche die HTML Ausgabe für den Titel des Plugins in der Debugleiste und für detaillierte Informationen im Panel beim Ausklappen des Plugins zurückgeben.

Nun muss das Plugin noch in den Optionen von ZFDebug eingetragen werden. Beim Bootstrapping der Applikation erstellen wir das ZFDebug_Controller_Plugin_Debug und registrieren es beim Frontcontroller. Zunächst gehen wir jedoch sicher, dass die Doctrine Ressource bereits existiert und erstellen eine Liste der Profiler aller DBAL-Verbindungen, die wir anschließend unserem Plugin übergeben. In den ZFDebug Optionen tragen wir unter Plugins unsere Klasse ZFDebug_Controller_Plugin_Debug_Plugin_Doctrine mit dessen Konfiguration ein:

[cc escaped="true" lang="php"]
$this->bootstrap(‘doctrine’);
$container = $this->getResource(‘doctrine’);
$profilers = array();
foreach ($container->getConnectionNames() as $connName) {
$profiler = $container->getConnection($connName)->getConfiguration()->getSQLLogger();
if ($profiler instanceof DoctrineExtensions\Profiler\ZFDebugProfiler) {
$profilers[$connName] = $profiler;
}
}
$options = array(
‘plugins’ => array(
‘ZFDebug_Controller_Plugin_Debug_Plugin_Doctrine’ => array(
‘profiler’ => $profilers
)
)
);
$debug = new ZFDebug_Controller_Plugin_Debug($options);
$this->bootstrap(‘frontController’);
$this->getResource(‘frontController’)->registerPlugin($debug);
[/cc]

Nun lassen sich alle Queries von Doctrine und deren Performance übersichtlich in ZFDebug anzeigen. Zu beachten ist vielleicht, falls man Bisna verwendet, dass durch den Aufruf von $container->getConnection($name)… die DBAL-Verbindungen tatsächlich gestartet werden. Ansonsten würden sie nicht unbedingt bereits beim Bootstrapping, sondern erst bei der ersten Verwendung gestartet. Da ZFDebug aber sowieso nur im Debug-Modus laufen sollte, muss diesbezüglich nur auf den Unterschied zwischen Debug- und Production-Modus geachtet werden.

Verwendete Versionen:
- Zend Framework 1.11.11
- Doctrine 2.1.5
- ZFDebug 1.5

Zend Framework 2

Da ZF2 wohl noch einige Monate auf sich warten lässt, kann man im Moment leider noch keine vollwertige Webapplikation darauf aufbauen. Spätestens zum Release wird aber eine Umstellung fällig werden, sollte die zweite Version das einhalten was sie momentan verspricht. Es wäre daher interessant zu wissen, ob ZFDebug auch für ZF2 erscheint, was wenn man sich die Projektaktivität ansieht eher zu bezweifeln ist – oder es vielversprechende Alternativen dazu gibt?

About

Daniel Leeb ist Mitarbeiter bei der Lorem Ipsum Mediengesellschaft m.b.H. und zuständig für die Neu- und Weiterentwicklung von Webanwendungen. Folgen Sie ihm via RSS.