jurian sluiman

{
Wissel naar

Mobile detection with Zend_Http_UserAgent

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

Comments

John Gills

Nice solution.

In addition to the mobile layout, I have different modules layouts which I switch in the bootstrap.

For my needs, it makes sense to do this:

$layout = Zend_Layout::getMvcInstance();
$layoutName = $layout->getLayout();
$layout->setLayout('mobile.' . $layoutName);

Wolfgang

Hey Jurian!
Amazing tutorial, you did a great job. There is however one little thing. (Maybe it changed in some newer version)

If you write the persistence.dir -parameter, you'll need to add another ".dir" to make it work, otherwise the configuration will not be recognized... So the last line of your ini-file would look like this:
resources.useragent.wurflapi.wurfl_config_array.persistence.dir.dir = APPLICATION_PATH "/../data/wurfl/cache/"

Jurian Sluiman

Hi Wolfgang,

I noticed the Wurl options changed at a later version from ZF. I need to make a notice about that for other users. Which version of ZF do you use?

Wolfgang

ZF-VERSION = '1.11.7';

application.xml now looks like this (and works):

<persistence>
<provider>File</provider>
<dir>
<dir>
path/to/data/wurfl/cache/
</dir>
</dir>
</persistence>

Everything else worked just fine. Thanks a lot...

Austin

Where are the class files for "Soflomo_Controller_Plugin_Mobile" and "Soflomo_Controller_Action_Helper_MobileContext" supposed to be stored?

Jurian Sluiman

Hi Austin,

The Soflomo_* files are another library we use besides the Zend library, which contains custom components. To use both these files, you should also create a directory for the Soflomo library.

Your root consists probably of an application/, public/ and library/ folder (and perhaps some more). Inside library/ you have a Zend/ folder with the Zend library code. Create another folder Soflomo (so library/Soflomo). Inside this, you need to place the two files:
Soflomo_Controller_Plugin_Mobile is placed in library/Soflomo/Controller/Plugin/Mobile.php and Soflomo_Controller_Action_Helper_MobileContext in library/Soflomo/Controller/Action/Helper/MobileContext.php

To enable the Soflomo library, you must add a rule to the application.ini to load Soflomo_* files:

autoLoaderNameSpaces[] = "Soflomo_"

To enable the frontcontroller plugin, you might add something like this:

resources.frontController.plugins.mobile = "Soflomo_Controller_Plugin_Mobile"

To enable action controller helpers, you might add something like this:

resources.frontController.actionHelperPaths.Soflomo_Controller_Action_Helper = "Soflomo/Controller/Action/Helper/"

Hope this helps a bit?

peter

Jurian, thanks a lot for the tutorial, it really helped a lot.
Just one question: how can I force ZF to always load the mobile view if it exists? I other words, for mobile it should check if the mobile specific view exists and if yes, load it.

Here is how I tried to do it inside the plugin's dispatchLoopStartup() method:

$viewRenderer = Zend_Controller_Action_HelperBroker::getExistingHelper('ViewRenderer');
$mobileView = APPLICATION_PATH . "/views/scripts/" . str_replace($viewRenderer->getViewSuffix(), "mobile.".$viewRenderer->getViewSuffix(), $viewRenderer->getViewScript());

if(Zend_Loader::isReadable($mobileView)) {
$viewPath = $viewRenderer->getViewScriptPathSpec();
$viewRenderer->setViewScriptPathSpec(str_replace(":suffix", "mobile.:suffix", $viewPath));
}

And it works fine until I try to load different view for action, e.g.:

public function indexAction() {
$this->view->form = new Application_Form_Auth_Login();
$this->render('login');
}

In this case it loads login.phtml instead of login.mobile.phtml

Thanks a lot for any help!

Jurian Sluiman

Hi Peter,

Resolving a template in Zend Framework 1 is a bit hard to replace. You could override the view renderer, but I can't come up with anything else.

The problem is you can set a view suffix, but it always uses that suffix then. So replace .phtml by .mobile.phtml is breaking your application when no .mobile.phtml file exists but only a .phtml file.

You could override the view renderer and use your own instead. I know the Zend Framework 2 view layer is much better to extend, so it is much simpler to add this kind of feature in this next major release. However, Zend Framework 2 is still in beta and not stable until the summer (at least).

zlippr

hi your article greatly help me, thanks alot, i have few questions on mobile template. So please keep in touch with my email.Thanks again ;-)

zlippr

hi when my url is point at localhost/public/ its load my default layout but when point at localhost/public/index/abouts i can load a mobile version views...where i made mistakes?

Jurian Sluiman

zlippr, have you correctly enabled the mobile context for the actions you want? Check the last part with the "init()" function at the bottom of the page. You need that in every controller and specify every action there explicitly.

zlippr

hi i have done what you say but how about layout.mobile.phtml, when i cal <?= $this->layout()->content ?> it loads default layout content..pls help me on this. thanks

Erik

Thank you. Very nice solution!

Place a comment

If you have a user account for this site, you can login to click here.

Please note your comment below will be removed if you login!

 
 

The address is stored internally but not displayed on this site. We will respect your privacy.

In your message no html is allowd. A blank line creates a new paragraph, an url gets a hyperlink.