Universal Mobilization with WAPL

By Gordon Pettey (petteyg)
Some potential future users of my current project requested mobile accessibility. Your users might appreciate the same. I found WAPL to be a good method for providing such accessibility.
This is now available as a plugin at http://github.com/petteyg/wapl
While searching for CakePHP and mobile accessibility options, I came across a tutorial by rgubby, http://bakery.cakephp.org/articles/view/mobilize-your-cake-app-in-minutes. I found it useful, and have improved upon the idea. My version consists of a Component and Helper. The Component determines whether the device is mobile, and the Helper converts the output appropriately.

The Component has a few options:
  • devKey - Required. Get one at http://wapple.net/architect.htm.
  • force - Optional. Default false. If true, Component will submit device headers to WAPL on every request.
  • path - Optional. Default true. If false, Component will set Controller layout/view extension. If true, Component will set Controller layout/view path.
  • test - Optional. Default false. If true, Component will always load Helper and Helper will submit output to WAPL. If 'pre', Component will load Helper, but Helper will not submit output to WAPL.

Component Class:

Download code <?php 
class WaplComponent extends Object {

    var 
$components = array('RequestHandler''Session');
    var 
$devKey;
    var 
$force false;
    var 
$path true;
    var 
$test false;
    
    function 
initialize(&$controller$settings = array()) {
        
$this->controller =& $controller;
        
$this->devKey $settings['devKey'];
        if (
array_key_exists('force'$settings)) {
            
$this->force $settings['force'];
        }
        if (
array_key_exists('path'$settings)) {
            
$this->path $settings['path'];
        }
        if (
array_key_exists('test'$settings)) {
            
$this->test $settings['test'];
        }
    }
    
    function 
startup(&$controller) {
        if (!
$this->Session->check('User.isMobile') || $this->force) {
            
$sClient = @new SoapClient('http://webservices.wapple.net/wapl.wsdl');
            if(
$sClient) {
                
$headers = array();
                foreach(
$_SERVER as $k => $v) {
                    
$headers[] = array('name' => $k'value' => $v);
                }
                
$params = array(    
                    
'devKey' => $this->devKey,
                    
'deviceHeaders' => $headers
                
);
            }
            
$isMobile false;
            if(
$sClient->isMobileDevice($params)) {
                
$isMobile true;
            }
            if (!
$this->force) {
                
$this->Session->write('User.isMobile'$isMobile);
            }
        }
        if (
$this->Session->read('User.isMobile') || $this->test) {
            
$this->RequestHandler->respondAs('xml');
            if (
$this->path) {
                
$this->controller->layoutPath .= 'wapl';
                
$this->controller->viewPath .= DS.'wapl';
            } else {
                
$this->controller->ext '.wapl';
            }
        }
    }

    function 
beforeRender(&$controller) {
        if (
$this->Session->read('User.isMobile') || $this->test) {
            
$helpers array_diff($this->controller->helpers, array('Wapl'));
            
$helpers['Wapl'] = array('devKey' => $this->devKey'test' => $this->test);
            
$this->controller->helpers $helpers;
        }
    }

}
?>
Do not place the Helper in var $helpers. The Component will load (or not load) it automatically, as needed.

Helper Class:

Download code <?php 
class WaplHelper extends AppHelper {

    var 
$tags = array(
        
'cell' => "<cell>%s</cell>\n",
        
'chars' => "<chars%s>\n<value>%s</value>\n</chars>\n",
        
'css' => "<css>\n%s</css>\n",
        
'easyChars' => "<easyChars>\n<value>%s</value>\n</easyChars>\n",
        
'externalImage' => "<externalImage%s>%s</externalImage>",
        
'head' => "<head>\n%s</head>\n",
        
'item' => "[*]%s[/*]\n",
        
'layout' => "<layout>\n%s</layout>\n",
        
'list' => "[listFIXME]\n%s\n"// Remove FIXME. Added because Bakery parses list as BBcode.
        
'row' => "<row>\n%s\n</row>\n",
        
'span' => "[span=%s]%s[/span]",
        
'title' => "<title>%s</title>\n",
        
'url' => "<url>%s</url>",
        
'words' => "<wordsChunk>\n<display_as>%s</display_as>\n<quick_text>%s</quick_text>\n</wordsChunk>\n",
    );
    
    var 
$devKey;
    var 
$test false;

    function 
__construct($settings) {
        
$this->devKey $settings['devKey'];
        
$this->test $settings['test'];
    }
    
    function 
_parseAttributes($data) {
        
$attributes '';
        foreach(
$data as $name => $value) {
            
$attributes .= ' '.$name.'="'.$value.'"';
        }
        return 
$attributes;
    }
    
    function 
_parseItems($data) {
        
$items '';
        if (
is_array($data)) {
            foreach(
$data as $item) {
                
$items .= sprintf($this->tags['item'], $item);
            }
        } else {
            
$items .= sprintf($this->tags['item'], $data)."\n";
        }
        return 
$items;
    }
    
    function 
_parseUrls($data) {
        
$urls '';
        if (
is_array($data)) {
            foreach(
$data as $url) {
                
$urls .= sprintf($this->tags['url'], $url)."\n";
            }
        } else {
            
$urls .= sprintf($this->tags['url'], $data)."\n";
        }
        return 
$urls;
    }

    function 
afterLayout() {
        if (
$this->test != 'pre') {
            
$View =& ClassRegistry::getObject('view');
            
$sClient = @new SoapClient('http://webservices.wapple.net/wapl.wsdl');
            
$headers = array();
            foreach(
$_SERVER as $k => $v) {
                
$headers[] = array('name' => $k'value' => $v);
            }
            if(
$sClient) {
                
$params = array(
                    
'devKey' => $this->devKey,
                    
'deviceHeaders' => $headers,
                    
'wapl' => $View->output
                
);
            }
            
$xml simplexml_load_string($sClient->getMarkupFromWapl($params));
            foreach (
$xml->header->item as $v) {
                
header($v);
            }
            
$View->output trim($xml->markup);
        }
    }

    function 
chars($data$options = array()) {
        return 
$this->output(sprintf($this->tags['chars'], $this->_parseAttributes($options), $data));
    }
    
    function 
css($data) {
        return 
$this->output(sprintf($this->tags['css'], $this->_parseUrls($data)));
    }

    function 
easyChars($data) {
        return 
$this->output(sprintf($this->tags['easyChars'], $data));
    }

    function 
externalImage($data$options = array()) {
        return 
$this->output(sprintf($this->tags['externalImage'], $this->_parseAttributes($options), sprintf($this->tags['url'], $data)));
    }

    function 
head($data) {
        return 
$this->output(sprintf($this->tags['head'], $data));
    }

    function 
layout($data) {
        return 
$this->output(sprintf($this->tags['layout'], $data));
    }
    
    function 
ul($data) {
        return 
$this->output(sprintf($this->tags['list'], $this->_parseItems($data)));
    }
    
    function 
span($data) {
        return 
$this->output(sprintf($this->tags['span'], $data));
    }

    function 
title($data) {
        return 
$this->output(sprintf($this->tags['title'], $data));
    }
    
    function 
wapl($data) {
        return 
$this->output(sprintf('<'.'?xml version="1.0" encoding="UTF-8" ?'.'>'."\n".'<wapl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://wapl.wapple.net/wapl.xsd">'."\n".'%s</wapl>'."\n"$data));
    }

    function 
waplend() {
        return 
$this->output('</wapl>');
    }
    
    function 
waplstart() {
        
$begin '<'.'?xml version="1.0" encoding="UTF-8" ?'.'>';
        
$begin .= '<wapl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://wapl.wapple.net/wapl.xsd">';
        return 
$this->output($begin);
    }
    
    function 
words($data$type 0) {
        switch(
$type) {
            case 
1:
                
$type 'h1';
                break;
            case 
2:
                
$type 'h2';
                break;
            case 
3:
                
$type 'h3';
                break;
            case 
4:
                
$type 'h4';
                break;
            case 
5:
                
$type 'h5';
                break;
            case 
6:
                
$type 'h6';
                break;
            default:
                
$type 'p';
                break;
        }
        return 
$this->output(sprintf($this->tags['words'], $type$data));
    }

}
?>
Sample Layout (depending on path setting, this could be views/layouts/wapl/default.ctp or views/layouts/default.wapl):

View Template:

Download code
<?php
e
(
        
$wapl->wapl(
                
$wapl->head(
                        
$wapl->title($title_for_layout)
                )
                .
                
$wapl->layout($content_for_layout)
        )
);
?>
Sample View (depening on path setting, this could be views/pages/wapl/wapltest.ctp or views/pages/wapltest.wapl):

View Template:

Download code
<?php
echo $wapl->easyChars('Test'); // easyChars outputs text.
echo $wapl->chars('ClassTest', array('class' => 'test')); // chars outputs text with CSS styles.
echo $wapl->words('PTest'); // words outputs text inside a <p> element.
echo $wapl->words('BigTest',1); // words with a second parameter (1-6) outputs text in a <h1-6> element.
?>

If you prefer to write plain WAPL without using the Helper methods, or want to use some elements that the Helper doesn't support yet, see http://wapl.info.

My Helper doesn't currently support all elements (such as forms), but I'll be updating it :)

 

Comments 986

CakePHP Team Comments Author Comments
 

Question

1 WAPL ERROR: Entity 'nbsp' not defined

If I do
foreach ($models as $model)
    echo $wapl->words($model['Model']['Name']);
in my index.wapl I get "WAPL ERROR: Entity 'nbsp' not defined".
What am I doing wrong?
Thanks for the article.
Posted Oct 5, 2009 by DJ
 

Comment

2 &nbsp;

If I do
foreach ($models as $model)
    echo $wapl->words($model['Model']['Name']);
in my index.wapl I get "WAPL ERROR: Entity 'nbsp' not defined".
What am I doing wrong?
Thanks for the article.

&nbsp; is not getting urlencoded. In the Helper's afterLayout(), change two lines:

'wapl' => $View->output
change to: 'wapl' => urlencode($View->output);

$View->output = trim($xml->markup);
change to: $View->output = trim(urldecode($xml->markup));

NOTE: I haven't tested that code. I'll be re-writing this as a plugin at some point.
Posted Oct 21, 2009 by Gordon Pettey