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

Chapter 11: Iteration 8: Making it Pretty - Design,Layout,Theme, and Internationalization(i18n)

In the previous iteration, we started to add a little beauty to our application by making our URLs more attractive to both, the user and to search engine bots that crawl the site. In this iteration, we are going to turn more focus to the look and feel of our application by covering the topics of page layouts and themes in Yii. Though we will live up to the title of this chapter by changing the look of our application to something we believe is slightly better looking, we will be focused on the approach one takes and the tools available to help design the front-end of a Yii application rather than design itself. So this iteration will focus more on how you can make your applications pretty, rather than spending a lot of time specifically designing our TrackStar application.

Iteration planning

This iteration aims to focus on the frontend. We want to create a new look for our site that is reusable and able to be implemented dynamically. We also want to accomplish without overwriting or otherwise removing our current design. Also, we are going to dig into the internationalization features of Yii, so we need to clearly understand how to accommodate application users from different geographic regions.

The following is a list of high-level tasks that we will need to complete in order to achieve our goals:

  • Create a new theme for our application by creating new layout, CSS and other asset files for providing the application with a new front-end design
  • Use the internationalization and localization features of Yii to help translate a portion of our application to a new language

Designing with layouts

One thing that you may have noticed is that we have added a lot of functionality to our application without adding any explicit navigation to access this functionality. Our home page has not yet changed from the default application we built. We still have the same navigation items as we did when we first created our new application. We need to change our basic navigation to better reflect the underlying functionality present in the application.

Thus far, we have not fully covered how our application is using all of the view files responsible for displaying the content. We know that our view files are responsible for our data display and housing the HTML sent back for each page request. When we create new controller actions, we also often create new views to handle the display of the returned content from these action methods. Most of these views are very specific to the action methods they support and not used across multiple pages. However, there are some things, like the main menu navigation, that are used across multiple pages throughout the site. These types of UI components are better suited to reside in what are called layout files.

A layout in Yii, is a special view file used to decorate other view files. Layouts typically contain markup or other user interface components that are common across multiple view files. When using a layout to render a view file, Yii embeds the view file into the layout.

Specifying a layout

There are two main places where a layout can be specified. One is the property called $layout of the CWebApplication itself. This defaults to protected/views/ layouts/main.php if not otherwise explicitly specified. As is the case with all application settings, this can be overridden in the main config file, protected/ config/main.php. For example, if we created a new layout file, protected/views/ layouts/newlayout.php, and wanted to use this new file as our application-wide layout file, we could alter our main config file to set the layout property as such:

return array(
    'layout'=>'newlayout',

The filename is specified without the .php extension and is relative to the $layoutPath property of CWebApplication, which defaults to Webroot/ protected/views/layouts (which itself could be overridden in a similar manner if this location does not suit your application's needs).

The other place to specify the layout is by setting the $layout property of the controller class. This allows for more granular control of the layout on a controller- by-controller basis. This is the way it was specified when we generated the initial application. Using the yiic tool to create our initial application automatically created a controller base class, Webroot/protected/components/Controller.php, from which all of our other controller classes extend. Opening up this file reveals that the $layout property has been set to "column1". Setting the layout file at the more granular controller level will override the setting in the CWebApplication class.

Applying and using a layout

The use of a layout file is implicit in the call to the CController::render() method. That is, when you make the call to the render() method to render a view file, Yii will embed the contents of the view file into the layout file specified in either the controller class, or the one specified at the application level. You can avoid applying any layout decoration of the rendered view file by calling the CController::renderPartial() method instead.

As previously mentioned, a layout file is typically used to decorate other view files. One example use of a layout is to provide a consistent header and footer layout to each and every page. When the render() method is called, what happens behind the scenes is first a call to renderPartial() on the specified view file. The output of this is stored in a variable called $content, which is then made available to the layout file. So, a simple layout file might look like the following:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> 
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta name="language" content="en" /> 
    </head>
    <body> 
        <div id="header">Some Header Content Here </div>
        <?php echo $content; ?>
        <div id="footer"> Some Footer Content Here</div> 
    </body> 
</html>

In fact, let's try this out. Create a new file called newlayout.php and place it in the default folder for layout files, /protected/views/layouts/. Add the above HTML content to this file and save it. Now we'll put this to use by altering our site controller to use this new layout. Open up SiteController.php and override the layout property set in the base class by explicitly adding it to this class, as such:

class SiteController extends Controller 
{
    public $layout='newlayout';

This will set the layout file to newlayout.php, but only for this controller. Now, every time we make the call to the render() method within SiteController, the newlayout.php layout file will be used.

One page that SiteController is responsible for rendering is the login page. Let's take a look at that page to verify these changes. If we navigate to http://localhost/trackstar/site/login (assuming we are not already logged in), we will see something similar to the following screenshot:

If we simply comment out the $layout attribute we just added, and refresh the login page again, we are back to using the original main.php layout and our page is now back to what it looked like before.

Deconstructing the main.php layout file

So far, all of our application pages have been using the main.php layout file to provide the primary layout markup. Before we go making changes to our page layout and design, it would serve us well to take a closer look at this main layout file. The following is a listing of the entire contents of that file:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> 
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta name="language" content="en" />
        <!-- blueprint CSS framework -->
        <link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->request->baseUrl; ?>/css/screen.css" 
            media="screen, projection" />
        <link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->request->baseUrl; ?>/css/print.css"
            media="print" />
 
        <!--[if lt IE 8]>
        <link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->request->baseUrl; ?>/css/ie.css" 
            media="screen, projection" />
        <![endif]-->
 
       <link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->request->baseUrl; ?>/css/main.css" />
       <link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->request->baseUrl; ?>/css/form.css" />
 
       <title><?php echo CHtml::encode($this->pageTitle); ?></title> 
    </head>
 
    <body> 
        <div class="container" id="page">
            <div id="header"> 
                <div id="logo"><?php echo CHtml::encode(Yii::app()->name); ?></div> 
            </div>
            <!-- header -->
 
            <div id="mainmenu"> 
            <?php $this->widget('zii.widgets.CMenu',array(
                'items'=>array( 
                    array('label'=>'Home', 'url'=>array('/site/index')), 
                    array('label'=>'About', 'url'=>array('/site/page','view'=>'about')), 
                    array('label'=>'Contact', 'url'=>array('/site/contact')), 
                    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 -->
 
           <?php $this->widget('zii.widgets.CBreadcrumbs', 
                    array( 'links'=>$this->breadcrumbs,
           )); ?>
           <!-- breadcrumbs -->
 
           <?php echo $content; ?>
 
           <div id="footer"> Copyright &copy; <?php echo date('Y'); ?> by My Company.<br/> 
                    All Rights Reserved.<br/> <?php echo Yii::powered(); ?>
           </div><!-- footer -->
        </div><!-- page -->
    </body> 
</html>

We'll walk through this starting at the top. The first five lines probably look somewhat familiar to you.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="language" content="en" />

These lines define a standard HTML document type declaration, followed by a starting <html> element, then the start of our <head> element. Within the <head> tag, we first have a meta tag to declare the standard, and very important, XHTML- compliant UTF-8 character encoding, followed by another <meta> tag that specifies English as the primary language in which the website is written.

Introducing the Blueprint CSS framework

The next several lines, beginning with the comment may be less familiar to you. Another great thing about Yii is that it utilizes other best-in-breed frameworks, when appropriate, and the Blueprint CSS framework is one such example.

The Blueprint CSS framework was included in the application as a by-product of using the yiic tool when we initially created our application. It is included to help standardize the CSS development. Blueprint is a CSS Grid framework. It helps standardize your CSS, provides cross-browser compatibility, and provides consistency in HTML element placement helping reduce CSS errors. It comes with many screen and print-friendly layout definitions and helps jumpstart your design by providing much of the css you need to get something that looks good and in place quickly. For more on the Blueprint framework, visit http://www.blueprintcss.org/.

So, the following lines of code are required by and specific to the Blueprint CSS framework:

<!-- blueprint CSS framework --> 
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()- >request->baseUrl; ?>/css/screen.css" 
    media="screen, projection" /> 
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()- >request->baseUrl; ?>/css/print.css" 
    media="print" /> 
<!--[if lt IE 8]> 
<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()- >request->baseUrl; ?>/css/ie.css" 
    media="screen, projection" /> 
<![endif]-->

Understanding the Blueprint installation

Yii by no means requires the use of Blueprint. However, as the default application generated does include the framework, understanding its installation and use will be beneficial.

The typical installation of Blueprint involves first downloading the framework files, and then placing three of its .css files into the Yii application's main CSS folder. If we take a peek under the main Webroot/css folder within our TrackStar application, we already see the inclusion of these three files:

  • ie.css
  • print.css
  • screen.css

So, luckily for us, the basic installation has already been completed as a consequence of our using the yiic webapp command to generate our application. In order to take advantage of the framework, the above tags needs to be placed under the tag for each web page. This is why these declarations are made in the layout file.

The next two tags:

<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()- >request->baseUrl; ?>/css/main.css" /> <link rel="stylesheet" type="text/css" href="<?php echo Yii::app()- >request->baseUrl; ?>/css/form.css" />

define some custom css definitions used to provide some layout declarations in addition to the ones specified in the Blueprint files. You should always place any custom ones below the ones provide by Blueprint, so that your custom declarations take precedence.

Setting the page title

Setting a specific and meaningful page title on a per page basis is important to properly indexing your website pages in search engines and helpful to users who want to bookmark specific pages of your site. The next line in our main layout file specifies the page title in the browser:

<title><?php echo CHtml::encode($this->pageTitle); ?></title>

Remember that $this in a view file refers to the controller class that initially rendered the view. The $pageTitle attribute is defined down in the Yii's CController base class and will default to the action name followed by the controller name. This is easily customized in the specific controller class, or even within each specific view file.

Defining a page header

It is often the case that websites are designed to have consistent header content repeated across many pages. The next few lines in our main layout file define the area for a page header:

<body> 
    <div class="container" id="page">
        <div id="header"> 
            <div id="logo"><?php echo CHtml::encode(Yii::app()->name); ?></div> 
        </div><!-- header -->

The first <div> tag with a class of "container" is required by the Blueprint framework in order to display the content as a grid.

Again, using the Blueprint CSS Grid framework, or any other CSS framework is not at all a requirement of Yii. It is just there to help you jumpstart your design layout if desired.

The next three lines layout the first of the main content we see on these pages. It displays the name of the application in large letters. So far, this has been displaying the text 'My Web Application'. I am sure that has been driving some of you crazy. Although we may change this later to use a logo image, let's go ahead and change this to the real name of our application, 'TrackStar'.

We could hardcode this name right here in the HTML. However, if we alter our application configuration to reflect our new name, the changes will propagate everywhere throughout the site wherever Yii::app()->name is being used. I am sure you could make this simple change in your sleep at this point. Simply open up the main configuration file where our application configuration settings are defined, /protected/config/main.php and change the value of the 'name' property from:

'name'=>'My Web Application',

To:

'name'=>'TrackStar'

Save the file, refresh your browser and the header on the home page should now look something similar to the following screen:

One thing we immediately notice in the above image is that the change has been made in two places. It just so happens that the view file responsible for our home page content, /protected/views/site/index.php, also uses the application name property. As we made the change in the application configuration file, it is reflected in both places.

Displaying menu navigation items

The main site navigation controls are often repeated across multiple pages in a web application, and housing this in a layout makes it easy to reuse. The next block of markup and code in our main layout file defines the top-level menu items:

<div id="mainmenu"> 
<?php $this->widget('zii.widgets.CMenu',array(
    'items'=>array( 
        array('label'=>'Home', 'url'=>array('/site/index')), 
        array('label'=>'About', 'url'=>array('/site/page', 'view'=>'about')), 
        array('label'=>'Contact', 'url'=>array('/site/contact')), 
        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 -->

Here we see that one of the official Zii extensions, called CMenu, is being used. We introduced Zii back in Chapter 9. To jog your memory, the Zii extension library is a set of extensions developed by the Yii developer team. This library comes packaged with the download of the Yii Framework. Any of these extensions are easily used within a Yii application by simply referring to the desired extension class file using a path alias in the form of zii.path.to.ClassName. The root alias, zii, is predefined by the application, and the rest of the path is relative to this framework folder. So, as this Zii menu extension resides on your filesystem at Path-to-your-Yii- Framework/zii/widgets/CMenu.php, we can simply use zii.widgets.CMenu when referring to this in our application code.

Without having to know too much about the specifics of CMenu, we can see it that it takes in an array of associative arrays that provide the menu item label, a URL to which that item should link, and an optional third value, visible, that can be set to a boolean value indicating whether or not that menu item should display. This is used here when defining the 'login' and 'logout' menu items. Obviously, we only want the 'login' menu item to display as a clickable link if the user is not already logged in. And, conversely, we would only want the "Logout" menu link to display if the user is already logged-in. The use of the visible element in the array that defines these menu items allows you to display these links dynamically based on whether the user is logged in or not. The use of Yii::app()->user->isGuest is used for this. This returns true if the user is not logged in (that is, they are a guest of the application) or false if the user is logged in. I am sure that you have already noticed that the 'login' option turns into a 'logout' option in our application's main menu whenever you are logged in, and vice versa.

Let's update our menu to provide a way to navigate to our specific TrackStar functionality. First off, we don't want anonymous users to be able to access any real functionality except the login. So we want to make sure that the login page is more or less the home page for anonymous users. Also, the main home page for logged-in users should just be a listing of their projects. We'll achieve this by making the following changes:

  1. Change our default home URL for the application to be the project listing page, rather than just site/index as it is now.
  2. Change the default action within our default controller, SiteController, to be the login action. This way, any anonymous user that visits the top-level URL, http://localhost/trackstar/, will be redirected to the login page.
  3. Alter our actionLogin() method to redirect the user to the project listing page if they are already logged in.
  4. Change the 'home' menu item to read 'project', and change the URL to be the project listing page.

These are actually very simple changes to make. Starting at the top, we can change the 'homeUrl' application property in our main application configuration file. Open up protected/config/main.php and add the following name=>value pair to the returned array:

'homeUrl'=>'/trackstar/project',

This is all that is needed to make that change.

For the next change, open up protected/controllers/SiteController.php and add the following to the top of the controller class:

public $defaultAction = 'login';

This sets the default action to be 'login'. Now if you visit your top-level URL for the application, http://localhost/trackstar/, you should be taken to the login page. The only issue with this is that you will continue to be taken to the login page from this top-level URL regardless of whether you are already logged in or not. Let's fix this by implementing step 3 above. Change the actionLogin() method within SiteController to include the following code at the beginning of the method:

public function actionLogin() 
{
    if(!Yii::app()->user->isGuest) 
    {
        $this->redirect(Yii::app()->homeUrl);
    }

This will redirect all logged-in users to the application homeUrl that we just previously set to be the project listing page.

Finally, let's alter the input array to our CMenu widget to change the specification for the "Home" menu item. Alter that block of code in the main.php layout file and replace:

array('label'=>'Home', 'url'=>array('/site/index')),

with:

array('label'=>'Projects', 'url'=>array('/project')),

With this replacement, all of our previously outlined changes are in place. If we now visit the TrackStar application as an anonymous user, we are directed to the login page. If we click on the Projects link, we are still directed to the login page. We can still access the About and Contact pages, which is fine for an anonymous user. If we log in we are directed to the project listing page. Now if we click the Projects link, we are allowed to see the project listings.

Creating a breadcrumb navigation

Turning back to our main.php layout file, the three lines of code that follow our menu widget define another Zii extension widget called CBreadcrumbs:

<?php $this->widget('zii.widgets.CBreadcrumbs', array( 
    'links'=>$this->breadcrumbs,
)); ?><!-- breadcrumbs -->

This widget is used to display a list of links indicating the position of the current page, relative to other pages, in the whole website. For example, a linked navigation list of the format:

Projects >> Project 1 > > Edit

indicates the user is viewing an Edit page for Project 1. This is helpful for the user to find their way back to where they started, which is a listing of all the projects, as well as easily see where they are in the website page hierarchy. This is why it is referred to as a breadcrumb. Many websites implement this type of UI navigational component in their design.

To use this widget, we need to configure its links property, which specifies the links to be displayed. The expected value for this property is an array that defines the breadcrumb path from a starting point, down to the specific page being viewed. Using our previous example, we could specify the links array as such:

array( 
    'Projects'=>array('project/index'), 
    'Project 1'=>array('project/view','id'=>1), 
   'Edit', 
)

The breadcrumbs widget, by default, adds in the very top level "Home" link automatically, based on the application configuration setting homeUrl. So, what would be generated from the above would be a breadcrumb like:

Home >> Projects >> Project 1 >> Edit

As we explicitly set our application $homeUrl property to be the project listings page, our first two links are the same in this case. The code in the layout file sets the link property to be the $breadcrumbs property of the controller class that is rendering the view. You can see this explicitly being set in several of the view files that were autogenerated for us when we created our controller files using the Gii code generation tool. For example, if you take a look at protected/views/project/ update.php, you see at the very top of that file the following:

<?php 
$this->breadcrumbs=array(
    'Projects'=>array('index'), 
    $model->name=>array('view','id'=>$model->id), 
    'Update',
);

And if we navigate to that page in the site, we see the following navigational breadcrumb generated, just below the main navigation:

Specifying the content being decorated by the layout

The next line in the layout file is where the content of the view file that is being decorated by this layout file is placed:

<?php echo $content; ?>

As was discussed earlier in this chapter, when you use $this->render() in a controller class to display a certain view file, the use of a layout file is implied. Part of what this method does is to place all of the content in the specific view file being rendered into a special variable called $content, which is then made available to the layout file. So, if we again take our project update view file as an example, the contents of $content would be the rendered content contained in the file protected/views/project/update.php.

Defining the footer

Just as with the header area, it is often the case that websites are designed to have consistent footer content repeated across many pages The final few lines of our main.php layout file define a consistent footer for very page:

<div id="footer"> 
    Copyright &copy; <?php echo date('Y'); ?> by My Company.<br/> 
    All Rights Reserved.<br/> 
    <?php echo Yii::powered(); ?>
</div><!-- footer -->

There is nothing special going on here but we should go ahead and update it to reflect our specific site. We can leave the Powered by Yii Framework line in there to help promote this great framework. So, just change the "My Company" in the above code to "TrackStar", and we're done. Refreshing the pages in the site now displays a consistent footer as depicted in the following figure:

Nesting the layouts

Though it is true that the original layout we have been seeing on our pages is utilizing the file protected/layouts/main.php, that is not the whole story. When our initial application was created, all of the controllers were created to extend from the base controller located at protected/components/Controller.php. If we take a peek into this file, we see that there is a layout property explicitly defined. But it does not specify the main layout file. It specifies "column1" as the default layout file for all child classes. You may have already noticed that when the new application was created, there were a few layout files generated for us as well, all in the protected/ views/layouts/ folder:

  • column1.php
  • column2.php
  • main.php

So, unless this is being explicitly overridden in a child class, our controllers are defining column1.php as the primary layout file, not main.php.

So, why did we spend all that time going through main.php, you ask? Well, it turns out that the column1.php layout file is itself decorated by the main.php layout file. So, not only can normal view files be decorated by layout files, but layout files themselves can be decorated by other layout files, forming a hierarchy of nested layout files. This allows for great flexibility in design and also greatly minimizes the need for any repeated markup in view files. Let's take a closer look at column1.php to see how this is achieved.

The contents of that file are as follows:

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

Here we see the use of a couple of methods we have not seen before. The use of the base controller methods beginContent() and endContent() are being used to decorate the enclosed content with the specified view. The view being specified here is our main layout page 'layouts/main'. The beginContent() method actually makes use of the built-in Yii widget, CContentDecorator, whose primary purpose is to allow for nested layouts. So, whatever content is between the calls to beginContent() and endContent() will be decorated with the view specified in the call to beginContent(). If nothing is specified, it will use the default layout specified either at the controller level, or if not specified at the controller level, at the application level.

The rest works just as a normal layout file. All of the markup in the specific view file will be contained in the variable $content when this column1.php layout file is rendered, and then the other markup contained in this layout file will be contained again in the variable $content made available to the final rendering of the main parent layout file, main.php.

Let's walk through an example. If we take the rendering of the login view as an example, i.e. the following code in the SiteController::actionLogin() method:

$this->render('login');

Behind the scenes, the following steps are taken:

  1. Render all of the content in the specific view file /protected/views/site/ login.php and make that content available via the variable $content to the layout file specified in the controller, which in this case is column1.php.
  2. As column1.php is itself being decorated by the layout main.php, the content between the beingContent() and endContent() calls is again rendered and made available to the main.php file, also again via the $content variable
  3. The layout file main.php is rendered and returned to the user, incorporating both the content from the specific view file for the login page, as well as the nested layout file, column1.php.

Another layout file that was autogenerated for us and being used in the application is column2.php. You probably won't be surprised to discover that this file lays out a two-column design. We can see this used in the project pages, where we have a little sub-menu Operations widget display along the right hand side. The contents of this layout are as follows, and we can see the same approach is being used to achieve the nested layout:

<?php $this->beginContent('/layouts/main'); ?> 
<div class="container">
    <div class="span-19"> 
        <div id="content">
            <?php echo $content; ?> 
        </div><!-- content -->
    </div> 
    <div class="span-5 last">
        <div id="sidebar"> 
        <?php
            $this->beginWidget('zii.widgets.CPortlet', array( 
                'title'=>'Operations',
            )); 
 
            $this->widget('zii.widgets.CMenu', array(
                'items'=>$this->menu,
                'htmlOptions'=>array('class'=>'operations'), 
            ));
            $this->endWidget(); 
        ?>
        </div><!-- sidebar --> 
    </div>
</div> 
<?php $this->endContent(); ?>

Creating themes

Themes provide a systematic way of customizing the design layout of a web application. One of the many benefits of an MVC architecture is the separation of the presentation tier from both the rest of the back-end stuff. Themes make great use of this separation by allowing you to easily and dramatically change the overall look and feel of a web application during runtime. Yii allows for an extremely easy application of themes to provide great flexibility in your web application design.

Building themes in Yii

In Yii, each theme is represented as a folder consisting of view files, layout files, and relevant resource files such as images, CSS files, JavaScript files, and so on. The name of a theme is the same as its folder name. By default, all themes reside under the same folder WebRoot/themes. Of course, as is the case with all other application settings, this default folder can be configured to be a different one. To do so, simply alter the basePath and the baseUrl properties of the themeManager application component.

Contents under a theme folder should be organized in the same way as those under the application base path. For example, all view files must be located under views/, layout view files under views/layouts/, and system view files under views/ system/. For example, if we have created a new theme, called custom, and we want to replace the update view of our ProjectController with a new view under this theme, we need to create a new update.php view file and save it in our application project as themes/custom/views/project/update.php.

Creating a Yii theme

Let's take this for a spin to give our TrackStar application a little facelift. We need to name our new theme and create a folder under the Webroot/themes folder with this same name. We'll exercise our extreme creativity and call our new theme, new.

Create a new folder to hold this new theme located at Webroot/themes/new. Also under this newly created folder, create two other new folders called css/ and views/. The former is not required by the theming system, but helps us keep our CSS organized. The latter is required if we are going to make any alterations to our default view files, which we are. As we are going to change the main.php layout file just a little, we need yet another folder under this newly created views/ folder called layouts/ (remember the folder structure needs to mirror that in the default Webroot/protected/views/ folder).

Now let's make some changes. As our view file markup is already referencing CSS class and ID names currently defined in the Webroot/css/main.css file, the fastest path to a new face on the application is to use this as a starting point, and make changes to it as needed to implement a new design. Of course, this is not a requirement, as we could re-create every single view file of our application in the new theme. However, to keep things simple, we'll create our new theme by making a few changes to the main.css file that was auto-generated for us when we created the application, as well as the primary layout file, main.php.

To begin with, let's make a copy of these two files and place them in our new theme folder. Copy Webroot/css/main.css to Webroot/themes/new/css/main.css and also copy Webroot/protected/views/layouts/main.php to Webroot/themes/ new/views/layouts/main.php.

Now, open the newly copied version of the main.css file remove the contents and then add all of the following:

body 
{
    margin: 0; 
    padding: 0; 
    color: #555; 
    font: normal 10pt Arial,Helvetica,sans-serif; 
    background: #d6d6d6 url(background.gif) repeat-y center top;
}
 
#page 
{
    margin-bottom: 20px; 
    background: white; 
    border: 1px solid #898989; 
    border-top:none; 
    border-bottom:none;
}
 
#header 
{
    margin: 0; 
    padding: 0; 
    height:100px; 
    background:white url(header.jpg) no-repeat left top; 
    border-bottom: 1px solid #898989;
}
 
#content 
{
    padding: 20px; 
}
 
#sidebar
{
    padding: 20px 20px 20px 0;
}
 
#footer 
{
    padding: 10px; 
    margin: 10px 20px; 
    font-size: 0.8em; 
    text-align: center; 
    border-top: 1px solid #C9E0ED;
}
 
#logo 
{
    padding: 10px 20px; 
    font-size: 200%; 
    /* HIDES LOGO TEXT */ 
    text-indent:-5000px;
}
 
#mainmenu 
{
    background:white url(bg2.gif) repeat-x left top; 
    border-top:1px solid #CCC; 
    border-bottom: 1px solid #7d7d7d;
}
 
#mainmenu ul 
{
    padding:6px 20px 5px 20px; 
    margin:0px;
}
 
#mainmenu ul li 
{
    display: inline;
}
 
#mainmenu ul li a 
{
    color:#333; 
    background-color:transparent; 
    font-size:12px; 
    font-weight:bold; 
    text-decoration:none; 
    padding:5px 8px;
}
 
#mainmenu ul li a:hover, #mainmenu ul li a.active 
{
    color: #d11e1e;
    background-color:#ccc; 
    text-decoration:none;
}
 
div.flash-error, div.flash-notice, div.flash-success 
{
    padding:.8em; 
    margin-bottom:1em; 
    border:2px solid #ddd;
}
 
div.flash-error 
{
    background:#FBE3E4; 
    color:#8a1f11; 
    border-color:#FBC2C4;
}
 
div.flash-notice 
{
    background:#FFF6BF; 
    color:#514721; 
    border-color:#FFD324;
}
 
div.flash-success 
{
    background:#E6EFC2; 
    color:#264409; 
    border-color:#C6D880;
}
 
div.flash-error a 
{
    color:#8a1f11;
}
 
div.flash-notice a 
{
    color:#514721;
}
 
div.flash-success a 
{
    color:#264409;
}
 
div.form .rememberMe label 
{
    display: inline;
} 
 
div.view 
{
    padding: 10px; 
    margin: 10px 0; 
    border: 1px solid #C9E0ED;
}
 
div.breadcrumbs 
{
    font-size: 0.9em; 
    padding: 10px 20px;
}
 
div.breadcrumbs span 
{
    font-weight: bold;
}
 
div.search-form 
{
    padding: 10px; 
    margin: 10px 0; 
    background: #eee;
}
 
.portlet 
{
}
 
.portlet-decoration 
{
    padding: 3px 8px; 
    background:white url(bg2.gif) repeat-x left top;
}
 
.portlet-title 
{
    font-size: 12px; 
    font-weight: bold; 
    padding: 0; 
    margin: 0;
    color: #fff;
}
 
.portlet-content 
{
    font-size:0.9em; 
    margin: 0 0 15px 0; 
    padding: 5px 8px; 
    background:#ccc;
}
 
.operations li a 
{
    font: bold 12px Arial; 
    color: #d11e1e; 
    display: block; 
    padding: 2px 0 2px 8px; 
    line-height: 15px; 
    text-decoration: none;
}
 
.portlet-content ul 
{
    list-style-image:none; 
    list-style-position:outside; 
    list-style-type:none; 
    margin: 0;
    padding: 0;
}
 
.portlet-content li 
{
    padding: 2px 0 4px 0px;
}
 
.operations 
{
    list-style-type: none; 
    margin: 0; 
    padding: 0;
}
 
.operations li 
{
    padding-bottom: 2px;
}
 
.operations li a 
{
    font: bold 12px Arial; 
    color: #0066A4; 
    display: block; 
    padding: 2px 0 2px 8px; 
    line-height: 15px; 
    text-decoration: none;
}
 
.operations li a:visited 
{
    color: #d11e1e;
}
 
.operations li a:hover 
{
    background: #fff;
}

You may have noticed that some of these changes are referencing image files that do not yet exist in our project. We have added a background.gif image reference in the body declaration, a new bg2.gif image referenced in the #mainmenu ID declaration and a new header.jpg image in the #header ID declaration. These can be viewed, downloaded and used by viewing the site online or accessing the images directly from http://www.yippyii.com/trackstar/themes/new/css/background.gif, http://www.yippyii.com/trackstar/themes/new/css/bg2.gif, and http://www.yippyii.com/trackstar/themes/new/css/header.jpg.

We need to place these new images into the same CSS folder we are using for this theme, namely Webroot/themes/new/css/.

After these changes are in place, we need to make a couple of small adjustments to our main.php layout file in this new theme. For one, we need to alter the markup in the element to properly reference our new main.css file. Currently the main.css file is being pulled in via this line:

<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()- >request->baseUrl; ?>/css/main.css" />

This is referencing the application request baseUrl property to construct the relative path to the CSS file. However, we want to use our new main.css file located in our new theme. For this, we can lean on the theme manager application component, defined by default to use the Yii built-in CThemeManager.php class. We access the theme manager in the same way as we access other application components. So, rather than use the request base URL, we should use the one defined by the theme manager, which knows what theme the application is using at any given time. So, we need to alter the above line in /themes/new/views/layouts/main.php as follows:

<link rel="stylesheet" type="text/css" href="<?php echo Yii::app()- >theme->baseUrl; ?>/css/main.css" />

Once we configure our application to use our new theme (something we have not yet done), this baseUrl will resolve to a relative path to where our theme folder resides.

The other small change we need to make is to remove the display of the application title from the header. As we altered our CSS to use a new image file to provide our header and logo information, we don't need to display the application name in this section. So, again in /themes/new/views/layouts/main.php, we simply need to change this:

<div id="header"> 
    <div id="logo"><?php echo CHtml::encode(Yii::app()->name); ?></div>
</div><!-- header -->

To the following:

<div id="header"></div><!-- header image is embeded into the #header declaration in main.css -->

We have put in a comment to remind us where our header image is defined.

One final change we need to make is to the other two layout files used in the application that we are not copying over to our new theme folder, namely protected/ views/layouts/column1.php and protected/views/layouts/column2.php. As previously discussed in the section on nesting layouts, these two layout files also use the main layout file via explicit calls to the beginContent() and endContent(). These files were auto-generated by the Gii code generation tool, and are explicitly referencing the main layout file in protected/views/layouts/ folder. We need to change the input specified to the beginContent() method so that, if available, our new theme layout will be used. Open both the column1.php and column2.php files and change the following line of code:

$this->beginContent('application.views.layouts.main');

To be the following:

$this->beginContent('/layouts/main');

Now, once we configure the application to use our new theme, it will first look for a main.php layout in the themes folder and use that file.

Configuring the application to use a theme

Okay, with our new theme now created and in place, we need to tell the application itself to use it. Doing so is easy. We just alter the main application's theme property setting by changing the main application configuration file. By now, we are old pros at doing this. Simply add the following name=>value pair to the returned array in the/protected/config/main.php file:

'theme'=>'new',

Once this is saved, our application is now using our newly created theme, new, and our application has a brand new face. Taking a look at the login page, which is also our default home page if not logged-in, we now see what is depicted in the following figure:

This, of course is not a huge change. We have kept the changes fairly minimal, but it does illustrate the process of creating a new theme. The application will first look for view files in this new theme and use them if they exists, otherwise, it will pull them from the default location. You can see how easy it is to give the application a new look and feel. You could create a new theme for each season, or maybe based on your different moods and then change the application to fit the season or mood quickly and easily as desired.

Translating the site to other languages

Before we leave this iteration, we are going to talk about internationalization (i18n) and localization (L10n) in Yii. Internationalization refers to the process of designing software applications in such a manner that it can be adapted to various languages without having to make underlying engineering changes. Localization refers to the process of adapting internationalized software applications for a specific geographic location or language by adding locale-dependent formatting and translating text. Yii provides support for these in the following ways:

  • It provides the locale data for nearly every language and region
  • It provides services to assist in the translation of text message and file
  • It provides locale-dependent date and time formatting
  • It provides locale-dependent number formatting

Defining locale and language

Locale refers to a set of parameters that define the user's language, country, and any other user interface preferences that may be relevant to a user's location. It is typically identified by a composite ID consisting of a language identifier and a region identifier. For example, a locale ID of en_US stands for the English language and the region of the United States. For consistency, all locale IDs in Yii are standardized to the format of either LanguageID or LanguageID_RegionID in lower case (for example, en or en_us).

In Yii, locale data is represented as an instance of the CLocale class, or a child class thereof. It provides locale-specific information including currency and numeric symbols; currency, number, date, and time formats; date-related names like months, days of week, and so on. Given a locale ID, one can get the corresponding CLocal instance by either using the static method CLocal::getInstance($localeID) or using the application. The following example code creates a new instance based on the en_us local identifier using the application component:

Yii::app()->getLocale('en_us');

Yii comes with locale data for nearly every language and region. The data comes from the Common Locale Data Repository (CLDR) (http://cldr.unicode.org/) and is stored in files that are named according to their respective locale id in the Yii Framework folder framework/i18n/data/. So, in the above example of creating a new CLocale instance, the data used to populate the attributes came from the file framework/i18n/data/en_us.php. If you look under this folder, you will see data files for a great many languages and regions.

So, going back to our example, if we wanted to get, say, the names of the months in English specific to the US region, we could execute the following code:

$locale = Yii::app()->getLocale('en_us'); 
print_r($locale->monthNames);

Which would produce the following:

Array ( [1] => January [2] => February [3] => March [4] => April [5] => May [6] => June [7] => July [8] => August [9] => September [10] => October [11] => November [12] => December )

Where as if we wanted, say, the same month names for the Italian language, we could do the same, but create a different CLocale instance:

$locale = Yii::app()->getLocale('it'); 
print_r($locale->monthNames);

Which would produce the following:

Array ( [1] => gennaio [2] => febbraio [3] => marzo [4] => aprile [5] => maggio [6] => giugno [7] => luglio [8] => agosto [9] => settembre [10] => ottobre [11] => novembre [12] => dicembre )

The first instance is based on the data file framework/i18n/data/en_us.php and the latter on framework/i18n/data/it.php. If desired, the application's localeDataPath property can be configured in order to specify a custom folder in which to add your custom locale data files.

Performing language translation

Perhaps the most desired feature of i18n is language translation. As mentioned previously, Yii provides both message translation and view translation. The former translates a single text message to a desired language, and the latter translates an entire file to the desired language.

A translation request consists of the object to be translated (either a string of text or a file), the source language that the object is in and the target language to which the object is to be translated. A Yii application makes a distinction between its target language and its source language. The target language is the language (or locale) that we are targeting for the user, where the source language refers to the language in which the application files are written. So far, our TrackStar application has been written in English and also targeted to English language users. So our target and source languages thus far have been the same. The internationalization features of Yii, which include translation, are applicable only when these two languages are different.

Performing message translation

Message translation is performed by calling the application method:

t(string $category, string $message, array $params=array ( ), string $source=NULL, string $language=NULL)

This method translates the message from the source language to the target language.

When translating a message, the category must be specified to allow a message to be translated differently under different categories (contexts). The category yii is reserved for messages used by the Yii Framework core code.

Messages can also contain parameter placeholders which will be replaced with the actual parameter values upon calling Yii::t(). The following example depicts the translation of an error message. This message translation request would replace the {errorCode} placeholder in the original message with the actual $errorCode value:

Yii::t('category', 'The error: "{errorCode}" was encountered during the last request.',    array('{errorCode}'=>$errorCode));

The translated messages are stored in a repository called message source. A message source is represented as an instance of CMessageSource or its child class. When Yii::t() is invoked, it will look for the message in the message source and return its translated version if it is found.

Yii comes with the following types of message sources:

  • CPhpMessageSource: This is the default message source. The message translations are stored as key-value pairs in a PHP array. The original message is the key and the translated message is the value. Each array represents the translations for a particular category of messages and is stored in a separate PHP script file whose name is the category name. The PHP translation files for the same language are stored under the same folder named as the locale ID. All these folders are located under the folder specified by basePath.
  • CGettextMessageSource: The message translations are stored as GNU Gettext files.
  • CDbMessageSource: The message translations are stored in database tables.

A message source is loaded as an application component. Yii pre-declares an application component named messages to store messages that are used in a user application. By default, the type of this message source is CPhpMessageSource and the base path for storing the PHP translation files is protected/messages.

An example will go a long way to helping bring all of this together. Let's translate the form field labels on our Login form into a fictitious language we'll call Reversish. Reversish is written by taking an English word or phrase and writing it in reverse. So, here are the Reversish translations of our login form field labels:

EnglishReversish
UsernameEmanresu
PasswordDrowssap
Remember me next timeEmit txen em rebmemer

We'll use the default CPhpMessageSource implementation to house our message translations. So, the first thing we need to do is create a PHP file containing our translations. We'll make the locale ID be 'rev' and we'll just call the category 'default' for now. So, we need to create a new file under the messages base folder that follows the format /localeID/CategoryName.php. So, for this example, we need to create a new file located at /protected/messages/rev/default.php, and then add the following translation array:

<?php return array(
    'Username' => 'Emanresu', 
    'Password' => 'Drowssap', 
    'Remember me next time' => 'Emit txen em rebmemer',
);

The next thing we need to do is to set the application target language to be Reversish. We could do this in the application configuration file, so that it would impact the entire site. However, as we only have translations for our login form, we'll just set it down in the SiteController::actionLogin() method, so that it will only apply when rendering the login form for now. So, open that file and set the application target language right at the beginning of that method:

public function actionLogin() 
{
    Yii::app()->language = 'rev';

Now, the last thing we need to do is to make our calls to Yii::t() so that these form field labels are sent through the translation. These form field labels are defined in the LoginForm:: attributeLabels() method. Replace that entire method with the following:

/** 
   * Declares attribute labels. 
   */
public function attributeLabels() 
{
    return array( 
        'rememberMe'=>Yii::t('default','Remember me next time'), 
        'username'=>Yii::t('default', 'Username'), 
        'password'=>Yii::t('default', 'Password'),
    );
}

Now if we visit our login form again, we see a new Reversish version as depicted in the following screenshot:

Performing file translation

Yii also provides the ability to use different files based on the target locale ID setting of the application. File translation is accomplished by calling the application method CApplication::findLocalizedFile(). This method takes in the path to a file and this method will look for a file with the same name, but under a directory named the same as the target locale ID specified either as explicit input to the method, or what is specified in the application configuration.

Let's try this out. All we really need to do is to create the appropriate translation file. We'll stick with translating the login form. So, create a new view file /protected/ views/site/rev/login.php and add the following contents that have already been translated to Reversish:

<?php 
$this->pageTitle='Nigol'; 
$this->breadcrumbs=array(
    'Nigol',
); 
?>
 
<h1>Nigol</h1> 
 
<p>Slaitnederc nigol ruoy htiw mrof gniwollof eht tuo llif esaelp:</p>
 
<div class="form"> 
<?php $form=$this->beginWidget('CActiveForm', array(
    'id'=>'login-form',
    'enableAjaxValidation'=>true, )); 
?>
    <p class="note">Deriuqer era <span class="required">*</span> htiw sdleif.</p>
    <div class="row"> 
        <?php echo $form->labelEx($model,'username'); ?> 
        <?php echo $form->textField($model,'username'); ?> 
        <?php echo $form->error($model,'username'); ?>
    </div>
    
    <div class="row"> 
        <?php echo $form->labelEx($model,'password'); ?> 
        <?php echo $form->passwordField($model,'password'); ?> 
        <?php echo $form->error($model,'password'); ?> 
        <p class="hint">
            <tt>nimda\nimda</tt> ro <tt>omed\omed</tt> htiw nigol yam uoy:tnih 
        </p>
    </div>
    
    <div class="row rememberMe"> 
        <?php echo $form->checkBox($model,'rememberMe'); ?> 
        <?php echo $form->label($model,'rememberMe'); ?> 
        <?php echo $form->error($model,'rememberMe'); ?>
    </div>
    
    <div class="row buttons"> 
        <?php echo CHtml::submitButton('Nigol'); ?>
    </div>
    
    <?php $this->endWidget(); ?>
</div><!-- form -->

We are already setting the target language for the application within the SiteController::actionLogin() method, and the call to get the localized file will be taken care of for us behind the scenes when calling render('login'). So, with this in place, our login form now looks as shown in the following screenshot:

Summary

In this iteration, we have seen how a Yii application allows you to quickly and easily polish up the design. We were introduced to the concept of layout files and walked through how to use these in an application to layout content and design that needs to be implemented in a similar manner across many different web pages. This also introduced us to the CMenu and CBreadcrumbs built-in widgets that provide easy to use and implement UI navigational constructs on each page.

We then introduced the idea of a theme within Web applications and how they are specifically implementing within a Yii application. We saw that themes allow you to easily put a new face on an existing Web application and allow you to re-design your application without re-building any of the functionality or backend

Finally, we looked at changing the face of the application through the lens of i18n and language translation. We learned how to set the target locale of the application to enable localization settings and language translations.

We have made a few references in this and past chapters to modules, but have yet to dive into what exactly these are within a Yii application. That is going to be the focus of the next chapter.

评论 X

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