《Agile Web Application Development with Yii1.1 and PHP5》

Chapter 12: Iteration 9: Modules - Adding Administration

So far we have added a lot of functionality to our TrackStar application. If you recall back in Chapter 8, we introduced user access controls to restrict certain functionality based on a user role hierarchy. This was helpful in restricting access to some of the administrative functions on a per-project basis. For example, within a specific project, you may not want to allow all members of the team access to delete the project. We used a role based access control implementation to assign users to specific roles within a project, and then allowed/restricted access to functionality based on those roles.

However, what we have not yet addressed are the administrative needs of the application as a whole. Web applications such as TrackStar often require the ability for very special users to have full access to administer everything. One example is the ability to manage all the CRUD operations for every single user of the system, regardless of the project. A system administrator of our application should be able to log in and remove or update any user, any project, any issues, moderate all comments, and so on. Also, it is often the case that we build extra features that apply to the whole application, like the ability to leave site-wide system messages to all users, manage e-mail campaigns, turn on/off certain application features, manage the roles and permissions hierarchy itself, change the site theme, and so on. As the functionality exposed to the administrator can differ greatly from the functionality exposed to normal users, it is often a good idea to keep these features separate from the rest of the application. We will be accomplishing this separation by building all of our administrative functionality in what is called a module in Yii.

Iteration planning

In this iteration, we will focus on the following granular development tasks:

  • Creating a new module to house administrative functionality
  • Creating the ability for administrators to add system-wide messages for application users to view on the projects listing page
  • Applying a new theme to the module
  • Creating a new table to hold the system message data
  • Generating all CRUD functionality for our system messages
  • Limiting access to all functionality within the new module only to admin users
  • Displaying new system messages on the projects listing page

Modules

A module is similar to an entire mini-application contained within a larger application. It has a similar structure, containing models, views, controllers, and other supporting components. However, modules cannot be deployed themselves as stand-alone applications, they must reside within an application.

Modules are useful in helping architect your application in a modular fashion. Large applications can often be segmented into discrete application features that could be separately built using modules. Site features such as adding a user forum, user blogs, or site-administrator functionality are some example candidates that could be segmented from the main site features allowing them to be developed separately and easily reused in future projects. We are going to use a module to create a distinct place in our application to house our administrative functionality.

Creating a module

Creating a new module is a snap using our old friend, the Gii code generation tool. With our URL changes in place, the tool is now accessible via http://localhost/ trackstar/gii. Navigate there, and choose the Module Generator option from the left menu. You will be presented with the following screen:

We need to provide a unique name for the module. As we are creating an admin module, we'll be super creative and give it the name admin. So type this in for the Module ID field, and click on the Preview button. As the following screenshot shows, it will present you with all of the files it intends to generate, allowing you to preview each of these files prior to creating them:

Then click the Generate button to have it create all of these files. You will need to ensure that your /protected folder is writable by the web server process for it to automatically create the required folders and files. The following screenshot shows a successful module generation:

Let's take a closer look at what the module generator created for us. A module in Yii is organized as a folder, the name of which is the same as the unique name of the module. By default, all module folders reside under protected/modules. The structure of each module folder is very similar to that of our main application. What this command has done for us is to create the skeleton folder structure for the admin module. As this was our first module, the top-level folder protected/modules was created, and then an admin/ folder underneath. The following shows all of the folders and files that were created when we executed the module command:

Name of folderUse/contents
admin/ 
  AdminModule.phpthe module class file
  components/containing reusable user components
  controllers/containing controller class files
    DefaultController.phpthe default controller class file
  messages/stores message translations specific to the module
  models/containing model class files
  views/containing controller view and layout files
    default/containing view files for DefaultController
      index.phpthe index view file
    layouts/containing layout view files

A module must have a module class that extends either directly or from a child of CWebModule. The module class name is created by combining the module ID (that is, the name we supplied when we created the module, admin) and the string Module. The first letter of the module ID is also capitalized. So, in our case, our admin module class file is named AdminModule.php. The module class serves as the central place for storing information shared by the module code. For example, we can use the params property of CWebModule to store module specific parameters, and use its components property to share application components at the module level. This module class serves a similar role to the module as the application class does to the entire application. So CWebModule is to our module what CWebApplication is to our application.

Using a module

Just as the successful creation message indicated, before we can use our new module we need to configure the modules property of the main application to include it for use. We did this before when we added the gii module to our application, which allowed us to access the Gii code generation tool. We make this change in the main configuration file, protected/config/main.php. The following highlighted code indicates the required change:

'modules'=>array( 
    'gii'=>array(
        'class'=>'system.gii.GiiModule', 
        'password'=>'iamadmin',
    ),
    'admin',
),

After saving this change, our new admin module is wired-up for use. We can take a look at the simple index page that was created for us by visiting http://localhost/trackstar/admin/default/index. The request routing structure to access pages in our module is similar to that for our main application pages, except that we need to include the moduleID in the route as well. So our routes will be of the general form / moduleID/controllerID/actionID. Our URL request /admin/default/index is requesting the admin module's default controller's index method. When we visit this page, we see something similar to the following screenshot:

Theming a module

We immediately notice that there doesn't seem to be any layout applied to this view. One might guess that maybe the controller that is rendering this view is calling renderPartial() rather than render(). However, upon inspection of our default admin controller file, /protected/modules/admin/controllers/ DefaultController.php, we see that it is, in fact, using the render() method. Thus, we expect a layout file (if one exists) to be applied.

The issue is that almost everything is separate in a module, including the default path for layout files. The default layout path for web modules is /protected/ modules/[moduleID]/views/layouts, where moduleID in our case is admin. We can see that there are no files under this folder, so there is no default layout to be applied.

There is slightly more to the story in our case, however. In the previous iteration, we implemented a new theme, called new. We can also manage all of our module view files, including the layout view files, within this theme as well. If we were to do that, we need to add to our theme folder structure to accommodate our new module. The folder structure is very much as expected. It is of a general form: / themes/[themeName]/views/[moduleID]/layouts/ for layout files and /themes/ [themeName]/views/[moduleID]/[controllerID]/ for controller view files.

To clarify, let's walk through Yii's decision-making process when it is trying to decide what view files to use for our new admin module. Here is what is happening when $this->render('index') is issued in the DefaultController.php file within our admin module:

  1. As render() is being called, as opposed to renderPartial(), it is going to attempt to decorate the specified index.php view file with a layout file. Our application is currently configured to use a theme called new, so it is going to look for layout files under this theme folder. Our new module's DefaultController class extends our application component Controller. php, which has column1 specified as its $layout property. This property is not overridden so it is also the layout file for DefaultController. Finally, as this is all happening within the admin module, Yii first looks for the following layout file: /themes/new/views/admin/layouts/column1.php. Notice the inclusion of the moduleID in this folder structure.
  2. This file does not exist, so it reverts to looking in the default location for the module. As previously mentioned, the default layout folder is specific to each module. So, in this case it will attempt to locate the following layout file: /protected/modules/admin/views/layouts/column1.php.
  3. This file also does not exist, so it will be unable to apply a layout. It will now simply attempt render the specified index.php view file without a layout. However, again as we have specified the specific "new" theme for the application, it first looks for the following view file: /themes/new/ views/admin/default/index.php.
  4. This file also does not exist, so it will look again in the default location for this controller (DefaultController.php) within this module (AdminModule), namely: /protected/modules/admin/views/default/index.php.

This explains why the page http://localhost/trackstar/admin/default/index is rendered without any layout. To keep things completely separate and simple for now, let us manage our view files in the default location for our module, rather than under the new theme. Also, let's apply to our admin module the same design as our original application had, that is, how the application looked before we applied the new theme. This way our admin pages will have a different look from our normal application pages, which will help remind us that we are in the special admin section, but we won't have to spend any time coming up with a new design.

Applying a theme

First, let's set a default layout value for our module. We set our module-wide configuration settings in the init() method within our module class, /protected/modules/admin/AdminModule.php. So open that file and add the following code in bold:

class AdminModule extends CWebModule 
{
    public function init() 
    {
        // this method is called when the module is being created
        // you may place code here to customize the module or the application
 
        // import the module-level models and components 
        $this->setImport(array(
            'admin.models.*', 
            'admin.components.*',
        ));
 
        $this->layout = 'main';
    } 
    ...

This way, if we have not specified a layout file at a more granular level, like in a controller class, all of the module views will be decorated by the layout file main.php located in the default layout folder for our module, namely /protected/modules/ admin/views/layouts/.

Now, of course, we need to create this file. Make a copy of the two layout files from the main application: /protected/views/layouts/main.php and /protected/ views/layouts/column1.php, and place them both in the /protected/modules/ admin/views/layouts/ folder. After you have copied those over, we need to make a few changes to both of them.

First let's alter column1.php. Remove the explicit reference to /layouts/main in the call to beginContent() as follows:

<?php $this->beginContent(); ?>
<div class="container"> 
    <div id="content">
        <?php echo $content; ?> 
    </div><!-- content -->
</div> 
<?php $this->endContent(); ?>

Not specifying an input file when calling beginContent() will result in it using the default layout for our module, which we just set to be our newly copied main.php file.

Now let's make a few changes to our main.php layout file. We are going to add Admin Console to our application header text to underscore that we are in a separate part of the application. We will also alter our menu items to have a link to the admin home page, as well as a link to go back to the main site. We can remove the About and Contact links from this menu, as we don't need to repeat those options in our admin section. The changes to the file are highlighted below:

... 
<div class="container" id="page"> 
<div id="header">
    <div id="logo">
    <?php echo CHtml::encode(Yii::app()->name) . " Admin Console"; ?>
    </div>
</div><!-- header --> 
 
<div id="mainmenu">
<?php $this->widget('zii.widgets.CMenu',array( 
    'items'=>array(
        array('label'=>'Back To Main Site', 'url'=>array('/proj-ect')),
        array('label'=>'Admin', 'url'=>array('/admin/default/in-dex')),
        array('label'=>'Login', 'url'=>array('/site/login'), 'visible'=>Yii::app()->user->isGuest),
        array('label'=>'Logout ('.Yii::app()->user->name.')', 'url'=>array('/site/logout'), 
            'visible'=>!Yii::app()->user->isGuest)), 
)); ?>
</div><!-- mainmenu -->
...

We can leave the rest of the file unchanged. Now if we visit our admin module page, we see something similar to the following screenshot:

If we click on the Back To Main Site link, we see that we are taken back to our newly themed version of our main application.

Restricting admin access

One problem you may have already noticed is that anyone, including guest users, can access our new admin module. We are building this admin module to expose application functionality that should only be accessible to users with administrative access. So, we need to address this issue.

Luckily, we have already implemented an RBAC access model in our application back in Chapter 8. All we need to do now is extend it to include a new role for administrators and new permissions available to that role.

If you recall from chapter 8, we used a Yii shell command to implement our RBAC structure. We need to add to that. So, open up the file containing that shell command, /protected/commands/shell/RbacCommand.php and add the following:

//create a general task-level permission for admins 
$this->_authManager->createTask("adminManagement", "access to the application administration functionality"); 
//create the site admin role, and add the appropriate permissions
$role=$this->_authManager->createRole("admin"); 
$role->addChild("owner"; $role->addChild("reader"); 
$role->addChild("member"); 
$role->addChild("adminManagement");
//ensure we have one admin in the system (force it to be user id #1) 
$this->_authManager->assign("admin",1);

With these changes in place, we have to rerun our command to update the database with these changes. To do so, just fire-up the yiic shell, and execute the rbac command:

% cd Webroot/trackstar 
% protected/yiic shell 
>> rbac

With these changes to our RBAC model in place, we can add an access check to the AdminModule::beforeControllerAction() method so that nothing within the admin module will be executed unless the user has the admin role:

public function beforeControllerAction($controller, $action) 
{
    if(parent::beforeControllerAction($controller, $action)) 
    {
        // this method is called before any module controller action is performed
        // you may place customized code here
        if( !Yii::app()->authManager->checkAccess("admin", Yii::app()- >user->id) )
        {
            throw new CHttpException(403,Yii::t('yii','You are not au- thorized to perform this action.'));
        }
        else {
            return true;
        }
    } 
    else
        return false;
}

With this in place, if a user who has not been assigned the admin role now attempts to visit any page within the admin module, they will be met with an authorization error page. For example, if you are not logged in and you attempt to visit the admin page, you will be met with the following result:

The same holds true for any user that has not been assigned to the admin role.

Now we can conditionally add a link to the admin section of the site to our main application menu. This way, users with administrative access won't have to remember a cumbersome URL to navigate to the admin console. As a reminder, our main application menu is located in our application's theme default application layout file /themes/new/views/layouts/main.php. Open that file and add the following highlighted code to the menu section:

<div id="mainmenu"> 
    <?php $this->widget('zii.widgets.CMenu',array(
        'items'=>array( 
            array('label'=>'Projects', 'url'=>array('/project')), 
            array('label'=>'About', 'url'=>array('/site/page', 'view'=>'about')), 
            array('label'=>'Contact', 'url'=>array('/site/contact')),
            array('label'=>'Admin', 'url'=>array('/admin/default/index'), 
                'visible'=>Yii::app()->authManager->checkAccess("admin", Yii::app()- >user->id)),
            array('label'=>'Login', 'url'=>array('/site/login'),
                'visible'=>Yii::app()->user->isGuest), 
            array('label'=>'Logout ('.Yii::app()->user->name.')', 'url'=>array('/site/logout'), 
                'visible'=>!Yii::app()->user->isGuest) ),
    )); ?> 
</div><!-- mainmenu -->

Now, upon logging in to the application as a user with admin access, we will see a new link in our top navigation that will take us to our newly added admin section of the site.

Adding a system-wide message

As a module can really be thought of as a mini-application itself, adding functionality to a module is really the same process as adding functionality to the main application. Let's add some new functionality just for administrators; the ability to manage system-wide messages displayed to users when they first log into the application.

Creating the database table

As is often the case with brand new functionality, we need a place to house our data. We need to create a new table to store our system-wide messages. For our purposes, we can keep this simple. Here is the definition for our table:

CREATE TABLE `tbl_sys_message` 
(
    `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, 
    `message` TEXT NOT NULL, 
    `create_time` DATETIME, 
    `create_user_id` INTEGER,
    `update_time` DATETIME, 
    `update_user_id` INTEGER
)

Create this new table in both the main trackstar_dev and our trackstar_test databases.

Creating our model and CRUD scaffolding

With the table in place, our next step is to generate the model class using our favorite tool, the Gii code generator. We'll first use the Model Generator option to create the model class, and then the Crud Generator to create our basic scaffolding to quickly interact with this model. Go ahead and navigate to the Gii tool form for creating a new model. This time, as we are doing this within the context of a module, we need to explicitly specify the model path. Fill out the form with the values depicted as shown in the following screenshot (though, of course, your Code Template path value should be specific to your local setup):

Now we can create the CRUD scaffolding in the same way. Again, the only real difference between what we have done previously and what we are doing now is our specification that the location of the model class is in the admin module. After choosing the Crud Generator option from the Gii tool, fill out the Model Class and Controller ID form fields as shown in the following screenshot:

This alerts the tool to the fact that our model class is under the admin module and that our controller class, as well as all other files related to this code generation should be placed within the admin module as well.

Complete the creation by first clicking on the Preview button, and then Generate. The following is a list of all of the files that are created by this action:

Adding a link to our new functionality

Let's add a new menu item within the main admin navigation that links to our newly created message functionality. Open the file that contains our main menu navigation for our module, /protected/modules/admin/views/layouts/main.php, and add the following array item to the menu widget:

array('label'=>'System Messages', 'url'=>array('/admin/sysMessage/ index')),

As the auto-created controller and view files for our new system message functionality were created to use a 2-column layout file, we can do one of two things. We can alter the controller class to use our existing single column layout file, or we can add a 2 column layout file to our module layout files. The latter is going to be slightly easier and will also look better, as all of the view files are created to have their sub-menu items (that is, the links to all the CRUD functionality) display in a second right-hand column. Here is all we have to do:

  1. Copy the 2 column layout from our main application to our module: That is, copy /protected/views/layouts/column2.phpto/protected/modules/ admin/views/layouts/column2.php.
  2. Remove /layouts/main as input to the beginContent() method call on the first line in the newly copied column2.php file.
  3. Alter the SysMessage model class to extend TrackstarActiveRecord (If you recall, this adds the code to automatically update our create_time/ user and update_time/user properties. Alter the SysMessageController controller class to use the new column2.php layout file from within the module folder and not the one from the main application. The autogenerated code has specified $layout='application.views.layouts.column2', but we need this to be simply $layout='column2'.
  4. As we are extending TrackstarActiveRecord, we can remove the unnecessary fields from our autogenerated sys messages creation form and remove their associated rules from the model class. Remove the following two rules from the SysMessage::rules() method: array('create_user, update_user', 'numerical', 'integerOnly'=>true), and array('create_time, update_time', 'safe').

You don't absolutely have to do this last step, but it is good to get in to the habit of only specifying rules for those fields that the user can input.

One last change we should make is to update our simple access rules to reflect the requirement that only users in the admin role can access our action methods. This is mostly for illustrative purposes as we already took care of the access using our RBAC model approach in the AdminModule::beforeControlerAction method itself. We could actually just remove the accessRules entirely. However, let's just update them to reflect the requirement so you can see how that would work using the access rule approach. In the SysMessageController::accessRules() method, change the entire contents to the following:

public function accessRules() 
{
    return array( 
        array('allow', // allow only users in the 'admin' role access to our actions 
            'actions'=>array('index','view', 'create', 'update', 'admin', 'delete'), 
            'roles'=>array('admin'),
        ), 
        array('deny', // deny all users
            'users'=>array('*'),
        ),
    );
}

Okay, with all of this in place, if we now access our new message input form by visiting http://localhost/trackstar/admin/sysMessage/create, we are presented with something similar to the following screenshot:

Fill out this form with the message Hello Users! This is your admin speaking... and then click Submit. The application will redirect you to the details listing page for this newly created message as shown in the following screenshot:

Displaying the message to users

Now that we have a message in our system, let's display it to the user on the application's home page.

Importing the new model class for application-wide access

In order to access the newly created model from anywhere in our application, we need to import it as a part of the application configuration. Alter protected/ config/main.php to include the new admin module models folder:

// autoloading model and component classes 
'import'=>array(
    'application.models.*',
    'application.components.*',
    'application.modules.admin.models.*',
),

Selecting the most recently updated message

We'll restrict the display to just one message, and we'll choose the most recently updated message, based on the update_time column in the table. As we want to add this to the main projects listing page, we need to alter the ProjectController::act ionIndex() method. Alter that method by adding the following highlighted code:

public function actionIndex() 
{
    $dataProvider=new CActiveDataProvider('Project');
 
    Yii::app()->clientScript->registerLinkTag( 
        'alternate',
        'application/rss+xml', 
        $this->createUrl('comment/feed'));
 
    //get the latest system message to display based on the update_time column
    $sysMessage = SysMessage::model()->find(array( 
        'order'=>'t.update_time DESC',
    )); 
 
    if($sysMessage != null)
        $message = $sysMessage->message;
    else
        $message = null;
 
    $this->render('index',array( 
        'dataProvider'=>$dataProvider,
        'sysMessage'=>$message,
    ));
}

Now we need to alter our view file to display this new bit of content. Add the following to views/project/index.php, just above the <h1>Projects</h1> header text:

<?php if($sysMessage != null):?> 
    <div class="sys-message">
        <?php echo $sysMessage; ?> 
    </div>
<?php endif; ?>

Now when we visit our projects listing page (that is, our application's home page) we can see it display as shown in the following screenshot:

Adding a little design tweak

Okay. This does what we wanted it to do, but this message does not really stand out very well to the user. Let's change that by adding a little snippet to our main css file (/themes/new/css/main.css):

div.sys-message 
{
    padding:.8em; 
    margin-bottom:1em; 
    border:3px solid #ddd; 
    background:#9EEFFF; 
    color:#FF330A; 
    border-color:#00849E;
}

With this in place, our message now really stands out on the page. The following screenshot shows the message with these changes in place:

One might argue that this design tweak went a little too far. Users might get a headache if they have to stare at those message colors all day. Rather than toning down the colors, let's use a little JavaScript to fade the message out after five seconds. As we will display the message every time the user visits this home page, it might be nice to prevent them from having to stare at it for too long.

We'll make things easy on ourselves and take advantage of the fact that Yii comes shipped with the powerful JavaScript framework jQuery. jQuery is an open source JavaScript library that simplifies the interaction between the HTML Document Object Model (DOM) and JavaScript. It is outside the scope of this book to dive into the details of jQuery, but it is well worth visiting its documentation to become a little acquainted with its features. As Yii comes shipped with jQuery, you can simply register jQuery code in view files and Yii will take care of including the core jQuery library for you.

We'll also use the application helper component CClientScript to register our jQuery JavaScript code for us in the resulting web page. It will make sure it is placed in the appropriate place as well as properly tagged and formatted.

So, let's alter what we previously added to include a snippet of JavaScript that will fade out the message. Replace what we just added to views/project/index.php with the following:

<?php if($sysMessage != null):?> 
    <div class="sys-message">
        <?php echo $sysMessage; ?> 
    </div>
<?php 
    Yii::app()->clientScript->registerScript(
        'fadeAndHideEffect',
        '$(".sys-message").animate({opacity: 1.0}, 5000). fadeOut("slow");'
    ); 
endif; ?>

Now if we reload our main projects listing page, we see the message fade out after five seconds. For more information on cool jQuery effects you can easily add to your pages, take a look at the JQuery API documentation at: http://api.jquery.com/category/effects/

Finally, to convince yourself everything is working as expected, you can add another system-wide message. As this newer message will have a more recent update_time property, it will be the one to display on the projects listing page.

Summary

In this iteration, we have introduced the concept of a Yii module and demonstrated its practicality by using one to create an administrative section of the site. We demonstrated how to create a new module, how to apply a theme, how to add application functionality within the module, and even how to take advantage of an existing RBAC model to apply authorization access controls to a functionality within a module. We also demonstrated how to use jQuery to add a dash of UI flare to our application.

With the addition of this administrative interface, we now have all of the major pieces of the application in place. Though the application is incredibly simple, we feel it is time to get it ready for production. The next iteration will focus on preparing our application for production deployment.

评论 X

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