Frau mit Fernglas und Weihnachtsmütze

Doctrine DBAL ab TYPO3 Version 8 nutzen

Mit der TYPO3 Version 8 wurde mit Doctrine eine Datenbankabstraktionsschicht in das System integriert. Alle bisherigen Datenbankaufrufe im Core wurden entsprechend konventiert. Nun sind die Extension-Entwickler dran, die Technologie zu nutzen. Dabei sollte man sich nicht abschrecken lassen. Die neuen Funktionen sind bei unseren Symfony-Entwicklern bereits bekannt und leicht zu erlernen.

Unser erster Blog-Beitrag zur Weihnachtszeit ist ein kleines Geschenk für TYPO3-Entwickler. Kurz vor der Veröffentlichung von TYPO3 9.0 geben wir Ihnen einen Einblick in Doctrine mit allerhand Beispielen.

Was ist Doctrine?

Das Doctrine Projekt beinhaltet verschiedene PHP-Bibliotheken, deren primärer Fokus auf Datenbanken und Objekt-Mapping liegt. Zu den Kernprojekten gehören der Object Relational Mapper (ORM) und der Database Abstraction Layer (DBAL, deutsch: Datenbankabstraktionsschicht).

Einfach gesagt dient Doctrine dazu eine Ebene zwischen PHP und Datenbank zu schaffen. Der Einsatz dieser Ebene ermöglicht es mit ein und denselben PHP-Aufrufen unterschiedliche Datenbankmanagementsysteme (DBMS) anzusprechen. Zu diesen Systemen gehören derzeit u.a. MySQL, Oracle, PostgreSQL und Microsoft SQL Server. Dadurch ist die PHP-Anwendung und deren Entwickler weitestgehend unabhängig von dem System der Datenbank.

Weitere Vorteile von Doctrine sind laut der eigenen Webseite:

  • entwickelt seit 2006, mit einer sehr stabilen und hoch qualitativen Codebasis
  • sehr flexibles und mächtiges Objekt-Mapping und Abfrage-Funktionen
  • große Community und Integration in viele verschiedene Frameworks,  u.a. Symfony und Zend Framework

Doch wie wurde Doctrine in TYPO3 integriert? Das beleuchten wir im nächsten Abschnitt.

Doctrine und TYPO3

Eine Datenbankabstraktionsschicht ist in TYPO3 kein neues Anliegen. Bisher gab es die Erweiterungen dbal und adodb, welche Teil des TYPO3-Kerns waren. Allerdings mussten sie erst bei bedarf aktiviert werden. Standardmäßig wurde TYPO3 mit MySQL betrieben. Durch die Erweiterungen war es möglich weitere DBMS zu nutzen. Nun soll dafür Doctrine zum Einsatz kommen. Deswegen wurden die dbal und adodb aus dem Kern entfernt. Wer sie dennoch benötigt, kann sie im TYPO3 Extension Repository (TER) herunterladen und installieren.

Nun aber zurück aus der Vergangenheit hinein in die Zukunft. Ein wesentliches Feature der TYPO3 Version 8.7 LTS ist die Nutzung von Doctrine. Dazu wurden bis Version 8.4 alle Datenbankzugriffe im Kern umgeschrieben. Am einfachsten lassen sich die dazugehörigen Änderungen anhand von Praxisbeispielen zeigen.

Praxisbeispiele und CodeSnippets

Bisher wurden Datenbankzugriffe über die Variable $GLOBALS[‚TYPO3_DB‘] abgelbildet. Diese ist eine Instanz der PHP-Klasse DatabaseConnection. Diese Klasse existiert weiterhin in TYPO3 Version 8, wird allerdings in TYPO3 Version 9 entfernt. Somit wird ein Umstieg auf Doctrine für Entwickler unausweichlich.

Doch kein Grund zur Panik. Wer in einem Extbase-Repository bereits eine Abfrage implementiert hat, wird sich an die neue Syntax schnell gewöhnen.

Die wichtigsten Klassen im Umgang mit Doctrine in TYPO3 heißen ConnectionPool und QueryBuilder. Die erste wird dazu genutzt, um eine Verbindung zu einer Tabelle aufzubauen oder einen QueryBuilder zu instanzieren. Dieser wiederrum dient dazu komplexe Abfragen aufzubauen.

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Query Builder für eine Tabelle instanzieren
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                  ->getQueryBuilderForTable('tt_content');

// oder eine Verbindung aufbauen
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
                ->getConnectionForTable('tt_content');

Eine Verbindung kann für kleine Abfragen genutzt werden, der QueryBuilder für komplexe. In der Praxis sieht das dann wie folgt aus.

Select – Abfrage von Datensätzen

/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable($table);

$limit = 4;
$offset = 0;

$data = $queryBuilder
 ->select('*')
 ->from($table)
 ->where($queryBuilder->expr()->eq('deleted', 0))
 ->orWhere(
      $queryBuilder->expr()->like(
        'teaser', 
        '%' . $queryBuilder->escapeLikeWildcards($search) . '%'
      ),
      $queryBuilder->expr()->like(
        'bodytext', 
        '%' . $queryBuilder->escapeLikeWildcards($search) . '%'
      )
   )
 ->andWhere(
      $queryBuilder->expr()->eq(
        'pid', 
        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
      )
   )
 ->orderBy('date', 'DESC')->addOrderBy('title')
 ->groupBy('author')
 ->setFirstResult($offset)
 ->setMaxResults($limit)
 ->execute()
 ->fetchAll();

Hier sehen Sie bereits ein relativ komplexes Beispiel einer Select-Abfrage. Allerdings lässt es sich einfach lesen. Bei einigen Ausdrücken, wie ->where() und ->orderBy() ist darauf zu achten, dass sie nur einmal pro Abfrage verwendet werden sollten. Noch wichtiger ist, alle Ausdrücke abzusichern. Weiter unten im Text erfahren Sie mehr dazu.

Mit ->execute() erhalten Sie ein Statement-Objekt zurück. Mit einer ->fetch() Schleife lassen sich die einzelnen Zeilen abfragen oder Sie nutzen direkt ->fetchAll() um ein Array mit allen Einträgen zu erhalten.

Join – Tabellen miteinander verknüpfen

/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable($table);

$table = 'tt_content';
$joinTable = 'sys_category_record_mm';

$statement = $queryBuilder
   ->select('*')
   ->from($table)
   ->leftJoin(
      $table,
      $joinTable,
      'categoryMM',
      $queryBuilder->expr()->eq(
        'categoryMM.uid_foreign', 
        $queryBuilder->quoteIdentifier('tt_content.uid')
      )
   )
   ->where(
      $queryBuilder->expr()->eq('categoryMM.tablenames', 
         $queryBuilder->createNamedParameter($table)),
      $queryBuilder->expr()->eq('categoryMM.fieldname', 
         $queryBuilder->createNamedParameter('categories')),
      $queryBuilder->expr()->eq(
        'categoryMM.uid', 
        $queryBuilder->createNamedParameter(42, \PDO::PARAM_INT)
      )
   )
   ->execute();

Alle möglichen Ausdrücke für die Verknüpfung von Tabellen lauten: ->join(), ->innerJoin(), ->rightJoin() und ->leftJoin().

Count – Anzahl von Datensätzen erfragen

/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable($table);
$count = $queryBuilder
 ->count('uid')
 ->from($table)
 ->execute()
 ->fetchColumn(0);

Insert – Neue Datensätze einfügen

/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable($table);

$arrayNewData = ['title' => 'Neuer Datensatz', 'tx_csseo_no_index' => 1];

$queryBuilder
  ->insert($table)
  ->values($arrayNewData)
  ->execute();

Bulk Insert / Insert multiple rows

/** @var Connection $databaseConnection */
$databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getConnectionForTable($table);

$arrayWithNewData = [['title' => 'TYPO3'], ['title' => 'clickstorm GmbH']];
$arrayWithColNames = ['title'];


$databaseConnection->bulkInsert(
 $table,
 $arrayWithNewData,
 $arrayWithColNames
);

Update – Aktualisieren von Datensätzen

/** @var Connection $databaseConnection */
$databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getConnectionForTable($table); 

$arrayUpdateData = ['deleted' => 1];
$arrayWhere = ['uid' => 'IN (' . implode(',', $currentIds) . ')'];

$databaseConnection->update(
 $table,
 $arrayUpdateData,
 $arrayWhere
);

Delete – Löschen von Datensätzen

/** @var Connection $databaseConnection */
$databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getConnectionForTable($table);

$arrayWhere = ['hidden' => 1];

$databaseConnection->delete($table, $arrayWhere);

Doctrines wichtige Helferfunktionen

Absichern von Nutzer-Eingaben mit createNamedParameter()

->where(
   $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter($search))
)

Jede Eingabe, egal woher sie kommt ist mit createNamedParameter abzusichern! Darüberhinaus ist darauf zu achten, dass bei ->expr() immer das zweite Argument mit ->createNamedParameter() oder ->quoteIdentifier()  versehen wird. Doctrine selbst übernimmt nur die Vermeidung von SQL-Injections bei ->setFirstResult() und ->setMaxResults().

escapeLikeWildcards()

->where(
  $queryBuilder->expr()->like(
   'bodytext',
   $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($search) . '%')
)

Wie diese Funktion bereits durch ihren Namen verrät, dient sie dazu mögliche Platzhalter, welche bei einer Like-Abfrage genutzt werden können, entsprechend abzufangen.

Debug mit getSQL()

/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable($table);

$queryBuilder->select('*')->from($table);

\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump($queryBuilder->getSQL());

$statement = $queryBuilder->execute();

Analog dazu kann die Methode getParameters() genutzt werden. Mit ihr erfährt der Entwickler, mit welchen Werten die Platzhalter in den Statements belegt werden.

Ausblick

Weitere ausführliche Beispiele finden Sie in der TYPO3-Dokumentation. Wir hoffen, dass wir Ihnen einen soliden Einblick in Doctrine geben konnten. Unsere eigenen Extensions wie die cs_seo oder cs_youtube_data werden wir zeitnah an die TYPO3 Version 9 anpassen. Sollten Sie Anregungen oder Fragen zum Thema haben, hinterlassen Sie gerne einen Kommentar oder nehmen Sie Kontakt mit uns auf.

  • 08/03/2018

    Kommentar von Michael Grundkötter

    Cooler Beitrag. Und wenn man die Daten so aus der DB geladen hat (mit Doctrine QueryBuilder), kann man auch Extbase Objekte daraus machen. Das geht z.B. mit

    $this->getDataMapper()->map(
    EureModelKlasse::class,
    $queryBuilder->execute()->fetchAll()
    );

    wobei man noch diese Methode hinzufügen muss (bei mir per Trait):

    /**
    * @return DataMapper
    */
    protected function getDataMapper() {
    return GeneralUtility::makeInstance(DataMapper::class);
    }

    und entsprechend weitere use:
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;

    • 09/03/2018

      Kommentar von Marc

      Hallo Michael, danke für deinen Beitrag. Viele Grüße, Marc

  • 28/02/2019

    Kommentar von frank

    Super, vielen Dank! Der Beitrag hat mir sehr geholfen.

    • 01/03/2019

      Kommentar von Marc

      Hallo Frank, vielen Dank für dein Feedback. Das freut uns.

  • 19/03/2019

    Kommentar von Kurt

    Beim Select ist in Zeile 31 ein kleiner Fehler. Es muss setMaxResults statt setMaxResult heissen.
    Sonst Top!

    • 19/03/2019

      Kommentar von Marc

      Super, vielen Dank für den Hinweis. Ist angepasst.

  • 26/04/2019

    Kommentar von Andrea

    Danke für die tolle Übersicht!
    Bei Insert ist noch ein kleiner Fehler, das values fehlt:
    $queryBuilder
    ->insert(‚tablename‘)
    ->values($arrayNewData)
    ->execute();

    • 29/04/2019

      Kommentar von Marc

      Danke für den Hinweis. Ist angepasst.

  • 26/09/2019

    Kommentar von Kurt

    Hier noch ein Fehler im Code von „Join – Tabellen miteinander verknüpfen.“ Statt:
    $queryBuilder->expr()->eq(‚categoryMM.tablenames‘, $table),
    $queryBuilder->expr()->eq(‚categoryMM.fieldname‘, ‚categories‘),
    muss es lauten:
    $queryBuilder->expr()->eq(‚categoryMM.tablenames‘, $queryBuilder->createNamedParameter($table)),
    $queryBuilder->expr()->eq(‚categoryMM.fieldname‘, $queryBuilder->createNamedParameter(‚categories‘)),
    Getestet mit TYPO3 9. Also zumindest in TYPO3 9 muss es so lauten.

    • 26/09/2019

      Kommentar von Marc

      Hallo Kurt, vielen Dank für deinen Hinweis. Ich habe die Änderung übernommen. Viele Grüße, Marc

Kommentar hinzufügen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert