Idea
As the GUI of Zend Server is based on the open-source Zend Framework, you can easily extend it. This article demonstrates how to integrate new functionality to Zend Server which makes it possible see the content and some meta information of cached items.
For this example I use the Data Cache API from Zend Server, but in combination with the Zend Framework component Zend_Cache and its Zend_Cache_Backend_ZendServer_Disk backend adapter.
Preparation
Before we can extend the Zend Server GUI, we have to do some preparatory work.
Behind the scenes: Using the Apache with XML-RPC
With an installed Zend Server you do not only have an installed Apache, but also a Lighttpd web server (only on *nix systems, not on Windows). The Lighttpd is responsible for serving the GUI. But if you request e.g. the phpinfo page in the GUI, the Lighttpd requests the information from the Apache via XML-RPC: Therefore the Apache is listening to port 10083 for internal/Lighttpd requests. It’s absolutely necessary to ask the Apache for the phpinfo information: If we called phpinfo() in a PHP script which is served by the Lighttpd, we wouldn’t display the phpinfo of the application server, but of the Lighttpd…
Interesting fact: we are talking about two different applications – one is served by the Apache, the other by Lighttpd. But both use the same source code (Zend Framework application) which is located under /usr/local/zend/gui.
Extend Zend Framework
Unfortunately there is no Zend Server API function which delivers all cached items, so we have to implement it ourselves and extend therefore the Zend Framework class Zend_Cache_Backend_ZendServer_Disk with Zx_Cache_Backend_ZendServer_Disk. The new class is necessary because we want to store a list of all cached items. Prefix Zx is choosen because of …, well…, I don’t know. ZendX already exists and I decided to use Zx.
$backend = new Zx_Cache_Backend_ZendServer_Disk();
$cache = Zend_Cache::factory('Core',
$backend,
$frontendOptions);
For using the Zend Server cache and the new adapter in your application, the Zend Framework cache object has to be instantiated as in the code above. For more information about how to use Zend_Cache you can have a look here: Zend Cache(Zend Cache)
The class definition looks as following:
class Zx_Cache_Backend_ZendServer_Disk extends Zend_Cache_Backend_ZendServer_Disk implements Zend_Cache_Backend_Interface
{
const CACHE_ID_LIST_KEY = 'metadata_cacheidlist';
public function save($data, $id, $tags = array(),
$specificLifetime = false)
{
if ($idList = $this->load(self::CACHE_ID_LIST_KEY)) {
if (!in_array($id, $idList)) $idList[] = $id;
} else {
$idList = array($id);
}
parent::save($idList, self::CACHE_ID_LIST_KEY, array(), 999999);
return parent::save($data, $id, $tags, $specificLifetime);
}
public function loadMetadata($id) {
return $this->load('internal-metadatas---' . $id);
}
}
Zx_Cache_Backend_ZendServer_Disk::save() is the important method. In the last line it calls the parent method without changing any params. But before a list (array) of id’s of all cached items is fetched from the cache, the new id is added to the list and stored back into the cache. As you can see, I chose a very high cache lifetime (999999), so that the cache of the id list will expire in the far future – and it will only expire if no new item is cached in the meantime.
With cache key Zx_Cache_Backend_ZendServer_Disk::CACHE_ID_LIST_KEY we can now get a list of all cached items.
Zx_Cache_Backend_ZendServer_Disk::loadMetadata() is just a convenient method for returning the metadata of a cached item
GUI
Now we’re able to use the Partial Content / Data Cache of Zend Server with the Zend_Cache package, but also get a list of all cached items. It’s time to take the first steps for extending the Zend Server GUI.
New Tab
It’s very easy to add a new tab to the GUI. Find the file /usr/local/zend/gui/application/data/layout.xml and add a new “item” tag to the “menu” tag. You can copy the following lines
<item name="Data Cache Control" controller="Data-Cache-Control" action="Overview">
<identifier>datacachecontrol-tab</identifier>
<image>admin</image>
<sidebarid>datacachecontrol-sidebar</sidebarid>
<menu>
<item name="Overview" controller="Data-Cache-Control" action="Overview">
</item>
<item name="Content" controller="Data-Cache-Control" action="content">
</item>
</menu>
</item>
Just reload the GUI and you can see a new Tab “Data Cache Control” with a sub menu. The ‘identifier’ and ‘sidebar’ tags are used for internal mapping for highlighting and connecting the right tabs and sub-tabs.
I like this feature, because it’s so easy: Just add a few lines to layout.xml and you get a frame for new functionality.
As one can see, for the tab and for each menu entry there is a controller and an action defined – so we have to implement them now.
Controller Action (Client)
In the application folder (/usr/local/zend/gui/application) we see a ‘common’ and a ‘PE’ directory. The ‘PE’ folder represents the default module for the GUI application. I added a new controller class DataCacheControlController (do you remember the attributes in the modified layout.xml?) with two methods: One for displaying all the cached items and one for showing the details. Both Actions use DataCache_ZwasServer::getCacheMetadataAll() resp. DataCache_ZwasServer::getCacheMetadata() for getting information from the Apache via a XML-RPC call. We’ll see later how these methods are implemented.
DataCacheControlController::overviewAction()
The overviewAction() is responsible for collecting all the metadata of the cached items and prepare the data for the view script. This is not rocket science – the method should be self-explanatory:
public function overviewAction() {
$userServer = new DataCache_ZwasServer();
$metadata = $userServer->getCacheMetadataAll();
$time = time();
foreach ($metadata as $id => &$data) {
$duration = $data['expire'] - $data['mtime'];
$expired = $time - $data['mtime'];
$data['expired'] = $expired;
$data['progressRel'] = (int) (100 / $duration * $expired);
if ($data['progressRel'] > 100 || $data['progressRel'] == 0) {
unset($metadata[$id]);
continue;
}
$data['start'] = date('Y-m-d, H:i:s', $data['mtime']);
$data['end'] = date('Y-m-d, H:i:s', $data['expire']);
}
$this->view->metadata = $metadata;
$this->view->now = date('Y-m-d, H:i:s');
}
DataCacheControlController::contentAction()
Unlike overviewAction(), contentAction() only handles metadata of one single cached item. Additionally, the content of the cached item is fetched and simply dumped into a view variable.
public function contentAction() {
$datacacheContentId = $this->getTrimmedParam('datacacheContentId');
if (!$datacacheContentId) return;
$this->view->datacacheContentId = $datacacheContentId;
$userServer = new DataCache_ZwasServer();
$metadata = $userServer->getCacheMetadata($datacacheContentId);
if (!$metadata) {
$this->view->noCacheHit = true;
return;
}
$this->view->data = Zend_Debug::dump(
$userServer->getCacheData($datacacheContentId),
$datacacheContentId, false);
$this->view->start = date('Y-m-d, H:i:s', $metadata['mtime']);
$this->view->end = date('Y-m-d, H:i:s', $metadata['expire']);
$this->view->now = date('Y-m-d, H:i:s');
}
DataCache_ZwasServer and DataCache_Interface
As there are methods of a DataCache_ZwasServer instance called in the controller, we need to define this class and the appropriate methods. The class extends the ZwasServer which provides the base functionality for the XML-RPC client and it should implement the DataCache_Interface. In every method the parent::remoteAction is called – there the magic stuff happens: The XML-RPC service is requested. The class file should be saved in application/common/models/DataCache/ZwasServer.php.
class DataCache_ZwasServer extends ZwasServer implements
DataCache_Interface {
protected function getNameSpace() {
return 'DataCache_Interface';
}
/**
* @return array
*/
public function getCacheIdList() {
return $this->remoteAction(__FUNCTION__);
}
public function getCacheMetadata($id) {
return $this->remoteAction(__FUNCTION__, array($id));
}
public function getCacheData($id) {
return $this->remoteAction(__FUNCTION__, array($id));
}
public function getCacheMetadataAll() {
return $this->remoteAction(__FUNCTION__);
}
}
The interface just describes all the implemented XML-RPC service methods and is used by the client and by the server. The file should be saved in application/common/models/DataCache/Interface.php.
interface DataCache_Interface {
/**
* @return array
*/
public function getCacheIdList();
/**
* @param int $id
* @return array
*/
public function getCacheMetadata($id);
/**
* @param int $id
* @return array
*/
public function getCacheData($id);
/**
* @return array
*/
public function getCacheMetadataAll();
}
View Scripts
We obviously need view scripts for displaying the data, so I created overview.phtml and content.phtml, and saved them in application/PE/views/scripts/data-cache-control. I don’t want to go into detail here – I just copied & pasted some stylesheets and javascripts from the existing Zend Server GUI in order to have a consistent UI and I’m using the data which I put into the View object in the controller.
Now: <?php echo $this->now ?>
<table cellpadding="0" cellspacing="0" class="extensions t100">
<thead>
<tr class="header">
<th>Id</th>
<th>Start</th>
<th>End</th>
<th>Progress %</th>
<th class="w100 last">Progress</th>
</tr>
<tr class="header-separator">
<td colspan="5"> </td>
</tr>
</thead>
<tbody>
<?php foreach ($this->metadata as $id => $data) : ?>
<tr>
<td><a onclick="layoutManager.contentLoader('/ZendServer/Data-Cache-Control/content/datacacheContentId/<?php echo $id ?>', 'datacachecontrolcontent');"><?php echo $id ?></a></td>
<td class="no-wrap"><?php echo $data['start'] ?></td>
<td class="no-wrap"><?php echo $data['end'] ?></td>
<td class="no-wrap"><?php echo $data['progressRel'] ?> %</td>
<td class="no-wrap">
<div style="position: relative; float: left; width: <?php echo ($data['progressRel']) ?>%; height: 15px; background-color: #85b2ca; border: 1px solid #444a5f;"></div>
<div style="position: relative; float: left; width: <?php echo (99 - $data['progressRel']) ?>%; height: 15px; background-color: #e7ebe4; border: 1px solid #444a5f; border-left: 0px;"></div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<script type="text/javascript">
dojo.require("zend.widget.dialog");
</script>
<div id="action-bar" class="reset-margin-bottom">
<table cellpadding="0" cellspacing="0" border="0" class="w100">
<tr>
<td class="no-wrap" style="padding-right:0; width: 100px;">
Go to data cache content by Id:
</td>
<td class="no-wrap" style="padding-right:0; width: 100px;">
<form dojoType="zend.form.submitable"
id="datacache-content-by-id"
action="/ZendServer/Data-Cache-Control/content">
<input type="text" dojoType="zend.widget.input.text"
id="datacache-content-id"
name="datacacheContentId"
size="5"
value="<?php echo $this->escape(
$this->datacacheContentId); ?>"
/>
</form>
</td>
<td style="width: 100px;">
<?php echo $this->button(Zwas_Translate::_('Go'),
"dijit.byId('datacache-content-by-id').submit()",
'',
'datacache-content-issue-by-id'); ?>
</td>
<td> </td>
</tr>
</table>
</div>
<?php if ($this->noCacheHit && $this->datacacheContentId) : ?>
<br />
No valid cache content for id
'<?php echo $this->datacacheContentId ?>'!
<?php endif; ?>
<?php if ($this->data) : ?>
<br />
<table>
<tr>
<td>Start:</td>
<td><?php echo $this->start ?></td>
</tr>
<tr>
<td>End:</td>
<td><?php echo $this->end ?></td>
</tr>
<tr>
<td>Now:</td>
<td><?php echo $this->now ?></td>
</tr>
</table>
<h2>Data:</h2>
<?php echo $this->data ?>
<?php endif; ?>
Model (Server)
Ok, we implemented the client side of the XML-RPC request – it will fail because the service methods are not available yet.
DataCache_UserServer
The DataCache_UserServer class provides the data, which is originally requested in the DataCacheControlController. The file UserServer.php can also be saved in application/common/models/DataCache. I mentioned before that the frontend and the backend application is stored in the same directory structure. Big advantage: The DataCache_UserServer class can now easily implement the DataCache_Interface, because the Interface file is in the same directory.
The constructor instantiates a new Zx_Cache_Backend_ZendServer_Disk object –we need this object for fetching the metadata and the content of the cached item. As the cache id list is stored in the cache, it’s now possible to load it with the Zx_Cache_Backend_ZendServer_Disk::load() method and the key constant Zx_Cache_Backend_ZendServer_Disk::CACHE_ID_LIST_KEY. Other methods should be self-explanatory
class DataCache_UserServer implements DataCache_Interface {
private $_backend;
public function __construct() {
$this->_backend = new Zx_Cache_Backend_ZendServer_Disk();
}
public function getCacheIdList() {
$data =
$this->_backend
->load(
Zx_Cache_Backend_ZendServer_Disk::CACHE_ID_LIST_KEY
);
return $data;
}
/**
* @param string $id
* @return array
*/
public function getCacheMetadata($id) {
$data = $this->_backend->loadMetadata($id);
return $data;
}
/**
* @param string $id
* @return array
*/
public function getCacheData($id) {
$data = $this->_backend->load($id);
return $data;
}
/**
* @return array
*/
public function getCacheMetadataAll() {
$idList = $this->getCacheIdList();
sort($idList);
$tmp = array();
foreach ($idList as $id) {
$tmp[$id] = $this->getCacheMetadata($id);
}
return $tmp;
}
}
UserServer
Last but not least we need to extend the UserServer class (application/PE/models/UserServer.php). The UserServer::setClasses() method registers all XML-RPC service classes and the appropriate interfaces. The UserServer class is already implemented, but one has to add the last line to register the new DataCache Service.
class UserServer extends Common_UserServer {
protected function setClasses() {
$this->server->setClass('PhpGlobals_Api_UserServer',
'PhpGlobals_Api_Interface');
$this->server->setClass('PhpMemory_UserServer',
'PhpMemory_Interface');
$this->server->setClass('ZwasComponents_Util_Api_UserServer',
'ZwasComponents_Util_Api_Interface');
$this->server->setClass('ZwasComponents_DataCache_Api_UserServer',
'ZwasComponents_DataCache_Api_Interface');
$this->server
->setClass('ZwasComponents_Accelerator_Api_UserServer',
'ZwasComponents_Accelerator_Api_Interface');
$this->server->setClass('ZwasComponents_PageCache_Api_UserServer',
'ZwasComponents_PageCache_Api_Interface');
$this->server->setClass('ZwasComponents_JavaBridge_Api_UserServer',
'ZwasComponents_JavaBridge_Api_Interface');
$this->server->setClass('DataCache_UserServer',
'DataCache_Interface');
}
}
Test
Time to test: we need a simple application which uses the new cache backend adapter for storing items in the cache. The following short script creates cache objects and saves 20 $data arrays to the cache. The specific lifetime for every item is generated randomly (20 seconds to 360 seconds).
require_once 'Zend/Cache.php';
require_once 'Zx/Cache/Backend/ZendServer/Disk.php';
for ($i = 1; $i <= 20; $i++) {
$frontendOptions = array(
'lifetime' => 20 + (int) rand(0, 340),
'automatic_serialization' => true
);
$backend = new Zx_Cache_Backend_ZendServer_Disk();
// getting a Zend_Cache_Core object
$cache = Zend_Cache::factory('Core',
$backend,
$frontendOptions);
$data['time'] = time();
$data['zeit'] = date('Y-m-d H:i:s');
$cache->save($data, 'key'.$i);
}
Here you can see screenshots of the new Zend Server GUI after running the script above:


ToDo’s
This article is currently intended for experimental purposes. The code is not production ready! For example there is no proper error handling and the keys of the expired cache items are still in the item list.
But this article should give you some ideas on how to extend the Zend Server GUI. Of course it’s possible to adapt this implementation to different cache mechanisms. Why not display all cached memcache items? Or APC? One could also build in a remove or reset button for the items…
As you can see, there are many ideas on what can be achieved by extending the Zend Server GUI.




September 17, 2009 at 11:32 pm
Very nice tutorial on extending Zend’s Server GUI, but the problem is that every time that there is an update on Zend Server you have to redo the extension.
For an extension like the one described here it’s ok, not a great trouble but if you extend the GUI in many ways and you add some other modules too then it would be trouble for you to redo everything.
But in anyway it’a a great example on how to extend Zend Server’s GUI
Thanks
September 23, 2009 at 4:43 pm
@panosru: You’re absolutely right, a Zend Server update might destroy your extension (of course your own code is not supported by Zend), so a backup is recommended
. That’s why we’re thinking about a more pluggable solution: Just adding a new module folder with the extension code and modify the application config file. But currently I cannot publish a production ready solution – maybe soon…