Eines der Enterprise-Edition Features ist der Full-Page Cache (FPC).
Dieser ermöglicht es Magento die meisten Seiten mit viel weniger Overhead auszuliefern, als es von Haus aus in der CE Version möglich ist. Das ganze funktioniert auch ganz gut, solange keine dynamischen Inhalte in einer Seite angezeigt werden. Zum Glück hat Magento jedoch auch das berücksichtigt: dynamisch Blöcke wie zum Beispiel die zuletzt angesehenen Produkte oder der Mini-Warenkorb werden individuell nach Kunde gecached, und dann in eine Seite aus dem Full-Page Cache während einer Anfrage eingefügt.
Ziel dieses Posts ist es, diesen Prozess zu verdeutlichen, und so Magento-Entwickler zu befähigen, eigene dynamische Blöcke effizient mit dem Full-Page Cache zu verwenden.
Der Full-Page Cache Prozess
Der Full-Page Cache unterscheidet bei Anfragen verschiedene Fälle:
- Eine Anfrage darf nicht gecached werden
- Eine Anfrage darf gecached werden, liegt aber nicht gecached vor
- Eine Anfrage kann aus dem Cache bedient werden und enthält keine dynamischen Inhalte
- Eine Anfrage kann aus dem Cache bedient werden, die Seite enthält gecachte dynamische Inhalte
- Eine Anfrage kann aus dem Cache bedient werden, die Seite enthält ungecachte dynamische Inhalte
Von Interesse für diesen Post sind Fall 3, 4 und 5, also wenn eine Anfrage aus dem Cache bedient wird. Magento versucht den Prozess so effizient wie möglich zu gestalten. Aus diesem Grund wird der FPC schon sehr früh im Bootstrap Prozess eingeschaltet, bevor die Initialisierung von Magento abgeschlossen ist.
Hier ein Ausschnitt aus Mage_Core_Model_App::run()
...
if ($this->_cache->processRequest()) {
$this->getResponse()->sendResponse();
} else {
$this->_initModules();
...
Sofern also processRequest() true zurückgibt wird das meiste Bootstrapping übersprungen, wie zum Beispiel das Routing und das dispatchen eines Action Controllers.
Eine ganze Seite zu cachen ist, denke ich, nicht weiter spannend. Interessant ist das Einfügen der dynamischen Inhalte in eine gecachte Seite.
Lädt der Full-Page Cache eine Seite aus dem Cache, sucht dieser in dem Seiten-Inhalt nach Platzhaltern für dynamische Blöcke. Sind welche vorhanden, instanziiert Magento für jeden dynamischen Block eine Container-Klasse, welcher die Aufgabe zukommt, die dynamischen Inhalte des Blocks in den Content einzufügen.
Zuerst wird auf dem Block-Container die Methode applyWithoutApp() aufgerufen.
Nun versucht der Container, den Content aus dem Block-Cache zu laden und in den Seiten-Inhalte einzufügen. Für diese Blöcke können auch für Cache-Einträge für individuelle Kunden geschrieben werden.
Konnten die Inhalte für alle Platzhalter von den Containern aus dem Cache geladen werden, liegt die Seite jetzt vollständig vor, processRequest() gibt true zurück und die Seite wird an den Kunden ausgeliefert.
Wurden jedoch nicht alle dynamischen Inhalte von den Containern im Block-Cache gefunden, so beginnt die nächste Phase.
Damit der Content generiert werden kann, benötigen die entsprechenden Blöcke jedoch eine voll aufgesetzte Laufzeit-Umgebung: der Full-Page Cache setzt das Request Objekt auf die Route pagecache/request/process und bestimmt durch das Flag isStraight(true) das keine URL Rewrites geladen werden sollen.
...
Mage::register('cached_page_content', $content);
Mage::register('cached_page_containers', $containers);
Mage::app()->getRequest()
->setModuleName('pagecache')
->setControllerName('request')
->setActionName('process')
->isStraight(true);
$routingInfo = array(
'aliases' => $this->getMetadata('routing_aliases'),
'requested_route' => $this->getMetadata('routing_requested_route'),
'requested_controller' => $this->getMetadata('routing_requested_controller'),
'requested_action' => $this->getMetadata('routing_requested_action')
);
Mage::app()->getRequest()->setRoutingInfo($routingInfo);
...
Dann wird von processRequest() false zurückgegeben, und der Magento dispatch Vorgang nimmt seinen Lauf.
Steigen wir wieder ein im dem Action Controller des Enterprise_PageCache Moduls, Enterprise_PageCache_RequestController::processAction()
Hier nun wird auf den noch nicht verarbeiteten Containern aus der Registry die Methode applyInApp() aufgerufen.
Hierbei wird die Block-Klasse instanziiert und toHtml() aufgerufen, und die Ausgabe in den Seiten-Content eingefügt.
Dynamische Blöcke einfügen
Wenn irgend möglich, sollten ein Block immer gecached werden, damit applyWithoutApp() ausreicht um die Inhalte in die Seite einzufügen.
Wenn applyInApp() auf dem Container verwendet werden muss, fällt ein Großteil des Nutzens des FPC weg, da der ganze dispatch-Process von Magento durchlaufen wird.
Welche Schritte sind nun nötig, um eigene Blöcke dynamisch im Full-Page Cache anzeigen zu lassen?
Um einen Block dynamisch in den Full-Page Cache einzugliedern, muss zuerst in dem etc/ Verzeichnis des Moduls eine cache.xml Datei angelegt werden.
<?xml version="1.0" encoding="UTF-8"?>
<config>
<placeholders>
<webguys_example>
<block>webguys_example/view</block>
<name>product.info.example</name>
<placeholder>CACHE_TEST</placeholder>
<container>Webguys_Example_Model_Container_Cachetest</container>
<cache_lifetime>86400</cache_lifetime>
</webguys_example>
</placeholders>
</config>
Der
<block type="webguys_example/view" name="product.info.example" as="example" template="webguys/example.phtml" />
Der
Der
Und schließlich der
Nachdem die etc/cache.xml nun existiert ist der nächste Schritt die Container Klasse anzulegen. Sie muss Enterprise_PageCache_Model_Container_Abstract erweitern. Zumindest eine Methode muss implementiert werden: _renderBlock(). Sie gibt den Inhalt des Blocks zurück in dem Fall das applyInApp() aufgerufen wird. Üblicherweise wird der Block mit new() instanziiert, da das Layout Objekt aus Performance-Gründen besser nicht verwendet wird. Dann wird ganz normal toHtml() aufgerufen um den Block zu rendern. Der Block sollte also so wenig Resourcen wie möglich benutzen, damit der Vorteil des Full-Page Caches nicht verloren geht.
Damit applyWithoutApp() auch funktioniert muss noch _getCacheId() in der Container Klasse implementiert werden. Die Methode liefert, welch Überraschung, den Cache-Identifier für den Block zurück. Wenn jeder Kunde seine eigene Variante des Blocks sehen soll, kann $this->_getCookieValue(Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER, '') in die Cache-Id eingefügt werden.
Wenn der Container die Klasse Enterprise_PageCache_Model_Container_Customer extended wird die Cache-Lifetime des Block Caches automatisch auf die Dauer der Kunden-Session gesetzt.
Hier ein Beispiel für einen Container:
class Webguys_Example_Model_Container_Cachetest
extends Enterprise_PageCache_Model_Container_Abstract {
protected function _getIdentifier() {
return $this->_getCookieValue(Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER, '');
}
protected function _getCacheId() {
return 'EXAMPLE_' . md5($this->_placeholder->getAttribute('cache_id') . '_' . $this->_getIdentifier();
}
protected function _renderBlock() {
$blockClass = $this->_placeholder->getAttribute('block');
$template = $this->_placeholder->getAttribute('template');
$block = new $blockClass;
$block->setTemplate($template)
return $block->toHtml();
}
}
Werte durchreichen
Wenn der Block beim Rendern Werte aus der Magento Umgebung benötigt, die nicht während einer pagecache/request/process Anfrage zur Verfügung stehen, kann folgender Trick verwendet werden. Von dem von $block->getCacheKeyInfo() zurückgegebenen Array werden alle Werte, die einen String als Key haben, als Placeholder Attribut gespeichert. Hier ein Beispiel für eine Produkt-Id.
In der Block Klasse wird der Wert dem CacheKeyInfo Array hinzugefügt:
public function getCacheKeyInfo()
{
$info = parent::getCacheKeyInfo();
if (Mage::registry('current_product')) {
$info['product_id'] = Mage::registry('current_product')->getId();
}
return $info;
}
In der _getCacheId() und der _renderBlock() Methode des Containers wird die Id dann einfach mit $this->_placeholder->getAttribute('product_id') ausgelesen.
$block = new $blockClass;
$block->setTemplate($template)
->setProductId($this->_placeholder->getAttribute('product_id'));
Insgesamt ist es also sehr einfach dynamische Inhalte in den Full-Page Cache einzufügen. Mit dem nötigen Know-How und etwas Vorsicht kann dabei die Performance erhalten bleiben. Ein einfaches Beispielmodul für diesen Beitrag kann zum Testen heruntergeladen werden.