Mehrere Navigationen mit Zend_Navigation (und Zend_Cache) (Update)
Am Wochenende, bin ich auf das “komische” Verhalten von den View Helfern vom Zend Framework gestoßen. Bis ich einfach mal versucht habe, ein Zend_Navigation Objekt direkt zu rendern:
$this->navigation()->render($this->module_navigation);
Was schon annähernd das war, was ich mir so vorgestellt habe. Der einzige Haken an der Sache ist oder besser war, dass so Zend_Navigation Objekte und Renderings relativ groß werden können. (siehe hier) Allgemein sollte man sehr sparsam mit einigen Teilen des Zend_Frameworks umgehen oder aber so gut es geht cachen (am Besten natürlich mit Zend_Cache, weil dafür ist es ja da
).
Und genau das habe ich mal versucht und ich denke, da ist was brauchbares mit Hilfe des Postings von Benjamin Steininger drauß geworden.
Kurz noch mal erwähnt ich nutze in meinem Fall entweder das APC – Backend oder aber das memcached – Backend. Aber genug der langen Vorreden, wollen wir doch mal schauen, was ich da gebastelt habe. Zu allerst mal das Caching – Geraffel:
class App_Model_CacheModel
{
protected static $_frontend_options = array('Core' => array('lifetime' => 1800 , 'automatic_serialization' => true));
const CACHE_TYPE_CLASS = 'class';
const CACHE_TYPE_SYSTEM = 'system';
/**
* @return Zend_Cache_Frontend_Core
* @param string $type
*/
static public function getCache ($type = self::CACHE_TYPE_SYSTEM, $entity = null)
{
return self::_getCache($type, $entity);
}
/**
* @throws My_Exception
* @return Zend_Cache_Frontend_Core
* @param unknown_type $type
*/
static protected function _getCache ($type)
{
switch (strtolower($type)) {
case self::CACHE_TYPE_SYSTEM:
return self::_getSystemCache();
break;
case self::CACHE_TYPE_CLASS:
return self::_getClassCache($entity);
default:
throw new My_Exception('Der Cache Typ:' . $type . ' ist leider nicht verfügbar.');
break;
}
}
/**
*
*/
static protected function _getClassCache ($entity)
{
$frontend = 'Class';
$frontend_options = array('cached_entity' => $entity);
if (extension_loaded('apc') && ! extension_loaded('memcache')) {
$backend = 'Apc';
$backend_options = array();
} elseif (extension_loaded('apc') && extension_loaded('memcache')) {
$backend = 'Memcached';
if (Zend_Registry::isRegistered('config') && isset(Zend_Registry::get('config')->memcache)) {
$backend_options = Zend_Registry::get('config')->memcache->toArray();
} else {
$backend_options = array('compatibility' => true , 'servers' => array(array('host' => 'localhost' , 'port' => 11211 , 'persistent' => true , 'weight' => 1 , 'timeout' => 5 , 'retry_interval' => 15 , 'status' => true , 'failure_callback' => '') , array('host' => 'localhost' , 'port' => 11212 , 'persistent' => true , 'weight' => 2 , 'timeout' => 5 , 'retry_interval' => 15 , 'status' => true , 'failure_callback' => '')));
}
} else {}
return Zend_Cache::factory($frontend, $backend, $frontend_options, $backend_options);
}
static protected function _getSystemCache ()
{
$frontend = 'Core';
$frontend_options = self::$_frontend_options[$frontend];
if (extension_loaded('apc') && ! extension_loaded('memcache')) {
$backend = 'Apc';
$backend_options = array();
} elseif (extension_loaded('apc') && extension_loaded('memcache')) {
$backend = 'Memcached';
if (Zend_Registry::isRegistered('config') && isset(Zend_Registry::get('config')->memcache)) {
$backend_options = Zend_Registry::get('config')->memcache->toArray();
} else {
$backend_options = array('compatibility' => true , 'servers' => array(array('host' => 'localhost' , 'port' => 11211 , 'persistent' => true , 'weight' => 1 , 'timeout' => 5 , 'retry_interval' => 15 , 'status' => true , 'failure_callback' => '') , array('host' => 'localhost' , 'port' => 11212 , 'persistent' => true , 'weight' => 2 , 'timeout' => 5 , 'retry_interval' => 15 , 'status' => true , 'failure_callback' => '')));
}
} else {}
return Zend_Cache::factory($frontend, $backend, $frontend_options, $backend_options);
}
}
An sich noch nichts weltbewegendes, eigentlich nur, dass ich über die statische Methode: getCache sagen kann, welches Frontend wir denn gerne hätten. Die Class Cache Methode ist leider noch fratze, weil ich erst mal nur das Core Frontend testen wollte.
Evtl. könnte man noch soweit gehen und optional einen Parameter für das Backend einbauen, aber das wären mir dann wieder zu viele Parameter. Ich hasse viele Parameter in Methoden oder Funktionsaufrufen, die führen meist nur zu unnötigen Fehlern.
So und jetzt haben wir noch eine Klasse mit zwei statischen Methoden, die im Grunde über die getCachedNavigation Methode auf das CachingModel aufbaut:
class App_Model_NavigationModel
{
static public function getCachedNavigation ($xml, $section = 'navi')
{
$filter = new My_Filter_Word_FilePathToCamelCase();
$cache_id = 'NavigationsObjekt';
$cache_id .= $filter->filter($xml);
$cache = App_Model_CacheModel::getCache(App_Model_CacheModel::CACHE_TYPE_SYSTEM);
if(!($navigation = $cache->load($cache_id))){
$navigation = self::getNavigation($xml, $section);
$cache->save($navigation, $cache_id);
}
return $navigation;
}
/**
* @return Zend_Navigation
* @throws My_Exception
* @param $xml
* @param $section
*/
static public function getNavigation ($xml, $section = 'navi')
{
try {
$config = new Zend_Config_Xml($xml, $section);
$navigation = new Zend_Navigation($config);
return $navigation;
} catch (Zend_Exception $e) {
throw new My_Exception('Die Navigation konnte nicht geladen werden: ' . $e->getMessage());
}
}
}
Auch hier wieder keine höhere Kunst, wir bauen uns ein Zend_Config_Xml Objekt, mit den Navigationselementen, packen die in das Zend_Navigation Objekt und cachen das Ganze falls erwünscht und erforderlich.
Jetzt hab ich mir noch einen Zend_View Helfer gebaut:
class My_View_Helper_RenderNavigation extends Zend_View_Helper_Abstract
{
/**
* @var Zend_View
*/
public $view;
function renderNavigation ($xml, $section = 'navi')
{
if (file_exists($xml) && is_readable($xml)) {
/**
* @todo auslagern
* @var unknown_type
*/
$filter = new My_Filter_Word_FilePathToCamelCase();
$cache_id = $filter->filter($xml);
$cache_id .= 'Navigation' . sha1($_SERVER['REQUEST_URI']);
$cache = App_Model_CacheModel::getCache(App_Model_CacheModel::CACHE_TYPE_SYSTEM);
if (! ($navigation = $cache->load($cache_id))) {
$navigation = (string) $this->view->navigation()->render(App_Model_NavigationModel::getCachedNavigation($xml, $section));
$cache->save($navigation, $cache_id);
}
return $navigation;
}
return null;
}
}
Auch hier nutze ich wieder um den Server ein wenig zu entlasten das Caching Model und returne am Ende nur noch das komplett gerenderte Navigationsbäumchen.
Was etwas unschön noch ist, ist folgendes:
$this->renderNavigation(APPLICATION_PATH . '/configs/navi.xml'); </pre> Ich muss nämlich immer wissen wo das Navigations XML File liegt. Deutlich was ich meine wird glaube ich hier: <pre lang="php"> $this->renderNavigation(APPLICATION_PATH . '/modules/' . strtolower($this->module) . '/configs/navi.xml')
Unglücklich kann man mit dieser Lösung werden, wenn man viele Navigationselemente rendert, wie zum Beispiel Breadcrumbs… Aber vielleicht fällt mir ja noch was dazu ein, wie man das nen bissel schicker machen kann. Alles in allem, denke ich kann man das aber soweit schon verwenden. Vielleicht kann ich die Tage ja mal das Class Frontend vorstellen.
Ah eine Klasse habe ich noch vergessen und zwar FilePathToCamelCase, die Klasse wandelt mir Verzeichnispfade (im Moment nur Unix) in CamelCase Wörter um, weil Zend_Cache mit / und Co leider nix anfangen kann als ID:
class My_Filter_Word_FilePathToCamelCase extends Zend_Filter
{
function __construct ()
{
$this->addFilter(new Zend_Filter_Word_SeparatorToCamelCase('/'));
$this->addFilter(new Zend_Filter_Word_SeparatorToCamelCase('.'));
}
}
Ich hoffe der ein oder andere kann die Snibbel irgendwie gebrauchen und ich konnte jemandem helfen.
Update:Hier mal die “fertige” Caching Klasse:
<?php
/**
*
* Copyright (C) 2009 Stefan Riedel <sr@riedel-st.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @category My
* @package My_Cache
* @copyright Copyright (c) 2009-2010 SRIT Stefan Riedel (http://www.srit.biz)
*/
/**
* @author Stefan Riedel
* @copyright Copyright (c) 2009-2010 SRIT Stefan Riedel (http://www.srit.biz)
*/
class My_Cache
{
/**
* Standard Frontend Optionen
* @var array
*/
protected static $_frontend_core_options = array('lifetime' => 1800 , 'automatic_serialization' => true);
/**
* Konstante für Klassen Caches
*/
const CACHE_TYPE_CLASS = 'class';
/**
* Konstante für den System Cache
*/
const CACHE_TYPE_SYSTEM = 'system';
/**
* Konstante für den dynamischen Cache
*/
const CACHE_TYPE_DYNAMIC_DATA = 'dynamic_data';
/**
* gibt im erfolgsfall eine Zend_Cache_Core Instanz zurück
* @return Zend_Cache_Core|Zend_Cache_Frontend_*
* @param string $type
* @param array $frontend_options
* @see _getCache
*/
static public function getCache ($type = self::CACHE_TYPE_SYSTEM, array $frontend_options = array())
{
return self::_getCache($type, $frontend_options);
}
/**
* gibt im erfolgsfall eine Zend_Cache_Core Instanz zurück
* @throws My_Exception
* @return Zend_Cache_Core|Zend_Cache_Frontend_*
* @param string $type
* @param array $frontend_options
* @see getCache
*/
static protected function _getCache ($type, $frontend_options)
{
switch (strtolower($type)) {
case self::CACHE_TYPE_SYSTEM:
return self::_getSystemCache($frontend_options);
break;
case self::CACHE_TYPE_CLASS:
return self::_getClassCache($frontend_options);
break;
case self::CACHE_TYPE_DYNAMIC_DATA:
return self::_getDynamicDataCache($frontend_options);
break;
default:
throw new My_Exception('Der Cache Typ:' . $type . ' ist leider nicht verfügbar.');
break;
}
}
/**
* ist eine Cache Methode die für Dynamische Daten ausgelegt ist
* wie beispielsweise Daten aus einem Datencontainer (DB)
* @return Zend_Cache_Core
* @see _getCache, CACHE_TYPE_DYNAMIC_DATA
*/
static protected function _getDynamicDataCache (array $frontend_options = array())
{
$frontend = 'Core';
$frontend_options = array_merge(self::$_frontend_core_options, $frontend_options);
$backend = 'File';
if (Zend_Registry::isRegistered('config') && isset(Zend_Registry::get('config')->file_cache)) {
$backend_options = Zend_Registry::get('config')->file_cache->toArray();
} else {
$backend_options = array('cache_dir' => '/tmp' , 'file_name_prefix' => 'crm_cache');
}
return Zend_Cache::factory($frontend, $backend, $frontend_options, $backend_options);
}
/**
* ist eine Cache Methode die ein/e Klasse/ Objekt
* cached. Kann zum Beispiel für große Doctrine_Results
* genutzt werden
* @throws My_Exception
* @param array $frontend_options
* @return Zend_Cache_Frontend_Class
* @see _getCache, CACHE_TYPE_CLASS
*/
static protected function _getClassCache ($frontend_options)
{
if (empty($frontend_options)) {
throw new My_Exception('Das Class Frontend benötigt zusätzliche Parameter, wie zum Beispiel der Klassenname, der gecached werden soll.');
}
$frontend = 'Class';
$backend = 'File';
if (Zend_Registry::isRegistered('config') && isset(Zend_Registry::get('config')->file_cache)) {
$backend_options = Zend_Registry::get('config')->file_cache->toArray();
} else {
$backend_options = array('cache_dir' => '/tmp' , 'file_name_prefix' => 'crm_cache');
}
return Zend_Cache::factory($frontend, $backend, $frontend_options, $backend_options);
}
/**
* ist eine Cache Methode die für dauerhafte Caches ausgelegt ist,
* wie zum Beispiel Navigationen oder allgemein Dynamisch generierte
* View Elemente. Es wird davon ausgegangen, dass {@link http://pecl.php.net/package/APC APC} und/oder
* {@link http://www.danga.com/memcached Memcache} installiert wurde.
* @see _getCache, CACHE_TYPE_SYSTEM
*/
static protected function _getSystemCache (array $frontend_options = array())
{
$frontend = 'Core';
$frontend_options = self::$_frontend_core_options;
if (extension_loaded('apc') && ! extension_loaded('memcache')) {
$backend = 'Apc';
$backend_options = array();
} elseif (extension_loaded('apc') && extension_loaded('memcache')) {
$backend = 'Memcached';
if (Zend_Registry::isRegistered('config') && isset(Zend_Registry::get('config')->memcache)) {
$backend_options = Zend_Registry::get('config')->memcache->toArray();
} else {
$backend_options = array('compatibility' => true , 'servers' => array(array('host' => 'localhost' , 'port' => 11211 , 'persistent' => true , 'weight' => 1 , 'timeout' => 5 , 'retry_interval' => 15 , 'status' => true , 'failure_callback' => '') , array('host' => 'localhost' , 'port' => 11212 , 'persistent' => true , 'weight' => 2 , 'timeout' => 5 , 'retry_interval' => 15 , 'status' => true , 'failure_callback' => '')));
}
} else {}
return Zend_Cache::factory($frontend, $backend, $frontend_options, $backend_options);
}
}
Tags: PHP, Zend Framework, Zend_Cache, Zend_Navigation

Der Ansatz mit dem FilePathToCamelCase gefällt mir, ich denke das schau ich mir mal näher an. Da das memcached-backend Probleme mit sonderzeichen hat , hatte ich zuerst base64 in erwägung gezogen, aber das liefert halt = und das wollte memcached auch nicht. schlussendlich hab ich bei mir sha1-hashes genutzt, was allerdings reinoptisch für die eys nicht sonderlich toll ist.
Die einzige Gefahr die ich bei FilePathToCamelCase unter unixoiden Systemen sehe ist dass die Gefahr einer Kollision vorhanden ist, weil ich ja theorhetisch Ordner bzw. Pfadstrukturen mit gleichem Namen aber unterschiedlicher Groß und Kleinschreibung anlegen kann, aber solange man das weis ist das eigentlich kein Problem und obwohl ich jetzt schon lange Linux nutze, hab ich das glaubd ich noch nie gemacht.
Ich denke aber, dass wenn man abolute Pfade benutzt, sollte das Risiko von Id Kollisionen recht gering sein. Ganz auszuschließen ist es sicher nicht, aber man hat ja bei Dateipfaden in der Regel die Möglichkeit Kollisionen, durch generelles kleinschreiben, Umwandeln von Dateinamen etc. zu vermeiden.