Zend Framework Transform View
Posted by John Kleijn • Wednesday, July 30. 2008 • Category: Zend FrameworkI implemented Transform View (XSLT) for Zend Framework. It's a relatively basic class that allows layout templates and calling Zend Framework View Helpers from XSL templates. Read on to view the class and some basic explanation.
<?php
/**
* Implementation of Transform View for Zend Framework
*
*/
class Foo_View_Transform extends Zend_View_Abstract
{
/**
* XSLT processor
*
* @var XSLTProcessor
*/
private $_processor;
/**
* Data in XML representation
*
* @var DOMDocument
*/
private $_dataDoc;
/**
* XSL Stylesheet
*
* @var DOMDocument
*/
private $_xslDoc;
/**
* Model data
*
* @var array
*/
private $_data = array();
/**
* Disable use of layout flag
*
* @var bool
*/
private $_layoutDisabled;
/**
* Allow template recursion flag
*/
private static $_allowRecursion = false;
/**
* When false, clearVars is ignored
*/
private $_allowClearVars = true;
/**
* Instance in use while rendering
*
* When not rendering this is always NULL
*
* @var Foo_View_Transform
*/
private static $_instance;
/**
* Array of sheets previously rendered
*
* Used for recursion detection
*
* @var array
*/
private static $_renderedSheets = array();
/**
* Constructor.
*
* @param array $config Configuration key-value pairs.
*/
public function __construct($config = array())
{
if(!extension_loaded('xsl'))
{
throw new Foo_Exception("The XSL extension is not loaded.");
}
parent::__construct($config);
$this->_setup();
}
/**
* Setup the view's docs and processor
*
*/
private function _setup()
{
$this->_processor = new XSLTProcessor();
$this->_xslDoc = new DOMDocument();
$this->_dataDoc = new DOMDocument();
$this->_processor->registerPHPFunctions();
}
/**
* Allow the same stylesheet to be rendered twice or more
*
* Note that while this flag is disabled, the rendered scripts
* are not kept track of
*
* @param bool $bool
*/
public static function allowRecursion($bool = true)
{
self::$_allowRecursion = $bool;
}
/**
* When set to false, clearVars() will be ignored
*
* @param bool $bool
*/
public function allowClearVars($bool = true)
{
$this->_allowClearVars = $bool;
}
/**
* Call a helper in XSL sheets, returning as string
*
* Trying to call the Action View Helper will cause an infinite loop
*
* Use Foo_View_Transform::callAction() instead
*
* @param helper name $name
* @return string
*/
public static function helper()
{
$args = func_get_args();
$name = array_shift($args);
$helper = self::$_instance->getHelper($name);
return call_user_func_array(array($helper, $name), $args);
}
/**
* Proxies to helper() but returns DOMDocument
*
* Wraps a DIV around the generated XML
*
* @todo Lose the DIV and make sure the XML is still well-formed
*
* @param helper name $name
* @return DOMDocument
*/
public static function xmlHelper()
{
$args = func_get_args();
$domDoc = call_user_func_array(array('Foo_View_Transform', 'helper'), $args);
if(!$domDoc instanceof DOMDocument)
{
$xml = $domDoc;
$domDoc = new DOMDocument();
$domDoc->loadXML("<div>$xml</div>");
}
return $domDoc;
}
/**
* Shortcut for the Action View Helper in XSL sheets
*
* Use disable-output-escaping="yes" in the calling stylesheet to insert
* fragments of XML
*
* @param string $action
* @param string $controller
* @param string $module
*
* @return string
*/
public static function callAction($action, $controller = null, $module = null, $clearVars = true)
{
$params = func_get_args();
if(count($params) > 3)
{
$params = array_slice($params, 3);
}
$clearVars = (bool) $clearVars;
/**
* Do some instance juggling
*/
$instance = self::$_instance;
$oldClearVars = $instance->_allowClearVars;
$instance->allowClearVars($clearVars);
$actionHelper = $instance->getHelper('action');
$reponseBody = $actionHelper->action($action, $controller, $module, $params);
self::$_instance = $instance;
/**
* Reset clearVars
*/
self::$_instance->allowClearVars($oldClearVars);
return $reponseBody;
}
/**
* Processes a view script and returns the output.
*
* @param string $name The script script name to process.
* @return string The script output.
*/
public function render($name)
{
return $this->_run($this->_script($name));
}
/**
* Render the data with given stylesheet
*
* @todo Use configurable layout path/file name
*
* @param string $name
* @return string
*/
protected function _run()
{
self::$_instance = $this;
$scriptPath = func_get_arg(0);
$this->_checkRecursion(realpath($scriptPath));
if(!$this->layoutIsDisabled())
{
$baseScriptDir = realpath(dirname($scriptPath) . '/..');
$layoutScript = $baseScriptDir . DIRECTORY_SEPARATOR . 'layout.xsl';
$this->_checkRecursion($layoutScript);
$this->_xslDoc->load($layoutScript);
/**
* Create import elements at the top of the document
*/
$import = $this->_xslDoc->createElementNs("http://www.w3.org/1999/XSL/Transform", 'xsl:import');
$this->_xslDoc->documentElement->insertBefore(
$import, $this->_xslDoc->documentElement->firstChild
);
$import->setAttribute('href',
str_replace(
array(
$baseScriptDir . DIRECTORY_SEPARATOR,
''
),
array(
'',
'/'
),
realpath($scriptPath)
)
);
}
else
{
$this->_xslDoc->load(realpath($scriptPath));
}
$this->_dataDoc = $this->_buildDataDoc();
$this->_processor->importStyleSheet($this->_xslDoc);
$xml = $this->_processor->transformToXML($this->_dataDoc);
self::$_instance = null;
return $xml;
}
/**
* Assert that the script has not yet been rendered
*
* @param string $realPath
*/
private function _checkRecursion($realPath)
{
if(!self::$_allowRecursion)
{
if(in_array($realPath, self::$_renderedSheets))
{
throw new Zend_View_Exception("Recursion detected rendering stylesheet '$realPath'.");
}
self::$_renderedSheets[] = $realPath;
}
}
/**
* Build an XML document from the internal data,
* and return an instance of DOMDocument loaded with it
*
* @return DOMDocument
*/
private function _buildDataDoc()
{
$this->_dataDoc->loadXML($this->renderData());
return $this->_dataDoc;
}
/**
* Render the internal data as an XML string
*
* @return string
*/
public function renderData()
{
return "<?xml version=\\"1.0\\"?><data>{$this->_buildXmlStringFromData()}</data>";
}
/**
* Iterate over the internal data,
* returning a basic XML string
*
* @param array $data
*
* @return string
*/
private function _buildXmlStringFromData($data = null)
{
$string = '';
if($data === null)
{
$data = $this->_data;
}
foreach($data as $key => $value)
{
if(is_numeric($key))
{
$key = 'array';
}
$string .= "<$key>";
if(is_array($value) || $value instanceof Traversable)
{
$string .= $this->_buildXmlStringFromData($value);
}
else
{
$string .= "<![CDATA[$value]]>";
}
$string .= "</$key>";
}
return $string;
}
/**
* Set the use of the layout stylesheet
* disabled (or enabled)
*
* @param bool $bool
*/
public function disableLayout($bool = true)
{
$this->_layoutDisabled = $bool;
}
/**
* Check if layout is disabled
*
* @return bool
*/
public function layoutIsDisabled()
{
return $this->_layoutDisabled;
}
/**
* Get the data document (NULL before render)
*
* @return DOMDocument
*/
public function getDataDoc()
{
return $this->_dataDoc;
}
/**
* Get the data document (NULL before render)
*
* @return DOMDocument
*/
public function getXslSheet()
{
return $this->_xslDoc;
}
/**
* Get the data document XML of the last render
*
* @return string
*/
public function getDataXml()
{
if($this->_dataDoc === null)
{
$this->_buildDataDoc();
}
return $this->_dataDoc->saveXML();
}
/**
* Assigns values to the internal data via differing strategies.
*
* Zend_View::assign('name', $value) assigns a variable called 'name'
* with the corresponding $value.
*
* Zend_View::assign($array) assigns the array keys
* names (with the corresponding array values).
*
* @param string
* @param mixed $value
*
* @return Zend_View_Abstract Fluent interface
*
* @throws Zend_View_Exception if $spec is neither a string nor an array,
* or if an attempt to set a private or protected member is detected
*/
public function assign($keyOrArray, $value = null)
{
if (is_string($keyOrArray))
{
$this->_data[$keyOrArray] = $value;
}
elseif(is_array($keyOrArray))
{
$this->_data = array_merge($this->_data, $keyOrArray);
}
else
{
throw new Zend_View_Exception('assign() expects a string or array, received ' . gettype($keyOrArray), $this);
}
return $this;
}
/**
* Removes values
*
* @param string $key
*
* @return Zend_View_Abstract Fluent interface
*/
public function unAssign($key)
{
unset($this->_data[$key]);
return $this;
}
/**
* Allows acces to internal data as if it we're a property
*
* @param string $key
* @return null
*/
public function __get($key)
{
return $this->_data[$key];
}
/**
* Allows testing with empty() and isset()
* to work on internal data
*
* @param string $key
* @return boolean
*/
public function __isset($key)
{
return isset($this->_data[$key]);
}
/**
* Assigns a variable to the data document.
*
* @param string $key The variable name.
* @param mixed $value The variable value.
* @return void
*/
public function __set($key, $value)
{
$this->assign($key, $value);
}
/**
* Allows unset() on object properties to work
*
* @param string $key
*
* @return void
*/
public function __unset($key)
{
$this->unAssign($key);
}
/**
* Clear all assigned variables
*
* This call is ignored when _allowClearVars is set to FALSE
*
* Clears all variables assigned to Zend_View either via {@link assign()} or
* property overloading ({@link __set()}).
*
* @return void
*/
public function clearVars()
{
if(!$this->_allowClearVars)
{
return;
}
foreach($this->_data as $key => $value)
{
$this->unAssign($key);
}
}
/**
* Invoke _setup and helper/view references upon cloning
*
* @todo Fix the action helper discrepancy
*
*/
public function __clone()
{
$this->_setup();
/**
* Re-setup helpers
*/
foreach($this->getHelpers() as $helper)
{
if($helper instanceof Zend_View_Helper_Action)
{
continue;
}
if(method_exists($helper, 'setView'))
{
$helper->setView($this);
}
}
}
}
You'll find two @todo's in there, because, well, I haven't felt the need to fix it yet.
You'll notice the helper() and callAction() method. The problem with calling php from XSL templates is that it doesn't appear to exist in any scope. Meaning you're limited to globally accessible procedures, in other words, static methods and global functions. With the static instance trick I pull, I'm able to call helpers from XSL templates like this:
Now that calls the Action View Helper (for which I had to implement some special case logic), but using helper() you can call any helper. Some of that special case logic involves restoring the instance after calling the Action View Helper. If I didn't do that, the instance would be NULL, because the helper's own dispatch loop would overwrite the static instance.



ShareThis