Typo3 Caching-Framework in eigener Extension nutzen

Mittwoch, 03. September 2014

Caching – Grundüberlegungen

Häufig entwickelt man eine Extension, deren Inhalt unabhängig von der restlichen Seite gecached werden soll, weil die Aktualisierung einer eigenen Logik folgen muss. Beispiel: Eine Kalender- oder eine News-Extension, bei der die Darstellung täglich oder stündlich wechselt. Das normale Seitencaching von Typo3 würde diese Extension unbrauchbar machen, da die gerenderte Seite inkl. meines PlugIns im Typo3-Cache landet und von dort ausgelesen wird bis der Redakteur den “Blitz” drückt um alle Caches zu leeren.

Das wird er aber ungern einmal am Tag machen. Deshalb bedienen sich die meisten Entwickler innerhalb ihrer Extension eines einfachen Tricks:

   $GLOBALS['TSFE']->set_no_cache();

Dieser Aufruf ist aus vielen Gründen tödlich: Zum einen wird der komplette Seitencache deaktiviert, d.h. alle Extensions und Renderings der Seite werden nicht im Cache gespeichert, was man deutlich an der Performance spürt. Aber auch andere Nebeneffekte sind unschön: indexed_search indiziert die Seiteninhalte nicht mehr – dadurch wird die Seite über die Suche nicht mehr gefunden.

Viel besser wäre es, wenn die komplette Seite zwar gecached wird – aber mein PlugIn “ungerendert” bleibt und in die gerenderte Seite eingefügt wird. Beim Beispiel des Kalenders: Der Kalender soll tagesaktuell sein – also reicht es, wenn er das PlugIn einmal am Tag “erneuert”. Bei den restlichen Aufrufen über den Tag verteilt, darf er das PlugIn fertig gerendert aus dem Cache zurückgeben.

Um das zu realisieren und auch selbst das Caching meiner Extension kontrollieren zu können gibt es das Caching Framework von Typo3.


Caching Framework von Typo3 nutzen

Dazu gibt es zwei simple Funktionen:

t3lib_pageSelect::getHash( $hash, $expTime ) und 
t3lib_pageSelect::setHash( $hash, $data, $ident )

Mehr Infos zu den Funktionen

Die Daten (ein einfacher String z.B. HTML-Code-Schnipsel oder ein in einen String per serialize() umgewandeltes Array) werden in den Typo3 Tabellen cf_cache_pages, cf_cache_hash, cf_cache_pagesections etc. gespeichert.

Man könnte eine Klasse schreiben, die sich um das Caching kümmert. Natürlich in schönem extBase – hier einfach als Ideenskizze:

 
class Tx_MeineExtension_Utilities_CacheUtility extends extends Tx_Extbase_MVC_Controller_ActionController {
 
   /*
    Alle möglichen Dinge, die ich an pi-Base geliebt habe...
    Jochen, Sören, Martin: Kann das mal einer sauber machen? ;)
   */
   protected $configurationManager;
   protected $request;
   protected $cObj;
   protected $settings;
   protected $configuration;
 
   public function __construct () {
      $objectManager = t3lib_div::makeInstance('Tx_Extbase_Object_ObjectManager');
      $this->configurationManager = $objectManager->get('Tx_Extbase_Configuration_ConfigurationManager');
      $this->request = t3lib_div::makeInstance('Tx_Extbase_MVC_Request');
      $this->cObj = $this->configurationManager->getContentObject();
      $this->configuration = $this->configurationManager->getConfiguration(Tx_Extbase_Configuration_ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
      $this->settings = $this->configuration['settings'];
      $this->gpVars = $this->request->getArguments();   
   }
 
   /*
    Cache laden, falls vorhanden
    $hashVars = Array mit Werten, um Cache zu identifizieren, z.B array('uid'=>1, 'view'=>'liste')
   */
 
   public function getCache ( $hashVars = null ) {
      $data = t3lib_pageSelect::getHash( $this->getCacheID($hashVars) );
      return !$data ? false : unserialize($data);
   }
 
   /*
    Cache speichern
    $data = Array mit Daten
    $hashVars = Array mit Werten, um Cache zu identifizieren, s.o.
   */
 
   public function setCache ( $data = null, $hashVars = null ) {
     // Um Mitternacht endet der Cache, wenn man möchte		
     $lifetime = mktime(23,59,59)+1-time();
     $cacheID = $this->getCacheID($hashVars);
     t3lib_pageSelect::storeHash( $cacheID, serialize($data), $this->extKey.'_cache', $lifetime );
     return $data;
   }
 
   /*
    Hier generieren wir eine eindeutige Cache-ID für die Anfrage
    $hashVars = Array mit Werten, um Cache zu identifizieren
   */
 
   private function getCacheID ( $hashVars = null ) {
    $additionalHashVars = array(
         // Auf Seiten-ID beschränken
         'pid'       => $GLOBALS['TSFE']->id, 
         // Auf Sprache beschränken
         'lang'      => $GLOBALS['TSFE']->sys_language_uid,
         // Auf UID dieses Content-Elementes (PlugIns) beschränken
         'uid'       => $this->cObj->data['uid'],
         // Auf heutiges Datum beschränken
         'date'      => date('Y-m-d'),
         // ...oder: Alle 10 Sekunden aktualisieren
         'time'      => floor(mktime()/10)
         // ... usw ...
      );
 
      // Vielleicht noch alle Get/Post-Variablen berücksichtigen?
      $additionalHashVars = array_merge($additionalHashVars, $this->gpVars);
 
      // Und dann noch die übergebenen Variablen?
      $hashVars = array_merge($additionalHashVars, $hashVars);
 
      // Einen String aus allem generieren
      $hashString = join('|',array_values($hashVars)).join('|', array_keys($hashVars));
 
      // ... und als kurzen, unique md5 zurückgeben
      return md5($hashString);      
   }
 
}

Und im Einsatz:

 
class MeineExtension ... {
 
   /**
   * @var Tx_MeineExtension_Utilities_CacheUtility
   * @inject
   */
   protected $cacheUtility;
 
   public function showAction () {
      if ($cache = $this->cacheUtility->getCache( array('action'=>$this->actionMethodName) )) return $cache['_rendering'];
 
      // Kein Cache gefunden...
      $data = Lade\die\Daten\aus\dem\Repository...
      $this->view->assign('data', $data);
      $rendering = $this->view->render();
 
      $this->cacheUtility->setCache( array('_rendering'=>$rendering), $this->actionMethodName );
      return $rendering;
   }
 
}

Damit die Extension nicht über den “normalen” Typo3 Cache gespeichert wird, muss das in der ext_localconf.php festgelegt werden:

<?php
 
if (!defined ('TYPO3_MODE')) die ('Access denied.');
 
Tx_Extbase_Utility_Extension::configurePlugin(
   $_EXTKEY,
   'Pi1',
 
   array(
        'MeineExt' => 'show',
    ),
 
    // Actions, die NICHT gecached werden sollen!
    array(
        'MeineExt' => 'show',
    )
 
); 
?>

USER_INT Caching bei alten pi-Base Extensions

Bei den alten PiBase-Extensions waren der Schlüssel übrigens diese Zeile in der main()-Funktion der Extension, die das PlugIn als USER_INT-Objekt definert haben:

class tx_meineextension_pi1 extends tslib_pibase {
   ...
   var $allowCaching;
   var $pi_checkCHash = TRUE;
 
   public function main( $content, $conf) {
      $this->cache = 1;
      $this->allowCaching = 1;
      $this->pi_USER_INT_obj = 1;
      ...
   }
}

Und zusätzlich beim Einbinden des PlugIns in der ext_localconf.php die “0″ am Ende:

t3lib_extMgm::addPItoST43($_EXTKEY, 'pi1/class.tx_dbfindit_pi1.php', '_pi1', 'list_type', 0);

Nicht vergessen, dass im Typoscript Setup das Plugin auch als USER_INT definert sein muss. Diese Einstellung überschreibt sonst alle Bemühungen oben:

plugin.tx_dbfindit_pi1 = USER_INT
plugin.tx_dbfindit_pi1 {
   ...
}

Eigenen Blitz einfügen

Damit der Cache in der Extension auch manuell über das Backend gelöscht werden kann, muss nicht zwingend eine neue Funktion geschrieben werden, da der “Blitz -> Alle Frontend Caches löschen” auch die cf_cache_*-Tabellen löscht, die von der Funktion “t3lib_pageSelect::storeHash” befüllt werden.

In manchen Fällen möchte man aber noch besondere, andere Dinge machen oder hat die Daten in einer gesonderten Cache-Datei oder Tabellen gespeichert. Hier wäre es am Schönsten, einen eigenen Menüpunkt in den “Blitz” für die Cache-Löschen-Funktion einzubauen.

Dazu bekommt die ext_localconf.php noch einen ergänzenden Eintrag. Hier als Beispiel in einer Extension:

 
// Fügt einen Menüpunkt in das Dropdown "Cache löschen" (Blitz oben rechts) ein...
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['additionalBackendItems']['cacheActions'][] = 'EXT:dbresponsiveimages/lib/class.dbresponsiveimagesCacheFunction.php:&tx_dbresponsiveimages_clearcachemenu';
 
// ...und linkt den Ajax-Befehl an die entsprechende Funktion
$TYPO3_CONF_VARS['BE']['AJAX']['dbresponsiveimages::cleardbresponsiveimagescache'] = 'EXT:dbresponsiveimages/lib/class.dbresponsiveimagesCacheFunction.php:&tx_dbresponsiveimages_clearcachemenu->clear_cache';
 
// Hook, der in t3lib/class.t3lib_tcemain.php Zeile 6870 aufgerufen wird
$TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'][] = 'EXT:dbresponsiveimages/lib/class.dbresponsiveimagesCacheFunction.php:&tx_dbresponsiveimages_clearcachemenu->clear_cache';
 
// "Clearing additional cache tables" - Hook, der in t3lib/class.t3lib_tcemain.php Zeile 6806 aufgerufen wird und die angegebene Tabelle löscht
//$TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearAllCache_additionalTables'] = 'tx_dbelements_cache';

Im Ordner “lib/” liegt ein Script “class.dbresponsiveimagesCacheFunction.php” mit der Klasse “tx_dbresponsiveimages_clearcachemenu” und der Funktion “clear_cache()”. Klar: Auch das ist noch nicht ganz extBase-Konform – aber das Prinzip ist klar, denke ich. Das Ganze sieht zum Beispiel so aus:

<?php
 
 
class tx_dbresponsiveimages_clearcachemenu implements backend_cacheActionsHook {
 
 
   /**
    * Adds a new entry to the cache menu items array
    *
    * @param array array Cache menu items
    * @param array array of access configuration identifiers (typically used by userTS with options.clearCache.identifier)
    * @return void
    */
 
    public function manipulateCacheActions(&$cacheActions, &$optionValues) {
 
      $title = 'Resp. images cache';
      $cacheActions[] = array (
         'id'    => 'cleardbresponsiveimagescache',
         'title' => $title,
         'href'  => $GLOBALS['BACK_PATH'] . 'ajax.php?ajaxID=dbresponsiveimages::cleardbresponsiveimagescache',
         'icon'  => '<img src="' . t3lib_extMgm::extRelPath('dbresponsiveimages') .
            'ext_icon.gif" width="16" height="16" title="' . htmlspecialchars($title) . '" alt="" />'
      );
 
      $optionValues[] = 'cleardbresponsiveimagescache';
 
   }
 
 
   public function clear_cache () {
 
      /* Nur ein Beispiel, wie auch Bilder aus dem Cache-Verzeichnis gelöscht werden könnten */
      $files = t3lib_div::getFilesInDir( PATH_site . 'typo3temp/dbresponsiveimages/', '', true );
      foreach ($files as $file) {
         @unlink($file);
      }
   }
}
 
if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/dbresponsiveimages/lib/class.dbresponsiveimagesCacheFunction.php']) {
   include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/dbresponsiveimages/lib/class.dbresponsiveimagesCacheFunction.php']);
}
?>

Quick’n'Dirty RAM-Cache Lösung

Eine der Techniken, für die man von einem “echten” Entwickler geköpft wird – aber (zur Ausrede) inspiriert durch die Art, wie im Core von Typo3 z.B. locallang-Werte gespeichert werden: Statt bei jedem einzelnen Label eine komplette Sprachdatei aus dem File-System oder der Datenbank zu lesen und zu parsen, macht der Core es einfach über eine globale Variable.

Macht natürlich nur begrenzt Sinn, z.B. wenn beim Aufbau einer Seite wiederholt die gleichen Datensätze aus der Datenbank geladen werden. Und wenn man das nicht übermäßig “mißbraucht” (RAM schrumpft). Aber es bringt schon erstaunliche Performance-Schübe:

Hier in einfacher Form “übersetzt”:

 
function hole_label_aus_datenbank ( $welchesLabel ) {
 
   // Tabelle mit Übersetzungen wurde bereits geladen?
   if ($GLOBALS['meinCacheName']) return $GLOBALS['meinCacheName'][$welchesLabel];
 
   // Noch nichts geladen... also eine Abfrage starten
   $data = \Hole\Alle\Daten\aus\Datenbank->machEs();
 
   // Und das Ergebnis im RAM cachen
   $GLOBALS['meinCacheName'] = $data;
 
   return $data[$welchesLabel];
}

Das Ganze geht genau so mit komplexeren Abfragen, Arrays oder Variablen.

 
function hole_was_aus_datenbank ( $key, $parameter_array, $sonstwas, $flag ) {
 
   // Lass uns einfach einen eigenen Identifier für den Cache generieren:
   $cacheID = md5($key . array_values($parameter_array) . $sonstwas . $flag);
 
   // Tabelle mit Übersetzungen wurde bereits geladen?
   if ($GLOBALS["extname_{$cacheID}"]) return $GLOBALS["extname_{$cacheID}"];
 
   // Noch nichts geladen... also eine Abfrage starten
   $data = \Hole\Alle\Daten\aus\Datenbank->machEs();
 
   // Und das Ergebnis im RAM cachen
   $GLOBALS["extname_{$cacheID}"] = $data;
 
   return $data;
}