《Yii Rapid Application Development Hotshot》

Chapter 2. Turn That DB into a Personal Mobile App

We will put a mobile face on our web application in this mission. Once your site is deployed to a production server, you might want to connect to it with your smartphone from a comic book store to check or update your collection.

Mission Briefing

The mission, should you choose to accept it, is to detect if a mobile browser is viewing the site, and if so, to display a new custom mobile view that we will create. We will need to configure a mobile device to connect to our local network, and then allow it to access our development website. We will go ahead and create UI controls that use the associations between book and author, book and illustrator, and book and publisher. We will add an "issue number" field to the book object, making our site more useful for comic book collectors.

Why Is It Awesome?

While regular websites are often usable with smartphones and tablets, mobile views make it much easier to use and navigate with these devices. Mobile views give your site a native-mobile look and feel. If you have ever tried to access something from your mobile device while you are in a hurry, you can appreciate the utility of good mobile interfaces.

Your Hotshot Objectives

  • Setting Up Your Mobile Device
  • Detecting Mobile Browser
  • Creating a Mobile View
  • Finishing Touches for the Mobile View
  • Detecting Mobile Browser – The Real Deal
  • Adding Issue Number to the Book Object
  • Relationship Therapy
  • Creating a Mobile View Widget

Mission Checklist

In order to follow this chapter, you need a network that you can connect to from both your development machine and a mobile device. If you don't have a device, you can use one of the various mobile development emulators. Check out the Google Android Developer SDK or Apple's iOS Dev Center if you want to get started on mobile development.

This project assumes that you have a web development environment prepared. If you do not have one, the tasks in Project 1, Develop a Comic Book Database, will guide you through setting one up. In order to work this project, you will need to set up the project files that have been provided with the book. Refer to the Preface of the book for instructions on downloading these files. The files for this project include a Yii project directory with a database schema. To prepare for the project, follow these steps, replacing the username jhamilton with your own username.

  1. Copy the project files into your working directory.
cpr ~/Downloads/project_files/Chapter\ 2/project_files ~/projects/cbdb/ch2
cd ~/projects/ch2/
sudo chown -R jhamilton:www-data protected/runtime assets
  1. If you already have a cbdb link in your webroot, delete it, so we're only looking at one project at a time (for now).
sudo rm /opt/lampp/htdocs/cbdb
  1. Create a link in the webroot directory to the copied directory.
cd /opt/lampp/htdocs
sudo ln -s /home/jhamilton/projects/ch2 cbdb
  1. Import the project into NetBeans and configure for Yii development with PHPUnit.
  2. Create a database named cbdb and load the database schema (~/projects/cbdb/protected/data/schema.sql) into it. If you already have a cbdb database from the previous chapter, you might want to back it up. In order to create a new cbdb database, the corresponding database with the same name from the first chapter has to be dropped.
  3. If your web location is different, or if your access to MySQL is restricted, you will need to update the Yii configuration file (~/projects/cbdb/ch2/protected/config/main.php).
  4. Download JQuery Mobile from http://code.jquery.com/mobile/1.1.0/ (current stable release at the time of writing) and save it in ~/projects/cbdb/ch2/js/.
cd projects/ch2/js
wget http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.zip
unzip jquery.mobile-1.1.0.zip
cp jquery.mobile-1.1.0/ jquery.mobile-1.1.0.min.js .
cp jquery.mobile-1.1.0/jquery.mobile-1.1.0.min.css ../css/
cpR jquery.mobile-1.1.0/images/ ../css/
rm jquery.mobile-1.1.0.zip

Setting Up Your Mobile Device

In this task, we set up your network so you can connect to your site with your mobile device. This will come in handy when we want to test the look and feel of the mobile version of the site.

Prepare for Lift Off

In order to test the parts of the site intended for mobile browsers, we will need to connect the computer hosting the CBDB app to a trusted Wi-Fi network, and then connect your mobile device to this network as well. The network should be encrypted and secured properly with a password, and should only allow connections from trusted entities.

Engage Thrusters

  1. Follow the Wi-Fi connection instructions for your mobile device to connect it to your wireless network.
  2. Make sure the computer with your LAMP stack is connected to the network as well.
  3. You can determine the local IP address of your computer in Linux by typing ifconfig in the terminal.
  4. Use your mobile device to look at the computer by putting the IP address in the mobile browser http://192.168.3.23/cbdb. You should see the Comic Book Database web application. If you cannot see the website, you may need to configure your router to allow network nodes to see each other.

Objective Complete - Mini Debriefing

Now we have set up a way to view and test the site from a mobile device.

Classified Intel

It is important to understand that we have set up a development environment, and not a production environment. There are many steps that are beyond the scope of this book that you should take to secure a production web server. For example, if you have gone with the default configurations so far, your installation of MySQL has no root password. I cannot state strongly enough that you should not use this configuration on the Internet or even an untrusted local network such as a coffee shop (or possibly your workplace). If these restrictions are unacceptable to you, you should at least configure XAMPP to only allow connections from localhost (you will also need to set it up to allow your mobile device to connect for the current task).

Detecting Mobile Browser

When a browser connects, we should determine if it is a mobile browser. We will need to write some code to do this.

Engage Thrusters

  1. Open ch2 | SourceFiles | protected | views | layouts | main.php and add these lines in the head section after the css includes and before the title tag:
<?php 
    Yii::app()->clientScript->registerCoreScript('jquery'); 
    Yii::app()->clientScript->registerScriptFile(
    Yii::app()->request->baseUrl . '/js/detectmobilebrowser.js'); 
?>

This will load jQuery (it comes with Yii) and our custom browser detection script.

  1. Now, to test our handiwork, temporarily add the following lines at the beginning of the mainmenu div, after the opening php tag:
Yii::app()->clientScript->registerScript('detectmobilebrowser',"
   if (isMobileBrowser(navigator.userAgent||navigator.vendor||window.opera)) {
     alert('Mobile');
   }    
   else {
     alert('Non-Mobile');
   }
",CClientScript::POS_READY);
  1. Go ahead and open the site on your computer with your favorite browser. You should see the following screenshot:
  1. Now open the site in a browser on your mobile device. You should see the following screenshot:

Objective Complete - Mini Debriefing

The registerCoreScript statement includes the jQuery package, so we can use jQuery in conjunction with JavaScript. The registerScriptFile statement makes code in detectmobilebrowser.js available to the layout. The registerScript statement runs the quoted snippet of JavaScript when the page is loaded. The JavaScript snippet checks to see if the requesting browser is a known mobile user-agent. If it is, an alert saying Mobile will be displayed. If the browser is not a known mobile user-agent, the alert displayed will say Non-Mobile. Any page that uses the main layout will display this. This change is just temporary, to check that we are properly detecting mobile browsers with JavaScript.

Classified Intel

There is a Yii extension named detectmobilebrowser. We could use this to detect if you are viewing the website with a cellular phone or another mobile device. At the time of writing, the extension can be found at http://www.yiiframework.com/extension/detectmobilebrowser/. However, there is a school of thought that believes mobile-browser detection belongs in client-side JavaScript rather than the controller. We will adhere to this school of thought and therefore will use JavaScript to detect this instead. We have borrowed detection code for jQuery from http://detectmobilebrowsers.com. We have included a modified version of this file, along with a copy of its very unrestrictive license, in ~/projects/cbdb/ch2/js/detectmobilebrowser.js.

Creating a Mobile View

In this section, we will make a mobile view. We can test our handiwork by using the URL mobile to turn the mobile version of the layout on and off. In addition to this, we are going to add attributes to elements in our layouts to make them function properly with jQuery Mobile and make our mobile view look even better. jQuery Mobile uses particular attributes for HTML elements to determine the role and appearance of those elements in the jQuery Mobile page layout. There is a wonderful documentation about this, titled Anatomy of a Page, on http://jquerymobile.com in the documentation section about pages and dialogs.

Engage Thrusters

  1. There is an empty skeleton of a layout at ch2 | Source Files | protected | views | layouts | mobile.php. Open it.
  2. After the following code snippet:
<!--CSS includes here -->
  1. Add the following line:
<link rel="stylesheet" type="text/css" 
    href="<?php echo Yii::app()->request->baseUrl; ?>/css/jquery.mobile-1.1.0.min.css" 
        media="screen, projection" />
  1. After the following code snippet:
Yii::app()->clientScript->registerCoreScript('jquery');

Add the following line:

Yii::app()->clientScript->registerScriptFile(
           Yii::app()->request->baseUrl . '/js/jquery.mobile-1.1.0.min.js');
  1. Now, in the body section, add:
<div data-role="page">
    <div data-role="content">
    <?php echo $content; ?>
    </div><!-- content -->
     <div data-role="footer">
        <center>
            <?php echo Yii::powered(); ?>
        </center>
    </div><!-- footer -->
</div><!-- page -->
  1. We are now going to override the base class CController to use the new mobile layout if the parameter mobile has a value of on. In order to do this, we will first replace the test code you wrote in the previous task in ch2 | Source Files | protected | views | layouts | main.php with code to redirect to the current URL, with the mobile parameter set to a value of on. Replace the following code:
if(isMobileBrowser(navigator.userAgent||navigator.vendor||window.opera)) {
    alert('Mobile');
}  
else {
    alert('Non-Mobile');
}

with:

if(isMobileBrowser(navigator.userAgent||navigator.vendor||window.opera)) {
    if (window.location.search.search('mobile') == -1) {
        if (window.location.search.length) {
            window.location.replace(document.URL + '&mobile=on');
        }    
        else {
            window.location.replace(document.URL + '?mobile=on');
        }
    }
}
  1. Add the following function to ch2 | Source Files | protected | components | Controller.php:
/* Override beforeAction() to change to the mobile layout if URL param['mobile'] == 'on' */
protected function beforeAction($action) {
    if (Yii::app()->getRequest()->getQuery('mobile') == 'on') {
        Yii::app()->user->setState('mobile', true);
    }
    else if (Yii::app()->getRequest()->getQuery('mobile') == 'off') {
        Yii::app()->user->setState('mobile', false);
    }
    if (Yii::app()->user->getState('mobile')) {
        $this->layout = '//layouts/mobile';
    }
    return true;
}
  1. If all has gone well, you should now be able to switch between http://localhost/cbdb/ and http://localhost/cbdb/?mobile=on and see different layouts (see the following screenshots).

Full layout:

Mobile layout:

  1. Open ch2 | Source Files | protected | views | layouts | mobile.php and add the following lines between <div data-role="page"> and <div data-role="content">:
<?php 
    $htmlOptions = array('data-role' => 'controlgroup', 'class' => 'localnav');
    $linkOptions = array('data-role' => 'button',  'data-theme' => 'b', 'rel' => 'external');
    $items = array();  
    if (Yii::app()->user->isGuest) {
        $items[] = array('label'=>'Login', 'url'=>array('/site/login'), 
            'linkOptions' => $linkOptions);
    } 
    else {
        $items[] = array('label'=>'Home', 'url'=>array('/site/index'), 
            'linkOptions' => $linkOptions);
        $items[] = array('label'=>'Comic Books', 'url'=>array('/book'), 
            'linkOptions'=> $linkOptions);
        $items[] = array('label'=>'Logout (' . Yii::app()->user->name . ')', 
            'url'=>array('/site/logout'), 'linkOptions' => $linkOptions);
    }
    $non_mobile_uri = preg_replace('/mobile=on/', 'mobile=off', 
        /*'/site/login');*/ Yii::app()->request->baseUrl);
    $items[] = array('label'=>'Turn off mobile view', 'url'=>array('?mobile=off'), 
        'linkOptions' => $linkOptions);
    $this->widget('zii.widgets.CMenu',array(
        'activeCssClass' => 'active',
        'activateParents' => true,
        'htmlOptions' => $htmlOptions,
        'items'=> $items
    )
);
?>
  1. If you switch to mobile view and then click on the link to log in, you will notice that the form is broken when you try to submit. This is because jQuery mobile needs one additional attribute to tell it how this form is intended to be used. Open the file ch2 | Source Files | protected | views | site | login.php, and, in your declaration of the CActiveForm widget, below the line 'enableClientValidation'=>true, add the following line of code:
'htmlOptions' => array('data-ajax' => 'false'),
  1. The whole declaration should now look like the following code snippet:
$form=$this->beginWidget('CActiveForm', array(
    'id'=>'login-form',
    'enableClientValidation'=>true,
    'htmlOptions' => array('data-ajax' => 'false'),
    'clientOptions'=>array(
        'validateOnSubmit'=>true,
        ),
));
  1. Now, the mobile version of the login form should work. The non-mobile version of this form should also continue working the way it always has.

Objective Complete - Mini Debriefing

We included the CSS for jQuery Mobile and registered the JavaScript library so we could use it in our layout. We updated the main layout to notice if a mobile browser is detected and if the URL parameter mobile isn't set. If so, then we set the URL parameter mobile to on and redirect it back to the current URL.

We put a beforeAction action in components/controller (the subclass of CController that is provided to customize and override CController behavior) to check to see if the mobile parameter is set to on, and if so, to set the state of mobile to true in the session. Putting the mobile browser detection in the view and setting the mobile parameter abstracts the controller from mobile detection. By doing it this way, we break the problem into smaller pieces, and gain the ability to also manually select which layout we are using. At a later time, we can add a link that is labeled Click for non-mobile view to the mobile layout.

We've made changes to the instantiation of the CMenu widget in our mobile view to add attributes that jQuery Mobile uses for rendering. The data-role localnav lets jQuery know that this menu is for navigating our site. For the buttons, setting data-theme to b tells jQuery Mobile to use the built-in blue theme for the buttons. If you don't set rel to external, the links won't work properly because jQuery mobile will expect the link-targets to contain jQuery Mobile specific divs and data-roles that are not there. We've made a small change to our login form to let jQuery Mobile know how to submit the form. If you click on Comic Books in the mobile view, you'll notice that we've lost the operations menu for the books, and the list needs some sprucing up. We're going to fix that in the next task.

Classified Intel

The JavaScript we placed in the main layout to detect a mobile device and set the mobile parameter is by no means fool proof or robust. For example, the value of another parameter in the query string could be mobile, and the code wouldn't set mobile to on because of this. We have simply put it there as it is to serve as a proof of concept. Ultimately, we will rework this piece of code with JavaScript functions for processing URL parameters.

Finishing Touches for the Mobile View

With a few changes to the layout, we can get to where we need to go.

Engage Thrusters

  1. Open ch2 | Source Files | protected | views | layouts | mobile.php. Change the lines between <div data-role="page"> and <div data-role="content"> as follows:
<div data-role="collapsible" data-theme="b">
    <h3>Main Menu</h3>
    <?php 
        $htmlOptions = array('data-role' => 'controlgroup', 'class' => 'localnav');
        $linkOptions = array('data-role' => 'button',  'data-theme' => 'b', 
            'rel' => 'external'); 
        $items = array();    
        if (Yii::app()->user->isGuest) {
            $items[] = array('label'=>'Login', 'url'=>array('/site/login'), 
                'linkOptions' => $linkOptions);
        } 
        else {
            $items[] = array('label'=>'Home', 'url'=>array('/site/index'), 
                'linkOptions' => $linkOptions);
            $items[] = array('label'=>'Comic Books', 'url'=>array('/book'), 
                'linkOptions'=> $linkOptions);
            $items[] = array('label'=>'Logout (' . Yii::app()->user->name . ')', 
                'url'=>array('/site/logout'), 'linkOptions' => $linkOptions);
        }
        $non_mobile_uri = preg_replace('/mobile=on/', 'mobile=off', 
            /*'/site/login');*/ Yii::app()->request->baseUrl);
        $items[] = array('label'=>'Turn off mobile view', 'url'=>array('?mobile=off'), 
            'linkOptions' => $linkOptions);
        $this->widget('zii.widgets.CMenu',array(
            'activeCssClass' => 'active',
            'activateParents' => true,
            'htmlOptions' => $htmlOptions,
            'items'=> $items
        ));
    ?>
</div><!-- collapsible -->
<?php
if (count($this->menu) > 0) {
    echo "<div data-role='collapsible' data-theme='b'>\n";
    echo "\t<h3>Operations</h3>\n";
    foreach ($this->menu as $key=>$item) {
        $this->menu[$key]['linkOptions'] = $linkOptions;
    }
    $this->widget('zii.widgets.CMenu', array(
        'items'=>$this->menu,
        'htmlOptions'=> $htmlOptions,
    ));
    $this->endWidget();
    echo "</div><!-- collapsible -->\n";
}
?>
  1. Reload the Comic Books page while logged in, with the mobile view on.

Objective Complete - Mini Debriefing

We made the decision to use collapsible menus to alleviate the cumbersome task of dealing with a large number of menu items. To do this, we create a div with a data-role of collapsible for each collapsible menu. We use the <h3> tag to indicate the title of the top-level collapsible control. If an object has operations that can be performed on it, it adds the items to $this-<menu, so we check that to see if it has elements in the second PHP segment. If it does, we build and display the items.

Detecting Mobile Browser – The Real Deal

In the previous tasks, we have quickly added mobile browser detection. In this task, we will refine that code.

Prepare for Lift Off

The file ch2| Source Files| js| url_param_proc.js contains functions for manipulating the parameter strings in the URL query string. We will include this file in our mobile layout and use it to manage the mobile parameter.

Open ch2 | Source Files | protected | views | layouts | main.php. After the following code snippet:

Yii::app()->clientScript->registerScriptFile(
    Yii::app()->request->baseUrl . '/js/detectmobilebrowser.js'
);

Add the following lines of code:

Yii::app()->clientScript->registerScriptFile(
    Yii::app()->request->baseUrl . '/js/url_param_proc.js'
);

Engage Thrusters

  1. The file ch2 | Source Files | js | url_param_proc.js contains the following block of code:
function get_param_array() {
    var param_array = {};
    if (window.location.search.length) {
        var query_string = window.location.search.substring(1);
        var params = query_string.split("&");
        for (var count = 0; count < params.length; count++) {
            var param_pair = params[count].split("=");
            param_array[param_pair[0]] = param_pair[1];
        }
    }
    return param_array;
}
 
function build_query_string(param_array) {
    var query_string = "";
 
    for (key in param_array) {
        query_string += key + "=" + param_array[key];    
    }
    if (query_string.length) {
        query_string = "?" + query_string;
    }
    return query_string;
}
 
function get_base_uri() {
    var base_uri = document.location.protocol + "//" + document.location.hostname;
    if (document.location.port.length) {
        base_uri += ":" + document.location.port;
    }
    base_uri += document.location.pathname;
    return base_uri;
}

The get_param_array() function processes the query string and returns an object with the keys as the parameter names and the values as the parameter values. The build_query_string() function reassembles the object back into a query string. The get_base_uri() function returns the URL without the query portion of the string.

  1. In ch2 | Source Files | protected | views | layouts | main.php, change the following code snippet:
if (isMobileBrowser(navigator.userAgent||navigator.vendor||window.opera)) {
    if (window.location.search.search('mobile') == -1) {
        if (window.location.search.length) {
            window.location.replace(document.URL + '&mobile=on');
        }    
        else {
            window.location.replace(document.URL + '?mobile=on');
        }
    }
}

To look like this:

if (isMobileBrowser(navigator.userAgent||navigator.vendor||window.opera)) {
    var param_array = get_param_array();
    if (!('mobile' in param_array)) {
        param_array['mobile'] = 'on';
         window.location.replace(get_base_uri() + build_query_string(param_array));
    }
}
  1. Test this by opening the site in your mobile browser.

Objective Complete - Mini Debriefing

We used some small Javascript functions to help us manage the URL parameters. It has helped us clean up our code and the code is now more resistant to unexpected results. The code basically does the same thing we intended for it to do before, but now our check for the presence of the mobile parameter differentiates between keys and values, and we build the query string in a more elegant fashion.

Adding Issue Number to the Book Object

We will add a field for issue number to our book object by adding it as a column for the already existing book table in our cbdb database. Since we have already created our model, view, and controller with Gii, we will need to manually modify these files to use the new field.

Engage Thrusters

  1. In the Services tab, open the connection to cbdb and open the list of tables:
  1. Right-click on the book table and go to Add Column. Name the field issue_number, select VARCHAR for the type and 10 for the size, and click on OK as shown in the following screenshot:
  1. Now let's make the model aware of our change. Open ch2 | Source Files | protected | models | Book.php. In the comment block at the top of the file, add the field description below * @property integer $bagged:
* @property integer $issue_number
  1. In the rules() function, add the issue number in the relevant places:
return array(
    array('title', 'required'),
    array('signed, bagged', 'numerical', 'integerOnly'=>true),
    array('title', 'length', 'max'=>256),
    array('type_id, value, price, grade_id', 'length', 'max'=>10),
    array('publication_date, notes', 'safe'),
    array('issue_number', 'length', 'max'=>10),
    // The following rule is used by search().
    // Please remove those attributes that should not be //searched.
    array('id, title, type_id, publication_date, value, price, notes, signed, grade_id, 
        bagged, issue_number', 'safe', 'on'=>'search'),
)
  1. In the attributeLabels() function, once again add the appropriate information for issue_number:
return array(
    'id' => 'ID',
    'title' => 'Title',
    'type_id' => 'Type',
    'publication_date' => 'Publication Date',
    'value' => 'Value',
    'price' => 'Price',
    'notes' => 'Notes',
    'signed' => 'Signed',
    'grade_id' => 'Grade',
    'bagged' => 'Bagged',
    'issue_number' => 'Issue Number',
);
  1. Finally, in the search() function, add the criterion for the field:
$criteria->compare('signed',$this->signed);
$criteria->compare('grade_id',$this->grade_id,true);
$criteria->compare('bagged',$this->bagged);
$criteria->compare('issue_number', $this->issue_number, true);
 
return new CActiveDataProvider($this, array(
    'criteria'=>$criteria,
));
  1. Now that the model knows about issue_number, we need to add it to the book views. Open ch2 | Source Files | protected | views | book | _view.php and add the following highlighted lines:
<b><?php echo CHtml::encode($data->getAttributeLabel('title')); ?>:</b>
<?php echo CHtml::encode($data->title); ?>
<br />
 
<b><?php echo CHtml::encode($data->getAttributeLabel('issue_number')); ?>:</b>
<?php echo CHtml::encode($data->issue_number); ?>
<br />
 
<b><?php echo CHtml::encode($data->getAttributeLabel('type_id')); ?>:</b>
<?php echo CHtml::encode($data->type_id); ?>
<br />
  1. Now open ch2 | Source Files | protected | views | book | _form.php. This view shows what fields will be displayed for create and update. Add the following code snippet:
<div class="row">
  <?php echo $form->labelEx($model,'title'); ?>
  <?php echo $form->textField($model,'title',array('size'=>60,'maxlength'=>256)); ?>
  <?php echo $form->error($model,'title'); ?>
</div>
 
<div class="row">
  <?php echo $form->labelEx($model,'issue_number'); ?>
  <?php echo $form->textField($model,'issue_number',array('size'=>20,'maxlength'=>10)); ?>
  <?php echo $form->error($model,'issue_number'); ?>
</div>
 
<div class="row">
  <?php echo $form->labelEx($model,'type_id'); ?>
  <?php echo $form->dropDownList($model, 'type_id', $model->getTypeOptions()); ?>
  <?php echo $form->error($model,'type_id'); ?>
</div>
  1. Now do the same for the search by opening ch2 | Source Files | protected | views | book | _search.php and adding the following code snippet:
<div class="row">
  <?php echo $form->label($model,'title'); ?>
  <?php echo $form->textField($model,'title',array('size'=>60,'maxlength'=>256)); ?>
</div>
 
<div class="row">
  <?php echo $form->label($model,'issue_number'); ?>
  <?php echo $form->textField($model,'issue_number',array('size'=>20,'maxlength'=>10)); ?>
</div>
 
<div class="row">
  <?php echo $form->label($model,'type_id'); ?>
  <?php echo $form->textField($model,'type_id',array('size'=>10,'maxlength'=>10)); ?>
</div>
  1. Add the field after title in ch2 | Source Files | protected | views | book | view.php:
<?php $this->widget('zii.widgets.CDetailView', array(
  'data'=>$model,
  'attributes'=>array(
    'id',
    'title',
    'issue_number',
    'type_id',
    'publication_date',
    'value',
    'price',
    'notes',
    'signed',
    'grade_id',
    'bagged',
  ),
)); ?>
  1. Add the field after title in ch2 | Source Files | protected | views | book | admin.php and in ch2 | Source Files | protected | views | site | index.php.
  2. Let's see where this puts us.
  3. The following is the create/update form:
  1. The following is the mobile version of the create/update form, also with issue number added and working:
  1. The following is the index page:
  1. The following screenshot is the mobile view of the same page:

We have successfully added a column manually for issue number. It shows up on our forms, our views, and it flows from the UI to the database, and back. We were able to accomplish all of this with relatively little effort, due to the power of Yii's well-implemented MVC design pattern (http://www.yiiframework.com/doc/guide/1.1/en/basics.mvc).

Objective Complete - Mini Debriefing

In the model, rules() provides low-level rules for field-level validation. We limit the length of issue_number to 10, because we set its length to 10 in the database. The attributeLabels() function defines the labels for each field. In search(), we added a criterion for issue number. In the Yii documentation, the prototype for CDbCriteria.compare is as follows:

public CDbCriteria compare(string $column, mixed $value, boolean $partialMatch=false, 
    string $operator='AND', boolean $escape=true)

These are the only changes we needed to make for the model to correctly use and expose issue_number. We then changed the views, in a very obvious fashion, to allow them to use the same field. The view _form.php is shared by create.php and update.php, via $this->renderPartial(). In the file index.php, _view.php is used. We manually added issue_number to the field lists in the other views.

Relationship Therapy

We will make use of the books' many-to-many relationship with authors. We will have to make moderately extensive changes to the model, the controller, and pertinent views. Hang on to your hat!

Engage Thrusters

  1. Update the book model to capture the author relationship (open ch2 | Source Files | protected | models | Book.php):
'authors' => array(self::MANY_MANY, 'Person', 'bookauthor(author_id, book_id)', 'index'=>'id'),
'bookauthors' => array(self::HAS_MANY, 'BookAuthor', 'book_id', 'index' => 'author_id'),
  1. Update loadModel() in the book controller to include related author data (open ch2 | Source Files | protected | controllers | BookController.php):
$model=Book::model()->with("authors")->findByPk($id);
  1. Add an authors display to the book edit form in ch2 | Source Files | protected | views | book | _form.php.
<div class="row">
  <?php echo $form->labelEx($model,'author'); ?>
  <?php
    echo "<ul class=\"authors\">";
    foreach ($model->authors as $auth) {
      echo "<li>" . CHtml::encode($auth->fname . " " . $auth->lname) . "</li>";
    }
    echo "</ul>";
  ?>
</div>
  1. This will display the author(s) associated with a comic book. You can view the results by clicking on book number 4. It has an author already associated with it. Next, we will update the form so we can add authors.
  2. Make an addAuthor() function in the book model.
public function addAuthor($author) {
  if ($author->isNewRecord()) {
    $author->save();
    $bookauthor = new BookAuthor();
    $bookauthor->book_id = $this->id;
    $bookauthor->author_id = $author->id;
    $bookauthor->save();
  }
}
  1. Add a createAuthor() function to the book controller.
protected function createAuthor($book) {
  $author = new Person();
 
  if(isset($_POST['Person'])) {
    $author->attributes=$_POST['Person'];
    if ($book->addAuthor($author)) {
      Yii::app()->user->setFlash('authorAdded',
        "Added author " . CHtml::encode($author->fname . " " . $author->lname));
      $this->refresh();
    }
  }
  return $author;
}
  1. Include the call to createAuthor() in the book controller actionUpdate().
public function actionUpdate($id)
{
  $model=$this->loadModel($id);
  $author= $this->createAuthor($model);
  1. Add the result to the call to render at the end of the action.
$this->render('update',array(
   'model'=>$model,
   'author'=>$author,
));
  1. Similarly, update the create action in the book controller to include. createAuthor():
public function actionCreate()
{
  $model=new Book;
  $author= $this->createAuthor($model);
  1. Pass the author value to render.
$this->render('create',array(
   'model'=>$model,
   'author'=>$author,
));
  1. Add a field for the new author name to the book form.
<?php echo $form->labelEx($model,'author'); ?>
<?php if(Yii::app()->user->hasFlash('authorAdded')) { ?>
  <div class="flash-success">
    <?php echo Yii::app()->user->getFlash('authorAdded'); ?>
  </div>
<?php } else {
  echo $this->renderPartial('/person/_form', array(
      'model' => $author,
    )); 
} 
?>
<?php
  echo "<ul class=\"authors\">";
  foreach ($model->authors as $auth) {
    echo "<li>" . CHtml::encode($auth->fname . " " . $auth->lname) . "</li>";
  }
  echo "</ul>";
?>
  1. Update the last line in both create.php and update.php to pass author object to renderPartial().
<?php echo $this->renderPartial('_form', array('model'=>$model, 'author' =>$author)); ?>
  1. Create a partial file named _li.php for the author list element and add a delete button to each element.
<?php
    echo "<li id=\"author-" . $author->id. "\">" .
        CHtml::encode($author->fname . " " . $author->lname) .
        " <input class=\"delete\" " . "type=\"button\" url=\"" .
            Yii::app()->controller->createUrl("removeAuthor", array("id" => $model->id,
                "author_id"=>$author->id, "ajax"=>1)) .
                "\" author_id=\"". $author->id.
                "\" value=\"delete\" />" .
        "</li>";
  1. Update the book form to call renderPartial() to render the list element.
<?php
    if (count($model->authors)) {
      echo "<ul class=\"authors\">";
      foreach ($model->authors as $auth) {
        echo $this->renderPartial('_li', array(
                'model' => $model,
                'author' => $auth,
          ));
      }
      echo "</ul>";
    }
?>
  1. Add the primaryKey() function to the BookAuthor model.
public function primaryKey()
{
  return array('book_id', 'author_id');
}
  1. Add the removeAuthor() function to the book model.
public function removeAuthor($author_id) { 
    $pk = array('book_id'=>$this->id, 'author_id' => $author_id);
    BookAuthor::model()->deleteByPk($pk);
}
  1. Add the removeAuthor() action to the book controller.
public function actionRemoveAuthor($id) {
    // request must be made via ajax
    if(Yii::app()->request->isAjaxRequest()) {
        $model=$this->loadModel($id);
        $model->removeAuthor($_GET['author_id']);
    }
    else {
        throw new CHttpException(400,'Invalid request.');
    }
}
  1. Add the removeAuthor action to the list of actions requiring an authenticated user.
'actions'=>array('create','update', 'removeAuthor'),
  1. Change the person form to make it work in the create form and to use AJAX to submit in the update form.
<div class="ajax-form">
    <div class="row">
    <?php echo CHtml::activeLabel($model,'fname'); ?>
    <?php echo CHtml::activeTextField($model,'fname',array('size'=>32,'maxlength'=>64)); ?>
    <?php echo CHtml::activeLabel($model,'lname'); ?>
    <?php echo CHtml::activeTextField($model,'lname',array('size'=>32,'maxlength'=>64)); ?>
  </div>
</div><!-- form -->
  1. Record the book/author association in the book create action.
if($model->save()) {
    // record book/author association
    $ba = new BookAuthor;
    $ba->book_id = $model->id;
    $ba->author_id = $author->id;
    $ba->save();
 
    $this->redirect(array('view','id'=>$model->id));
}
  1. Add a submit button to add authors on the update form.
<?php } else {
    echo $this->renderPartial('/person/_form', array(
            'model' => $author,
            'subform' => 1
        ));
    if (Yii::app()->controller->action->id != 'create') {
?>
        <div class="row buttons">
        <input class="add" type="button" 
            obj="Person" 
            url="<?php
            echo Yii::app()->controller->createUrl(
                "createAuthor",
                array("id"=>$model->id)); ?>"
            value="Add"/>
        </div>
<?php }
 
} ?>
  1. Add the createAuthor action to the book controller.
public function actionCreateAuthor($id) {
  // request must be made via ajax
  if(isset($_GET['ajax']) && isset($_GET['Person'])) {
    $model=$this->loadModel($id);
    $author = new Person();
    $author->attributes=$_GET['Person'];
    if (($author->fname != null) &&
       ($author->lname !=null) )
    {
      $model->addAuthor($author);
      $this->renderPartial('_li',array(
          'model'=>$model,
          'author'=>$author,
      ), false, true);
    }
  }
  else {
    throw new CHttpException(400,'Invalid request.');
  }
}
  1. Add createAuthor to authorized actions.
array('allow', // allow authenticated user to perform 'create' and 'update' actions
    'actions'=>array('create','update', 'removeAuthor', 'createAuthor'),
    'users'=>array('@'),
),
  1. Let's see what we've got. The create form is as follows:

The revised update form is as follows:

Objective Complete - Mini Debriefing

As you cannot add multiple authors to an object until you have created it, for now we put this capability on the update form. This task has shown us how to make extensive changes to the existing Yii infrastructure, and gives a feel for the kind of real work you can expect to do with the framework.

Creating a Mobile View Widget

We will create a widget to customize the list view for books. When we are finished, the mobile view for the book list will look a lot better.

Engage Thrusters

  1. We will create a directory for our extension to keep everything together. Create an extensions directory in ~/ch2/protected as follows:
cd ~/projects/cbdb/ch2/protected
mkdir extensions
  1. Make a directory for the widget under extensions named mobile.
cd ~/projects/cbdb/ch2/protected/extensions
mkdir mobile
  1. In the widget directory, create a file named ListView with an init and run function. The init function will prepare any assets that your view needs, but our mobile layout has already taken care of this for us. The run function will render the widget.
<?php
class ListView extends CWidget
{
  public $dataProvider;
  public $itemView; 
 
  public function init()
  {
    parent::init();
    // add any assets here
  }
 
  public function run()
  {
    parent::run();
    if($this->dataProvider===null)
      throw new CException(Yii::t('ext.mobile','"dataProvider" field must be set.'));
      if($this->itemView===null)
        throw new CException(Yii::t('ext.mobile','"itemView" field must be set.'));
 
    $this->render('body');
  }
}
  1. Create a view directory.
cd ~/ch2/protected/extensions/mobile
mkdir views
  1. Create the view for the widget in the views directory (ch2 | Source Files | protected | extensions | mobile | views | body.php). The view will bracket our data in a jQuery mobile list and render our itemView template in each list element.
<ul data-role="listview" data-theme="g">
<?php
    $data = $this->dataProvider->getData();
    $owner = $this->getOwner();
    foreach ($data as $i=>$item) {
      echo "<li>";
      $owner->renderPartial($this->itemView, $item);
      echo "</li>";
    }
?>
</ul>
  1. Copy the _view.php view file in the book view directory to a file named _mview.php. Edit _mview.php. Remove the div tags and the headers. Remove all fields except for title and notes. Add an h1 tag around title and a p tag around notes. Put the whole thing inside a single PHP tag so it looks like this:
<?php 
  echo "<h1>" . CHtml::encode($data->title) . "</h1>"; 
  echo "<p>" . CHtml::encode($data->notes) . "</p>"; 
?>
  1. Copy the index.php view file in the book view directory to a file named mobile_index.php. Remove the header, change the widget call from zii.widgets.CListView to our new widget, and change the item view to _mview.php. Put everything in a single set of PHP tags. The file will look like this:
<?php
$this->menu=array(
    array('label'=>'Create Book', 'url'=>array('create')),
    array('label'=>'Manage Book', 'url'=>array('admin')),
);
 
$this->widget('ext.mobile.ListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_mview',
)); ?>
  1. Update the index action in the book controller to render the mobile view if access is from a mobile device.
public function actionIndex()
{
   $view = 'index';
   $dataProvider=new CActiveDataProvider('Book');
   if (Yii::app()->user->getState('mobile')) {
     $view = 'mobile_index';
   } 
   $this->render($view,array(
     'dataProvider'=>$dataProvider,
   ));
}
  1. At this point, the book index should display a nice readable list of books.
  2. To make the list more manageable, we can add a search feature. jQuery makes this easy by providing built-in support for a list search. Just add data-filter="true" to the list tag.
<ul data-role="listview" data-theme="g" data-filter="true">
  1. Let's look at what we have now:
  1. The list looks a lot nicer now. Let's see how our mobile-optimized, all-in-one search filter works.
  1. It is awesome, indeed. Try a different filter:

That is some powerful stuff.

  1. Let's add issue numbers to this new view. You could then use this view to see what books you have while you are shopping at the comic book store.
  2. Open ch2 | Source Files | protected | views | book | _mview.php again and change it up:
<?php 
  echo '<h1>' . CHtml::encode($data->title);
  if (!is_null($data->issue_number)) {
    echo  ' ' . CHtml::encode($data->issue_number);
  }
  echo '</h1>'; 
  echo '<p>' . CHtml::encode($data->notes) . '</p>'; 
?>
  1. Better yet, let's change the view so that you can click on the list items to pull up the detailed view of the record. Look at what your previous changes did, then try changing it to the following code snippet:
<?php 
  echo '<a href="/cbdb/index.php/book/' . $data->id . '">';
  echo '<h1>' . CHtml::encode($data->title);
  if (!is_null($data->issue_number)) {
    echo  ' Issue: ' . CHtml::encode($data->issue_number);
  }
  echo '</h1>'; 
  echo '<p>' . CHtml::encode($data->notes) . '</p></a>'; 
?>
  1. After adding the preceding code, the list will look like the following screenshot:

Here is what the detailed view looks like:

This is a useful set of changes.

Objective Complete - Mini Debriefing

Let's look at what we've accomplished: We created a widget for listing book objects in the mobile view and called it ListView, we made changes necessary to provide full mobile functionality to the list view for comic books, and we added a slick mobile search.

Mission Accomplished

We have learned a great deal about adding mobile functionality to a Yii project. We have seen how to include jQuery Mobile and use it in our layouts, views, and forms. We know how to make a nice mobile search. We have examples of how to add functionality and fields to an existing Yii project.

You Ready to go Gung HO? A Hotshot Challenge

Here are some suggestions to try for yourself with this project:

  • Mobile optimization
    • Try out your mobile view with several different mobile devices. Does it work for tablets? If you find unsupported devices, extend the device identifier algorithm.
    • Review the forms and other pages of the app and optimize them for mobile viewing.
  • Book form extending
    • Add confirmation dialog to author delete action
    • Add Ajax error handling to author add and delete actions
    • Add Ajax confirmation flash for author add and delete actions
    • Include publisher, illustrator, and tag fields in book form
  • Book view perfecting
评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com