In the new versions of Zend Framework (since 1.11.0, released 2 November 2010) it is possible to detect devices with the new Zend_Http_UserAgent component. The component detects the User-Agent of a client and the corresponding capabilities. This with the help of WURFL, Tera_WURFL or DeviceAtlas.

In my case the WURFL library is the easiest solution and sufficient for my use cases. With Zend_Http_UserAgent and WURFL, the possibility is availble to detect a mobile device and switch the layout and/or the view accordingly. Nonetheless it is not easy to accomplish this in a generic way. This article suggest the usage of a frontController plugin to detect the mobile devices and switch the layout. An action helper is also introduced to switch to mobile view scripts on an actionController's action base.

Installation of WURFL

First it is required to install the WURFL library and initiate the UserAgent application resource. These are the steps:

  1. Make sure you use Zend Framework 1.11.0 or above
  2. Create a Wurfl directory inside your /library/ (besides /library/Zend/ etc) and put all the WURFL files and subdirectories into this directory (so Application.php, CapabilitiesHolder.php etc).
  3. Create a data directory for WURFL (I called this /data/wurfl/) and put two files into this directory: the wurfl-latest.zip and the web_browsers_patch.xml [1]
  4. Create inside the data directory a cache subdirectory (/data/wurfl/cache/) and make this writable for Apache.
  5. Put the following rules in your application.ini to enable the UserAgent application resource:
resources.useragent.storage.adapter             = "Session"
resources.useragent.wurflapi.wurfl_api_version  = "1.1"
resources.useragent.wurflapi.wurfl_lib_dir      = APPLICATION_PATH "/../library/Wurfl/"
resources.useragent.wurflapi.wurfl_config_array.wurfl.main-file      = APPLICATION_PATH "/../data/wurfl/wurfl-latest.zip"
resources.useragent.wurflapi.wurfl_config_array.wurfl.patches[]      = APPLICATION_PATH "/../data/wurfl/web_browsers_patch.xml"
resources.useragent.wurflapi.wurfl_config_array.persistence.provider = "file"
resources.useragent.wurflapi.wurfl_config_array.persistence.dir      = APPLICATION_PATH "/../data/wurfl/cache/"

At this stage, the WURFL installation is complete and you can utilize the UserAgent device detection in your code [2].

Layouts

The next step is to create a frontController plugin which uses the UserAgent application resource and checks whether a mobile device views your website. At an early stage (I choose dispatchLoopStartup) the application resource is requested for its Zend_Http_UserAgent and a check is performd whether the device is categorized under "mobile" by WURFL.

<?php

class Soflomo_Controller_Plugin_Mobile extends Zend_Controller_Plugin_Abstract
{
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        $frontController = Zend_Controller_Front::getInstance();
        $bootstrap       = $frontController->getParam('bootstrap');

        if (!$bootstrap->hasResource('useragent')) {
            throw new Zend_Controller_Exception('The mobile plugin can only be loaded when the UserAgent resource is bootstrapped');
        }

        $userAgent = $bootstrap->getResource('useragent');
        // Load device settings, required to perform $userAgent->getBrowserType()
        $userAgent->getDevice();

        if ($userAgent->getBrowserType() === 'mobile') {
            if ($frontController->getParam('mobileLayout') === "1") {
                $suffix = $bootstrap->getResource('layout')->getViewSuffix();
                $bootstrap->getResource('layout')->setViewSuffix('mobile.' . $suffix);
            }

            if ($frontController->getParam('mobileViews') == "1") {
                Zend_Controller_Action_HelperBroker::getStaticHelper('MobileContext')->enable();
            }
        }
    }
}

If the device is a "mobile" device, the layout gets an additional suffix "mobile.". By default, the suffix is .phtml and when a mobile device visits your website, all layout scripts are changed into .mobile.phtml. If you changed your default suffix in something else, it is automatically taken into account. Make sure you have the mobile layouts available, otherwise you get an error about the missing scripts!

The plugin is also depending on application.ini settings. I found it useful to enable on an application level whether mobile layouts and views are a possibility. To enable them both, put these rules in your application.ini:

resources.frontController.params.mobileLayout = true;
resources.frontController.params.mobileViews  = true;

Views

The last step is to switch a view for an actionController's action to the appropriate script. I took the AjaxContext as an example: you can "ajaxify" actions by adding action contexts for specific actions. You are in control which action is ajaxable and which not. When the context is enabled, the suffix for the view script changes into .ajax.phtml when the action is requested with an XmlHttpRequest.

This is similar to the mobile context: you should be in control which action has a special view script. When this context is added to an action, the action view script changes to .mobile.phtml when the device is a mobile device.

To know whether a device is mobile or not, the frontController plugin enables the action helper. Only when the helper is enabled and the context is added to the action, the change in suffix takes place. In code, it looks like this:

<?php

class Soflomo_Controller_Action_Helper_MobileContext extends Zend_Controller_Action_Helper_ContextSwitch
{
    /**
     * Flag to enable layout based on WURFL detection
     * @var bool
     */
    protected $_enabled = false;

    /**
     * Controller property to utilize for context switching
     * @var string
     */
    protected $_contextKey = 'mobileable';

    /**
     * Whether or not to disable layouts when switching contexts
     * @var boolean
     */
    protected $_disableLayout = false;

    /**
     * Constructor
     *
     * Add HTML context
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
        $this->addContext('html', array('suffix' => 'mobile'));
    }

    /**
     * Enable the mobile contexts
     *
     * @return void
     */
    public function enable ()
    {
        $this->_enabled = true;
    }

     /**
     * Add one or more contexts to an action
     *
     * The context is by default set to 'html' so no additional context is required for mobile
     *
     * @param  string       $action
     * @return Zend_Controller_Action_Helper_ContextSwitch|void Provides a fluent interface
     */
    public function addActionContext($action, $context = 'html')
    {
        return parent::addActionContext($action, $context);
    }

    /**
     * Initialize AJAX context switching
     *
     * Checks for XHR requests; if detected, attempts to perform context switch.
     *
     * @param  string $format
     * @return void
     */
    public function initContext($format = 'html')
    {
        $this->_currentContext = null;

        if (false === $this->_enabled) {
            return;
        }

        return parent::initContext($format);
    }
}

The similarity with the AjaxContext is huge. To enable the views for a specific action (in this case "index"), you should add the action context and initiate the context as follows:

public function init ()
{
    $mobileContext = $this->_helper->getHelper('MobileContext');
    $mobileContext->addActionContext('index')
                  ->initContext();
}

And that's all. It is now very easy to add mobile views and layouts:

  1. Set mobileLayout to true in your application.ini and your layouts automatically change to mobile versions (when necessary, of course);
  2. Set mobileViews to true & add the context to your actions and your views automatically change to mobile versions (when necessary, of course).

Notes

[1] These files can be found in the WURFL package in the tests/resources/ directory

[2] See the Zend Framework manual for the examples and in-depth information