19

I have developed a full website with CakePHP framework and we'd like to make a very light version of the website for mobile devices (mainly iPhone/iPad).

Is there a way to use the existing website with a new sub domain (for instance mobile.mywebsite.com) which will render specific views? I would like to avoid copying and simplifying the current one to match the new one requirements. I do not want to have to "re-develop" a new CakePHP website and do the changes twice every time I need to change a controller action.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Nicolas
  • 2,754
  • 6
  • 26
  • 41

8 Answers8

31

I've done this using a quick addition to the beforeFilter() in my app_controller.php file.

function beforeFilter() {
     if ($this->RequestHandler->isMobile()) {
        $this->is_mobile = true;
        $this->set('is_mobile', true );
        $this->autoRender = false;
     }
}

This uses the CakePHP RequestHandler to sense if it's a mobile device visiting my site. It sets a property and view variable to allow actions an views to adjust themselves based on the new layout. Also turns off the autoRender because we'll take care of that in an afterFilter.

In the afterFilter() it looks for and uses a mobile view file if one exists. Mobile versions are stored in a 'mobile' folder inside the controller's view folder and are named exactly the same as the normal non-mobile versions. (ie. add.ctp becomes mobile/add.ctp)

    function afterFilter() {
        // if in mobile mode, check for a valid view and use it
        if (isset($this->is_mobile) && $this->is_mobile) {
            $view_file = new File( VIEWS . $this->name . DS . 'mobile/' . $this->action . '.ctp' );
            $this->render($this->action, 'mobile', ($view_file->exists()?'mobile/':'').$this->action);
        }
     }
BenMorel
  • 34,448
  • 50
  • 182
  • 322
Dan Berlyoung
  • 1,639
  • 2
  • 17
  • 36
  • Hi Dan, do you think it would be possible use your solution with a sub-domain and then let the users run the full website via a link if he wants to? Cheers, – Nicolas Oct 08 '10 at 16:42
  • 1
    Possibly. My solution autosenses which device is browsing. Maybe just add a link that sets a cookie to override the browser sensing. Then no subdomain is necessary. Just a 'view full site' link that sets the cookie. – Dan Berlyoung Oct 08 '10 at 16:44
  • The cookie could be a good idea indeed. I'll think about it this weekend, but it is most likely that your easy to configure solution will have my vote. Cheers, – Nicolas Oct 08 '10 at 16:57
  • So I'm back, just to tell you I choose your option. I just needed to add a meta "viewport" in order to make the same layout on all mobile devices. Thanks! Nicolas. – Nicolas Oct 15 '10 at 07:52
  • Excellent solution, when I first looked into this I thought it might be very difficult, your solution is great. Many thanks. – Bear May 22 '12 at 22:21
  • Instead of `if(isset($this->is_mobile && $this->is_mobile)`, could we just do `if(!empty($this->_is_mobile))` ? – Dave Aug 14 '12 at 04:38
  • The `$this->is_mobile` value isn't set in the beforeFilter if it is not a mobile device. The `isset($x) && $x` pattern tolerates this value not existing and exits gracefully. Otherwise it would generate a warning. – Dan Berlyoung Aug 20 '12 at 19:20
  • 1
    In cake 2.x this would be *$this->RequestHandler->is('mobile')* – styks Mar 04 '13 at 16:58
  • 1
    One thing which I would to like add in this solution, here $this->name is being used, for getting controller name, this works fine when there is no underscore in controller name in URL, in case of underscore this will cause error, so better $this->params['controller'] should be used, hence $view_file = new File( VIEWS . $this->params['controller'] . DS . 'mobile/' . $this->action . '.ctp' ); should be used for safety. – Sabeeh Chaudhry Jul 11 '13 at 11:28
  • One thing neither of these answers address is the fact that if you are rendering a different view from your controller via $this->render(), then the view of the action is displayed as opposed to the other view you have specifically called. – bowlerae Apr 22 '15 at 20:58
5

You can use Theme feature in CakePHP 2.x for mobile layout.

Simply do:

if($this->RequestHandler->isMobile())
    $this->theme = 'mobile';

I found this better, as you can share View file on mobile and desktop theme easily.

Shadow The GPT Wizard
  • 66,030
  • 26
  • 140
  • 208
unnamedx
  • 51
  • 1
  • 1
  • I like the theme approach as well. One change though, it would be better to use `CakeRequest` instead of the RequestHandlerComponent. `if ($this->request->is('mobile')) {`... – chronon Feb 25 '13 at 19:41
5

Dan's answer worked for me. However, I used file_exists instead of the File constructor and added the ability to use mobile layouts. The before filter was the same, but the afterFilter looked like this:

function afterFilter() {
    // if in mobile mode, check for a valid view and use it
    if (isset($this->is_mobile) && $this->is_mobile) {
        $view_file = file_exists( VIEWS . $this->name . DS . 'mobile/' . $this->action . '.ctp' );
        $layout_file = file_exists( LAYOUTS . 'mobile/' . $this->layout . '.ctp' );

        $this->render($this->action, ($layout_file?'mobile/':'').$this->layout, ($view_file?'mobile/':'').$this->action);
    }
 }
Chris K
  • 51
  • 1
  • 1
  • In CakePHP 1.3 on a linux server you may want to add strtolower($this->name) as the Controller name will have first letter captial, causing file not found on a linux server as its case sensitive. – happyhardik Jul 13 '12 at 11:14
  • 1
    Instead of `if(isset($this->is_mobile && $this->is_mobile)`, could we just do `if(!empty($this->_is_mobile))` ? – Dave Aug 14 '12 at 04:39
4

I modified this technique for a CakePHP 2.1 app. Here is my beforeFilter():

public function beforeFilter() {

if ($this->request->isMobile()){
    $this->is_mobile = true;
        $this->set('is_mobile', true );
        $this->autoRender = false;
}

}

And here is my afterFilter():

function afterFilter() {
    // if in mobile mode, check for a valid view and use it
    if (isset($this->is_mobile) && $this->is_mobile) {
        $view_file = file_exists( 'Views' . $this->name . DS . 'mobile/' . $this->action . '.ctp' );
        $layout_file = file_exists( 'Layouts' . 'mobile/' . $this->layout . '.ctp' );
        if($view_file || $layout_file){
            $this->render($this->action, ($layout_file?'mobile/':'').$this->layout, ($view_file?'mobile/':'').$this->action);
        }
    }
 }

This helps account for deprecated verbiage and constants in CakePHP 2.

deewilcox
  • 852
  • 2
  • 12
  • 24
  • @bancer's technique works well for CakePHP 2.1, too, if you have one mobile layout that can handle all of the pages on your site. – deewilcox Sep 14 '12 at 15:35
2

The simple solution is to create a new 'mobile' layout with respective stylesheet(s) and turn it on in AppController:

public $components = array('RequestHandler');
public function beforeRender() {
    parent::beforeRender();
    if ($this->RequestHandler->isMobile()) {
        $this->layout = 'mobile';
    }
}

It is important to do that in beforeRender() in case if you change $this->layout in your controllers' methods.

bancer
  • 7,475
  • 7
  • 39
  • 58
2

CakePHP v2.2.1 Solution (+ Cookies to persist mobile/desktop/other layout)

This solution is based on answers by @Dan Berlyoung, @deewilcox and @Chris K.

Parts of those answers failed to work (for me) in CakePHP 2.2.1.

I also extended the solution to support "forcing" a mobile/desktop/other layout from the frontend - useful for debugging and for users that don't want to be stuck on a "mobile" themed layout.

/app/Controller/AppController.php

class AppController extends Controller {

  public $components = array('Cookie');
  public $is_mobile = false;
  public $layouts = array('desktop', 'mobile');

  // executed before every action in the controller
  function beforeFilter() 
  {
    // Using "rijndael" encryption because the default "cipher" type of encryption fails to decrypt when PHP has the Suhosin patch installed. 
    // See: http://cakephp.lighthouseapp.com/projects/42648/tickets/471-securitycipher-function-cannot-decrypt
    $this->Cookie->type('rijndael');

    // When using "rijndael" encryption the "key" value must be longer than 32 bytes.
    $this->Cookie->key = 'qSI242342432qs*&sXOw!adre@34SasdadAWQEAv!@*(XSL#$%)asGb$@11~_+!@#HKis~#^'; // When using rijndael encryption this value must be longer than 32 bytes.

    // Flag whether the layout is being "forced" i.e overwritten/controlled by the user (true or false)
    $forceLayout = $this->Cookie->read('Options.forceLayout');

    // Identify the layout the user wishes to "force" (mobile or desktop)
    $forcedLayout = $this->Cookie->read('Options.forcedLayout');

    // Check URL paramaters for ?forcedLayout=desktop or ?forcedLayout=mobile and persist this decision in a COOKIE
    if( isset($this->params->query['forcedLayout']) && in_array($this->params->query['forcedLayout'], $this->layouts) )
    {
        $forceLayout = true;
        $forcedLayout = $this->params->query['forcedLayout'];
        $this->Cookie->write('Options.forceLayout', $forceLayout);
        $this->Cookie->write('Options.forcedLayout', $forcedLayout);
    }

    // We use CakePHP's built in "mobile" User-Agent detection (a pretty basic list of UA's see: /lib/Cake/Network/CakeRequest.php)
    // Note: For more robust detection consider using "Mobile Detect" (https://github.com/serbanghita/Mobile-Detect) or WURL (http://wurfl.sourceforge.net/)
    if( ( $forceLayout && $forcedLayout == 'mobile' ) || ( !$forceLayout && $this->request->is('mobile') ) )  {
        $this->is_mobile = true;
        $this->autoRender = false; // take care of rendering in the afterFilter()
    }

    $this->set('is_mobile', $this->is_mobile);
  }

  // executed after all controller logic, including the view render.
  function afterFilter() {

    // if in mobile mode, check for a vaild layout and/or view and use it
    if( $this->is_mobile ) {
        $has_mobile_view_file = file_exists( ROOT . DS . APP_DIR . DS . 'View' . DS . $this->name . DS . 'mobile' . DS . $this->action . '.ctp' );
        $has_mobile_layout_file = file_exists( ROOT . DS . APP_DIR . DS . 'View' . DS . 'Layouts' . DS . 'mobile' . DS . $this->layout . '.ctp' );

        $view_file = ( $has_mobile_view_file ? 'mobile' . DS : '' ) . $this->action;
        $layout_file = ( $has_mobile_layout_file ? 'mobile' . DS : '' ) . $this->layout;

        $this->render( $view_file, $layout_file );
    }
  }
}

/app/View/Elements/default_footer.ctp

<ul>
    <?php
        $paramsQuery = $this->params->query;
        if(!is_array($paramsQuery))
        {
            $paramsQuery = array();
        }
        $paramsQuery['url'] = ( isset($paramsQuery['url']) ) ? $paramsQuery['url'] : '';
        $url = $paramsQuery['url'];
        unset($paramsQuery['url']);
        $params = $paramsQuery;

        $mobile_url = '/' . $url . '?' . http_build_query( array_merge( $params, array( 'forcedLayout' => 'mobile' ) ) );
        $desktop_url = '/' . $url . '?' . http_build_query( array_merge( $params, array( 'forcedLayout' => 'desktop' ) ) );
    ?>

    <?php if($is_mobile): ?>
        <li><?= $this->Html->link('Desktop Site', $desktop_url, array('target' => '', 'class' => '')) ?></li>
    <?php else: ?>
        <li><?= $this->Html->link('Mobile Site', $mobile_url, array('target' => '', 'class' => '')) ?></li>
    <?php endif; ?>
</ul>

/app/View/Layouts/default.ctp

<h1>Desktop Site Layout</h1>
<?= $this->fetch('content') ?>

/app/View/Layouts/mobile/default.ctp

<h1>Mobile Site Layout</h1>
<?= $this->fetch('content') ?>

/app/View/Pages/home.ctp

<h2>Home - on Desktop</h2>
<?= $this->element('default_footer') ?>

/app/View/Pages/mobile/home.ctp

<h2>Home - on Mobile</h2>
<?= $this->element('default_footer') ?>

Usage

Use the default_footer links to change layout - or these direct urls
http://example.com/pages/home?forcedLayout=desktop
http://example.com/pages/home?forcedLayout=mobile

A session COOKIE persists the option you've chosen... e.g. try setting to "mobile" and then visit a url without the forcedLayout= param.
http://example.com/pages/home

The default_footer links persist existing params (except for the "fragment" #gohere)
http://example.com/pages/home/a/b/c:d?this=that&foo=bar#gohere

Desktop Site url is:
http://example.com/pages/home/a/b/c:d?this=that&foo=bar&forcedLayout=desktop

For more robust device User-Agent detection consider using the Mobile Detect PHP Library ... you could then target Tablets, and even specific devise OS versions.... Oh what fun! ^_^

Community
  • 1
  • 1
Chris Jacob
  • 11,878
  • 7
  • 47
  • 42
  • Thank you Chris for answer. Most of the customers look for a m.example.com style subdomain. Do you have any suggestions to integrate your solution with subdomain usage? – trante Mar 25 '13 at 18:54
1

The solution I went with was a lightweight modification based on a few of the answers here, for CakePHP 2.5.5. The handling is all done in the beforeRender (note that beforeRender is only run on controller actions that will actually render a page, so this saves overhead as opposed to beforeFilter/afterFilter for private methods):

$mobile = $this->request->is('mobile');
$this->set('mobile',$mobile);
//Check if an alternate mobile view and/or layout exists for this request.
if($mobile){
    if(file_exists(APP.'View'.DS.$this->name.DS.'mobile'.DS.$this->view.'.ctp')){
        //Render this action on its mobile view.
        $this->view = 'mobile'.DS.$this->view;
    }
    if(file_exists(APP.'View'.DS.'Layouts'.DS.'mobile'.DS.$this->layout.'.ctp' )){
        //Render this action on its mobile layout.
        $this->layout = 'mobile'.DS.$this->layout;
    }
}

The $mobile variable can be used on any view if you have small tweaks to make, otherwise you can optionally replace any view with View/{controller}/mobile/same_file_name.ctp or layout with View/Layouts/mobile/same_file_name.ctp to have a separate page structure entirely.

Note that this uses $this->view and $this->layout and then modifies them, instead of using $this->action and $this->render(view,layout), because your view won't always match your action (same view, multiple actions, for example, break using $this->action), and this solution prevents needing to worry about when $this->render() will be forced, and allows it to happen naturally.

T.J. Compton
  • 405
  • 2
  • 11
0

Yes, you can re use all of your domain and controllers, have a look to Tera-WURLF

And even better, you don't need a subdomain for the mobile version.

mcabral
  • 3,524
  • 1
  • 25
  • 42
  • Hi mcabral, thank you for your suggestion. I really like the idea of a sub domain because people could then switch to the full site if they want. Any idea on this feature? Have you ever used it? Cheers, – Nicolas Oct 08 '10 at 14:32
  • @Nicolas yes, i have tried it with Zend Framework. It's a fine tool. – mcabral Oct 08 '10 at 14:45