View Source

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{zone-template-instance:ZFDEV:Zend Proposal Zone Template}
{composition-setup}

{zone-data:component-name}
Zend_Controller_Action_Helper_AjaxContext
{zone-data}

{zone-data:proposer-list}
[~matthew]
{zone-data}

{zone-data:revision}
0.8.0 - 03 January 2008: Initial proposal
0.9.0 - 04 January 2008: Updated proposal based on user feedback
0.9.1 - 04 January 2008: Added ContextSwitch helper to proposal; refactored to allow setting arbitrary context types
0.9.2 - 04 January 2008: Updated use cases to follow current laboratory code
0.9.3 - 06 January 2008: Updated use cases and skeletons to show getCurrentContext() method
0.9.4 - 07 January 2008: Updated UC-01 per Darby's comments
0.9.5 - 08 January 2008: Updated with feedback from Zend team; documents context enabling, adds support for building action controller context arrays, adds support for multiple headers, updates requirements to specify that the context will be prepended to existing view suffix for context suffix
{zone-data}

{zone-data:overview}
Zend_Controller_Action_Helper_AjaxContext enables view context switching and sets appropriate response headers when XmlHttpRequests are detected.
{zone-data}

{zone-data:references}
* None
{zone-data}

{zone-data:requirements}
* This component *will* be optionally loaded, on-demand, as an action helper.
* This component *will* disable layouts when XHR requests are detected.
* This component *will* use properties from the current action controller and request object to determine if context switching should occur.
* This component *will* utilize view script suffixes and HTTP response headers to accomplish context switching.
* This component *will* default to HTML responses unless otherwise requested.
* This component *will* use a *configurable* request parameter to determine the response context to utilize.
* This component *will* allow configuration of the headers to send for the contexts supported.
** This component *will* allow multiple configurable headers per context
* This component *will* allow configuring the view suffix to use when context switching.
** This component *will* simply prepend the suffix previously configured in the ViewRenderer with the new format.
** This component *will* provide helper methods for creating and configuring the action controller context array
{zone-data}

{zone-data:dependencies}
* Zend_Controller_Action_Helper_Abstract
* Zend_Controller_Action_Helper_ContextSwitch (part of this proposal)
* Zend_Controller_Action_Helper_ViewRenderer
* Zend_Controller_Request_Http
* Zend_Controller_Response_Http
* Zend_Layout
{zone-data}

{zone-data:operation}
Zend_Controller_Action_Helper_AjaxContext is designed to make detecting XmlHttpRequests and switching view context trivial. When called, it checks to see if the request is an XHR request; if so, it then disables layouts. A check is made on the 'ajaxable' property of the associated action controller, and if the current action is in that list, it then determines the response format requested; the viewRenderer is then updated with a new view script suffix representing the format, and appropriate response headers are set.

View scripts for handling AJAX contexts still end in the default view suffix, but have an additional suffix prepended. For instance, if the ViewRenderer's default suffix is '.phtml', and an XML context is selected, the new suffix will be 'xml.phtml'; the JSON context becomes 'json.phtml', and HTML becomes 'ajax.phtml' (to differentiate from the standard HTML returned by the action).

AjaxContext would itself extend another helper, ContextSwitch. This does the heavy lifting; AjaxContext itself only performs the isXmlHttpRequest() check. It also allows specifying arbitrary contexts via accessors.

To select a context, you simply add an extra parameter to the request. By default, this parameter is 'format'. As an example, if you were visiting the path /foo/bar and wanted the XML context, you could specify /foo/bar?format=xml, or, using path parameters as supported by the default router, /foo/bar/format/xml.
{zone-data}

{zone-data:milestones}
* Milestone 1: \[DONE\] Initial proposal published for review
* Milestone 2: \[DONE\] Tests and initial code checked into the laboratory
* Milestone 3: Final code with >80% coverage checked into incubator
* Milestone 4: Documentation completed
* Milestone 5: Promotion to core
{zone-data}

{zone-data:class-list}
* Zend_Controller_Action_Helper_ContextSwitch
* Zend_Controller_Action_Helper_AjaxContext
{zone-data}

{zone-data:use-cases}
{deck:id=Use Cases}
{card:label=UC-01}
The most basic use case simply defines actions that are AJAX-aware, and adds a call to the AjaxContext helper to initialize the context switch. The developer then needs only create appropriately-named view scripts for handling the requested context. In the example below, the 'bar' and 'bat' actions are considered AJAX-aware, and would require one or more additional view scripts, based on the formats the developer wishes to support.

In the example below, we configure the 'bar' action to return HTML to the XHR, and the 'bat' action to return JSON or XML. The 'all' action can return any context. This requires the following view scripts:
{code}
views/
scripts/
foo/
all.phtml
all.ajax.phtml
all.json.phtml
all.xml.phtml
bar.phtml
bar.ajax.phtml
baz.phtml
bat.phtml
bat.json.phtml
bat.xml.phtml
{code}

{code:php}
class FooController extends Zend_Controller_Action
{
/**
* Actions that allow an AJAX context
*
* - 'bar' allows only HTML context
* - 'bat' alows 'xml' and 'json' contexts
* - 'all' allows any context
*/
public $ajaxable = array(
'bar' => array('html'),
'bat' => array('xml', 'json')
'all' => true
);

public function preDispatch()
{
$this->_helper->ajaxContext()->initContext();
}

public function barAction()
{
}

public function bazAction()
{
}

public function batAction()
{
}

public function allAction()
{
}
}
{code}
{card}

{card:label=UC-02}
You may wish to modify the view suffix used for the context switch, or even the response header sent. Each of the contexts supported has methods for this. In the following example, we'll modify the XML context to use the suffix '.xml', and send a response header of Content-Type 'application/xml':
{code:php}
class FooController extends Zend_Controller_Action
{
/**
* Actions that allow an AJAX context
*/
public $ajaxable = array(
'bar' => array('html'),
'bat' => array('json', 'xml')
);

public function preDispatch()
{
$ajaxContext = $this->_helper->getHelper('ajaxContext');
$ajaxContext->setSuffix('xml', 'xml')
->setHeader('xml', 'Content-Type', 'application/xml');
$ajaxContext->initContext();
}
}
{code}
{card}

{card:label=UC-03}
You may want to force all AJAX requests in a given controller to send the same format (for instance, if all of them would send HTML responses). This can be done by passing the $format parameter to the initContext() method:

{code:php}
class FooController extends Zend_Controller_Action
{
/**
* Actions that allow an AJAX context
*/
public $ajaxable = array('bar' => array('xml', 'html'), 'bat' => array('json', 'html'));

public function preDispatch()
{
// Force HTML context for AJAX responses:
$this->_helper->ajaxContext()->initContext('html');
}
}
{code}
{card}

{card:label=UC-04}
While this proposal primarily deals with AJAX context switching, the parent class, ContextSwitch, allows you to perform arbitrary context switching -- which can be useful when serving REST or supporting other return formats. In this example, we'll show such context switching, as well as examine how to support arbitrary formats, in this case YAML.

Unlike the AjaxContext helper, it uses the 'contexts' property of the controller to define the contexts available; this allows you to have separate contexts for AJAX actions versus other contexts.

{code:php}
class FooController extends Zend_Controller_Action
{
/**
* Actions with multiple contexts
*/
public $contexts = array(
'bar' => array('xml'),
'bat' => array('xml', 'yaml')
);

public function preDispatch()
{
$contextSwitch = $this->_helper->getHelper('contextSwitch');

// Add YAML context:
$contextSwitch->addContext('yaml', 'yaml.phtml', 'Content-Type', 'application/x-yaml');

// Enable context switching:
$contextSwitch->initContext();
}
}
{code}
{card}

{card:label=UC-05}
In your action, you may need to know if a context switch has occurred, and/or what context is currently active, in order to branch the logic. The API has a getCurrentContext() method which returns null if no context switch has occurred, and the string context if one has:

{code:php}
class FooController extends Zend_Controller_Action
{
/**
* Actions that allow an AJAX context
*
* - 'bar' allows only HTML context
* - 'bat' alows 'xml' and 'json' contexts
*/
public $ajaxable = array(
'bar' => array('html'),
'bat' => array('xml', 'json');

public function preDispatch()
{
$this->_helper->ajaxContext()->initContext();
}

public function barAction()
{
}

public function bazAction()
{
}

public function batAction()
{
$context = $this->_helper->ajaxContext()->getCurrentContext();
switch ($context) {
case 'xml':
// XML-related processing here
break;
case 'json':
// JSON-related processing here
break;
case null:
default:
// No context switch occurred; default processing rules:
break;
}
}
}
{code}
{card}

{deck}
{zone-data}

{zone-data:skeletons}
{deck:id=Skeletons}
{card:label=Zend_Controller_Action_Helper_ContextSwitch}
{code:php}
class Zend_Controller_Action_Helper_ContextSwitch extends Zend_Controller_Action_Helper_Abstract
{
protected $_contexts = array(
'json' => array(
'suffix' => 'json.phtml',
'headers' => array(
'Content-Type' => 'application/json'
)
),
'xml' => array(
'suffix' => 'xml.phtml',
'headers' => array(
'Content-Type' => 'text/xml'
)
),
);

protected $_defaultContext = 'xml';

protected $_disableLayout = true;

/**
* Initialize context detection and switching
*/
public function initContext($format = null);

/**
* Customize view script suffix to use when switching context.
*
* Passing an empty suffix value to the setters disables the view script
* suffix change.
*/
public function setSuffix($type, $suffix);
public function getSuffix($type);

/**
* Customize response headers to use when switching context
*/
public function addHeader($context, $header, $content);
public function setHeader($context, $header, $content);
public function addHeaders($context, array $headers);
public function setHeaders($context, array $headers);
public function getHeader($context, $header);
public function getHeaders($context = null);
public function removeHeader($context, $header);
public function clearHeaders($context = null);

/**
* Set name of parameter to use when determining context format
*/
public function setContextParam($name);
public function getContextParam();

/**
* Indicate default context to use when no context format provided
*/
public function setDefaultContext($type);
public function getDefaultContext();

/**
* Set flag indicating if layout should be disabled
*/
public function setAutoDisableLayout($flag);
public function getAutoDisableLayout();

/**
* Manipulate contexts
*/
public function addContext($type, $suffix = null, $headerName = null, $headerContent = '');
public function setContext($type, $suffix = null, $headerName = null, $headerContent = '');
public function getContexts();
public function removeContext($type);
public function clearContexts();

/**
* Manipulate and create context associations in action controller
*/
public function addActionContext($action, $context);
public function setActionContext($action, $context);
public function addActionContexts($action, array $contexts);
public function setActionContexts($action, array $contexts);
public function getActionContexts($action = null);
public function removeActionContext($action, $context = null);
public function clearActionContexts($action = null);

/**
* Determine current context, if any, after initContext() has been run
*/
public function getCurrentContext();
}
{code}
{card}

{card:label=Zend_Controller_Action_Helper_AjaxContext}
{code:php}
class Zend_Controller_Action_Helper_AjaxContext extends Zend_Controller_Action_Helper_ContextSwitch
{
protected $_defaultContext = 'html';

public function __construct()
{
$this->addContext('html', 'ajax.phtml');
}

public function initContext($format = null)
{
if (!$this->getRequest()->isXmlHttpRequest()) {
return;
}

return parent::initContext($format);
}
}
{code}
{card}
{deck}
{zone-data}

{zone-template-instance}]]></ac:plain-text-body></ac:macro>