3. Now, if we go to http://example.com/website/ok, we'll be redirected to http://example.com/website/index and a success message will be displayed. Moreover, if we go to http://example.com/website/bad, we will be redirected to the same page, but with an error message. Refreshing the index page will hide the message. Download at www.Pin5i.Com Router, Controller, and Views 64 How it works... We are setting a flash message with Yii::app()->user->setFlash('success', 'Everything went fine!'), for example, calling CWebUser::setFlash. Internally, it is saving a message into a user state, so in the lowest level, our message is being kept in $_SESSION until Yii::app()->user->getFlash('success') is called and the $_SESSION key is deleted. There's more… The following URL contains an API reference of CWebUser and will help you to understand flash messages better: http://www.yiiframework.com/doc/api/CWebUser Using controller context in a view Yii views are pretty powerful and have many features. One of them is that you can use controller context in a view. So, let's try it. Getting ready Set up a new application using yiic webapp. How to do it... 1. Create a controller as follows: class WebsiteController extends CController { function actionIndex() { $this->pageTitle = 'Controller context test'; $this->render('index'); } function hello() { if(!empty($_GET['name'])) echo 'Hello, '.$_GET['name'].'!'; } } Download at www.Pin5i.Com Chapter 2 65 2. Now, we will create a view showing what we can do:

pageTitle?>

Hello call. hello()?>

widget('zii.widgets.CMenu',array( 'items'=>array( array('label'=>'Home', 'url'=>array('index')), array('label'=>'Yiiframework home', 'url'=>'http://yiiframework.ru/', ), ))?> How it works... We are using $this in a view to refer to a currently running controller. When doing it, we can call a controller method and access its properties. The most useful property is pageTitle which refers to the current page title and there are many built-in methods that are extremely useful in views such as renderPartials and widget. There's more… The following URL contains API documentation for CController where you can get a good list of methods you can use in your view: http://www.yiiframework.com/doc/api/CController Reusing views with partials Yii supports partials, so if you have a block without much logic that you want to reuse or want to implement e-mail templates, partials are the right way to look. Getting ready 1. Set up a new application using yiic webapp. 2. Create a WebsiteController as follows: class WebsiteController extends CController { function actionIndex() { $this->render('index'); } } Download at www.Pin5i.Com Router, Controller, and Views 66 How to do it... We will start with a reusable block. For example, we need to embed a YouTube video at several website pages. Let's implement a reusable template for it. 1. Create a view file named protected/views/common/youtube.php and paste an embed code from YouTube. You will get something like: param> param> param> 2. Now, we need to make it reusable. We want to be able to set video ID, width, and height. Let's make width and height optional, as follows: param> param> embed> 3. Now, you can use it in your protected/views/website/index.php like this: renderPartial('////common/youtube', array( 'id' => '8Rp-CaIKvQs', // you can get this id by simply looking at video URL 'width' => 320, 'height' => 256, ))?> Looks better, right? Note that we have used // to reference a view. This means that Yii will look for a view starting from protected/views not taking controller name into account. 4. Now, let's send some e-mails. As we are unable to write unique letters to thousands of users, we will use a template but will make it customized. Let's add a new method to protected/controllers/WebsiteController.php as follows: class WebsiteController extends CController { function actionSendmails() Download at www.Pin5i.Com Chapter 2 67 { $users = User::model->findAll(); foreach($users as $user) { $this->sendEmail('welcome', $user->email, 'Welcome to the website!', array('user' => $user)); } echo 'Emails were sent.'; } function sendEmail($template, $to, $subject, $data) { mail($to, $subject, $this->renderPartial ('//email/'.$template, $data, true)); } } 5. Here is our template protected/views/email/welcome.php: Hello name?>, Welcome to the website! You can go check our new videos section. There are funny raccoons. Yours, Website team. How it works... CController::renderPartial does the same template processing as CController::render except the former does not use layout. As we can access current controller in a view using $this, we can use its renderPartial to use view within another view. renderPartial is also useful when dealing with AJAX as you don't need layout rendered in this case. There's more… For further information, refer to the following URL: http://www.yiiframework.com/doc/api/CController/#renderPartial-detail See also ff The recipe named Using controller context in a view in this chapter Download at www.Pin5i.Com Router, Controller, and Views 68 Using clips One of the Yii features you can use in your views is clips. The basic idea is that you can record some output and then reuse it later in a view. A good example will be defining additional content regions for your layout and filling them elsewhere. Getting ready Set up a new application using yiic webapp. How to do it... 1. For our example, we need to define two regions in our layout: beforeContent and footer. Open protected/views/layouts/main.php and insert the following just before the content output (): clips['beforeContent'])) echo $this->clips['beforeContent']?> Then, insert the following into

: clips['footer'])) echo $this->clips['footer']?> 2. That is it. Now, we need to fill these regions somehow. We will use a controller action for the beforeContent region. Open protected/controllers/ SiteController.php and add the following to actionIndex: $this->beginClip('beforeContent'); echo 'Your IP is '.Yii::app()->request->userHostAddress; $this->endClip(); 3. As for footer, we will set its content from a view. Open protected/views/site/ index.php and add the following: beginClip('footer')?> This application was built with Yii. endClip()?> 4. Now, when you open your website's index page, you should get your IP just before the page content and "built with" note in the footer. How it works... We mark regions with the code that just checks for existence of a clip specified and, if clip exists, the code outputs it. Then, we record content for clips we defined using special controller methods named beginClip and endClip. Download at www.Pin5i.Com Chapter 2 69 See also ff The recipe named Using controller context in a view in this chapter Using decorators In Yii, we can enclose content into a decorator. The common usage of decorators is layout. Yes, when you are rendering a view using the render method of your controller, Yii automatically decorates it with the main layout. Let's create a simple decorator that will properly format quotes. Getting ready Set up a new application using yiic webapp. How to do it... 1. First, we will create a decorator file protected/views/decorators/quote.php:

“”,

2. Now in protected/views/site/index.php, we will use our decorator: beginContent('//decorators/quote', array('author' => 'Edward A. Murphy'))?> If anything bad can happen, it probably will endContent()?> 3. Now, your homepage should include the following markup:

“If anything bad can happen, it probably will”, Edward A. Murphy

How it works... Decorators are pretty simple. Everything between beginContent and endContent is rendered into a $content variable and passed into a decorator template. Then, the decorator template is rendered and inserted in the place where endContent was called. We can pass additional variables into decorator using a second parameter of beginContent, such as the one we did for the author. Download at www.Pin5i.Com Router, Controller, and Views 70 Note that we have used //decorators/quote as view path. This means that the view will be searched starting from either theme views root or application views root. There's more… The following URL provides more details about decorators: http://www.yiiframework.com/doc/api/CContentDecorator/ See also ff The recipe named Defining multiple layouts in this chapter ff The recipe named Using controller context in a view in this chapter Defining multiple layouts Most applications use a single layout for all their views. However, there are situations when multiple layouts are needed. For example, an application can use different layouts at different pages: Two additional columns for blog, one additional column for articles, and no additional columns for portfolio. Getting ready Set up a new application using yiic webapp. How to do it... 1. Create two layouts in protected/views/layouts: blog and articles. Blog will contain the following code: beginContent('//layouts/main')?>

id?> - title?>

widget('CLinkPager', array( 'pages' => $pages, ))?> 3. Try to load http://example.com/post. You should get a working pagination and links that allow sorting list by ID or by title. How it works... First, we get total models count and initialize new pagination component instance with it. Then, we use the applyLimit method to apply limit and offset to criteria we have used for count request. After that, we create sorter instance for the model, specifying model attributes we want to sort by and applying order conditions to criteria by calling applyOrder. Then, we pass modified criteria to findAll. At this step, we have models list, pages, data used for link pager, and sorter that we use to generate sorting links. In a view, we are using data we gathered. First, we are generating links with CSort::link method. Then, we are listing models. Finally, using CLinkPager widgets we are rendering pagination control. There's more… Visit the following links to get more information about pagination and sorting: ff http://www.yiiframework.com/doc/api/CPagination/ ff http://www.yiiframework.com/doc/api/CSort/ Download at www.Pin5i.Com 3 AJAX and jQuery In this chapter, we will cover: ff Loading a block through AJAX ff Managing assets ff Including resources into the page ff Working with JSON ff Passing configuration from PHP to JavaScript ff Handling variable number of inputs Introduction Yii's client side is built with jQuery—the most widely used JavaScript library which is very powerful and simple to learn and use. In this chapter, we will focus on Yii-specific tricks rather than jQuery itself. If you need to learn more about jQuery, then please refer to its documentation at http://docs.jquery.com/. Loading a block through AJAX Nowadays, it's common when a part of a page is loaded asynchronously. Let's implement the quotes box which will display random quotes and will have the "Next quote" link to show the next one. Getting ready ff Create a fresh Yii application using yiic webapp as described in the official guide ff Configure application to use clean URLs Download at www.Pin5i.Com AJAX and jQuery 76 How to do it... Carry out the following steps: 1. Create a new controller named protected/controllers/QuoteController. php as follows: quotes[array_rand($this->quotes, 1)]; } function actionIndex() { $this->render('index', array( 'quote' => $this->getRandomQuote() )); } function actionGetQuote() { $this->renderPartial('_quote', array( 'quote' => $this->getRandomQuote(), )); } } Download at www.Pin5i.Com Chapter 3 77 2. We will require two views. The first is protected/views/quote/index.php:

Quote of the day

renderPartial('_quote', array( 'quote' => $quote, ))?>

'#quote-of-the-day'))?> The second view named protected/views/quote/_quote.php is as follows: “”, 3. That is it. Now, try to access your quote controller and click on the Next quote link, as shown in the following screenshot: How it works... First, we define a list of quotes in the controller's private property $quotes and create a method to get a random quote. In a real application, you will probably get a quote from a database using DAO or an active record. Then, we define a view for the index action and _quote which is used in the getQuote action that renders it without layout and the index view as a partial. In the index action, we use CHtml::ajaxLink to create a link which makes a request to the getQuote action and updates the HTML element with the ID of quote-of-the-day. This is done with a response CHtml::ajaxLink that generates the following code in the resulting HTML page (reformatted): As jQuery is being used, Yii includes it in the page automatically and does it only once, no matter how many times we are using it. You can see that Yii generated a #yt0 ID for us. That is great because you don't have to worry about setting IDs manually. Nevertheless, if you are loading a part of the page through AJAX and this part includes JavaScript-enabled widgets or CHtml AJAX helpers, then you need to set IDs manually because of possible IDs intersection. There's more... If you want to customize the success callback, then you can do this by setting it through a third parameter as follows: 'js:function(data){ alert(data); }'))?> Note that we used the js: prefix, which is required when you want to use JavaScript instead of a string, as in this example. In some cases, it is not desirable to allow a non-AJAX access to the getQuote action. There are two ways through which we can limit its usage to AJAX-only. First, you can use the built-in ajaxOnly filter as follows: class QuoteController extends Controller { function filters() { Download at www.Pin5i.Com Chapter 3 79 return array( 'ajaxOnly + getQuote', ); } … After adding this, ones who try to use the getQuote action directly will get an HTTP error: 400 Bad Request. The second way is to detect if request is made through AJAX with a special request method. For example, if we want to show the standard 404 "Not found" page, we can do this as follows: function actionGetQuote() { if(!Yii::app()->request->isAjaxRequest) throw new CHttpException(404); $this->renderPartial('_quote', array( 'quote' => $this->getRandomQuote(), )); } Similarly, we can use one action to serve both AJAX and non-AJAX responses: function actionGetQuote() { $quote = $this->getRandomQuote(); if(Yii::app()->request->isAjaxRequest) { $this->renderPartial('_quote', array( 'quote' => $quote, )); } else { $this->render('index', array( 'quote' => $quote, )); } } Download at www.Pin5i.Com AJAX and jQuery 80 Prevent including a bundled jQuery Sometimes, you need to suppress including a bundled jQuery. For example, if your project code relies on version specific functionality. To achieve this, you need to configure a clientScript application component using protected/config/main.php as follows: return array( // … // application components 'components'=>array( // … 'clientScript' => array( 'scriptMap' => array( 'jquery.js'=>false, 'jquery.min.js'=>false, ), ), ), // … ); Further reading For further information, refer to the following URLs: ff http://api.jquery.com/ ff http://docs.jquery.com/Ajax/jQuery.ajax#options ff http://www.yiiframework.com/doc/api/CHtml#ajax-detail ff http://www.yiiframework.com/doc/api/CHtml#ajaxButton-detail ff http://www.yiiframework.com/doc/api/CHtml#ajaxSubmitButton- detail ff http://www.yiiframework.com/doc/api/CHtml#ajaxLink-detail See also ff The recipes named Managing assets and Working with JSON in this chapter ff The recipe named Passing configuration from PHP to JavaScript in this chapter Download at www.Pin5i.Com Chapter 3 81 Managing assets An ability to manage assets is one of the greatest parts of Yii. It is especially useful in the following cases: ff When you want to implement an extension that stores its JavaScript, CSS, and images in its own folder and is not accessible from a browser ff When you need to pre-process your assets: combine JavaScript, compress it, and so on ff When you use assets multiple times per page and want to avoid duplicates While the first two cases could be considered as bonus ones, the third one solves many widget reusing problems. Let's create a simple Facebook event widget which will publish and use its own CSS, JavaScript, and an image. Getting ready ff Create a fresh Yii application using yiic webapp as described in the official guide ff Check that assets directory under application's webroot (where index.php is) has write permissions; assets will be written there ff Generate and download a preloader image from http://ajaxload.info/ How to do it... Let's do some planning first. In Yii, you can place your widgets virtually inside any directory and often, it is protected/components. It is acceptable to have one or two classes inside, but when the number of classes increases, it can create problems. Therefore, let's place our widget into protected/extensions/facebook_events. Create an assets directory inside the widget and put inside the ajax-loader.gif you have just downloaded. Also, create facebook_events.css and facebook_events.js in the same directory. 1. Therefore, let's start with the widget class itself protected/extensions/ facebook_events/EFacebookEvents.php: url, urlencode($this->keyword)); } public function init() { $assetsDir = dirname(__FILE__).'/assets'; $cs = Yii::app()->getClientScript(); $cs->registerCoreScript("jquery"); // Publishing and registering JavaScript file $cs->registerScriptFile( Yii::app()->assetManager->publish( $assetsDir.'/facebook_events.js' ), CClientScript::POS_END ); // Publishing and registering CSS file $cs->registerCssFile( Yii::app()->assetManager->publish( $assetsDir.'/facebook_events.css' ) ); // Publishing image. publish returns the actual URL // asset can be accessed with $this->loadingImageUrl = Yii::app()->assetManager->publish( $assetsDir.'/ajax-loader.gif' ); } public function run() { $this->render("body", array( 'url' => $this->getUrl(), 'loadingImageUrl' => $this->loadingImageUrl,, 'keyword' => $this->keyword, )); } } Download at www.Pin5i.Com Chapter 3 83 2. Now let's define body view we are using inside run method protected/ extensions/ facebook_events/views/body.php:

"; container.html(html); }); }); }); 4. Write the following in facebook_events.css created previously: .facebook-events { padding: 10px; width: 400px; float: left; } .facebook-events ul { padding: 0; } .facebook-events li { list-style: none; border: 1px solid #ccc; padding: 10px; margin: 2px; } Download at www.Pin5i.Com AJAX and jQuery 84 5. That is it. Our widget is ready. Let's use it. Open your protected/views/site/ index.php and add the following code to it: widget("ext.facebook_events.EFacebookEvents", array( 'keyword' => 'php', ))?> widget("ext.facebook_events.EFacebookEvents", array( 'keyword' => 'jquery', ))?> 6. Now, it is time to check our application homepage. There should be two blocks with Facebook events named php events and jquery events, as shown in the following screenshot: Download at www.Pin5i.Com Chapter 3 85 How it works... When we use $this->widget in the site/index view, two EFacebookEvents methods are run: init which publishes assets and connects them to the page, and run which renders widget HTML. First, we use CAssetManager::publish to copy our file into the assets directory visible from the web. It returns URL that can be used to access the resource. In the case of JavaScript and CSS, we use CClientScript methods that add necessary 3. Now, run the index action of the news controller and try to add a few records into the news database table by running the addRandomNews action. Do not refresh the index page. News added will appear on the index page once every two seconds, as shown in the following screenshot: How it works... The index action does nothing special. It simply renders a view that includes a div container and some JavaScript code. As we are using jQuery, we need to ensure it is included: clientScript->registerCoreScript("jquery")?> Download at www.Pin5i.Com AJAX and jQuery 92 Then, we define a function named updateNews and run it every 2,000 milliseconds using the core JavaScript setInterval function. In updateNews, we make an AJAX request to the data action of the same controller, to process the JSON response and replace the current content of placeholder div with formatted data. In actionData, we get the latest news and convert them to JSON format by passing the result to CJSON::encode. There's more For further information, refer to the following URLs: ff http://api.jquery.com/category/ajax/ ff http://www.yiiframework.com/doc/api/CJSON/ ff http://www.yiiframework.com/doc/api/CClientScript/#registerCore Script-detail See also ff The recipe named Loading a block through AJAX in this chapter Passing configuration from PHP to JavaScript You can store application parameters in your configuration file protected/config/ main.php that we can access using Yii::app()->params['paramName']. When your application uses the JavaScript code, it is handy to have these parameters available for it. Let's see how to do it in a simple and effective way. Getting ready 1. Set up a fresh application using the yiic webapp tool. It should generate application parameters array in protected/config/main.php: 'params'=>array( // this is used in contact page 'adminEmail'=>'webmaster@example.com', ), 2. Add additional parameters as follows: 'params'=>array( // this is used in contact page 'adminEmail'=>'webmaster@example.com', 'alert' => array( Download at www.Pin5i.Com Chapter 3 93 'enabled' => true, 'message' => 'Hello there!', ), ), How to do it... 1. Create a controller named protected/controllers/AlertController.php as follows: params->toArray()); Yii::app()->clientScript->registerScript ('appConfig', "var config = ".$config.";", CClientScript::POS_HEAD);); $this->render('index'); } } 2. Moreover, create view named protected/views/alert/index.php as follows: 3. Now, if you will run the alert controller index action, you should get a standard JavaScript alert window saying Hello there!, as shown in the following screenshot: Download at www.Pin5i.Com AJAX and jQuery 94 How it works... We use CJavaScript::encode that converts PHP data structures into JavaScript ones to turn Yii application parameters into a JavaScript array. Then, we register a script that assigns it to a global variable config. Then, in our view JavaScript code, we just use this global variable config. See also ff The recipe named Managing assets in this chapter ff The recipe named Loading a block through AJAX in this chapter Handling variable number of inputs Sometimes an application requires a form with variable number of inputs. For example, a task management application can provide a screen where you can add one or more tasks to your task list. An example of such an application is shown in the following screenshot: By default, the page will display one task and two buttons: Add task will add another empty task and Save will reload a form with all the tasks added. Let's check how we can solve it. Download at www.Pin5i.Com Chapter 3 95 Getting ready Create a fresh application using yiic webapp. How to do it... For our example, we will not save any data into the database. Instead, we will learn how to get a form with variable number of fields up and running, and how to collect data submitted with it. 1. Therefore, we will start with the task model. As we agreed not to use database, CFormModel will be enough. protected/models/Task.php: setAttributes($taskData); if($model->validate()) $models[] = $model; } } if(!empty($models)){ // We've received some models and validated them. // If you want to save the data you can do it here. } else $models[] = new Task(); $this->render('index', array( 'models' => $models, )); } public function actionField($index) { $model = new Task(); $this->renderPartial('_task', array( 'model' => $model, 'index' => $index, )); } } 3. Now, the pretected/views/task/index.php view:

Download at www.Pin5i.Com AJAX and jQuery 98 5. That is it. Now run the index action of the task controller and check it in action, as shown in the following screenshot: How it works... Let's review how it works starting from the controller's index action. As we are working with more than one data item, we need to collect them accordingly. Same as with a single model, a form will pass its data in $_POST['model_name']. The only difference is that in our case, there will be an array of data items as follows: if(!empty($_POST['Task'])) { foreach($_POST['Task'] as $taskData) { $model = new Task(); $model->setAttributes($taskData); if($model->validate()) $models[] = $model; } } Download at www.Pin5i.Com Chapter 3 99 For each data item, we are creating a Task model, setting its attributes with data item and if it is valid, storing a model into the $models array as follows: if(!empty($models)){ // We've received some models and validated them. // If you want to save the data you can do it here. } else $models[] = new Task(); If the $models array is not empty, then there is data passed from the form and it is valid. If there is no data in the $models, then we still need at least one item to show it in the form. We are not actually saving data in this example. When we use Active Record models, we can actually save data using $model->save().Then, we just render the index view when we render a form as follows: renderPartial('_task', array( 'model' => $models[$i], 'index' => $i, ))?> As there are many models, we need to render fields for each one. That is done in the _task view partial:

Note how we use active labels and active fields. For each one, we specify a model and a name in format [model_index]field_name. The preceding code will generate the following HTML:

Title Download at www.Pin5i.Com AJAX and jQuery 100

Text

Title

Text

Fields such as Task[0][title] are a special PHP feature. When submitted, parameters with such names are automatically parsed into PHP arrays. Now, let's review how the Add task button works: 'tasks-add'))?> clientScript->registerCoreScript("jquery")?> Download at www.Pin5i.Com Chapter 3 101 We are adding a button with a class tasks-add and a jQuery script that attaches the onClick handler to it. On click, we send an AJAX request to the field action of our controller with parameter index. Parameter's value equals the number of data items in the form. The field action responds with a part of an HTML form that we append to what we already have. There's more… You can find additional information about handling multiple inputs in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/form.table See also ff The recipe named Loading a block through AJAX in this chapter Download at www.Pin5i.Com Download at www.Pin5i.Com 4 Working with Forms In this chapter, we will cover: ff Writing your own validators ff Uploading files ff Adding CAPTCHA ff Customizing CAPTCHA ff Creating a custom input widget with CWidget Introduction Yii makes working with forms a breeze and the documentation on it is almost complete. Still, there are some areas that need clarification and examples. We will describe them in this chapter. Writing your own validators Yii provides a good set of built-in form validators which cover the most typical of developer needs and are highly configurable. However, in some cases, the developer may face a need to create a custom validator. A good example would be the website ownership validation. For a few of their services, Google requires you to upload a file with the name and content specified to your website and then checks if it is there. We will do the same. There are two ways to achieve it. First, you can use a class method as validator, and the second way is to create a separate class. Download at www.Pin5i.Com Working with Forms 104 Getting ready Create a new application using yiic webapp as described in the official guide. How to do it... 1. We will start with the class method approach. First, we need to implement a form model. So, create protected/models/SiteConfirmation.php as follows: url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); curl_close($ch); if(trim($output)!='code here') $this->addError('url','Please upload file first.'); } } 2. Now we will use our model from our test controller. Create protected/ controllers/TestController.php as follows: url = 'http://yiicookbook.org/verify.html'; if($confirmation->validate()) echo 'OK'; else echo 'Please upload a file.'; } } Download at www.Pin5i.Com Chapter 4 105 3. Now try to run the test controller. You should get OK because there is a file with the code here text available from http://yiicookbook.org/verify.html. If you replace the confirmation URL with another one, then you get Please upload file first as there is no such file uploaded. How it works... In the SiteConfirmation model, we define a $url field and add a rules method that defines a single validation rule for this field. As there is no built-in validator named confirm, Yii assumes that we want to describe the validation rule in a method named confirm. In this method, we use standard PHP's CURL to get verify.html file contents from a remote host and compare its content with the code here string. If the file content is different, then we add an error using the addError method. Optionally, we can use two validation method arguments: $attribute and $params. For example, if we specify the validation rule in the following way: array('url', 'confirm', 'param1' => 'value1', 'param2' => 'value3'), then we get the $attribute value set to 'url' and the $params value set to the following: array ( 'param1' => 'value1' 'param2' => 'value3' ) There's more... As we probably want to reuse this kind of validator, we will move its functionality from a model method to a separate class. So, create a file named protected/components/ RemoteFileValidator.php as follows: $attribute; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $value); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); Download at www.Pin5i.Com Working with Forms 106 curl_close($ch); if(trim($output)!=$this->content) $this->addError($object,$attribute,'Please upload file first.'); } } The custom validator class should extend CValidator and implement its abstract validateAttribute method. Arguments passed on to it are $object, which is an instance of the model validated, and $attribute that contains validated attribute name. Parameters passed are assigned to corresponding public properties of the validator class. That is it. Now, we will use it. In the SiteConfirmation model, we should change the validation rule to the following: array('url', 'RemoteFileValidator', 'content' => 'code here'), Here we have used an external validator name. If there is no method with the same name in the model and no same named built-in validator, then Yii will try to find an external validator class with the name or path alias specified. The rest of the code stays untouched and now you can reuse the validator in other models. Further reading For further information, refer to the following URLs: ff http://www.yiiframework.com/doc/api/CValidator/ ff http://www.yiiframework.com/doc/api/CModel#rules-detail Uploading files Handling file uploads is a pretty common task for a web application. Yii has some helpful classes built-in. Let's create a simple form that will allow uploading ZIP archives and storing them in protected/uploads. Getting ready ff Create a fresh application using yiic webapp ff In your protected directory, create an uploads directory Download at www.Pin5i.Com Chapter 4 107 How to do it... 1. We will start with the model, so create protected/models/Upload.php as follows: 'zip'), ); } } 2. Now we will move on to the controller, so create protected/controllers/ UploadController.php: attributes=$_POST['Upload']; $file=CUploadedFile::getInstance($model,'file'); if($model->validate()){ $uploaded = $file->saveAs($dir.'/'.$file->getName()); } } $this->render('index', array( 'model' => $model, 'uploaded' => $uploaded, 'dir' => $dir, )); } } Download at www.Pin5i.Com Working with Forms 108 3. Finally, a view protected/views/upload/index.php:

File was uploaded. Check .

'multipart/form-data'))?> 4. That is it. Now run the upload controller and try uploading both ZIP archives and other files, as shown in the following screenshot: How it works... The model we use is pretty simple. We define only one field named $file and a validation rule that uses file validator (CFileValidator) which reads "only zip files are allowed". Controller is a bit more complicated. We will review it line by line: $dir = Yii::getPathOfAlias('application.uploads'); $uploaded = false; $model=new Upload(); if(isset($_POST['Upload'])) { $model->attributes=$_POST['Upload']; $dir is a directory that will hold the ZIP archives uploaded. We set it to protected/ uploads using an alias. $uploaded is a flag that determines if we need to display a success message. Then, we create a model instance and fill it with data from $_POST if form is submitted. $file=CUploadedFile::getInstance($model,'file'); if($model->validate()){ Download at www.Pin5i.Com Chapter 4 109 $file->saveAs($dir.'/'.$file->getName()); $uploaded = true; Then, we use CUploadedFile::getInstance that gives us access to use CUploadedFile instance. This class is a wrapper around the $_FILE array that PHP fills when the file is uploaded. If we make sure that the file is a ZIP archive by calling the model's validate method, then we save the file using CUploadedFile::saveAs. The rest is passing some values to the view:

File was uploaded. Check .

If there is a $uploaded flag set to true, then we display a message. In order to upload a file, the HTML form must meet the following two important requirements: 1. Method should be set to POST. 2. The enctype attribute should be set to 'multipart/form-data'. We can generate such HTML using the CHtml helper or CActiveForm with htmlOptions set. This time, CHtml was used: 'multipart/ form-data'))?> The rest is the standard form: We display an error and a field for model's file attribute and render a submit button. There's more... If you want to upload multiple files, then you should modify the code in the following way: if(isset($_POST['Upload'])) { $model->attributes=$_POST['Upload']; $files=CUploadedFile::getInstance($model,'file'); if($model->validate()) { foreach($files as $file) $file->saveAs($dir.'/'.$file->getName()); Download at www.Pin5i.Com Working with Forms 110 In a view, you should echo file fields in the following way: File validation The file validator we use in a model allows us not only to limit files to a certain type, but also to set other limits, such as file size or number of files in case of a multiple file upload. For example, the following rule will only allow uploading images with file size less than one megabyte: array('file', 'file', 'types'=>'jpg, gif, png', 'maxSize' => 1048576), Further reading For further information, refer to the following URLs: ff http://www.yiiframework.com/doc/api/CFileValidator ff http://www.yiiframework.com/doc/api/CUploadedFile See also ff The recipe named Handling variable number of inputs in Chapter 3, AJAX and jQuery Adding CAPTCHA Nowadays, on the Internet, if you leave a form without a spam protection, you will get a ton of spam data entered in a short time. Yii includes a CAPTCHA component that makes adding such a protection a breeze. The only problem is that there is no systematic guide on how to use it. In the following example, we will add a CAPTCHA protection to a simple form. Getting ready 1. Create a fresh application using yiic webapp 2. Create a form model named protected/models/EmailForm.php as follows: setAttributes($_POST['EmailForm']); if($model->validate()) { $success = true; // handle form here } } $this->render('index', array( 'model' => $model, 'success' => $success, )); } } 4. Create a view named protected/views/email/index.php as follows:

Success!

Download at www.Pin5i.Com Working with Forms 112 5. Now, we have an e-mail submission form which validates the e-mail field. Let's add CAPTCHA. How to do it... 1. First, we need to customize the form model. We need to add $verifyCode which will hold the verification code entered and add a validation rule for it. !CCaptcha::checkRequirements()), ); } } 2. Then, we need to add an external action to the controller. Add the following code to it: public function actions() { return array( 'captcha'=>array( 'class'=>'CCaptchaAction', ), ); } Download at www.Pin5i.Com Chapter 4 113 3. In a view, we need to show an additional field and the CAPTCHA image. The following code will do this for us: user->isGuest):?>

widget('CCaptcha')?>

4. That is it. Now, you can run the email controller and check CAPTCHA in action, as shown in the following screenshot: If there are no errors on the screen and no CAPTCHA field in the form, then most probably, you don't have the GD PHP extension installed and configured. GD is required for CAPTCHA because it generates images. We have added several CCaptcha::checkRequirements() checks, so the application will not use CAPTCHA if the image cannot be displayed, but will still work. Download at www.Pin5i.Com Working with Forms 114 How it works... In a view, we call the CCaptcha widget that renders the array('captcha'), 'users'=>array('*'), ), array('deny', 'users'=>array('*'), ), ); } Further reading: For further information, refer to the following URLs: ff http://www.yiiframework.com/doc/api/CCaptcha/ ff http://www.yiiframework.com/doc/api/CCaptchaAction/ ff http://www.yiiframework.com/doc/api/CCaptchaValidator/ See also ff The recipe named Using external actions in Chapter 2, Router, Controller, and Views ff The recipe named Customizing CAPTCHA in this chapter Download at www.Pin5i.Com Chapter 4 115 Customizing CAPTCHA A standard Yii CAPTCHA is good enough to protect you from spam, but there are situations where you may want to customize it, such as the following: ff You face a spam-bot that can read image text and need to add more challenge ff You want to make it more interesting or easier to enter the CAPTCHA text In our example, we will modify Yii's CAPTCHA, so it will require the user to solve a really simple arithmetic puzzle instead of just repeating a text in an image. Getting ready As a starting point for this example, we will take the result of the Adding CAPTCHA recipe. Alternatively, you can take any form that uses CAPTCHA as we are not modifying the existing code a lot. How to do it... We need to customize CCaptchaAction which generates the code and renders its image representation. The code should be a random number and the representation should be an arithmetic expression which gives the same result. 1. Create protected/components/MathCaptchaAction.php as follows: minLength, (int)$this->maxLength); } public function renderImage($code) { parent::renderImage($this->getText($code)); } protected function getText($code) { $code = (int)$code; $rand = mt_rand(1, $code-1); $op = mt_rand(0, 1); if($op) Download at www.Pin5i.Com Working with Forms 116 return $code-$rand.»+».$rand; else return $code+$rand.»-».$rand; } } 2. Now, in our controller actions method, we need to replace CCaptchaAction with our own CAPTCHA action as follows: public function actions() { return array( 'captcha'=>array( 'class'=>'MathCaptchaAction', 'minLength' => 1, 'maxLength' => 10, ), ); } 3. Now, run your form and try the new CAPTCHA. It will show arithmetic expressions with numbers from 1 to 10 and will require entering an answer, as shown in the following screenshot: How it works... We override two CCaptchaAction methods. In generateVerifyCode, we generate a random number instead of text. As we need to render an expression instead of just showing text, we override renderImage. The expression itself is generated in our custom getText method. Download at www.Pin5i.Com Chapter 4 117 There's more... In order to learn more about CAPTCHA, you can use the following resources: ff http://www.yiiframework.com/doc/api/CCaptcha/ ff http://www.yiiframework.com/doc/api/CCaptchaAction/ ff http://www.yiiframework.com/doc/api/CCaptchaValidator/ See also ff The recipe named Using external actions in Chapter 3, Router, Controller, and Views ff The recipe named Adding CAPTCHA in this chapter Creating a custom input widget with CInputWidget Yii has a very good set of form widgets, but as every framework out there, Yii cannot have them all. In this recipe, we will learn how to create your own input widget. For our example, we will create a range input widget. Getting ready Create a fresh application by using yiic webapp. How to do it... We will start with the widget itself. 1. Create a widget class named protected/components/RangeInputField.php as follows: hasModel()) { echo CHtml::activeTextField ($this->model, $this->attributeFrom); echo ' → '; echo CHtml::activeTextField ($this->model, $this->attributeTo); } else { echo CHtml::textField($this->nameFrom, $this->valueFrom); echo ' → '; echo CHtml::textField($this->nameTo, $this->valueTo); } } } 2. Now we need to test how it works. We will need a form model named protected/ models/RangeForm.php: true), array('from', 'compare', 'compareAttribute' => 'to', 'operator' => '<=', 'skipOnError' => true), ); } } 3. Now create a controller named protected/controllers/RangeController. php as follows: setAttributes($_POST['RangeForm']); if($model->validate()) $success = true; } $this->render('index', array( 'model' => $model, 'success' => $success, )); } } 4. Create a view named protected/views/range/index.php as follows:

Success!

widget('RangeInputField', array( 'model' => $model, 'attributeFrom' => 'from', 'attributeTo' => 'to', ))?> 5. Now, run the range controller to see a widget in action, as shown in the following screenshot: Download at www.Pin5i.Com Working with Forms 120 How it works... A typical input widget can be used both with a model as an active field widget and without a model. Active field widget handles the value and validation automatically. As there are two fields (from and to) in our widget, we define three pairs of public properties: attribute, name, and value. The attribute pair is used if there is a model passed to a widget; this means that the widget is used as an active input. The name and value pairs are used if you want to generate the input with custom names and values. In our case, we simply override the run method to render two fields in a customized way. Actual field handling is delegated to CHtml::activeTextField and CHtml::textField. In order to render a widget in a view, we use the CController::widget method as follows: widget('RangeInputField', array( 'model' => $model, 'attributeFrom' => 'from', 'attributeTo' => 'to', ))?> All options set in an array are assigned to the corresponding public properties of a widget. There's more... In order to learn more about widgets, you can use the following resources: ff http://www.yiiframework.com/doc/api/CInputWidget/ ff http://www.yiiframework.com/doc/api/CWidget/ See also ff The recipe named Configuring components in Chapter 1, Under the Hood ff The recipe named Configuring widget defaults in Chapter 1 Download at www.Pin5i.Com 5Testing your Application In this chapter, we will cover: ff Setting up the testing environment ff Writing and running unit tests ff Using fixtures ff Testing the application with functional tests ff Generating code coverage reports Introduction In a small application, the value of testing can be unnoticeable. In large applications, it is different. When application grows and you start modifying your code, it becomes difficult not to break anything else relying on it. Even if you hire a team of professional testers, you are slowing the development a lot. However, automated testing can partially solve this problem. Another application of automated testing is TDD (Test Driven Development). The idea is simple: When you know how a component will work, write down your requirements in a form of automated test prior to implementing a component. This way in the end, you will know if your component works as expected and will not have to write tests later. Setting up the testing environment In this recipe, we will prepare a testing environment that can be used to run automated test supports: unit tests and functional tests. Unit tests in Yii are based on PHPUnit and functional tests are based on selenium server. Additionally, you need Xdebug to generate code coverage reports. Download at www.Pin5i.Com Testing your Application 122 Getting ready ff Make sure that you have properly configured PHP to work in a command-line mode. ff Use the yiic webapp tool to generate a fresh application. How to do it... We will start with PHPUnit. 1. To install it, we need to set up PEAR first. In most Linux environments, it is already set up, so you can skip this part if it already works. 2. To test if PEAR works, open console and type pear. You should get the following output: 3. If you get the preceding output after running pear, then everything is OK. If not, then you need to install it by carrying out the following steps:  Open http://pear.php.net/go-pear and save the content as a PHP file go-pear.php. Then, run it in the console with php go-pear.php and follow instructions.  In Windows, it is useful to add the PEAR location to the PATH environment variable, so it can be used simply as pear. 4. Now, it is time to install the PHPUnit. Open the console and type the following: pear channel-discover pear.phpunit.de pear channel-discover components.ez.no pear channel-discover pear.symfony-project.com pear install phpunit/PHPUnit Download at www.Pin5i.Com Chapter 5 123 5. Now, type phpunit and you should get the following output: 6. Done. Now let's install the Selenium Server. There is no PEAR package for it, so go to http://seleniumhq.org/download/ and download the latest release of the Selenium Server project. You should get an archive named like selenium-server- standalone-2.0rc2.jar. 7. In order to run the server, you should have the Java runtime environment installed. Go to the server directory and type the following: java -jar selenium-server-standalone-2.0rc2.jar You should get something similar to the following: 8. That is it. The server is up and running. Now, we move onto Xdebug. Go to http://www.xdebug.org/download.php and download the latest binaries or the source for your platform. On Linux, the extension should be built from the source. Under Windows, you just put dll somewhere. Then, in your php.ini you need to add the following: [xdebug] zend_extension=c:/path/to/your/php_xdebug_version.dll If you are running Linux, then you should provide the absolute path to php_xdebug_ version.so compiled according to http://www.xdebug.org/docs/install. Download at www.Pin5i.Com Testing your Application 124 In order to test if Xdebug is installed, you can use http://www.xdebug.org/ find-binary.php. Now, you should have all tools up and running. 9. Now, we will review the application generated by using yiic webapp. Everything test-related was put under protected/tests: fixtures functional report unit bootstrap.php WebTestCase.php phpunit.xml Folders generated are used to store different tests, fixtures, and code coverage reports. 10. bootstrap.php is used to initialize Yii application environment to run tests in it: // change the following paths if necessary $yiit=dirname(__FILE__).'/../../../framework/yiit.php'; $config=dirname(__FILE__).'/../config/test.php'; require_once($yiit); require_once(dirname(__FILE__).'/WebTestCase.php'); Yii::createWebApplication($config); As we can see, it uses a separate configuration file under protected/config/ test.php, so if you use database or cache, you need to configure it in this file. 11. WebTestCase.php is used as a base class for all functional tests. We need to edit it and set TEST_BASE_URL to the URL of your website. Finally, phpunit.xml is the standard PHPUnit configuration file. We don't need to touch it, unless you want to add more browsers for functional tests or to configure the global PHPUnit settings. There's more... In order to get a more detailed installation guide, you can refer to the following URLs: ff http://pear.php.net/manual/en/installation.php ff http://phpunit.de/ ff http://seleniumhq.org/docs/05_selenium_rc.html Download at www.Pin5i.Com Chapter 5 125 See also ff The recipe named Writing and running unit tests in this chapter ff The recipe named Using fixtures in this chapter ff The recipe named Testing the application with functional tests in this chapter ff The recipe named Generating code coverage reports in this chapter Writing and running unit tests Unit testing is generally used to test relatively standalone components of application, such as API wrappers of different services or classes by implementing your own custom logic. In this recipe, we will review the structure of a unit test, most useful methods of PHPUnit, and a way to run a test from yiic console. As an example, we will follow the TDD approach to create a class that will generate an HTML markup from a limited amount of BBCode tags. For simplicity, we will support only [b], [i], and [url]. Getting ready Make sure that you have a ready to use application and testing tools as described in Setting up the testing environment recipe in this chapter. How to do it... 1. First, let's define the syntax and some use cases: In Out [b]test[/b] test [i]test[/i] test [url]http://yiiframework. com/[/url] http://yiiframework.com/ [url=http://yiiframework. com/]yiiframework.com[/url] yiiframework.com [b]test1[/b] [b]test2[/b] test1test2 [b][i]test[/i][/b] test Download at www.Pin5i.Com Testing your Application 126 2. We will call our class EBBCode and its method to convert BBCode to HTML EBBCode::process, and write unit tests. Create a test file protected/tests/ unit/BBCodeTest.php as follows: process($bbCode); } function testSingleTags() { $this->assertEquals('test', $this->process('[b]test[/b]')); $this->assertEquals('test', $this->process('[i]test[/i]')); $this->assertEquals( ' http://yiiframework.com/', $this->process('[url]http://yiiframework.com/[/url]') ); $this->assertEquals( ' yiiframework.com', $this->process ('[url=http://yiiframework.com/]yiiframework.com[/url]') ); } function testMultipleTags() { $this->assertEquals( 'test1test2', $this->process('[b]test1[/b] [b]test2[/b]') ); $this->assertEquals( 'test', $this->process('[b][i]test[/i][/b]') ); } } Download at www.Pin5i.Com Chapter 5 127 3. Here we run the BBCode converter with different input strings and check if the output matches with what we are expecting. Now, we will run a test and make sure it fails. Open the console and type the following: cd path/to/protected/tests phpunit unit/BBCodeTest.php You should get an output similar to the following screenshot: 4. EE means that there are two tests and two errors each marked with E and a summary at the end doubles it with a readable text. In the middle, there are errors telling us why tests have failed. In our case, both tests failed because we have not created any implementation yet. 5. Now, we will fix it. As the error states, we need an EBBCode.php file with an EBBCode class inside. Create one in protected/components/EBBCode.php as follows: test, but we got an empty string. So, let's do some implementation to fix it: '$1', '~\[i\](.*)\[/i\]~i' => '$1', '~\[url\](.*)\[/url\]~i' => '$1', '~\[url=([^]]+)\](.*)\[/url\]~i' => '$2', ); return preg_replace(array_keys($preg), array_values($preg), $string); } } 8. Now, the test output should be similar to the one shown in the following screenshot: Download at www.Pin5i.Com Testing your Application 130 9. .F (in the preceding screenshot) means that one test passed and one failed. As we can see in the error message, nested tags were processed wrong. So, we will fix this by using a non-greedy modifier for regular expressions as follows: '$1', '~\[i\](.*?)\[/i\]~i' => '$1', '~\[url\](.*?)\[/url\]~i' => '$1', '~\[url=([^]]+)\](.*?)\[/url\]~i' => '$2', ); return preg_replace(array_keys($preg), array_values($preg), $string); } } 10. Now, test results are the ones we'd like to see: 11. All tests passed which means that we have the BBCode class implemented in the right way, or at least in the way we have planned. The preceding process, which allowed us to create a test that fails to implement the functionality that passes it, is called a Test Driven Development or TDD. It allows you to implement exactly what is planned and be sure that it will not break in the future. Download at www.Pin5i.Com Chapter 5 131 How it works... The CTestCase class we have used is a wrapper around PHPUnit PHPUnit_Framework_ TestCase. It does not provide any additional functionality, but it is used to initialize the PHPUnit class loader. We have defined two methods with names starting with test. This means that they will be run as test methods. Inside, we are performing the same processing with a different input and comparing the output with the result expected by using the PHPUnit_Framework_TestCase::assertEquals method. This method simply checks if two values are equal. If they are not, an error is generated and the test method is considered as failed. There's more... If you need to check different assertion types, then you can use additional PHPUnit_ Framework_TestCase methods, such as assertTrue, assertFileExists, or assertRegExp. There are more features in PHPUnit which are described in the official documentation at the following URL: http://www.phpunit.de/manual/current/en/ See also ff The recipe named Setting up the testing environment in this chapter Using fixtures Unit tests perform their job just fine when you test the class logic. However, when it comes to classes that work with environment and data, it becomes a little complicated. What data should we test against? How to get the same environment each time a test is being executed? In order to be useful, a unit test should be repeatable. That is why we need to reset the data state on every test run. In PHPUnit, we can do this by using a feature named fixtures. When it comes to database, Yii have a helpful database fixtures addition. For our example, we will test a coupon system which handles the coupon codes registration. The coupon will be stored in a database table named coupon having two columns: id and description. For simplicity, coupon component simply deletes the already registered coupon record and echoes its description. Download at www.Pin5i.Com Testing your Application 132 Getting ready ff Make sure that you have a ready to use application and testing tools as described in the Setting up the testing environment recipe in this chapter. ff Create an active record model for coupon protected/models/Coupon.php as follows: findByPk($code); if(!$coupon) return false; echo "Coupon registered. $coupon->description"; return $coupon->delete(); } } Download at www.Pin5i.Com Chapter 5 133 How to do it... OK, now we will write tests. Inside protected/tests/unit, create CouponTest.php. We will need two test cases. 1. The first case will test the existing coupon code handling and the second case will test the non-existing coupon code handling. In addition, we need to configure a database, create tables, and insert data in the tables prior to the execution of the test. In the end, we will have the following code: 'Coupon', ); public static function setUpBeforeClass() { if(!extension_loaded('pdo') || !extension_loaded('pdo_sqlite')) markTestSkipped('PDO and SQLite extensions are required.'); $config=array( 'basePath'=>dirname(__FILE__), 'components'=>array( 'db'=>array( 'class'=>'system.db.CDbConnection', 'connectionString'=>'sqlite::memory:', ), 'fixture'=>array( 'class'=>'system.test.CDbFixtureManager', ), ), ); Yii::app()->configure($config); $c = Yii::app()->getDb()->createCommand(); $c->createTable('coupon', array( 'id' => 'varchar(255) PRIMARY KEY NOT NULL', 'description' => 'text', )); } Download at www.Pin5i.Com Testing your Application 134 public static function tearDownAfterClass() { if(Yii::app()->getDb()) Yii::app()->getDb()->active=false; } protected function setUp() { parent::setUp(); $_GET['existing_code'] = 'discount_for_me'; $_GET['non_existing_code'] = 'non_existing'; } public function testCodeAcceptance() { $cm = new CouponManager(); $this->assertTrue($cm->registerCoupon ($_GET['existing_code'])); $this->assertFalse((boolean)Coupon::model()->findByPk ($_GET['existing_code'])); } public function testCodeNotFound() { $countBefore = Coupon::model()->count(); $cm = new CouponManager(); $this->assertFalse($cm->registerCoupon ($_GET['non_existing_code'])); $countAfter = Coupon::model()->count(); $this->assertEquals($countBefore, $countAfter); } } 2. Also, we need to define fixtures in protected/tests/fixtures/coupon.php as follows: 'free_book', 'description' => 'Choose one book for free!', ), array( Download at www.Pin5i.Com Chapter 5 135 'id' => 'merry_christmas', 'description' => '5% Christmas discount!', ), array( 'id' => 'discount_for_me', 'description' => '5% discount special for you!', ), ); Here, id and description keys corresponding to table or active record model fields and values are to be filled to these fields when the fixture is applied. 3. Now, try to run the test from the console as follows: cd path/to/protected/tests phpunit unit/CouponTest.php You should now get the following output: How it works... We will review the test execution flow. This time, we used a few special PHPUnit fixture methods named setUpBeforeClass and setUp. The setUpBeforeClass method executes once after the test class is instantiated and typically used to initialize things common for all test methods of this class. In our case, we check if we have PDO and SQLite required to run this test: if(!extension_loaded('pdo') || !extension_loaded('pdo_sqlite')) markTestSkipped('PDO and SQLite extensions are required.'); We then create a configuration test application and apply the following: $config=array( 'basePath'=>dirname(__FILE__), 'components'=>array( 'db'=>array( 'class'=>'system.db.CDbConnection', Download at www.Pin5i.Com Testing your Application 136 'connectionString'=>'sqlite::memory:', ), 'fixture'=>array( 'class'=>'system.test.CDbFixtureManager', ), ), ); Yii::app()->configure($config); We use an in-memory SQLite database to speed tests up and avoid creating and deleting database files. In addition, we connect a fixture component which we use to fill data. Now, it is time to create the database schema. In our case, it is the coupon table: $c = Yii::app()->getDb()->createCommand(); $c->createTable('coupon', array( 'id' => 'varchar(255) PRIMARY KEY NOT NULL', 'description' => 'text', )); That is it. The test class is created and PHPUnit starts to execute test methods one by one. Before each test method execution, it calls setUp. In our case, it is simple: parent::setUp(); $_GET['existing_code'] = 'discount_for_me'; $_GET['non_existing_code'] = 'non_existing'; parent::setUp refers to CDbTestCase::setup that reads data from fixtures and places it into the tables or models. Both fixtures and models are defined in the $fixtures property as follows: public $fixtures = array( 'coupon' => 'Coupon', ); This means that fixtures are read from protected/tests/fixtures/coupon.php and applied to the Coupon model. In order to apply fixtures to the table without using a model, we can define a $fixtures property in the following way: public $fixtures = array( 'coupon' => ':coupon', ); Then, we set the environment with $_GET variables. Similarly, you can set cookies, server variables, class properties, and so on. Download at www.Pin5i.Com Chapter 5 137 After executing testCodeAcceptance and before the testCodeNotFound method execution, setUp is executed again restoring the coupon table data from the fixture. Finally, when all test methods have been executed, we need to close the connection. We do this in the tearDownAfterClass method executed just before the class is destroyed. There's more... We have used Yii database fixtures and it allowed the data not to be cleaned up manually on each execution. If we need to clean up data, then we can use the PHPUnit teardown method that is executed after each test method execution. You can refer to the following sources to get more information about fixtures: ff http://www.phpunit.de/manual/current/en/fixtures.html ff http://www.yiiframework.com/doc/guide/en/test.fixture See also ff The recipe named Setting up the testing environment in this chapter Testing the application with functional tests While unit tests are used to test standalone components or component groups, functional tests allow testing the complete application like black box testing. We don't know what is inside and can only provide an input and get/verify the output. In our case, the input is actions the user carries out in a browser, such as clicking on buttons or links, loading pages, and so on, and the output is what happens in the browser. For our example, we will create a simple "check all" widget with a single button that checks and unchecks all checkboxes on the current page. Getting ready ff Make sure that you have a ready-to-use application and testing tools as described in the Setting up the testing environment recipe in this chapter ff Don't forget to run the server ff Drop the widget code into protected/components/ECheckAllWidget.php as follows: clientScript->registerCoreScript('jquery'); echo CHtml::button($this->uncheckedTitle, array( 'id' => 'button-'.$this->id, 'class' => 'check-all-btn', 'onclick' => ' switch($(this).val()) { case "'.$this->checkedTitle.'": $(this).val("'.$this->uncheckedTitle.'"); $("input[type=checkbox]").attr("checked", false); break; case "'.$this->uncheckedTitle.'": $(this).val("'.$this->checkedTitle.'"); $("input[type=checkbox]").attr("checked", true); break; } ' )); } } ff Now create protected/controllers/CheckController.php as follows: render('index'); } } ff In addition, create a view protected/views/check/index.php as follows: widget('ECheckAllWidget')?> Download at www.Pin5i.Com Chapter 5 139 How to do it... Let's imagine how we would test it manually: ff Load the page ff Verify that the button title is "Check all" ff Click on a button once ff Verify that all checkboxes are checked and the button title is "Uncheck all" ff Click on a button again ff Verify that all checkboxes are unchecked and the button title is "Check all" Now, we will implement the functional test doing exactly that. 1. Create protected/tests/functional/CheckAllWidgetTest.php as follows: open('check/index'); $this->assertEquals("Check all", $this->getAttribute("class=check-all-btn@value")); $this->click("class=check-all-btn"); $this->assertChecked("css=input[type=checkbox]"); $this->assertEquals("Uncheck all", $this->getAttribute("class=check-all-btn@value")); $this->click("class=check-all-btn"); $this->assertNotChecked("css=input[type=checkbox]"); $this->assertEquals("Check all", $this->getAttribute("class=check-all-btn@value")); } } Download at www.Pin5i.Com Testing your Application 140 2. Now open the console: cd path/to/protected/tests phpunit functional/CheckAllWidgetTest.php You should get the following output: 3. Now, try to break a widget and run the test again. For example, you can comment the onclick handler: Download at www.Pin5i.Com Chapter 5 141 This means that your widget does not work as expected. In this case, the test failed on line 11. $this->assertEquals("Uncheck all", $this->getAttribute("class=check-all-btn@value")); Actual value was still "Check all" but test expected "Uncheck all". How it works... PHPUnit starts executing all methods named like testSomething one by one. As we have only one method, it executes testWidget: $this->open('check/index'); It opens a page we created for testing: $this->assertEquals("Check all", $this->getAttribute ("class=check-all-btn@value")); It gets button text and checks if it equals the string provided. The getAttribute searches for a DOM element with a class check-all-btn and returns its value attribute text. $this->click("class=check-all-btn"); It clicks an element with class check-all-btn. That is the button. $this->assertChecked("css=input[type=checkbox]"); The assertChecked and assertNotChecked methods are used to find whether a checkbox is checked. This time we use a CSS selector input[type=checkbox] to get all checkboxes. Inside, we call methods such as getAttribute or assertChecked, and pass it to the Selenium Server that does the actual work and returns the result. There's more... In order to learn more about the functional testing, you can refer to the following resources: ff http://www.yiiframework.com/doc/guide/en/test.functional ff http://www.phpunit.de/manual/current/en/selenium.html ff http://seleniumhq.org/docs/05_selenium_rc.html ff http://seleniumhq.org/docs/04_selenese_commands.html Download at www.Pin5i.Com Testing your Application 142 See also ff The recipe named Setting up the testing environment in this chapter ff The recipe named Writing and running unit tests in this chapter Generating code coverage reports It is very important to know how well your application is tested. If you wrote all tests by yourself, then you can probably guess it, but if there is a team or you are working on a relatively old project, guessing will not work. Fortunately, there is a way to generate code coverage reports using PHPUnit and Xdebug. This report gives information about how well the application is tested, which lines are being executed while running tests, and which are not. As an example, we will generate a report for the Yii framework core base classes. Getting ready The Yii framework core tests are not included into release distributions, so we need to check it out from the SVN repository. Make sure that your test environment is setup properly. How to do it... 1. Go to http://code.google.com/p/yii/source/checkout and follow the given instructions to check out the trunk code using either command line or one of the GUI clients, such as SmartSVN or TortoiseSVN. 2. In the console, enter the following: cd path/to/checked/out/code/tests/ phpunit --coverage-html reports framework/base 3. After the report is generated, go to path/to/checked/out/code/tests/ reports and open index.html in your browser. How it works... The code coverage report that was generated can tell us how well the project was tested and which parts required more testing to be done. Download at www.Pin5i.Com Chapter 5 143 The code coverage report is generated based only on tests you have run. It is better to run the complete test pack for the project to get the actual information, but for simplicity and speed, only a few tests of the Yii framework code were executed. The first report page gives us an overview: which files were tested, how many lines, methods, and classes were executed. As we are interested in covering as much code as we can, things to look for are displayed in red and yellow. In the following screenshot, we can see that more tests for CApplication and CStatePersister need to be written: If we click on a class name, for example, on CComponent, then we get more details. The following screenshot shows the class level report: Download at www.Pin5i.Com Testing your Application 144 Moreover, the actual code shows what is covered in green: If we click on the dashboard link on the main report page, then we get a handy summary showing most risky untested classes and methods, as shown in the following screenshot: Download at www.Pin5i.Com Chapter 5 145 There's more... In order to learn more about the code coverage, refer to the PHPUnit manual at the following URLs: ff http://www.phpunit.de/manual/current/en/code-coverage-analysis. html ff http://www.phpunit.de/manual/current/en/selenium.html See also ff The recipe named Setting up the testing environment in this chapter Download at www.Pin5i.Com Download at www.Pin5i.Com 6Database, Active Record, and Model Tricks In this chapter, we will cover: ff Getting data from a database ff Defining and using multiple DB connections ff Using scopes to get models for different languages ff Processing model fields with AR event-like methods ff Applying markdown and HTML ff Highlighting code with Yii ff Automating timestamps ff Setting author automatically ff Implementing single table inheritance ff Using CDbCriteria Introduction There are three main methods to work with database in Yii: Active Record, query builder, and direct SQL queries through DAO. All three are different in terms of syntax, features, and performance. Download at www.Pin5i.Com Database, Active Record, and Model Tricks 148 In this chapter, we will learn how to work with the database efficiently, when to use models and when not to, how to work with multiple databases, how to automatically pre-process Active Record fields, and how to use powerful database criteria. In this chapter, we will use Sakila sample database Version 0.8 available at the official MySQL website: http://dev.mysql.com/doc/sakila/en/sakila.html. Getting data from a database Most applications today use databases. Be it a small website or a social network, at least some parts are powered by databases. Yii introduces three ways which allow you to work with databases: ff Active Record ff Query builder ff SQL via DAO We will use all these methods to get data from the film, film_actor, and actor tables and show it in a list. We will measure the execution time and memory usage to determine when to use these methods. Getting ready ff Create a new application by using yiic webapp as described in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/quickstart.first-app ff Download Sakila database from the following URL: http://dev.mysql.com/doc/sakila/en/sakila.html Execute the downloaded SQLs: first schema then data ff Configure the DB connection in protected/config/main.php to use sakila database ff Use Gii to create models actor and field tables How to do it... 1. We will create protected/controllers/DbController.php as follows: getExecutionTime()); $memory = round(memory_get_peak_usage()/(1024*1024),2)."MB"; echo "Time: $time, memory: $memory"; parent::afterAction($action); } public function actionAr() { $actors = Actor::model()->findAll(array('with' => 'films', 'order' => 't.first_name, t.last_name, films.title')); echo ''; foreach($actors as $actor) { echo '

'; } echo ''; } } Here we have three actions corresponding to three different methods of getting data from a database. Download at www.Pin5i.Com Chapter 6 151 2. After running the preceding db/ar, db/queryBuilder, and db/sql actions, you should get a tree showing 200 actors and 1,000 films they have acted in, as shown in the following screenshot: At the bottom, there are statistics that give information about the memory usage and execution time. Absolute numbers can be different if you run this code, but the difference between methods used should be about the same: Method Memory usage, megabytes Execution time, seconds Active Record 19.74 1.14109 Query Builder 17.98 0.35732 SQL (DAO) 17.74 0.35038 Download at www.Pin5i.Com Database, Active Record, and Model Tricks 152 How it works... Let's review the preceding code. The actionAr action method gets model instances by using the Active Record approach. We start with the Actor model generated with Gii to get all actors and specify 'with' => 'films' to get corresponding films using a single query or eager loading through relation which Gii built for us from InnoDB table foreign keys. Then, we simply iterate over all actors and for each actor—over each film. For each item, we print its name. actionQueryBuilder uses query builder. First, we create a query command for the current DB connection with Yii::app()->db->createCommand(). Then, we add query parts one by one with from, join, and leftJoin. These methods escape values, tables, and field names automatically. queryAll returns an array of raw database rows. Each row is also an array indexed with result field names. We pass the result to renderRows that renders it. With actionSql, we do the same except two things: we pass SQL directly instead of adding its parts one by one and we escape values manually with Yii::app()->db->quoteValue before using them in the query string. renderRows renders the query builder and DAO raw row requires you to add more checks and generally feels unnatural compared to rendering Active Record result. As we can see, all these give the same result in the end but have different performance, syntax, and extra features. We will do a comparison and figure out when to use which method: Method Active Record Query Builder SQL (DAO) Syntax Will do SQL for you. Gii will generate models and relations for you. Works with models, completely OO-style, and very clean API. Produces array of properly nested models as the result. Clean API, suitable for building query on the fly. Produces raw data arrays as the result. Good for complex SQL Manual values and keywords quoting. Not very suitable for building query on the fly. Produces raw data arrays as results. Performance Higher memory usage and execution time compared to SQL and query builder. OK. OK. Extra features Quotes values and names automatically. Behaviors. Before/after hooks. Validation. Quotes values and names automatically. None. Download at www.Pin5i.Com Chapter 6 153 Method Active Record Query Builder SQL (DAO) Best for Prototyping selects. Update, delete, and create actions for single models (model gives a huge benefit when using with forms). Working with large amounts of data, building queries on the fly. Complex queries you want to do with pure SQL and have maximum possible performance. There's more... In order to learn more about working with databases in Yii, refer to the following resources: ff http://www.yiiframework.com/doc/guide/en/database.dao ff http://www.yiiframework.com/doc/guide/en/database.query-builder ff http://www.yiiframework.com/doc/guide/en/database.ar ff http://www.yiiframework.com/doc/guide/en/database.ar See also ff The recipe named Defining and using multiple DB connections in this chapter ff The recipe named Using CDbCriteria in this chapter Defining and using multiple DB connections Multiple database connections are not used too often for new standalone web applications. However, when you are building an add-on application for an existing system, you will most probably need another database connection. From this recipe, you will learn how to define multiple DB connections and use them with DAO, query builder, and Active Record models. Getting ready ff Create a new application by using yiic webapp as described in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/quickstart.first-app ff Create two MySQL databases named db1 and db2 ff Create a table named post in db1 as follows: DROP TABLE IF EXISTS `post`; CREATE TABLE IF NOT EXISTS `post` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, Download at www.Pin5i.Com Database, Active Record, and Model Tricks 154 `title` VARCHAR(255) NOT NULL, `text` TEXT NOT NULL, PRIMARY KEY (`id`) ); ff Create a table named comment in db2 as follows: DROP TABLE IF EXISTS `comment`; CREATE TABLE IF NOT EXISTS `comment` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `text` TEXT NOT NULL, `postId` INT(10) UNSIGNED NOT NULL, PRIMARY KEY (`id`) ); How to do it... 1. We will start with configuring DB connections. Open protected/config/main. php and define a primary connection as described in the guide: 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=db1', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', ), 2. Then copy it, rename the 'db' component to 'db2' and change the connection string accordingly. Also, you need to add the class name as follows: 'db2'=>array( 'class'=>'CDbConnection', 'connectionString' => 'mysql:host=localhost;dbname=db2', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', ), 3. That is it. Now, you have two database connections and can use them with DAO and query builder as follows: $db1Rows = Yii::app()->db->createCommand($sql)->queryAll(); $db2Rows = Yii::app()->db2->createCommand($sql)->queryAll(); Download at www.Pin5i.Com Chapter 6 155 Now, what if we need to use Active Record models? This is a little tricky, but still possible. 1. First, we need to create models with Gii. As the connection to db1 is primary, we have no problems in generating and using Post. 2. In order to generate the Comment model, we need to temporarily make db2 as our primary connection as follows: /*'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=db1', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', ),*/ 'db'=>array( 'class'=>'system.db.CDbConnection', 'connectionString' => 'mysql:host=localhost;dbname=db2', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', ), 3. Then, generate the Comment model as usual, uncomment db, and restore the db2 configuration. We have a model, but have not specified that it should use the db2 connection. Add the following to protected/models/Comment.php as follows: class Comment extends CActiveRecord { // … public function getDbConnection() { return Yii::app()->db2; } // … } 4. That is it. Now, you can use the Comment model as usual. Create protected/ controllers/DbtestController.php as follows: title = "Post #".rand(1, 1000); Download at www.Pin5i.Com Database, Active Record, and Model Tricks 156 $post->text = "text"; $post->save(); echo '

Comments

'; $comments = Comment::model()->findAll(); foreach($comments as $comment) { echo $comment->text.""; } } } 5. Run dbtest/index multiple times and you should see records added to both databases, as shown in the following screenshot: Download at www.Pin5i.Com Chapter 6 157 How it works... In Yii, you can add and configure your own components through the configuration file. For non-standard components, such as db2, you have to specify the component class. Similarly, you can add db3, db4, or any other component, for example, facebookApi. The remaining array key-value pairs are assigned to the component's public properties respectively. There's more... Depending on the RDBMS used, there are additional things we can do to make it easier to use multiple databases. Cross-database relations If you are using MySQL, then it is possible to create cross-database relations for your models. In order to do this, you should prefix the Comment model's table name with database name as follows: class Comment extends CActiveRecord { //… public function tableName() { return 'db2.comment'; } //… } Now, if you have a comments relation defined in the Post model relations method, then you can use the following code: $posts = Post::model()->with('comments')->findAll(); Further reading For further information, refer to the following URL: http://www.yiiframework.com/doc/api/CActiveRecord See also ff The recipe named Getting data from a database in this chapter Download at www.Pin5i.Com Database, Active Record, and Model Tricks 158 Using scopes to get models for different languages Internationalizing your application is not an easy task: You need to translate interfaces, translate messages, format dates properly, and so on. Yii helps you to do it by giving access to CLDR (Unicode Common Locale Data Repository) data and providing translation and formatting tools. When it comes to applications with data in multiple languages, you have to find your own way. From this recipe, you will learn a possible way to get a handy model function that will help to get blog posts for different languages. Getting ready ff Create a new application by using yiic webapp as described in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/quickstart.first-app ff Set up the database connection and create a table named post as follows: DROP TABLE IF EXISTS `post`; CREATE TABLE IF NOT EXISTS `post` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `lang` VARCHAR(5) NOT NULL DEFAULT 'en', `title` VARCHAR(255) NOT NULL, `text` TEXT NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO `post`(`id`,`lang`,`title`,`text`) VALUES (1,'en_us','Yii news','Text in English'), (2,'de','Yii Nachrichten','Text in Deutsch'); ff Generate Post model using Gii How to do it... 1. Add the following methods to protected/models/Post.php as follows: class Post extends CActiveRecord { public function defaultScope() { return array( Download at www.Pin5i.Com Chapter 6 159 'condition' => "lang=:lang", 'params' => array( ':lang' => Yii::app()->language, ), ); } public function lang($lang){ $this->getDbCriteria()->mergeWith(array( 'condition' => "lang=:lang", 'params' => array( ':lang' => $lang, ), )); return $this; } } 2. That is it. Now, we can use our model. Create protected/controllers/ DbtestController.php as follows: findAll(); echo '

Default language

'; foreach($posts as $post) { echo '

'.$post->title.'

German

'; foreach($posts as $post) { echo '

'.$post->title.'

'; echo $post->text; } } } Download at www.Pin5i.Com Database, Active Record, and Model Tricks 160 3. Now, run dbtest/index and you should get an output similar to the one shown in the following screenshot: How it works... We have used Yii Active Record scopes in the preceding code. defaultScope returns the default condition or criteria that will be applied to all the Post model query methods. As we need to specify the language explicitly, we create a named scope named lang which accepts the language name. With $this->getDbCriteria(), we get the model's criteria in its current state and then merge it with the new condition. As the condition is exactly the same as in defaultScope, except the parameter value, it overrides the default one. In order to support chained calls, lang returns the model instance by itself. There's more... For further information, refer to the following URLs: ff http://www.yiiframework.com/doc/guide/en/database.ar ff http://www.yiiframework.com/doc/api/CDbCriteria/ See also ff The recipe named Getting data from a database in this chapter ff The recipe named Using CDbCriteria in this chapter ff The recipe named Processing model fields with AR events in this chapter Download at www.Pin5i.Com Chapter 6 161 Processing model fields with AR event-like methods Active record implementation in Yii is very powerful and has many features. One of these features is event-like methods that you can use to pre-process model fields before putting them into the database or getting them from a database, deleting data related to the model, and so on. In this recipe, we will "linkify" all URLs in the post text and will list all existing active record event-like methods. Getting ready ff Create a new application by using yiic webapp as described in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/quickstart.first-app ff Set up a database connection and create a table named post as follows: DROP TABLE IF EXISTS `post`; CREATE TABLE IF NOT EXISTS `post` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(255) NOT NULL, `text` TEXT NOT NULL, PRIMARY KEY (`id`) ); ff Generate the Post model using Gii How to do it... 1. Add the following method to protected/models/Post.php as follows: protected function beforeSave() { $this->text = preg_replace('~((?:https?|ftps?)://.*?)( |$)~iu', '\1\2', $this->text); return parent::beforeSave(); } Download at www.Pin5i.Com Database, Active Record, and Model Tricks 162 2. That is it. Now, try saving a post containing a link. Create protected/ controllers/TestController.php as follows: title='links test'; $post->text='test http://www.yiiframework.com/ test'; $post->save(); print_r($post->text); } } 3. Run test/index. You should get the following: How it works... beforeSave is implemented in the CActiveRecord class and executed just before saving a model. By using a regular expression, we replace everything that looks like a URL with a link that uses this URL and calls the parent implementation, so real events are raised properly. In order to prevent saving, you can return false. There's more... There are more event-like methods available as shown in the following table: Method name Description afterConstruct Called after a model instance is created by the new operator beforeDelete/afterDelete Called before/after deleting a record beforeFind/afterFind Method is invoked before/after each record is instantiated by a find method beforeSave/afterSave Method is invoked before/after saving a record successfully beforeValidate/afterValidate Method is invoked before/after validation ends Download at www.Pin5i.Com Chapter 6 163 Further reading In order to learn more about using event-like methods in Yii, you can refer to the following URLs: ff http://www.yiiframework.com/doc/api/CActiveRecord/ ff http://www.yiiframework.com/doc/api/CModel See also ff The recipe named Using Yii events in Chapter 1, Under the Hood ff The recipe named Applying markdown and HTML in this chapter ff The recipe named Highlighting code with Yii in this chapter ff The recipe named Automating timestamps in this chapter ff The recipe named Setting author automatically in this chapter Applying markdown and HTML When we create web applications, we will certainly have to deal with creating content. Of course, we can create it with pure text or HTML, but text is often too simple and HTML is too complex and insecure. That is why special markup languages such as BBCode, Textile, and markdown are used. In this recipe, we will learn how to create a model which will automatically convert markdown to HTML when it is being saved. Getting ready ff Create a new application by using yiic webapp as described in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/quickstart.first-app ff Set up a database connection and create a table named post as follows: DROP TABLE IF EXISTS `post`; CREATE TABLE IF NOT EXISTS `post` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(255) NOT NULL, `text` TEXT NOT NULL, `html` TEXT NOT NULL, PRIMARY KEY (`id`) ); ff Generate the Post model using Gii Download at www.Pin5i.Com Database, Active Record, and Model Tricks 164 How to do it... 1. Open the protected/models/Post.php file and add the following method: protected function beforeValidate() { $parser=new CMarkdownParser(); $this->html=$parser->transform($this->text); return parent::beforeValidate(); } 2. Now the Post model can be used transparently. Create protected/ controllers/TestController.php as follows: title = "I promise to share my opinion on Yii framework"; $post->text = "Recently I've started using [Yii framework](http://www.yiiframework.com/) and definitely will share my opinion as soon as I'll have some more free time."; $post->save(); echo "

$post->title

"; echo $post->html; } } 3. That is it. Now run test/index. You should get the following: Text marked up with markdown you have set for the text value will be automatically converted to HTML, ready to be displayed, and will be saved in the html database field. Therefore, html should be used at the "display post" screen and the markdown text should be used at "create post" or "edit post" screens. Download at www.Pin5i.Com Chapter 6 165 How it works... In the preceding code, we override CActiveRecord::beforeValidate to pre-process the data we have from the user input. This method is executed just before the validation that is called when we use $post->save(). Yii includes a wrapper around "PHP Markdown Extra" markdown parser. CMarkdownParser is used mainly in the Yii documentation and we can surely use it in our applications. Converting text from one format to another requires more CPU and memory resources, so should be avoided if possible. That is why we are not applying markdown on the fly and doing it only one time when saving a post. When we edit post, we need to get the markdown source somehow. For this reason, we save both the markdown source and the produced HTML into a database. Alternatively, we can use a markdown parser on viewing post and cache results until post is altered. There's more... In order to learn more about markdown, and how it can be used to build the Yii documentation, you can refer to the following resources: Markdown syntax ff http://daringfireball.net/projects/markdown/syntax ff http://michelf.com/projects/php-markdown/extra/ Yii markdown wrapper and usage ff http://www.yiiframework.com/doc/api/CMarkdownParser/ ff http://code.google.com/p/yiidoc/ See also ff The recipe named Processing model fields with AR event-like methods in this chapter ff The recipe named Highlighting code in this chapter ff The recipe named Automating timestamps in this chapter ff The recipe named Setting an author automatically in this chapter Download at www.Pin5i.Com Database, Active Record, and Model Tricks 166 Highlighting code with Yii If you are posting code, be it company's internal wiki or a public developer's blog, it is always better to have the syntax highlighted, so ones who read the code will feel comfortable. Yii has Pear Text_Highlighter code-highlighting class bundled. It is used to highlight Yii definitive guide examples and we can use it to do the same for our application. In this recipe, we will create a simple application that will allow adding, editing, and viewing code snippets. Getting ready ff Create a new application by using yiic webapp as described in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/quickstart.first-app ff Set up a database connection and create a table named snippet as follows: CREATE TABLE `snippet` ( `id` int(11) unsigned NOT NULL auto_increment, `title` varchar(255) NOT NULL, `code` text NOT NULL, `html` text NOT NULL, `language` varchar(20) NOT NULL, PRIMARY KEY (`id`) ); ff Generate a Snippet model by using Gii How to do it... 1. First, we will tweak the protected/models/Snippet.php model code. Change rules method to the following: public function rules() { return array( array('title, code, language', 'required'), array('title', 'length', 'max'=>255), array('language', 'length', 'max' => 20), ); } Download at www.Pin5i.Com Chapter 6 167 2. Add methods to the same Snippet model: protected function afterValidate() { $highlighter = new CTextHighlighter(); $highlighter->language = $this->language; $this->html = $highlighter->highlight($this->code); return parent::afterValidate(); } public function getSupportedLanguages() { return array( 'php' => 'PHP', 'css' => 'CSS', 'html' => 'HTML', 'javascript' => 'JavaScript', ); } 3. The model is ready. Now we will create a controller. Therefore, create protected/ controllers/SnippetController.php as follows: order = 'id DESC'; $models = Snippet::model()->findAll(); $this->render('index', array( 'models' => $models, )); } public function actionView($id) { $model = Snippet::model()->findByPk($id); if(!$model) throw new CException(404); $this->render('view', array( 'model' => $model, )); Download at www.Pin5i.Com Database, Active Record, and Model Tricks 168 } public function actionAdd() { $model = new Snippet(); $data = Yii::app()->request->getPost('Snippet'); if($data) { $model->setAttributes($data); if($model->save()) $this->redirect(array('view', 'id' => $model->id)); } $this->render('add', array( 'model' => $model, )); } public function actionEdit($id){ $model = Snippet::model()->findByPk($id); if(!$model) throw new CHttpException(404); $data = Yii::app()->request->getPost('Snippet'); if($data) { $model->setAttributes($data); if($model->save()) $this->redirect(array('view', 'id' => $model->id)); } $this->render('edit', array( 'model' => $model, )); } } 4. Now views; create protected/views/snippet/index.php as follows:

9. That is it. Now run the snippet controller and try creating code snippets, as shown in the following screenshot: 10. When it is viewed, it will look similar to the following: Download at www.Pin5i.Com Chapter 6 171 How it works... The snippet model's function is used to store the code and snippet title. Additionally, we have added the html and language fields. The first one (html) is used to store HTML representing the highlighted code and the language field is used for the snippet language (PHP, HTML, CSS, JavaScript, and so on). We need to store these, as we need them when we edit the the snippet. As we remove the safe rule from the Snippet model, we make title, code, and language as the required fields. There is no rule for html which means that it cannot be set through the form directly. The afterValidate method—as its name states—is executed after the validation gives us no errors. In this method, we transform the code which is stored in the code field to HTML representing the highlighted code in the html field by using the Yii's CTextHighlighter class and passing the language value to it. Note that you need to define CSS with php-hl-* classes defined to get highlighting. You can get the default style from framework/ vendors/TextHighlighter/highlight.css. getSupportedLanguages returns languages we want to support in the value-label array. We use this method in the snippet form. There's more... In order to learn more about code highlighting, you can use the following resources: Yii code highlighter ff http://www.yiiframework.com/doc/api/CTextHighlighter ff http://pear.php.net/package/Text_Highlighter/ More code highlighters If Text_Highlighter bundled with Yii does not fit your needs, then there are many code highlighters available on the Internet. A few good examples are: ff http://qbnz.com/highlighter/ ff http://softwaremaniacs.org/soft/highlight/en/ Download at www.Pin5i.Com Database, Active Record, and Model Tricks 172 See also ff The recipe named Processing model fields with AR event-like methods in this chapter ff The recipe named Applying markdown and HTML in this chapter ff The recipe named Automating timestamps in this chapter ff The recipe named Setting an author automatically in this chapter Automating timestamps Almost every model representing content should have creation and modification dates to show the content actuality, revisions, and so on. In Yii, there are two good ways to automate this, which are as follows: ff Overriding beforeValidate ff Using CTimestampBehavior behavior from Zii We will see how to apply these to blog posts. We will use UNIX timestamps to store the date and time. Getting ready ff Create a new application by using yiic webapp as described in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/quickstart.first-app ff Set up a database connection and create a table named post as follows: DROP TABLE IF EXISTS `post`; CREATE TABLE IF NOT EXISTS `post` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(255) NOT NULL, `text` TEXT NOT NULL, `created_on` INT(10) UNSIGNED NOT NULL, `modified_on` INT(10) UNSIGNED NOT NULL, PRIMARY KEY (`id`) ); ff Generate a Post model using Gii ff Remove everything about created_on and modified_on from the rules method of the model Download at www.Pin5i.Com Chapter 6 173 How to do it... 1. We will start with overriding the beforeValidate method. Open protected/ models/Post.php and add the following method: protected function beforeValidate() { if($this->getIsNewRecord()) $this->created_on = time(); $this->modified_on = time(); return true; } 2. Now add the following code to the new controller and run it: $post = new Post(); $post->title = "test title"; $post->text = "test text"; $post->save(); echo date('r', $post->created_on); 3. You should get a date and time. Since we have simply created a post, it will be about the current date and time. Another method is to use CTimestampBehavior. Delete the Post model and generate it one more time by using Gii. Remove everything about created_on and modified_on from the rules method of the model. Add the following method to the model: public function behaviors() { return array( 'timestamps' => array( 'class' => 'zii.behaviors.CTimestampBehavior', 'createAttribute' => 'created_on', 'updateAttribute' => 'modified_on', 'setUpdateOnCreate' => true, ), ); } How it works... The beforeValidate method executes just before the model validation starts. In this method, modified_on is always filled and created_on is filled only if the model is new, that means only when we are creating a post. Download at www.Pin5i.Com Database, Active Record, and Model Tricks 174 When we use the ready behavior from Zii, we specify createAttribute and updateAttribute to match the field names we have chosen. setUpdateOnCreate triggers filling modified_on when a record is inserted. The rest is done by the behavior function. There's more... In order to learn more about CTimestampBehavior, refer to the following API page: http://www.yiiframework.com/doc/api/CTimestampBehavior/ See also ff The recipe named Processing model fields with AR event-like methods in this chapter ff The recipe named Applying markdown and HTML in this chapter ff The recipe named Highlighting code with Yii in this chapter ff The recipe named Setting an author automatically in this chapter Setting an author automatically Almost all applications which involve multiple content authors should have a way to track who created the content or who is the owner. From this recipe, you will learn how to automate this by using a model. We assume that the application uses CUserIdentity to manage authorization and that Yii::app()- >user->id returns integer user ID. We don't need to change the original post author if someone else edited it. Getting ready ff Create a new application by using yiic webapp as described in the official guide at the following URL: http://www.yiiframework.com/doc/guide/en/quickstart.first-app ff Set up a database connection and create a table named post as follows: DROP TABLE IF EXISTS `post`; CREATE TABLE IF NOT EXISTS `post` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(255) NOT NULL, `text` TEXT NOT NULL, `author_id` INT(10) UNSIGNED NOT NULL, PRIMARY KEY (`id`) ); ff Generate the Post model using Gii Download at www.Pin5i.Com Chapter 6 175 How to do it... 1. Add the following method to the protected/models/Post.php model as follows: protected function beforeValidate() { if(empty($this->author_id)) $this->author_id = Yii::app()->user->id; return parent::beforeValidate(); } 2. That is it. Now we will test it. So, create protected/controllers/ TestController.php as follows: find(); if(!$post) $post = new Post(); $post->title = 'test'; $post->text = 'test'; $post->save(); echo $post->author_id; } } 3. Now log in and execute test/index. You should get an ID of the currently logged in user. Log in as another user and execute the code again. This time, you should get the same ID, which is exactly what we have planned. How it works... The beforeValidate method executes just before the model validation starts. In this method, we set the author_id value to Yii::app()->user->id only if the author ID is empty. Most likely, it will be post creation, but can also be when the original author is deleted (if you have properly set the foreign key with the on delete cascade option). Download at www.Pin5i.Com Database, Active Record, and Model Tricks 176 See also ff The recipe name Processing model fields with AR event-like methods in this chapter ff The recipe name Applying markdown and HTML in this chapter ff The recipe name Highlighting code with Yii in this chapter ff The recipe name Automating timestamps in this chapter Implementing single table inheritance Relational databases do not support inheritance. If we need to store inheritance in the database, then we should somehow support it through code. This code should be efficient, so it should generate as less JOINs as possible. A common solution to this problem was described by Martin Fowler and named single table inheritance. When we use this pattern, we store all the class tree data in a single table and use the type field to determine a model for each row. As an example, we will implement the single table inheritance for the following class tree: Car |- SportCar |- FamilyCar Getting ready ff Create a new application by using yiic webapp as described in the official guide ff Create and set up a database. Add the following table: CREATE TABLE `car` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `type` varchar(100) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO `car` (`name`, `type`) VALUES ('Ford Focus', 'family'), ('Opel Astra', 'family'), ('Kia Ceed', 'family'), ('Porsche Boxster', 'sport'), ('Ferrari 550', 'sport'); Download at www.Pin5i.Com Chapter 6 177 How to do it... 1. First, we will create the car model protected/models/Car.php as follows: "type='sport'", ); } } 3. Also implement protected/models/FamilyCar.php as follows: "type='family'", ); } } 4. Now create protected/controllers/TestController.php as follows: All cars"; $cars = Car::model()->findAll(); foreach($cars as $car) { // Each car can be of class Car, SportCar or FamilyCar echo get_class($car).' '.$car->name.""; } echo "

Sport cars only

"; $sportCars = SportCar::model()->findAll(); foreach($sportCars as $car) { Download at www.Pin5i.Com Chapter 6 179 // Each car should be SportCar echo get_class($car).' '.$car->name.""; } } } 5. Run test/index and you should get the following output: How it works... The base model Car is a typically used Yii AR model except two added methods. tableName explicitly declares the table name to be used for the model. For the Car model alone, this does not make sense but for child models, it will return the same car table which is just what we want—a single table for the entire class tree. instantiate is used by AR internally to create a model instance from the raw data when we call methods, such as Car::model()- >findAll(). We use a switch statement to create different classes based on the type attribute and use the same class if the attribute value is either not specified or points to the non-existing class. SportCar and FamilyCar models simply set the default AR scope, so when we find models with SportCar::model()-> methods, we will get the SportCar model only. There's more... Use the following references to learn more about the single table inheritance pattern and Yii Active Record implementation: ff http://martinfowler.com/eaaCatalog/singleTableInheritance.html ff http://www.yiiframework.com/doc/api/CActiveRecord/ Download at www.Pin5i.Com Database, Active Record, and Model Tricks 180 See also ff The recipe named Using scopes to get models for different languages in this chapter Using CDbCriteria When we use Yii's Active Record methods such as findAll or find, we can pass criteria as a parameter. It can be an array or an instance of the CDbCriteria class. This class represents query criteria, such as conditions, ordering by, limit/offset, and so on. How to do it... Usually, the criteria class is used as shown in the following example: $criteria = new CDbCriteria(); $criteria->limit = 10; $criteria->order= 'id DESC'; $criteria->with = array('comments'); $criteria->compare('approved', 1); $criteria->addInCondition('id', array(4, 8, 15, 16, 23, 42)); $posts = Post::model()->findAll($criteria); How it works... Internally, the criteria class does not build any queries by itself, but it stores only data and allows us to modify it. The actual work is being done inside AR methods where criteria are being used. The preceding code can be read as follows: Get 10 posts along with comments from approved posts with ID equals to 4, 8, 15, 16, 23 or 42 ordered by the ID descendant. Or SELECT * FROM post p JOIN comment c ON p.id = c.post_id WHERE p.approved = 1 AND p.id IN (4, 8, 15, 16, 23, 42) ORDER BY p.id DESC LIMIT 10 Download at www.Pin5i.Com Chapter 6 181 There's more... For further information, refer to the following URLs: ff http://www.yiiframework.com/doc/api/CDbCriteria/ ff http://www.yiiframework.com/doc/api/CPagination/ ff http://www.yiiframework.com/doc/api/CSort/ See also ff The recipe named Getting data from a database in this chapter Download at www.Pin5i.Com Download at www.Pin5i.Com 7 Using Zii Components In this chapter, we will cover: ff Using data providers ff Using grids ff Using lists ff Creating custom grid columns Introduction Yii have a useful library called Zii. It's bundled with framework and includes some classes aimed to make the developer's life easier. Its most handy components are grids and lists which allow you to build data in both the admin and user parts of a website in a very fast and efficient way. In this chapter you'll learn how to use and adjust these components to fit your needs. Also you'll learn about data providers. They are part of the core framework, not Zii, but since they are used extensively with grids and lists, we'll review them here. We'll use Sakila sample database version 0.8 available from official MySQL website: http://dev.mysql.com/doc/sakila/en/sakila.html. Using data providers Data providers are used to encapsulate common data model operations such as sorting, pagination and querying. They are used with grids and lists extensively. Because both widgets and providers are standardized, you can display the same data using different widgets and you can get data for a widget from various providers. Switching providers and widgets is relatively transparent. Download at www.Pin5i.Com Using Zii Components 184 Currently there are CActiveDataProvider, CArrayDataProvider, and CSqlDataProvider implemented to get data from ActiveRecord models, arrays, and SQL queries respectively. Let's try all these providers to fill a grid with data. Getting ready ff Create a new application using yiic webapp as described in the official guide. ff Download the Sakila database from http://dev.mysql.com/doc/sakila/en/ sakila.html and execute the downloaded SQLs: first schema then data. ff Configure the DB connection in protected/config/main.php. ff Use Gii to create a model for the film table. How to do it... 1. Let's start with a view for a grid controller. Create protected/views/grid/ index.php: widget('zii.widgets.grid.CGridView', array('dataProvider' => $dataProvider, ))?> 2. Then create a protected/controllers/GridController.php: array( 'pageSize'=>10, ), 'sort'=>array( 'defaultOrder'=> array('title'=>false), ) )); $this->render('index', array( 'dataProvider' => $dataProvider, )); } Download at www.Pin5i.Com Chapter 7 185 public function actionArray() { $yiiDevelopers = array( array( 'name'=>'Qiang Xue', 'id'=>'2', 'forumName'=>'qiang', 'memberSince'=>'Jan 2008', 'location'=>'Washington DC, USA', 'duty'=>'founder and project lead', 'active'=>true, ), array( 'name'=>'Wei Zhuo', 'id'=>'3', 'forumName'=>'wei', 'memberSince'=>'Jan 2008', 'location'=>'Sydney, Australia', 'duty'=>'project site maintenance and development', 'active'=>true, ), array( 'name'=>'Sebastián Thierer', 'id'=>'54', 'forumName'=>'sebas', 'memberSince'=>'Sep 2009', 'location'=>'Argentina', 'duty'=>'component development', 'active'=>true, ), array( 'name'=>'Alexander Makarov', 'id'=>'415', 'forumName'=>'samdark', 'memberSince'=>'Mar 2010', 'location'=>'Russia', 'duty'=>'core framework development', 'active'=>true, ), array( 'name'=>'Maurizio Domba', 'id'=>'2650', 'forumName'=>'mdomba', 'memberSince'=>'Aug 2010', Download at www.Pin5i.Com Using Zii Components 186 'location'=>'Croatia', 'duty'=>'core framework development', 'active'=>true, ), array( 'name'=>'Y!!', 'id'=>'1644', 'forumName'=>'Y!!', 'memberSince'=>'Aug 2010', 'location'=>'Germany', 'duty'=>'core framework development', 'active'=>true, ), array( 'name'=>'Jeffrey Winesett', 'id'=>'15', 'forumName'=>'jefftulsa', 'memberSince'=>'Sep 2010', 'location'=>'Austin, TX, USA', 'duty'=>'documentation and marketing', 'active'=>true, ), array( 'name'=>'Jonah Turnquist', 'id'=>'127', 'forumName'=>'jonah', 'memberSince'=>'Sep 2009 - Aug 2010', 'location'=>'California, US', 'duty'=>'component development', 'active'=>false, ), array( 'name'=>'István Beregszászi', 'id'=>'1286', 'forumName'=>'pestaa', 'memberSince'=>'Sep 2009 - Mar 2010', 'location'=>'Hungary', 'duty'=>'core framework development', 'active'=>false, ), ); Download at www.Pin5i.Com Chapter 7 187 $dataProvider = new CArrayDataProvider( $yiiDevelopers, array( 'sort'=>array( 'attributes'=>array('name', 'id', 'active'), 'defaultOrder'=>array('active' => true, 'name' => false), ), 'pagination'=>array( 'pageSize'=>10, ), )); $this->render('index', array( 'dataProvider' => $dataProvider, )); } public function actionSQL() { $count=Yii::app()->db->createCommand('SELECT COUNT(*) FROM film')->queryScalar(); $sql='SELECT * FROM film'; $dataProvider=new CSqlDataProvider($sql, array( 'keyField'=>'film_id', 'totalItemCount'=>$count, 'sort'=>array( 'attributes'=>array('title'), 'defaultOrder'=>array('title' => false), ), 'pagination'=>array( 'pageSize'=>10, ), )); $this->render('index', array( 'dataProvider' => $dataProvider, )); } } Download at www.Pin5i.Com Using Zii Components 188 3. Now run grid/aR, grid/array and grid/sql actions and try using the grids. How it works... The view is pretty simple and stays the same for all data providers. We are calling the grid widget and passing the data provider instance to it. Let's review actions one by one starting with actionAR: $dataProvider = new CActiveDataProvider('Film', array( 'pagination'=>array( 'pageSize'=>10, ), 'sort'=>array( 'defaultOrder'=>array('title'=>false), ) )); CActiveDataProvider works with active record models. Model class is passed as a first argument of class constructor. Second argument is an array that defines class public properties. In the code above, we are setting pagination to 10 items per page and default sorting by title. Note that instead of using a string we are using an array where keys are column names and values are true or false. true means order is DESC while false means that order is ASC. Defining the default order this way allows Yii to render a triangle showing sorting direction in the column header. Download at www.Pin5i.Com Chapter 7 189 In actionArray we are using CArrayDataProvider that can consume any array. $dataProvider = new CArrayDataProvider($yiiDevelopers, array( 'sort'=>array( 'attributes'=>array('name', 'id', 'active'), 'defaultOrder'=>array('active' => true, 'name' => false), ), 'pagination'=>array( 'pageSize'=>10, ), )); First argument accepts an associative array where keys are column names and values are corresponding values. Second argument accepts an array with the same options as in CActiveDataProvider case. In actionSQL, we are using CSqlDataProvider that consumes the SQL query and modifies it automatically allowing pagination. First argument accepts a string with SQL and a second argument with data provider parameters. This time we need to supply calculateTotalItemCount with total count of records manually. For this purpose we need to execute the extra SQL query manually. Also we need to define keyField since the primary key of this table is not id but film_id. To sum up, all data providers are accepting the following properties: pagination CPagination object or an array of initial values for new instance of CPagination. sort CSort object or an array of initial values for new instance of CSort. totalItemCount We need to set this only if the provider, such as CSqlDataProvider, does not implement the calculateTotalItemCount method. There's more... You can use data providers without any special widgets. Replace protected/views/grid/ index.php content with the following: data as $film):?> title?> widget('CLinkPager',array( 'pages'=>$dataProvider->pagination))?> Download at www.Pin5i.Com Using Zii Components 190 Further reading To learn more about data providers refer to the following API pages: ff http://www.yiiframework.com/doc/api/CDataProvider ff http://www.yiiframework.com/doc/api/CActiveDataProvider ff http://www.yiiframework.com/doc/api/CArrayDataProvider ff http://www.yiiframework.com/doc/api/CSqlDataProvider ff http://www.yiiframework.com/doc/api/CSort ff http://www.yiiframework.com/doc/api/CPagination See also ff The recipe named Using grids in this chapter ff The recipe named Using lists in this chapter Using grids Zii grids are very useful to quickly create efficient application admin pages or any pages you need to manage data on. Let's use Gii to generate a grid, see how it works, and how we can customize it. Getting ready Carry out the following steps: ff Create a new application using yiic webapp as described in the official guide. ff Download the Sakila database from http://dev.mysql.com/doc/sakila/en/ sakila.html. Execute the downloaded SQLs: first schema then data. ff Configure the DB connection in protected/config/main.php. ff Use Gii to create models for customer, address, and city tables. How to do it... 1. Open Gii, select Crud Generator, and enter Customer into the Model Class field. Press Preview and then Generate. 2. Gii will generate a controller in protected/controllers/ CustomerController.php and a group of views under protected/views/ customer/. Download at www.Pin5i.Com Chapter 7 191 3. Run customer controller and go to Manage Customer link. After logging in you should see the grid generated: How it works... Let's start with the admin action of customer controller: public function actionAdmin() { $model=new Customer('search'); $model->unsetAttributes(); // clear any default values if(isset($_GET['Customer'])) $model->attributes=$_GET['Customer']; $this->render('admin',array( 'model'=>$model, )); } Customer model is created with search scenario, all attribute values are cleaned up, and then filled up with data from $_GET. On the first request $_GET is empty but when you are changing the page, or filtering by first name attribute using the input field below the column name, the following GET parameters are passed to the same action via an AJAX request: Customer[address_id] = Customer[customer_id] = Customer[email] = Download at www.Pin5i.Com Using Zii Components 192 Customer[first_name] = alex Customer[last_name] = Customer[store_id] = Customer_page = 2 ajax = customer-grid Since scenario is search, the corresponding validation rules from Customer::rules are applied. For the search scenario, Gii generates a safe rule that allows to mass assigning for all fields: array('customer_id, store_id, first_name, last_name, email, address_ id, active, create_date, last_update', 'safe', 'on'=>'search'), Then model is passed to a view protected/views/customer/admin.php. It renders advanced search form and then passes the model to the grid widget: widget('zii.widgets.grid.CGridView', array( 'id'=>'customer-grid', 'dataProvider'=>$model->search(), 'filter'=>$model, 'columns'=>array( 'customer_id', 'store_id', 'first_name', 'last_name', 'email', 'address_id', /* 'active', 'create_date', 'last_update', */ array( 'class'=>'CButtonColumn', ), ), )); ?> Columns used in the grid are passed to columns. When just a name is passed, the corresponding field from data provider is used. Also we can use custom column represented by a class specified. In this case we are using CButtonColumn that renders view, update and delete buttons that are linked to the same named actions and are passing row ID to them so action can be done to a model representing specific row from database. Download at www.Pin5i.Com Chapter 7 193 filter property accepts a model filled with data. If it's set, a grid will display multiple text fields at the top that the user can fill to filter the grid. The dataProvider property takes an instance of data provider. In our case it's returned by the model's search method: public function search() { // Warning: Please modify the following code to remove attributes that // should not be searched. $criteria=new CDbCriteria; $criteria->compare('customer_id',$this->customer_id); $criteria->compare('store_id',$this->store_id); $criteria->compare('first_name',$this->first_name,true); $criteria->compare('last_name',$this->last_name,true); $criteria->compare('email',$this->email,true); $criteria->compare('address_id',$this->address_id); $criteria->compare('active',$this->active); $criteria->compare('create_date',$this->create_date,true); $criteria->compare('last_update',$this->last_update,true); return new CActiveDataProvider(get_class($this), array( 'criteria'=>$criteria, )); } This method is called after the model was filled with $_GET data from the filtering fields so we can use field values to form the criteria for the data provider. In this case all numeric values are compared exactly while string values are compared using partial match. There's more... Code generated by Gii can be useful in a lot of simple cases but often we need to customize it. Using data from related Active Record models In the code, the generated grid displays the store and address IDs instead of corresponding values. Let's fix the address and display city, district, and address instead of just ID. 1. We have the following relations in the Customer model: public function relations() { // NOTE: you may need to adjust the relation name and the related Download at www.Pin5i.Com Using Zii Components 194 // class name for the relations automatically generated below. return array( 'address' => array(self::BELONGS_TO, 'Address', 'address_id'), 'store' => array(self::BELONGS_TO, 'Store', 'store_id'), 'payments' => array(self::HAS_MANY, 'Payment', 'customer_id'), 'rentals' => array(self::HAS_MANY, 'Rental', 'customer_id'), ); } 2. We need to load address data along with the model. That means we have to add these to the with part of the criteria passed to the data provider in Customer::search. Since the address includes city ID, we need to load the city data using the city relation of the Address model. public function search() { // Warning: Please modify the following code to remove //attributes that should not be searched. $criteria=new CDbCriteria; $criteria->with = array('address' => array( 'with' => 'city' )); … Each relation used in the with part of the criteria can be specified in the way shown above where key is the relation name and value is an array representing criteria. These criteria will be applied to the related model. 3. Now let's modify the columns list passed to a grid in protected/views/ customer/admin.php: 'columns'=>array( 'customer_id', 'store_id', 'first_name', 'last_name', 'email', array( 'name'=>'address', 'value'=>'$data->address->address.", ".$data->address->city->city.", ".$data->address->district', ), Download at www.Pin5i.Com Chapter 7 195 4. In the preceding code, we are using a relation name of name and setting value to a string consisting of address, city, and district. Now check the grid. It should now list address, city, and district in the address field. 5. The only problems now are that address is not sortable and filtering using address does not work. Let's fix the sorting first. In the Customer::search method we need to create CSort instance, configure it, and pass it to the data provider: $sort = new CSort; $sort->attributes = array( 'address' => array( 'asc' => 'address, city, district', 'desc' => 'address DESC, city DESC, district DESC', ), Download at www.Pin5i.Com Using Zii Components 196 '*', ); return new CActiveDataProvider(get_class($this), array( 'criteria'=>$criteria, 'sort'=>$sort, )); CSort::attributes accepts a list of sortable attributes. We want all Customer attributes to be sortable so we are adding * to the list. Additionally we are specifying SQL for both ascending and descending sorting of the address attribute. That's it. Sorting should work. 6. Now let's fix filtering. First we need to add address to the safe attributes list in the model's rules method. Then we are replacing comparison in Customer::search: $criteria->compare('address_id',$this->address_id); should be replaced with: $criteria->compare('address',$this->address,true); $criteria->compare('district',$this->address,true,"OR"); $criteria->compare('city',$this->address,true,"OR"); 7. When a user enters california in the address filter field, the three compares above will result in SQL like the following: WHERE address LIKE '%california%' OR district LIKE '%california%' OR city LIKE '%california%' Download at www.Pin5i.Com Chapter 7 197 Further reading To learn more about grids and its properties refer to the following resources: ff http://www.yiiframework.com/doc/api/CGridView ff http://www.yiiframework.com/doc/api/CDbCriteria ff http://www.yiiframework.com/doc/api/CSort See also ff The recipe named Using data providers in this chapter ff The recipe named Using lists in this chapter ff The recipe named Creating custom grid columns in this chapter Download at www.Pin5i.Com Using Zii Components 198 Using lists Zii lists are a good tool to display data from any data provider to end users while handling pagination and sorting automatically. CListView is very customizable so it allows building any type of list page. Let's use Gii to generate a list, see how it works, and how we can customize it. Getting ready ff Create a new application using yiic webapp as described in the official guide. ff Download the Sakila database from http://dev.mysql.com/doc/sakila/en/ sakila.html. Execute the downloaded SQLs: first schema then data. ff Configure the DB connection in protected/config/main.php. ff Use Gii to create models for customer, store, address, and city tables. How to do it... 1. Open Gii, select Crud Generator, and enter Customer into the Model Class field. Press Preview and then Generate. 2. Gii will generate a controller in protected/controllers/ CustomerController.php and a group of views under protected/views/ customer/. 3. Run, index action of customer controller to see the customers list in action. How it works... Let's start with the index action of Customer controller: public function actionIndex() { Download at www.Pin5i.Com Chapter 7 199 $dataProvider=new CActiveDataProvider('Customer'); $this->render('index',array( 'dataProvider'=>$dataProvider, )); } It's very simple: new active data provider for the Customer model is created and passed into the protected/views/customer/index.php view where it is used in the CListView widget: widget('zii.widgets.CListView', array( 'dataProvider'=>$dataProvider, 'itemView'=>'_view', )); ?> itemView specifies a view partially used to render each data row. In our case it's protected/views/customer/_view.php that lists all attribute names and values like the following: getAttributeLabel( 'first_name')); ?>:first_name); ?> $data in the code above refers to a model representing a data row that came from the data provider. There's more... If you are using lists in real applications, most likely you'll want to customize them. Adding sorting Let's add ability to sort by last name and email. To implement it we need to define sortableAttributes property: widget('zii.widgets.CListView', array( 'dataProvider'=>$dataProvider, 'itemView'=>'_view', 'sortableAttributes'=>array( 'last_name', 'email', ), )); ?> Download at www.Pin5i.Com Using Zii Components 200 Customizing templates Let's add pagination and sorter at the top and the bottom, and remove the summary. To implement all these we need to customize just one widget property called template: widget('zii.widgets.CListView', array( 'dataProvider'=>$dataProvider, 'itemView'=>'_view', 'sortableAttributes'=>array( 'last_name', 'email', ), 'template' => '{sorter} {pager} {items} {sorter} {pager}', )); ?> Customizing markup and data displayed Let's tweak list markup and templates starting with the widget parameters: widget('zii.widgets.CListView', array( 'dataProvider'=>$dataProvider, 'itemView'=>'_view', 'itemsTagName' => 'ol', 'itemsCssClass' => 'customers', 'sortableAttributes'=>array( 'last_name', 'email', ), 'template' => '{sorter} {pager} {items} {sorter} {pager}', )); ?> Download at www.Pin5i.Com Chapter 7 201 itemsTagName defines a tag that will be used as a base tag for a list. In this case the list is ordered so we are using ol. itemsCssClass sets a class for this ol. We need to modify protected/views/customer/_view.php:

Download at www.Pin5i.Com Using Zii Components 202 Let's apply some CSS. Create protected/assets/customers.css: ol.customers { list-style: none; margin: 1em 0; } ol.customers>li { margin: 1em; padding:1em; background: #fcfcfc; border: 1px solid #9aafe5; } Then you need to add the following to protected/views/customer/_view.php: clientScript->registerCssFile( Yii::app()->assetManager->publish(Yii::getPathOfAlias('application. assets'). '/customers.css'))?> After applying styles, the list will look like this: Further reading To learn more about lists refer to the following API page: ff http://www.yiiframework.com/doc/api/CListView/ See also ff The recipe named Using data providers in this chapter Download at www.Pin5i.Com Chapter 7 203 Creating custom grid columns Most of the time you don't need to create your own grid column types since the ones included in Yii are pretty flexible and are suitable for most use cases. Still, there are situations when you need to create a custom column. Let's create a custom grid column that will allow a toggling Y/N value that will change the corresponding model value via AJAX. Getting ready ff Create a new application using yiic webapp as described in the official guide. ff Download the Sakila database from http://dev.mysql.com/doc/sakila/en/ sakila.html. Execute the downloaded SQLs: first schema then data. ff Configure the DB connection in protected/config/main.php. ff Use Gii to create model for the customer table. ff Open Gii, select "Crud Generator" and enter Customer into the "Model Class" field. Press "Preview" and then "Generate". ff Gii will generate the controller in protected/controllers/ CustomerController.php and a group of views under protected/views/ customer/. ff Run customer controller and go to the "Manage Customer" link. After logging in you should see the grid generated. How to do it... In the table we have an active field that we want to toggle with the flag column. Column should display Y or N depending on what the value is and should allow toggling of the value by clicking on it. Grid should stay on the same page. 1. Let's create protected/components/FlagColumn.php: getClientScript(); $gridId = $this->grid->getId(); $script = << The preceding will result in a script execution which is shown in the following screenshot: Note that instead of just alerting XSS, it is possible, for example, to steal page contents or perform some website-specific things such as deleting all users' data. How to do it... Carry out the following steps: 1. In order to prevent the XSS alert shown in the preceding screenshot, we need to escape the data before passing it onto the browser. We do this as follows: class XssController extends CController { public function actionSimple() { echo 'Hello, '.CHtml::encode($_GET['username']).'!'; } } Download at www.Pin5i.Com Security 276 2. Now instead of an alert, we will get properly escaped HTML as shown in the following screenshot: 3. Therefore, the basic rule is to always escape all dynamic data. For example, we should do the same for a link name: echo CHtml::link(CHtml::encode($_GET['username']), array()); 4. That is it. You have a page that is free from XSS. Now what if we want to allow some HTML to pass? We cannot use CHtml::encode anymore because it will render HTML as just a code and we need the actual representation. Fortunately, there is a tool bundled with Yii that allows filtering out the malicious HTML. It is named as HTML Purifier and can be used in the following way: public function actionHtml() { $this->beginWidget('CHtmlPurifier'); echo $_GET['html']; $this->endWidget(); } Alternatively, you can use it in the following way: public function actionHtml() { $purifier=new CHtmlPurifier(); echo $purifier->purify($_GET['html']); } 5. Now if we access the html action using a URL such as /xss/html?html=Hello, username! the HTML purifier will remove the malicious part and we will get the following result: Download at www.Pin5i.Com Chapter 10 277 How it works... Internally, CHtml::encode looks like the following: public static function encode($text) { return htmlspecialchars($text,ENT_QUOTES,Yii::app()->charset); } So basically, we use the PHP's internal htmlspecialchars function which is pretty secure if one does not forget to pass the correct charset in the third argument. CHtmlPurifier uses the HTML Purifier library which is the most advanced solution out there to prevent XSS inside of HTML. We have used its default configuration which is OK for most of the user-entered content. In order to learn more about how to configure it, refer to links mentioned in the Further reading subsection of this recipe. There's more... There are more things to know about XSS and HTML purifier, which are discussed in the following section: XSS types There are two main types of XSS injections, which are as follows: 1. Non-persistent 2. Persistent The first type is exactly the one that we have used in the recipe and is the most common XSS type that can be found in most insecure web applications. Data passed by the user or through a URL is not stored anywhere, so the injected script will be executed only once and only for the user who entered it. Still, it is not as secure as it looks. Malicious users can include XSS in a link to another website and their core will be executed when another user will follow the link. The second type is much more serious as the data entered by a malicious user is stored in the database and is shown to many, if not all, website users. Using this type of XSS, one can literally destroy your website by "commanding" all users to delete all data to which they have access. Configuring the HTML purifier The HTML purifier can be configured as follows: $p = new CHtmlPurifier(); $p->options = array('URI.AllowedSchemes'=>array( 'http' => true, Download at www.Pin5i.Com Security 278 'https' => true, )); $text = $p->purify($text); For a list of all possible keys which you can use in the options array, refer to the following URL: http://htmlpurifier.org/live/configdoc/plain.html HTML purifier performance As the HTML purifier performs a lot of processing and analysis, its performance is not so good. Therefore, it is a good idea not to process text every time you are outputting it. Instead, it can be saved in a separate database field as discussed in Chapter 6, Database, Active Record, and Model Tricks, in the Applying markdown and HTML or cached. Further reading In order to learn more about XSS and how to deal with it, refer to the following resources: ff http://htmlpurifier.org/docs ff http://ha.ckers.org/xss.html ff http://shiflett.org/blog/2007/may/character-encoding-and-xss See also ff The recipe named Applying markdown and HTML in Chapter 6 Preventing SQL injections SQL injection is a type of code injection that uses vulnerability at the database level and allows executing arbitrary SQL allowing malicious users to carry out such actions as deleting data or raising their privileges. In this recipe, we will see examples of vulnerable code and fix it. Getting ready ff Create a fresh application by using yiic webapp ff Create and configure a new database ff Execute the following SQL: CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(100) NOT NULL, Download at www.Pin5i.Com Chapter 10 279 `password` varchar(32) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO `user`(`id`,`username`,`password`) VALUES ( '1','Alex' ,'202cb962ac59075b964b07152d234b70'); INSERT INTO `user`(`id`,`username`,`password`) VALUES ( '2','Qiang ','202cb962ac59075b964b07152d234b70'); ff Generate a User model using Gii How to do it... 1. First, we will implement a simple action that checks if the username and password that came from a URL are correct. Create protected/controllers/ SqlController.php: db->createCommand($sql)->queryRow(); if($user) { echo "Success"; } else { echo "Failure"; } } } 2. Let's try to access it using the /sql/simple?username=test&password=test URL. As we are aware of neither the username nor password, it will—as expected— print "Failure". Download at www.Pin5i.Com Security 280 3. Now try another URL: /sql/simple?username=%27+or+%271%27%3D%271%2 7%3B+--&password=whatever. This time, it lets us in though we still don't know anything about actual credentials. The decoded part of the username value looks like the following: ' or '1'='1'; -- Close the quote, so that the syntax will stay correct. Add OR '1'='1' that makes the condition always true. Use ; -- to end the query and comment the rest. 4. As no escaping was done, the whole query executed was: SELECT * FROM user WHERE username = '' or '1'='1'; --' AND password = '008c5926ca861023c1d2a36653fd88e2' LIMIT 1; The best way to fix it is to use a prepared statement as follows: public function actionPrepared() { $userName = $_GET['username']; $password = md5($_GET['password']); $sql = "SELECT * FROM user WHERE username = :username AND password = :password LIMIT 1;"; $command = Yii::app()->db->createCommand($sql); $command->bindValue('username', $userName); $command->bindValue('password', $password); $user = $command->queryRow(); if($user) { echo "Success"; } else { echo "Failure"; } } 5. Now check /sql/prepared with the same malicious parameters. This time everything went fine and we have the "Failure" message. The same principle applies to Active Record. The only difference is that AR uses other syntax: public function actionAr() { $userName = $_GET['username']; $password = md5($_GET['password']); Download at www.Pin5i.Com Chapter 10 281 $result = User::model()->exists("username = :username AND password = :password", array( 'username' => $userName, 'password' => $password, )); if($result) { echo "Success"; } else { echo "Failure"; } } In the preceding code, we used the :username and :password parameters and passed parameter values as a second argument. If we had written the preceding code by just using the first argument, it would be vulnerable: public function actionWrongAr() { $userName = $_GET['username']; $password = md5($_GET['password']); $result = User::model()->exists("username = $userName AND password = $password"); if($result) { echo "Success"; } else { echo "Failure"; } } If used properly, prepared statements can save you from all types of SQL injections. Still there are some common problems: ff You can bind only one value to a single parameter, so if you want to query WHERE IN(1, 2, 3, 4), you will have to create and bind four parameters. ff Prepared statement cannot be used for table names, column names, and other keywords. Download at www.Pin5i.Com Security 282 When using Active Record, the first problem can be solved by using the criteria addInCondition method as follows: public function actionIn() { $criteria = new CDbCriteria(); $criteria->addInCondition('username', array('Qiang', 'Alex')); $users = User::model()->findAll($criteria); foreach($users as $user) { echo $user->username.""; } } The second problem can be solved in multiple ways. First is to rely on Active Record and PDO quoting: public function actionColumn() { $attr = $_GET['attr']; $value = $_GET['value']; $users = User::model()->findAllByAttributes(array ($attr => $value)); foreach($users as $user) { echo $user->username.""; } } The second and the most secure way is using the whitelist approach as follows: public function actionWhitelist() { $attr = $_GET['attr']; $value = $_GET['value']; $allowedAttr = array('username', 'id'); if(!in_array($attr, $allowedAttr)) throw new CException("Attribute specified is not allowed."); $users = User::model()->findAllByAttributes(array ($attr => $value)); foreach($users as $user) { echo $user->username.""; } } Download at www.Pin5i.Com Chapter 10 283 How it works... The main goal when preventing the SQL injection is to properly filter the input. In all cases except table names, we have used prepared statements—a feature supported by most relational database servers. It allows you to build statements once and then use them multiple times and provides a safe way to bind parameter values. In Yii, you can use prepared statements for both Active Record and DAO. When using DAO, it can be achieved by using either bindValue or bindParam. The latter is useful when we want to execute multiple queries of the same type while varying parameter values: $command = Yii::app()->db->createCommand($sql); $username, $password; $command->bindParam('username', $username); foreach($records as $record) { $username = $record['username']; $command->execute(); } Most Active Record methods accept either criteria or parameters. To be safe, you should use these instead of just passing the raw data in. As for quoting table names, columns, and other keywords, you can either rely on Active Record or use the whitelist approach. There's more... In order to learn more about SQL injections and working with database through Yii, refer to the following URLs: ff http://www.slideshare.net/billkarwin/sql-injection-myths-and- fallacies ff http://www.yiiframework.com/doc/api/CDbConnection ff http://www.yiiframework.com/doc/api/CDbCommand See also ff The recipe named Getting data from a database in Chapter 6 ff The recipe named Using CDbCriteria in Chapter 6 Download at www.Pin5i.Com Security 284 Preventing CSRF CSRF or XSRF stands for cross-site request forgery, where a malicious user tricks the user's browser to silently perform an HTTP-request to the website when the user is logged in. An example of such an attack is inserting an invisible image tag with src pointing to http://example.com/site/logout. Even if the image tag is inserted in another website, you will be immediately logged out from example.com. Consequences of CSRF could be very serious: destroying website data, preventing all website users from logging in, exposing private data, and so on. In this recipe, we will see how to make sure our application is CSRF-resistant. Getting ready Create a fresh application by using yiic webapp. How to do it... Let's start with some facts about CSRF: ff As CSRF should be performed by the victim user's browser, the attacker cannot normally change HTTP headers sent. However, there were both browser and Flash plugin vulnerabilities found that were allowing to spoof headers, so we should not rely on these. ff The attacker should pass the same parameters and values as the user would normally do. Considering these, a good method of dealing with CSRF is passing and checking the unique token during form submissions and additionally using GET according to the HTTP specification. Yii includes a built-in token generation and token checking. Additionally, it can automate inserting a token in HTML forms. 1. In order to turn the anti-CSRF protection on, we should add the following to protected/config/main.php as follows: 'components'=>array( … 'request'=>array( 'enableCsrfValidation'=>true, ), … ), Download at www.Pin5i.Com Chapter 10 285 2. After configuring the application, you should use CHtml::beginForm and CHtml::endForm instead of HTML form tags: public function actionCreate() { echo CHtml::beginForm(); echo CHtml::submitButton(); echo CHtml::endForm(); } 3. Yii will automatically add a hidden token field as follows:

4. If you save this form as HTML and try submitting it, you will get a message like the one shown in the following screenshot instead of the regular data processing: How it works... Internally, a part of CHtml::beginForm() looks like this: if($request->enableCsrfValidation && !strcasecmp($method,'post')) $hiddens[]=self::hiddenField($request->csrfTokenName, $request->getCsrfToken(),array('id'=>false)); if($hiddens!==array()) $form.="\n".self::tag('div',array('style'=>'display:none'), implode("\n",$hiddens)); In the preceding code getCsrfToken() generates a unique token value and writes it to a cookie. Then, on next requests, both the cookie and POST values are compared. If they don't match, an error message is shown instead of the usual data processing. If you need to perform a POST request but not build a form using CHtml, then you can pass a parameter with a name from Yii::app()->request->csrfTokenName and a value from Yii::app()->request->getCsrfToken(). Download at www.Pin5i.Com Security 286 There's more... There are more ways to improve your application security, which are discussed in the following subsections: Extra measures If your application requires a very high security level, such as a bank account management system, extra measures could be taken. First, you can turn off the "remember me" feature using protected/config/main.php as follows: 'components' => array( ... 'user'=>array( // enable cookie-based authentication 'allowAutoLogin'=>false, ), ... ), Then, you can lower the session timeout as follows: 'components' => array( ... 'session' => array( 'timeout' => 200, ), ... ), Of course, these measures will make the user experience worse, but they will add an additional level of security. Using GET and POST properly HTTP insists on not using GET for operations that change data or state. Sticking to this rule is a good practice. It will not prevent all types of CSRF, but at least will make some injections such as array( … 'authManager'=>array( 'class'=>'CDbAuthManager', 'connectionID'=>'db', ), ), … ); ff Add additional roles to protected/components/UserIdentity.php. The users array should look like the following: $users=array( // username => password 'demo'=>'demo', 'admin'=>'admin', 'readerA'=>'123', 'authorB'=>'123', 'editorC'=>'123', 'adminD'=>'123', ); Download at www.Pin5i.Com Security 288 How to do it... Carry out the following steps: 1. Create protected/controllers/RbacController.php as follows: array('deletePost'), 'roles' => array('deletePost'), ), array( 'allow', 'actions' => array('init', 'test'), ), array('deny'), ); } public function actionInit() { $auth=Yii::app()->authManager; $auth->createOperation('createPost','create a post'); $auth->createOperation('readPost','read a post'); $auth->createOperation('updatePost','update a post'); $auth->createOperation('deletePost','delete a post'); $bizRule='return Yii::app()->user->id==$params ["post"]->authID;'; Download at www.Pin5i.Com Chapter 10 289 $task=$auth->createTask('updateOwnPost','update a post by author himself',$bizRule); $task->addChild('updatePost'); $role=$auth->createRole('reader'); $role->addChild('readPost'); $role=$auth->createRole('author'); $role->addChild('reader'); $role->addChild('createPost'); $role->addChild('updateOwnPost'); $role=$auth->createRole('editor'); $role->addChild('reader'); $role->addChild('updatePost'); $role=$auth->createRole('admin'); $role->addChild('editor'); $role->addChild('author'); $role->addChild('deletePost'); $auth->assign('reader','readerA'); $auth->assign('author','authorB'); $auth->assign('editor','editorC'); $auth->assign('admin','adminD'); echo "Done."; } public function actionDeletePost() { echo "Post deleted."; } public function actionTest() { $post = new stdClass(); $post->authID = 'authorB'; echo "Current permissions:"; echo "

"; } } 2. Now run init once to create the RBAC hierarchy. Then, try to log in as readerA, authorB, editorC, and adminD (password is "123") and visit test and deletePost. How it works... The RBAC hierarchy is a directed acyclic graph, that is, a set of nodes (authorization items) and their directed connections or edges. There are three types of nodes available: roles, tasks, and operations: Task 2Task 1 Operation 2 Operation 3Operation 1 Task 3 Operation 4 Role Download at www.Pin5i.Com Chapter 10 291 A role is the authorization item attached to the user (that is, a moderator or an admin). Operation determines if an action can be performed (that is, deleting post, editing post, and so on). A task is a group of operations (that is, manage task, and so on). There are two ways to assign a role to a user, which are as follows: ff By using Yii::app()->authManager->assign() ff By configuring defaultRoles in the application configuration for the authManager component Default roles are typically used when we need to assign a role to a huge part of the users based on some PHP expression such as Yii::app()->user->isGuest. According to rules described in the definitive guide, it is forbidden to connect higher-level nodes to lower-level nodes. For example, connect a role to a task. The opposite is permitted, so we can connect a task to a role. When checking access, we typically pass the name of an operation and, optionally, some parameters. Internally, Yii tries to find a way from an operation specified to the current user's role using reversed breadth-first search (http://en.wikipedia.org/wiki/Breadth- first_search). Therefore, when we want to find out if a user with Role has an access to perform Operation 4, Yii will go the following way: Operation4 – Task3 – Task1 – Role Each node can contain a business rule or bizRule. This business rule is a string containing some PHP code that returns either true or false. The returned value determines if we can go through the node or not. In the end, we have either reached a role that means access is granted, or tried every possible path and failed which means access is denied. There are two ways we can check if user can perform an operation specified: 1. Using controller's accessRules specifying an operation, a task, or a role in the roles parameter of an access rule. 2. Using Yii::app()->user->checkAccess(). By using the second way, we can pass some data which makes its way through the authorization hierarchy and passes to every bizRule encountered. Download at www.Pin5i.Com Security 292 Now, we will get back to our example. The code of the init action uses the authManager component to create the following hierarchy: reader author admin editor updatepost deletepostcreatepostreadpost updateOwnPost For testing permissions, we have created two actions: test which lists CRUD permissions and deletePost which is limited through the access filter. The rule for the access filter contains the following code: array( 'allow', 'actions' => array('deletePost'), 'roles' => array('deletePost'), ), This means that we are allowing all users who have the deletePost permission to run the deletePost action. Yii starts checking with the deletePost operation and the only way it can go is admin. This means only users with the admin role will be able to delete a post. Besides the fact the access rule element is named roles, you can specify an RBAC hierarchy node, be it a role, task, or an operation. When we check for the readPost permission for a user logged in as authorB, Yii checks readPost, reader, and then editor. Checking for updatePost is complex: Yii::app()->user->checkAccess('updatePost', array('post' => $post)) We use a second parameter to pass a post (in our case, we have simulated it with stdClass). If a user is logged in as authorB, then to get access we need to go from updatePost to author. In the lucky case, we have to go through only updatePost, updateOwnPost, and author. As updateOwnPost has a bizRule defined, it will be run with a parameter passed to checkAccess. If the result is true, then access will be granted. As Yii does not know what the shortest way is, it tries to check all possibilities until either there is success or no possible ways left. Download at www.Pin5i.Com Chapter 10 293 There's more... There are some useful tricks that will help you to use RBAC efficiently, which are discussed in the following subsections: Naming RBAC nodes A complex hierarchy becomes difficult to understand without using some kind of a naming convention. One possible convention that helps not to get us confused is as follows: [group_][own_]entity_action Where own is used when the rule determines an ability to modify an element only if the current user is the owner of the element and group is just a namespace. Entity is a name of the entity we are working with and action is the action that we are performing. For example, if we need to create a rule that determines if the user can delete a blog post, we will name it as blog_post_delete. If the rule determines if a user can edit the own blog comment, the name will be blog_own_comment_edit. A way to keep the hierarchy simple and efficient Follow these recommendations when possible to maximize the performance and reduce hierarchy complexity: ff Avoid attaching multiple roles to a single user. ff Don't connect nodes of the same type. So, for example, avoid connecting one task to another one. Avoiding RBAC In order to keep the hierarchy even simpler, we can avoid creating and using additional nodes in some cases by replacing them with additional conditions. A good example is the editing of Post. We can create a blog_own_post_edit node with bizRule as follows: return Yii::app()->user->id==$params["post"]->author_id; Alternatively, we can add the same logic to the post selection routine as follows: $post = Post::model()->findByAttributes(array( 'id' => $id, 'author_id' => Yii::app()->user->id, )); If(!$post) throw new CHttpException(404); By using the second way, we will avoid getting an RBAC hierarchy node from storage and traversing it. Download at www.Pin5i.Com Security 294 Further reading In order to learn more about role-based access control, refer to the following resources: ff http://www.yiiframework.com/doc/guide/en/topics.auth#role- based-access-control ff http://en.wikipedia.org/wiki/Role-based_access_control ff http://en.wikipedia.org/wiki/Directed_acyclic_graph See also ff The recipe named Using controller filters in this chapter Download at www.Pin5i.Com 11 Performance Tuning In this chapter, we will cover: ff Following best practices ff Speeding up sessions handling ff Using cache dependencies and chains ff Profiling an application with Yii Introduction Yii is one of the fastest frameworks out there. Still, when developing and deploying an application, it is good to have some extra performance for free, as well as following best practices for the application itself. In this chapter, we will see how to configure Yii to gain extra performance. In addition, we will learn some best practices of developing an application that will run smoothly until we have very high loads. Following best practices In this recipe, we will see how to configure Yii for best performances and will see some additional principles of building responsive applications. These principles are both general and Yii-related. Therefore, we will be able to apply some of these even without using Yii. Getting ready ff Install APC (http://www.php.net/manual/en/apc.installation.php) ff Generate a fresh Yii application using yiic webapp Download at www.Pin5i.Com Performance Tuning 296 How to do it... Carry out the following steps: 1. First, we need to turn off the debug mode. This can be done by editing index.php as follows: defined('YII_DEBUG') or define('YII_DEBUG',false); 2. The next step is to use yiilite.php. Again, we need to edit index.php and change $yii=dirname(__FILE__).'/../framework/yii.php'; to the following: $yii=dirname(__FILE__).'/../framework/yiilite.php'; 3. Now we will move on to protected/config/main.php and replace it with the following: dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'name'=>'My Web Application', // preloading 'log' component 'preload'=>array('log'), // autoloading model and component classes 'import'=>array( 'application.models.*', 'application.components.*', ), 'modules'=>array( // uncomment the following to enable the Gii tool /* 'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>'Enter Your Password Here', Download at www.Pin5i.Com Chapter 11 297 // If removed, Gii defaults to localhost only. Edit carefully to taste. 'ipFilters'=>array('127.0.0.1','::1'), ), */ ), // application components 'components'=>array( 'user'=>array( // enable cookie-based authentication 'allowAutoLogin'=>true, ), 'urlManager'=>array( 'urlFormat'=>'path', 'rules'=>array( '/'=>'/view', '//'=> '/', '/'=>'/', ), ), 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=test', 'username' => 'root', 'password' => '', 'charset' => 'utf8', 'schemaCachingDuration' => 180, ), 'errorHandler'=>array( // use 'site/error' action to display errors 'errorAction'=>'site/error', ), 'log'=>array( 'class'=>'CLogRouter', 'routes'=>array( array( 'class'=>'CFileLogRoute', 'levels'=>'error, warning', ), // uncomment the following to show log messages on web pages /* Download at www.Pin5i.Com Performance Tuning 298 array( 'class'=>'CWebLogRoute', ), */ ), ), 'session' => array( 'class' => 'CCacheHttpSession', ), 'cache' => array( 'class' => 'CApcCache', ), ), // application-level parameters that can be accessed // using Yii::app()->params['paramName'] 'params'=>array( // this is used in contact page 'adminEmail'=>'webmaster@example.com', ), ); 4. That is it. Now we don't have to worry about the overhead of Yii itself and can focus on our application. How it works... When YII_DEBUG is set to false, Yii turns off all the trace level logging, uses less error handling code, stops checking the code (for example, Yii checks for invalid regular expressions in router rules), and uses minified JavaScript libraries. yiilite.php contains the most commonly executed Yii parts. By using it, we can avoid including the extra script and use less memory for APC cache. Note that the benefit of using yiilite.php varies according to the server setup and sometimes, it is slower when using it. It is a good idea to measure the performance and choose what works faster for you. Now we will review the additional component configuration which we performed: 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=test', 'username' => 'root', Download at www.Pin5i.Com Chapter 11 299 'password' => '', 'charset' => 'utf8', 'schemaCachingDuration' => 180, ), Setting schemaCachingDuration to a number of seconds allows caching the database schema used by Yii's Active Record. This is highly recommended for production servers and it significantly improves the Active Record performance. In order for it to work, you need to properly configure the cache component as follows: 'cache' => array( 'class' => 'CApcCache', ), The APC cache is one of the fastest cache solutions if you are using a single server. Enabling cache also has a positive effect on other Yii components. For example, Yii router or urlManager starts to cache routes in this case. Finally, we configure the session component as follows: 'session' => array( 'class' => 'CCacheHttpSession', ), The preceding code enables storing sessions in APC which is significantly faster than the default file-based session handling. There's more... Of course, you can get into a situation where the preceding settings will not help to achieve sufficient performance level. In most cases, it means either that the application itself is a bottleneck or you need more hardware. Server-side performance is just a part of the big picture Server-side performance is only one of the things that affect the overall performance. By optimizing the client side such as serving CSS, images, and JavaScript files proper caching and minimizing the amount of HTTP-requests can give a good visual performance gain even without optimizing the PHP code. Things to be done without using Yii Some things are better to be done without Yii. For example, image resizing on the fly is better to be done in a separate PHP script in order to avoid the extra overhead. Download at www.Pin5i.Com Performance Tuning 300 Active record versus query builder and SQL Use query builder or SQL in performance critical application parts. Generally, AR is most useful when adding and editing records, as it adds a convenient validation layer and is less useful when selecting records. Always check for slow queries first Database can become a bottleneck in a second if a developer accidentally forgets to add an index to a table that is being read often or vice versa, or adds too many indexes to a table we are writing to very often. The same goes for selecting unnecessary data and unneeded JOINs. Cache or save results of "heavy" processes If you can avoid running a "heavy" process in every page load, it is better to do so. For example, it is good practice to save or cache results of parsing the markdown text, purifying it (this is a very resource intensive process) once, and then using the ready to display HTML. Handling too much processing Sometimes there is too much processing to handle it immediately. It can be building of complex reports or just simple sending e-mails (if your project is heavily loaded). In this case, it is better to put it into a queue and process later by using cron or other specialized tools. Further reading For further information, refer to the following URL: http://www.yiiframework.com/doc/guide/en/topics.performance See also ff The recipe named Speeding up sessions handling in this chapter ff The recipe named Using cache dependencies and chains in this chapter ff The recipe named Profiling an application with Yii in this chapter Speeding up sessions handling Native session handling in PHP is fine in most cases. There are at least two possible reasons why you will want to change the way sessions are handled: ff When using multiple servers, you need to have a common session storage for both servers ff Default PHP sessions use files, so the maximum performance possible is limited by disk I/O In this recipe, we will see how to use an efficient storage for Yii sessions. Download at www.Pin5i.Com Chapter 11 301 Getting ready ff Generate a fresh Yii application using yiic webapp ff You should have php_apc and php_memcache extensions installed, as well as memcached itself to follow this recipe How to do it... We will stress test the website by using the Apache ab tool. It is being distributed with Apache binaries, so if you are using Apache, you will find it inside the bin directory. Run the following command replacing your.website with the actual hostname you are using: ab -n 1000 -c 5 http://your.website/index.php?r=site/contact This will send 1,000 requests, five at a time, and will output stats as follows: Z:\web\usr\local\apache\bin>ab -n 1000 -c 5 http://perf/index.php?r=site/ contact This is ApacheBench, Version 2.0.40-dev apache-2.0 Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright 2006 The Apache Software Foundation, http://www.apache.org/ Benchmarking perf (be patient) Server Software: Apache/2.2.4 Server Hostname: perf Server Port: 80 Document Path: /index.php?r=site/contact Document Length: 6671 bytes Concurrency Level: 5 Time taken for tests: 11.889185 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 7103000 bytes HTML transferred: 6671000 bytes Download at www.Pin5i.Com Performance Tuning 302 Requests per second: 84.11 [#/sec] (mean) Time per request: 59.446 [ms] (mean) Time per request: 11.889 [ms] (mean, across all concurrent requests) Transfer rate: 583.39 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 1.5 0 15 Processing: 28 58 58.6 47 830 Waiting: 27 57 58.5 47 827 Total: 30 58 58.7 47 830 Percentage of the requests served within a certain time (ms) 50% 47 66% 52 75% 57 80% 60 90% 70 95% 100 98% 205 99% 457 100% 830 (longest request) We are interested in requests per second metric. The number means that the website can process 84.11 requests per second if there are five requests at a time. Note that the debug is not turned off since we are interested in changes to session handling speed. Now add the following to the protected/config/main.php, components section: 'session' => array( 'class' => 'CCacheHttpSession', 'cacheID' => 'sessionCache', ), 'sessionCache' => array( 'class' => 'CApcCache', ), Download at www.Pin5i.Com Chapter 11 303 Run ab again with the same settings. This time, you should get better results. In my case, it was 131.33 requests per second. This means APC, as a session handler, performed 56% better than the default file-based session handler. Now let's try another cache backend—memcached. Change CApcCache in config to CMemCache and make sure that memcached is started. Then, run ab again. In my case, memcached performed a bit better than the file cache serving 92.04 requests per second. Don't rely on exact results provided here. It all depends on software versions, settings, and hardware used. Always try to run all tests yourself in an environment where you are going to deploy your application. You can get a significant performance gain by choosing a right session handling backend. Yii supports more caching backends out of the box, including eAccelerator, WinCache, XCache, and Zend data cache that comes with the Zend Server. Moreover, you can implement your own cache backend to use fast noSQL storages, such as Redis. How it works... By default, Yii uses native PHP sessions. This means, in most cases, that the filesystem is used. A filesystem cannot deal with high concurrency efficiently. For example, when changing the concurrency setting for the ab test to 10—while using the default session settings—ab is unable to finish tests with only 326 requests of 1,000 succeeded. Both APC and memcached perform fine in this situation: 'session' => array( 'class' => 'CCacheHttpSession', 'cacheID' => 'sessionCache', ), 'sessionCache' => array( 'class' => 'CApcCache', ), In the preceding config section, we instruct Yii to use CCacheHttpSession as a session handler. With this component, we can delegate session handling to the cache component specified in cacheID. This time we are using CApcCache. When using APC or memcached backend, you should take into account the fact that when using these solutions, the application user can possibly lose session if the maximum cache capacity is reached. Download at www.Pin5i.Com Performance Tuning 304 Note that when using a cache backend for a session, you cannot rely on a session as temporary data storage, since then there will be no memory to store more data in either APC or memcached. In such a case, these will just purge all data or delete some of it. In the preceding tests, APC was the fastest backend but if you are using multiple servers, you cannot use it as there will be no way to share the session data between servers. In case of memcached, it is easy because it can be easily accessed from as many servers as you want. Various cache backends provide different levels of stability. For example, it is a known fact that APC becomes unstable when it is filled up or when you try writing to a single key from multiple processes. There's more... In order to learn more about caching and sessions, refer to the following resources: ff http://www.yiiframework.com/doc/api/CCache ff http://www.yiiframework.com/doc/api/CHttpSession/ ff http://php.net/manual/en/book.apc.php ff http://memcached.org/ ff http://stackoverflow.com/questions/930877/apc-vs-eaccelerator- vs-xcache See also ff The recipe named Following best practices in this chapter Using cache dependencies and chains Yii supports many cache backends, but what really makes Yii cache flexible is the dependency and dependency chaining support. There are situations when you cannot just simply cache data for an hour because the information cached can be changed at any time. In this recipe, we will see how to cache a whole page and still always get fresh data when it is updated. The page will be dashboard-type and will show five latest articles added and a total calculated for an account. Note that an operation cannot be edited as it was added, but an article can. Download at www.Pin5i.Com Chapter 11 305 Getting ready ff Install APC (http://www.php.net/manual/en/apc.installation.php) ff Generate a fresh Yii application by using yiic webapp ff Set up a cache in the components section of protected/config/main.php as follows: 'cache' => array( 'class' => 'CApcCache', ), ff Set up and configure a fresh database ff Execute the following SQL: CREATE TABLE `account` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `amount` decimal(10,2) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `article` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `text` text NOT NULL, PRIMARY KEY (`id`) ); ff Generate models for the account and article tables using Gii ff Configure the db and log application components through protected/config/ main.php, so we can see actual DB queries. In the end, the config for these components should look like the following: 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=test', 'username' => 'root', 'password' => '', 'charset' => 'utf8', 'schemaCachingDuration' => 180, 'enableProfiling'=>true, 'enableParamLogging' => true, ), 'log'=>array( 'class'=>'CLogRouter', 'routes'=>array( Download at www.Pin5i.Com Performance Tuning 306 array( 'class'=>'CProfileLogRoute', ), ), ), ff Create protected/controllers/DashboardController.php as follows: getDbConnection(); $total = $db->createCommand("SELECT SUM(amount) FROM account")->queryScalar(); $criteria = new CDbCriteria(); $criteria->order = "id DESC"; $criteria->limit = 5; $articles = Article::model()->findAll($criteria); $this->render('index', array( 'total' => $total, 'articles' => $articles, )); } public function actionRandomOperation() { $rec = new Account(); $rec->amount = rand(-1000, 1000); $rec->save(); echo "OK"; } public function actionRandomArticle() { $n = rand(0, 1000); $article = new Article(); $article->title = "Title #".$n; $article->text = "Text #".$n; $article->save(); echo "OK"; } } Download at www.Pin5i.Com Chapter 11 307 ff Create protected/views/dashboard/index.php as follows:

Total:

5 latest articles:

title?>

text?>

ff Run dashboard/randomOperation and dashboard/randomArticle several times. Then, run dashboard/index and you should see a screen similar to the one shown in the following screenshot: How to do it... Carry out the following steps: 1. We need to modify the controller code as follows: class DashboardController extends CController { public function filters() { return array( array( Download at www.Pin5i.Com Performance Tuning 308 'COutputCache +index', // will expire in a year 'duration'=>24*3600*365, 'dependency'=>array( 'class'=>'CChainedCacheDependency', 'dependencies'=>array( new CGlobalStateCacheDependency('article'), new CDbCacheDependency('SELECT id FROM account ORDER BY id DESC LIMIT 1'), ), ), ), ); } public function actionIndex() { $db = Account::model()->getDbConnection(); $total = $db->createCommand("SELECT SUM(amount) FROM account")->queryScalar(); $criteria = new CDbCriteria(); $criteria->order = "id DESC"; $criteria->limit = 5; $articles = Article::model()->findAll($criteria); $this->render('index', array( 'total' => $total, 'articles' => $articles, )); } public function actionRandomOperation() { $rec = new Account(); $rec->amount = rand(-1000, 1000); $rec->save(); echo "OK"; } public function actionRandomArticle() { $n = rand(0, 1000); $article = new Article(); $article->title = "Title #".$n; Download at www.Pin5i.Com Chapter 11 309 $article->text = "Text #".$n; $article->save(); Yii::app()->setGlobalState('article', $article->id); echo "OK"; } } 2. That is it. Now, after loading dashboard/index several times, you will get only one simple query, as shown in the following screenshot: Also, try to run either dashboard/randomOperation or dashboard/randomArticle and refresh dashboard/index after that. The data should change. How it works... In order to achieve maximum performance while doing minimal code modification, we use a full-page cache by using a filter as follows: public function filters() { return array( array( 'COutputCache +index', Download at www.Pin5i.Com Performance Tuning 310 // will expire in a year 'duration'=>24*3600*365, 'dependency'=>array( 'class'=>'CChainedCacheDependency', 'dependencies'=>array( new CGlobalStateCacheDependency('article'), new CDbCacheDependency('SELECT id FROM account ORDER BY id DESC LIMIT 1'), ), ), ), ); } The preceding code means that we apply full-page cache to the index action. Page will be cached for a year and the cache will refresh if one of the dependency data changes. Therefore, in general, the dependency works as follows: ff First run: Gets the fresh data as described in the dependency, saves for future reference, and updates cache. ff Gets the fresh data as described in dependency, gets the saved data, and then compares the two. ff If they are equal, uses the cached data. ff If not, updates cache, uses the fresh data, and saves the fresh dependency data for future reference. In our case, two dependency types are used: global state and DB. Global state dependency uses data from Yii::app()->getGlobalState() to decide if we need to invalidate cache while DB dependency uses the SQL query result for the same purpose. The question that you have now is probably, "why have we used DB for one case and global state for another?" That is a good question! The goal of using the DB dependency is to replace heavy calculations and select the light query that gets as little data as possible. The best thing about this type of dependency is that we don't need to embed any additional logic in the existing code. In our case, we can use this type of dependency for account operations, but cannot use it for articles as the article content can be changed. Therefore, for articles, we set a global state named article to the added article's ID which basically means that we are scheduling cache invalidation: Yii::app()->setGlobalState('article', $article->id); Note that if we edit the article 100 times in a row and view it only after that, the cache will be invalidated and updated only once. Download at www.Pin5i.Com Chapter 11 311 There's more... In order to learn more about caching and using cache dependencies, refer to the following URLs: ff http://www.yiiframework.com/doc/guide/en/caching.data#cache- dependency ff http://www.yiiframework.com/doc/guide/en/caching.page See also ff The recipe named Creating filters in Chapter 8, Extending Yii ff The recipe named Using controller filters in Chapter 10, Security Profiling an application with Yii If all of the best practices for deploying a Yii application are applied and you still do not have the performance you want, then most probably, there are some bottlenecks with the application itself. The main principle while dealing with these bottlenecks is that you should never assume anything and always test and profile the code before trying to optimize it. In this recipe, we will try to find bottlenecks in the Yii blog demo application. Getting ready ff Download the latest Yii 1.1.x version from the following URL: http://www.yiiframework.com/download/ ff Unpack demos/blog in your webroot and framework, one level above it: framework www … index.php … ff In index.php, correct the path to yii.php. It should be as follows: $yii=dirname(__FILE__).'/../framework/yii.php'; ff In protected/yiic.php, correct the path to yiic.php. It should be: $yiic=dirname(__FILE__).'/../../framework/yiic.php'; Download at www.Pin5i.Com Performance Tuning 312 ff Change protected/config/console.php with the following: return array( 'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'name'=>'My Console Application', 'import'=>array( 'application.models.*', 'application.components.*', ), 'components'=>array( 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=blog', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', 'tablePrefix' => 'tbl_', ), ), ); ff In protected/config/main.php, comment the SQLite db settings and use MySQL: /*'db'=>array( 'connectionString' => 'sqlite:protected/data/blog.db', 'tablePrefix' => 'tbl_', ),*/ // uncomment the following to use a MySQL database 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=blog', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', 'tablePrefix' => 'tbl_', ), ff Create a new database in MySQL and import protected/data/schema.mysql. sql. ff As there is not that much data, we need to generate more. Create protected/ commands/DataCommand.php as follows: db; echo "Creating tags.\n"; for($t=1; $t<=50; $t++) { $db->createCommand()->insert('tbl_tag', array( 'name' => "tag $t", 'frequency' => rand(1, 20), )); } echo "Done.\n"; for($i=1; $i<=1000; $i++) { $tags = array(); for($rt=1; $rt<=10; $rt++) { $tags[] = "tag ".rand(1, 100); } $db->createCommand()->insert('tbl_post', array( 'title' => "Post #$i", 'content' => "Hello! This is the content #$i", 'tags' => implode(", ", $tags), 'status' => Post::STATUS_PUBLISHED, 'create_time' => time(), 'update_time' => time(), 'author_id' => 1, )); $postId = $db->getLastInsertID(); for($j=1; $j<=10; $j++) { $db->createCommand()->insert('tbl_comment', array( 'content' => "Comment text $j.", 'status' => Comment::STATUS_APPROVED, 'create_time' => time(), 'author' => "Commenter $j", 'email' => "commenter$j@example.com", Download at www.Pin5i.Com Performance Tuning 314 'url' => "http://example.com/", 'post_id' => $postId, )); } if($i%50==0) echo "\nAdded $i posts.\n"; } echo "All done.\n"; } } ff Run it by entering yiic data in console and have a cup of coffee. How to do it... We have a blog with lots of posts and comments and it works somehow but not fast enough. We want to check it page-by-page and get the bottlenecks for each one. 1. We will start with using proper configuration for caching and turn on the SQL profiler. Your protected/config/main.php should look like the following: … return array( … 'components'=>array( … 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=blog', 'username' => 'root', 'password' => '', 'charset' => 'utf8', 'tablePrefix' => 'tbl_', 'schemaCachingDuration' => 180, 'enableProfiling'=>true, 'enableParamLogging' => true, ), … 'log'=>array( 'class'=>'CLogRouter', 'routes'=>array( array( 'class' => 'CProfileLogRoute', Download at www.Pin5i.Com Chapter 11 315 ), ), ), 'session' => array( 'class' => 'CCacheHttpSession', ), 'cache' => array( 'class' => 'CApcCache', ), ), … ); 2. Now run the front page of the blog several times and check what the profiler screen shows us: 3. The slowest query is the following: SELECT `t`.`id` AS `t0_c0`, `t`.`content` AS `t0_c1`, `t`.`status` AS `t0_c2`, `t`.`create_time` AS `t0_c3`, `t`.`author` AS `t0_ c4`, `t`.`email` AS `t0_c5`, `t`.`url` AS `t0_c6`, `t`.`post_ id` AS `t0_c7`, `post`.`id` AS `t1_c0`, `post`.`title` AS `t1_ c1`, `post`.`content` AS `t1_c2`, `post`.`tags` AS `t1_c3`, `post`.`status` AS `t1_c4`, `post`.`create_time` AS `t1_c5`, `post`.`update_time` AS `t1_c6`, `post`.`author_id` AS `t1_c7` FROM `tbl_comment` `t` Download at www.Pin5i.Com Performance Tuning 316 LEFT OUTER JOIN `tbl_post` `post` ON (`t`.`post_id`=`post`.`id`) WHERE (t.status=2) ORDER BY t.create_time DESC LIMIT 10 4. Now we can add EXPLAIN in front of it and run it through the SQL console or any other SQL management tool. It will show us that there are no indexes used when filtering and sorting records. Therefore, if we add indexes for tbl_post.status and tbl_post.create_time, it will improve SELECT performance, as shown in the following screenshot: 5. Next, we have 10 queries used to get the author of each post. Most probably, there is a way to combine these into a single one. As we are running post/index, we will check the PostController, actionIndex methods: $criteria=new CDbCriteria(array( 'condition'=>'status='.Post::STATUS_PUBLISHED, 'order'=>'update_time DESC', 'with'=>'commentCount', )); if(isset($_GET['tag'])) $criteria->addSearchCondition('tags',$_GET['tag']); $dataProvider=new CActiveDataProvider('Post', array( 'pagination'=>array( 'pageSize'=>Yii::app()->params['postsPerPage'], ), 'criteria'=>$criteria, )); $this->render('index',array( 'dataProvider'=>$dataProvider, )); Download at www.Pin5i.Com Chapter 11 317 6. When the data provider is getting posts, it uses criteria defined earlier. As we can see, a criterion allows us to get the count of comments by using the most efficient query possible. commentCount is a relation defined in the Post model and if we check its relations method, we will find that there is an author relation as well. By changing the with part of the criterion to 'with'=> array('commentCount', 'author'), we have got rid of 10 additional queries. Instead, we have a single query that is performing very well: 7. Now the SQL part works better. We can improve it further, but you have an idea and can do it as homework. Overall, it is still not perfect. We will add profiling markers to the controller code as follows: Yii::beginProfile('preparing_data'); $criteria=new CDbCriteria(array( 'condition'=>'status='.Post::STATUS_PUBLISHED, 'order'=>'update_time DESC', 'with'=> array('commentCount', 'author'), )); if(isset($_GET['tag'])) $criteria->addSearchCondition('tags',$_GET['tag']); $dataProvider=new CActiveDataProvider('Post', array( 'pagination'=>array( 'pageSize'=>Yii::app()->params['postsPerPage'], ), 'criteria'=>$criteria, )); Yii::endProfile('preparing_data'); Download at www.Pin5i.Com Performance Tuning 318 Yii::beginProfile('rendering_data'); $this->render('index',array( 'dataProvider'=>$dataProvider, )); Yii::endProfile('rendering_data'); 8. Now run the front page again and check the profiler: 9. It looks like the rendering data part took most of the time. As rendering takes part in a view, let's check protected/views/post/index.php:

Download at www.Pin5i.Com Chapter 11 319 11. Now run the application again and check the profiler: 12. Something is obviously wrong with this view file. In order to determine what exactly, we are moving down while goes up. Finally, we should stop around beginWidget('CMarkdown', array('purifyOutput'=>true)); echo $data->content; $this->endWidget(); ?> If you are the only author of the blog and don't care about someone entering malicious code, then you can just leave echo $data->content. Other options will be caching the purified output or pre-processing it on saving a post. 13. Let's assume it is our case. Remove the widget code and run the profiler one more time: 14. A lot better. Finally, we have gotten down from 0.184 second to 0.078 second. That is about 42% less than the initial processing time. We can achieve more by performing more profiling and fixing. Probably, you will say these values were acceptable from the beginning. That is true, until you will get more readers and the server will not be able to generate pages for all these people in parallel. What this performance gain really means is that if you have, for example, 10,000 readers in the beginning and performance is starting to drop, after optimizing the code, you can handle an additional 4,200 readers without buying new hardware. Download at www.Pin5i.Com Performance Tuning 320 How it works... First, we configured the application cache and cached the DB schema to exclude these possible bottlenecks from profiling results. In a production environment, these will be cached for sure. Then, we turn on the profiling DB queries and run the application multiple times; in the first run, Yii will cache the schema and routes, and the second run will be clean. As the typical web application bottleneck is a database, we start to look at the SQL query anomalies—the most time-consuming queries and same type queries repeating multiple times. Long running queries are typically a bad DB design (wrong index placement or no indexes, too much normalization, and so on). Therefore, we can feed a query to MySQL adding EXPLAIN in front of the query and it will give back a query profile that tells us what to do. When a same type query is executed multiple times, most probably it is something we are getting for each entity we are displaying. In our case, it was the author for each blog post. In most cases, we can get these in a single query or even in the same query, which selects entities themselves. As for the non-SQL part, we divided the controller 50/50, then took the slowest part, and divided it again. We repeated that until a bottleneck was identified. Of course, we used some assumptions to do less the routine job of adding profiler marks, but sometimes it is better not to assume anything since a bottleneck can be hidden in a very innocent looking code. There's more... In order to learn more about profiling, refer to the following resources: ff http://www.yiiframework.com/doc/guide/en/topics. logging#performance-profiling ff http://www.yiiframework.com/doc/guide/en/topics. logging#profiling-sql-executions ff http://www.xdebug.org/docs/profiler ff http://pecl.php.net/package/xhprof See also ff The recipe named Using different log routes in Chapter 9, Error Handling, Debugging, and Logging ff The recipe named Following best practices in this chapter ff The recipe named Speeding up sessions handling in this chapter Download at www.Pin5i.Com 12 Using External Code In this chapter, we will cover: ff Using Zend Framework from Yii ff Customizing the Yii autoloader ff Using Kohana inside Yii ff Using PEAR inside Yii Introduction Typically, an application requires more than any framework can give. Sometimes, you need a full-featured library to send an e-mail and sometimes, it is just about implementation of the specific API. No framework can cover every possible task that a developer has. That is why Yii covers the most common ones and leaves the rest to the developer and external libraries. In this chapter, we will try to use a non-Yii code with Yii, including Zend Framework, Kohana, and Pear. Using Zend Framework from Yii Yii provides many excellent solutions with which you can build an application. Still, you probably will need more. One of the best places to look at is Zend Framework classes. These are of high quality and solve many tasks, such as using Google APIs or working with e-mails. In this recipe, we will see how to use the Zend_Mail package to send e-mails from Yii application. We will use both a simple approach of using the whole framework and will also implement a custom autoloader that will allow us to use only Zend_Mail and its dependencies. Download at www.Pin5i.Com Using External Code 322 Getting ready ff Create a fresh application by using yiic webapp ff Download the Zend Framework code from the following URL: http://framework.zend.com/download/current/ In this recipe, we have used Version 1.11.6 ff Extract library/Zend from the downloaded archive to protected/vendors/ Zend How to do it... Carry out the following steps: 1. We will create a simple controller that will send an e-mail. Create protected/ controllers/MailtestController.php as follows: setHeaderEncoding(Zend_Mime::ENCODING_QUOTEDPRINTABLE); $mail->addTo("alexander@example.com", "Alexander Makarov"); $mail->setFrom("robot@example.com", "Robot"); $mail->setSubject("Test email"); $mail->setBodyText("Hello, world!"); $mail->setBodyHtml("Hello, world!"); $mail->send(); echo "OK"; } } Download at www.Pin5i.Com Chapter 12 323 2. Now try to run mailtest/index and you will get the following result: 3. This means that the Yii autoloader failed to include the Zend_Mail class. This is expected because it knows nothing about the Zend Framework naming convention. So logically, we have the following two solutions to this:  Include classes explicitly  Create our own autoloader Download at www.Pin5i.Com Using External Code 324 4. We will now start with including classes. All Zend Framework classes do have require_once statements for all dependencies. These statements rely on adding an additional PHP include path and look like the following: require_once 'Zend/Mail/Transport/Abstract.php'; 5. When using Yii::import to import a directory, it works like adding a directory into the PHP include path, so we can solve our problem as follows: class MailtestController extends CController { public function actionIndex() { Yii::import('application.vendors.*'); require "Zend/Mail.php"; $mail = new Zend_Mail('utf-8'); $mail->setHeaderEncoding(Zend_Mime::ENCODING_QUOTEDPRINTABLE); $mail->addTo("alexander@example.com", "Alexander Makarov"); $mail->setFrom("robot@example.com", "Robot"); $mail->setSubject("Test email"); $mail->setBodyText("Hello, world!"); $mail->setBodyHtml("Hello, world!"); $mail->send(); echo "OK"; } } 6. Now it will send an e-mail properly without any error. This method will work if you don't have too many Zend Framework classes used. If you are using it heavily, then you will have to include a lot that will add unnecessary complexity. Now, let's use Zend_Loader_Autoloader to achieve this. 7. The best place to add another autoloader is in the index.php bootstrap. This way, you will be able to autoload classes during the whole execution flow: // change the following paths if necessary $yii=dirname(__FILE__).'/../framework/yii.php'; $config=dirname(__FILE__).'/protected/config/main.php'; // remove the following lines when in production mode defined('YII_DEBUG') or define('YII_DEBUG',true); // specify how many levels of call stack should be shown in each log message defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3); Download at www.Pin5i.Com Chapter 12 325 require_once($yii); $app = Yii::createWebApplication($config); // adding Zend Framework autoloader Yii::import('application.vendors.*'); require "Zend/Loader/Autoloader.php"; Yii::registerAutoloader(array('Zend_Loader_Autoloader', 'autoload'), true); $app->run(); 8. Now we can remove Yii::import('application.vendors.*'); require "Zend/Mail.php"; from MailtestController and it will still work fine without any errors meaning that Zend Framework autoloading now works. How it works... Let's review what is going on behind the scenes and how it works starting with the first way. We have used Yii::import that, when used like Yii::import('path.alias.*'), behaves like adding another PHP include path. As there was no autoloader in Zend Framework originally, it has all the necessary require_once calls. So, if you use a single component, such as Zend_Mail, you don't need more than one require_once. The second method doesn't force you to use a single require statement. As Yii allows using multiple autoloaders and in latest versions, Zend Framework has its own autoloader, we can use it in our application. The best time to do this is right after the application bootstrap was loaded, but application was not run. To achieve this, we break Yii::createWebAppl ication($config)->run() into two separate statements in index.php and insert an autoloader initialization between these: Yii::import('application.vendors.*'); require "Zend/Loader/Autoloader.php"; Yii::registerAutoloader(array('Zend_Loader_Autoloader', 'autoload'), true); We still need Yii::import('application.vendors.*') because Zend Framework classes will continue to use require_once. Then, we require an autoloader class and add it to the end of the PHP autoloading stack by using Yii::registerAutoloader with the second argument set to true. Download at www.Pin5i.Com Using External Code 326 There's more... In order to learn more about the Yii import, autoloading, and Zend Framework usage, refer to the following URLs: ff http://www.yiiframework.com/doc/api/YiiBase/#import-detail ff http://www.yiiframework.com/doc/api/ YiiBase/#registerAutoloader-detail ff http://framework.zend.com/ ff http://www.yiiframework.com/doc/guide/en/extension.integration ff http://framework.zend.com/manual/en/zend.loader.autoloader.html See also ff The recipe named Customizing the Yii autoloader in this chapter Customizing the Yii autoloader Yii uses a naming convention and an autoloader to load only classes which are really needed and to avoid including files explicitly. As other frameworks and libraries could use a different naming convention, Yii provides an ability to customize rules of autoloading classes. In the Using Zend Framework from Yii recipe in this chapter, we used Zend_Loader_Autoloader to be able to use Zend Framework classes without including them explicitly. If we are using only Zend Framework core classes, then its complex autoloader is a bit too much. Moreover, there are still require_once calls in each Zend Framework class, so it still loads tons of unused files. In this recipe, we will create a very simple and fast autoloader that will allow us to do the same, but faster. Getting ready ff Create a fresh application by using yiic webapp ff Download Zend Framework code from the following URL: http://framework.zend.com/download/current/ In this recipe, we have used Version 1.11.6 ff Extract library/Zend from the downloaded archive to protected/vendors/ Zend Download at www.Pin5i.Com Chapter 12 327 ff Create protected/controllers/MailtestController.php as follows: setHeaderEncoding(Zend_Mime::ENCODING_QUOTEDPRINTABLE); $mail->addTo("alexander@example.com", "Alexander Makarov"); $mail->setFrom("robot@example.com", "Robot"); $mail->setSubject("Test email"); $mail->setBodyText("Hello, world!"); $mail->setBodyHtml("Hello, world!"); $mail->send(); echo "OK"; } } How to do it... Carry out the following steps: 1. Create protected/components/EZendAutoloader.php as follows: run(); with $app = Yii::createWebApplication($config); // adding custom Zend Framework autoloader Yii::import("application.vendors.*"); Yii::import("application.components.EZendAutoloader", true); Yii::registerAutoloader(array('EZendAutoloader','loadClass'), true); $app->run(); Try running mailtest/index. It should send an e-mail and output "OK" which means autoloading works correctly. Still, we are importing the vendors directory to satisfy Zend Framework require_once calls by loading all possible classes explicitly. The only way to fix it is to remove all occurrences of require_once from Zend Framework. If you're under Linux you can use: % cd path/to/ZendFramework/library % find . -name '*.php' -not -wholename '*/Loader/Autoloader.php' \ -not -wholename '*/Application.php' -print0 | \ xargs -0 sed --regexp-extended --in-place 's/(require_once)/\/\/ \1/g' Download at www.Pin5i.Com Chapter 12 329 In addition, on MacOSX: % cd path/to/ZendFramework/library % find . -name '*.php' | grep -v './Loader/Autoloader.php' | \ xargs sed -E -i~ 's/(require_once)/\/\/ \1/g' % find . -name '*.php~' | xargs rm –f Or you can simply use IDE or other tools to replace require_once with //require_once. After doing it you can remove Yii::import("application.vendors.*") from index. php and try loading mailtest/index again. It should send another e-mail and output "OK". That means Zend Framework classes are now working without require_once. How it works... Frameworks and libraries that use autoloading rely on PHP SPL autoload. It is triggered when you use a class that is not included yet and PHP is going to fail. Using spl_autoload_ register, you can register multiple autoload callbacks. Therefore, if the first one fails, then another one will take initiative and will try to load a class. Yii is not an exception. By default, it uses its own autoloader implementation YiiBase:: autoload. We have used Yii::registerAutoloader in index.php to add an additional autoloader. The method implementation is as follows: public static function registerAutoloader($callback, $append=false) { if($append) { self::$enableIncludePath=false; spl_autoload_register($callback); } else { spl_autoload_unregister(array('YiiBase','autoload')); spl_autoload_register($callback); spl_autoload_register(array('YiiBase','autoload')); } } So internally, this is the same SPL autoloader, and the registerAutoloader method just adds another callback and by default makes sure it is registered before Yii's own autoloader. If we pass true as the second parameter value then the custom autoloader is registered after the Yii internal one. It does not allow triggering of the custom autoloader for Yii classes. Download at www.Pin5i.Com Using External Code 330 Now, we will move on to our custom autoloader. All SPL autoload callbacks accept a single argument containing the name of the class that needs to be loaded. Given this name, it should try to include a file containing a class with the specified name. In order to get more flexibility, we defined the following two properties: 1. The prefixes property defines a list of prefixes a class should begin with to be autoloaded with our custom autoloader. By default, it is Zend. 2. The basePath defines a path to a directory where the Zend directory is. By default, it equals to protected/vendors. For each prefix, we check if the class we are trying to load begins with it and if we need to use an autoloader. If it matches, then we replace "_" with "/" in the class name and use it as a complete path to the file we are including. In the final step, we get rid of require_once that prevents us from loading only classes that are absolutely required. There's more... As the class loading happens all the time, we use an external library; it should perform as fast as it can. This means that autoloading method should be simple and efficient. Other things to note are as follows: ff require_once is slower than just require ff Using file_exists or is_file will slow down the loading ff You should use absolute paths instead of relative ones to ensure that APC performs efficiently when apc.stat = 0 (it allows not to check if the file was changed and gain more performance on the production server) Further reading In order to learn more about Yii autoloading and APC, refer to the following URLs: ff http://www.yiiframework.com/doc/api/YiiBase/#import-detail ff http://www.yiiframework.com/doc/api/ YiiBase/#registerAutoloader-detail ff http://php.net/manual/en/function.spl-autoload.php ff http://www.php.net/manual/en/apc.configuration.php#ini.apc.stat See also ff The recipe named Using Zend Framework from Yii in this chapter Download at www.Pin5i.Com Chapter 12 331 Using Kohana inside Yii Sometimes to write a custom autoloader, you need to dig into another framework source code. An example of it is the Kohana framework. In this recipe, we will handle the image resizing by using one of the Kohana classes. Getting ready ff Create a fresh application using yiic webapp ff Download the Kohana framework archive from the following URL: http://kohanaframework.org/download In this recipe, we have used Version 3.1. ff Extract the system and modules directories to protected/vendors/Kohana. How to do it... Carry out the following steps: 1. First, we will need the actual code which performs the image resizing and displays an image. Create protected/controllers/ImageController.php as follows: resize(80, 80); Yii::app()->request->sendFile("image.png", $image->render()); } } Download at www.Pin5i.Com Using External Code 332 2. Try to run image/index and you will get the following error: 3. This means that Yii cannot find Kohana classes. In order to help it, we will need a custom autoloader. So, create protected/components/EKohanaAutoloader. php as follows: run(); with the following: $app = Yii::createWebApplication($config); // adding custom Kohana autoloader Yii::import("application.components.EKohanaAutoloader", true); EKohanaAutoloader::$paths = array(Yii::getPathOfAlias ("application.vendors.Kohana.modules.image")); Yii::registerAutoloader(array ('EKohanaAutoloader','loadClass'), true); $app->run(); Download at www.Pin5i.Com Using External Code 334 5. Now run image/index again and you should see a screen similar to one shown in the following screenshot instead of an error: That means Kohana classes were loaded successfully. Note that the Kohana class loader provided was not optimized in terms of performance and is not intended for intensive production use. How it works... Kohana 3 relies on autoloading and has a very special naming convention. As a result, calling its classes directly is too much work and creating an autoloader is the only reasonable way to implement it if we are not modifying Kohana classes. We will take a look at the Kohana autoloader which is present at the following location: protected/vendors/Kohana/system/classes/kohana/core.php The method name is auto_load: public static function auto_load($class) { try { // Transform the class name into a path $file = str_replace('_', '/', strtolower($class)); if ($path = Kohana::find_file('classes', $file)) { // Load the class file require $path; Download at www.Pin5i.Com Chapter 12 335 // Class has been found return TRUE; } // Class is not in the filesystem return FALSE; } catch (Exception $e) { Kohana_Exception::handler($e); die; } } From this part, we can say that it uses a class to form a relative path which is then used to find a file inside of the classes directory: $file = str_replace('_', '/', strtolower($class)); Now let's go deeper inside find_file: public static function find_file ($dir, $file, $ext = NULL, $array = FALSE) { if ($ext === NULL) { // Use the default extension $ext = EXT; } elseif ($ext) { // Prefix the extension with a period $ext = ".{$ext}"; } else { // Use no extension $ext = ''; } // Create a partial path of the filename $path = $dir.DIRECTORY_SEPARATOR.$file.$ext; if (Kohana::$caching === TRUE AND isset (Kohana::$_files[$path.($array ? '_array' : '_path')])) { Download at www.Pin5i.Com Using External Code 336 // This path has been cached return Kohana::$_files[$path.($array ? '_array' : '_path')]; } if (Kohana::$profiling === TRUE AND class_exists ('Profiler', FALSE)) { // Start a new benchmark $benchmark = Profiler::start('Kohana', __FUNCTION__); } if ($array OR $dir === 'config' OR $dir === 'i18n' OR $dir === 'messages') { // Include paths must be searched in reverse $paths = array_reverse(Kohana::$_paths); // Array of files that have been found $found = array(); foreach ($paths as $dir) { if (is_file($dir.$path)) { // This path has a file, add it to the list $found[] = $dir.$path; } } } else { // The file has not been found yet $found = FALSE; foreach (Kohana::$_paths as $dir) { if (is_file($dir.$path)) { // A path has been found $found = $dir.$path; // Stop searching break; } Download at www.Pin5i.Com Chapter 12 337 } } if (Kohana::$caching === TRUE) { // Add the path to the cache Kohana::$_files[$path.($array ? '_array' : '_path')] = $found; // Files have been changed Kohana::$_files_changed = TRUE; } if (isset($benchmark)) { // Stop the benchmark Profiler::stop($benchmark); } return $found; } As we know that our file extension is always .php, the directory is always classes, and as we don't care about the caching or profiling right now, the useful part is as follows: $path = $dir.DIRECTORY_SEPARATOR.$file.$ext; foreach (Kohana::$_paths as $dir) { if (is_file($dir.$path)) { // A path has been found $found = $dir.$path; // Stop searching break; } } We are pretty close. The only thing left is Kohana::$_paths: /** * @var array Include paths that are used to find files */ protected static $_paths = array(APPPATH, SYSPATH); Download at www.Pin5i.Com Using External Code 338 We don't care about the application, so we can omit the APPPATH part. Moreover, SYSPATH is a path to the system directory. As most of the Kohana classes are there, it is reasonable to make this a default. When the autoloader class is ready, we use Yii::registerAutoloader in index.php to register it. It is important to register the autoloader after the default one built in Yii, so we pass true as the Yii::registerAutoloader second parameter value. Our image class is not in the core and is located in the image module, so we set paths to the image module path in the following way: EKohanaAutoloader::$paths = array(Yii::getPathOfAlias ("application.vendors.Kohana.modules.image")); There's more... As image resizing is a common task, it is better from both reusability and performance perspectives to separate this task from the rest of the application and create a separate PHP script that will handle the image resizing. For example, it will allow using the following code: This means, take avatar.png as the source image and resize it to 100×100 pixels. Possible steps the image.php script will take are as follows: ff If an already processed image exists, serve it ff If there is no image yet, read the source image, resize it, and write it as the processed one In order to achieve better performance, you can configure the web server to serve existing images directly, avoid serving with a PHP script, and redirecting the non-existing ones to the processing script. Further reading In order to learn more about Yii autoloading and Kohana, refer to the following URLs: ff http://www.yiiframework.com/doc/api/ YiiBase/#registerAutoloader-detail ff http://kohanaframework.org/ See also ff The recipe named Customizing the Yii autoloader in this chapter Download at www.Pin5i.Com Chapter 12 339 Using PEAR inside Yii Another traditional place to look for PHP libraries is PEAR. There is a very special naming convention, so in order to use the PEAR code, we can either implement another autoloader or include files directly. In this recipe, we will use the PEAR Text_Password class to generate a random password. Getting ready ff Create a fresh application by using yiic webapp ff Make sure that PEAR is installed and configured properly (http://pear.php.net/ manual/en/installation.php) How to do it... The page of the PEAR package that we want to use is http://pear.php.net/package/ Text_Password. Important things: there are "Easy Install" and documentation. 1. We will install the package first. Open the console and type what is suggested in "Easy Install section": pear install Text_Password 2. It should respond with the following: downloading Text_Password-1.1.1.tgz ... Starting to download Text_Password-1.1.1.tgz (4,357 bytes) ..... done: 4,357 bytes install ok: channel://pear.php.net/Text_Password-1.1.1 3. Now we can try using it. We will generate 10 random passwords, the length of which equals to 8. Create protected/controllers/PasswordController.php as follows: class PasswordController extends CController { public function actionIndex() { require "Text/Password.php"; $textPassword = new Text_Password(); $passwords = $textPassword->createMultiple(10, 8); echo "

"; } } How it works... Using PEAR packages in Yii is easy. You don't need to configure Yii or write any additional code to the one provided in the PEAR package's guide. There's more... In order to learn more about PEAR, refer to the following URLs: ff http://pear.php.net/manual/en/installation.php ff http://pear.php.net/package/Text_Password ff http://pear.php.net/ See also ff The recipe named Using Zend Framework from Yii in this chapter Download at www.Pin5i.Com 13 Deployment In this chapter, we will cover: ff Changing the Yii directories layout ff Moving an application out of webroot ff Sharing the framework directory ff Moving configuration parts into separate files ff Using multiple configurations to simplify the deployment ff Implementing and executing cron jobs ff Maintenance mode Introduction In this chapter, we will cover various tips which are especially useful on application deployment and when developing an application in a team, or when you just want to make your development environment more comfortable. Changing the Yii directories layout Yii has a pre-defined convention for directories layout. It allows us to significantly lower the learning curve but sometimes, the custom directory structure fits the project better. In this recipe, we will rename a few directories and share common libraries around separate projects. Download at www.Pin5i.Com Deployment 342 The plan is to: ff Rename protected to app ff Create a shared directory where we can store components shared across multiple applications ff Move runtime out of app Getting ready ff Get a framework copy from the Yii website ff Set up the following directory structure: /var/www/example/ framework/ www/ ff Unpack the framework directory contents to /var/www/example/framework/ How to do it... Carry out the following steps: 1. Go to the framework directory. 2. Run yiic webapp /var/www/example/www/. 3. Go to /var/www/example/www and rename protected to app. 4. Replace all occurrences of protected with app in index.php and index-test. php. Now, we have a custom directory named protected. 5. Create /var/www/shared. 6. In your main.php config add: // uncomment the following to define a path alias Yii::setPathOfAlias('shared','/var/www/shared'); … // This is the main Web application configuration. Any writable // CWebApplication properties can be configured here. return array( … // autoloading model and component classes 'import'=>array( … 'shared.*', ), Download at www.Pin5i.Com Chapter 13 343 That is it. Now you can place your own components under /var/www/shared and the application will be aware of them. In addition, if you add the same settings to another application config, another application will be able to use these components as well. 1. Move runtime from the /var/www/example/www/app directory to /var/www/ example/runtime. 2. Modify your main.php config as follows: return array( … 'runtimePath' => Yii::getPathOfAlias('system').'/../runtime/', 3. That is it. Now the runtime directory is outside of the application directory. How it works... The application directory name and path are determined only at two places: index.php and index-test.php, so it is relatively easy to change these. We just need to update two bootstrap files after renaming the application directory. When creating a shared directory, we define a custom path alias. It is a very convenient way of using additional directories if you are referring to them often: Yii::setPathOfAlias('shared','/var/www/shared'); setPathOfAlias accepts two arguments. First is the name that we will use when setting options accepting paths, Yii::getPathOfAlias and Yii::import. Second is the actual path to the directory. As we want to use components transparently, we are adding shared.* to the list of application imports. This allows classes from the /var/www/shared directory to be loaded automatically. The last path we change is a path to the runtime directory. For this case, Yii defines an application property named runtimePath. We can set it to change the path used as follows: 'runtimePath' => Yii::getPathOfAlias('system').'/../runtime/', As we want to place runtime in the same file structure level where the framework directory is, we get the framework directory path by using getPathOfAlias and then append the relative path to our runtime directory. An application defines some other properties allowing you to change the extensions path, translations path, and modules path. Other paths such as views path or cache files path could be configured by changing another component's properties. For a view, it is CController::viewPath and for cache (in case of using file cache), it is CFileCache::cachePath. Download at www.Pin5i.Com Deployment 344 There's more... In order to learn more about Yii directory paths, refer to the following URLs: ff http://www.yiiframework.com/doc/api/YiiBase#getPathOfAlias- detail ff http://www.yiiframework.com/doc/api/YiiBase#setPathOfAlias- detail ff http://www.yiiframework.com/doc/api/YiiBase#import-detail ff http://www.yiiframework.com/doc/api/ CApplication#setExtensionPath-detail ff http://www.yiiframework.com/doc/api/ CApplication#setRuntimePath-detail ff http://www.yiiframework.com/doc/api/ CApplication#setLocaleDataPath-detail ff http://www.yiiframework.com/doc/api/CModule#setModulePath- detail ff http://www.yiiframework.com/doc/api/CController#viewPath-detail See also ff The recipe named Moving an application out of webroot in this chapter Moving an application out of webroot By default, when generating web application by using yiic webapp command, Yii puts both the index.php and protected directory in a single place that is typically the server's webroot. It allows running Yii in very restricted environments, but for security purposes and ease of development, it is better to keep your code out of webroot if it can be kept out of it. In this recipe, we will see how to move a Yii application out of the server's webroot located at /var/www/website/www/. Getting ready ff Copy the framework directory to /var/www/website/ ff Go to /var/www/website/framework/ and run yiic webapp /var/www/ website/www/ ff You should get the default web application files under /var/www/website/www/ Download at www.Pin5i.Com Chapter 13 345 How to do it... Carry out the following steps: 1. First, we need to move /var/www/website/www/protected/ to /var/www/ website/protected/. As the path was changed, the application will fail to run now. Both index.php and index-test.php need some fixing. The index.php file has the following content: // change the following paths if necessary $yii=dirname(__FILE__).'/../framework/yii.php'; $config=dirname(__FILE__).'/protected/config/main.php'; // remove the following lines when in production mode defined('YII_DEBUG') or define('YII_DEBUG',true); // specify how many levels of call stack should be shown in each log message defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3); require_once($yii); Yii::createWebApplication($config)->run(); 2. There are two paths defined: path to the framework directory that was not changed and path to config that was changed. Let's update the latter as follows: $config=dirname(__FILE__).'/../protected/config/main.php'; 3. We need to do the same in index-test.php: $config=dirname(__FILE__).'/../protected/config/test.php'; 4. That is it. Now try to run the application and it should show a standard welcome screen, as shown in the following screenshot: Download at www.Pin5i.Com Deployment 346 5. Now, we need to fix one more path in protected/yiic.php. We do this as follows: // change the following paths if necessary $yiic=dirname(__FILE__).'/../framework/yiic.php'; $config=dirname(__FILE__).'/config/console.php'; require_once($yiic); How it works... A Yii application can be moved to any place in a filesystem where we want it to be. The only thing you should correct is paths in index.php and index-test.php. This simple move gives you slightly better security as no application code will be executed directly, and there will be no leaks in the source code through version control meta files, and so on. Therefore, if your production environment allows moving the application code out of webroot, you certainly should consider doing it. There's more... The following article will give you an idea why it is better to have as little code as possible under webroot: http://www.smashingmagazine.com/2009/09/25/svn-strikes-back-a- serious-vulnerability-found/ See also ff The recipe named Sharing the framework directory in this chapter Sharing the framework directory If you run multiple Yii projects on a single web server, then you can consider sharing the framework code between the projects. This will save some disk space and will require less work when you upgrade your applications to a new framework version. Getting ready Copy the framework directory contents to /var/www/common/yii/latest. Download at www.Pin5i.Com Chapter 13 347 How to do it... Carry out the following steps: 1. Go to /var/www/common/yii/latest and run yiic webapp /var/www/ website1/www/. 2. Go to /var/www/common/yii/latest and run yiic webapp /var/www/ website2/www/. 3. You should get the default web application files under /var/www/website1/www/ and /var/www/website2/www/. 4. That is it. Try to run applications to make sure that everything works. How it works... Using yiic webapp from the single framework copy will create applications referencing to this framework instance. We have two applications using the same framework copy, so when upgrading framework, we should only replace a single directory's contents. If you have existing applications, then you can do the same by editing their index.php and index-test.php files, so $yii values will be as follows: $yii='/var/www/common/yii/latest/yii.php'; There's more... As the new Yii version can possibly introduce some backwards incompatible changes (typically, there are no such changes in minor releases), it is good to have a way to quickly roll everything back. For this purpose, you have to keep several framework versions under / var/www/common/yii/ (for example: 1.1.8 and 1.1.7). When upgrading an application to 1.1.8, you are changing path to yii.php in index.php and index-test.php for a single application and testing for regressions. If there are any, you can either fix them or roll everything back by quickly changing a path back to 1.1.7. If everything is fine, then you can safely move on to test the next application. See also ff The recipe named Moving application out of webroot in this chapter Download at www.Pin5i.Com Deployment 348 Moving configuration parts into separate files By default, a Yii application stores the entire web application configuration in a single file named protected/config/main.php. The same goes for the console application. It is good for both learning and small web applications where keeping everything inside of a single config file gives a developer the ability to quickly overview the whole application's settings. When we develop something bigger, we may face some inconvenience, such as the following: ff The configuration file becomes too bloated if there are many things to configure. Moreover, in a big application, there are typically many components used. ff If we need to adjust some settings, then we most probably end up repeating changes in both the web application config and console application config. Getting ready Create a fresh application by using yiic webapp. How to do it... Carry out the following steps: 1. We will review the default config first to identify parts we will reuse, as well as parts that will most probably be too large to have in a single file: dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'name'=>'My Web Application', // preloading 'log' component 'preload'=>array('log'), // autoloading model and component classes 'import'=>array( 'application.models.*', 'application.components.*', ), Download at www.Pin5i.Com Chapter 13 349 'modules'=>array( // uncomment the following to enable the Gii tool /* 'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>'Enter Your Password Here', //If removed, Gii defaults to localhost only. Edit carefully to taste. 'ipFilters'=>array('127.0.0.1','::1'), ), */ ), // application components 'components'=>array( 'user'=>array( // enable cookie-based authentication 'allowAutoLogin'=>true, ), // uncomment the following to enable URLs in path-format /* 'urlManager'=>array( 'urlFormat'=>'path', 'rules'=>array( '/'=>'/view', '//'=>' /', '/'=>' /', ), ), */ 'db'=>array( 'connectionString' => 'sqlite:'.dirname(__FILE__).'/../data/testdrive.db', ), // uncomment the following to use a MySQL database /* 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=testdrive', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', Download at www.Pin5i.Com Deployment 350 ), */ 'errorHandler'=>array( // use 'site/error' action to display errors 'errorAction'=>'site/error', ), 'log'=>array( 'class'=>'CLogRouter', 'routes'=>array( array( 'class'=>'CFileLogRoute', 'levels'=>'error, warning', ), // uncomment the following to show log messages on web pages /* array( 'class'=>'CWebLogRoute', ), */ ), ), ), // application-level parameters that can be accessed // using Yii::app()->params['paramName'] 'params'=>array( // this is used in contact page 'adminEmail'=>'webmaster@example.com', ), ); The imports list and module configuration are typically not too large. The same goes for the most components configuration. What can grow with the application complexity are the urlManager component routes and application-level parameters. As for reusing, we will probably need the same imports, database connection, and application-level parameters for both the web application and console application. 1. Now, we will create the following config files under protected/configs:  routes.php  params.php  import.php  db.php Download at www.Pin5i.Com Chapter 13 351 2. Now we need to move the corresponding sections of the main.php into separate files. We do this as follows: // uncomment the following to define a path alias // Yii::setPathOfAlias('local','path/to/local-folder'); // This is the main Web application configuration. Any writable // CWebApplication properties can be configured here. return array( 'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'name'=>'My Web Application', // preloading 'log' component 'preload'=>array('log'), // autoloading model and component classes 'import'=>require(dirname(__FILE__).'/import.php'), 'modules'=>array( // uncomment the following to enable the Gii tool /* 'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>'Enter Your Password Here', // If removed, Gii defaults to localhost only. Edit carefully to taste. 'ipFilters'=>array('127.0.0.1','::1'), ), */ ), // application components 'components'=>array( 'user'=>array( // enable cookie-based authentication 'allowAutoLogin'=>true, ), // uncomment the following to enable URLs in path-format 'urlManager'=>array( 'urlFormat'=>'path', 'rules'=>require(dirname(__FILE__).'/routes.php'), ), 'db'=>require(dirname(__FILE__).'/db.php'), 'errorHandler'=>array( Download at www.Pin5i.Com Deployment 352 // use 'site/error' action to display errors 'errorAction'=>'site/error', ), 'log'=>array( 'class'=>'CLogRouter', 'routes'=>array( array( 'class'=>'CFileLogRoute', 'levels'=>'error, warning', ), // uncomment the following to show log messages on web pages /* array( 'class'=>'CWebLogRoute', ), */ ), ), ), // application-level parameters that can be accessed // using Yii::app()->params['paramName'] 'params'=>require(dirname(__FILE__).'/params.php'), ); 3. Each new config will contain the same values that were there in the main config. For example, protected/configs/params.php will contain the following: 'webmaster@example.com', ); 4. Now we need to change the console application protected/configs/console. php. We do this as follows: // This is the configuration for yiic console application. // Any writable CConsoleApplication properties can be configured here. return array( 'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'name'=>'My Console Application', Download at www.Pin5i.Com Chapter 13 353 // autoloading model and component classes 'import'=>require(dirname(__FILE__).'/import.php'), // application components 'components'=>array( 'db'=>require(dirname(__FILE__).'/db.php'), ), // application-level parameters that can be accessed // using Yii::app()->params['paramName'] 'params'=>require(dirname(__FILE__).'/params.php'), ); 5. That is it. Now we have separate configuration files for imports, database configuration, application routes, and application parameters. How it works... The preceding technique relies on the fact that Yii configuration files are native PHP files with arrays: require(dirname(__FILE__).'/db.php') It reads the file specified, and, if there is a return statement inside this file, it returns a value. Therefore, moving a part out of the main configuration file into a separate file requires creating a separate file, moving the configuration part into it right after the return statement, and using require in the main configuration file. If separate applications (in our example, these are web applications and console applications) require some common configuration parts, then we can use require to move these into a separate file. There's more... In order to learn more about PHP require and include statements, refer to the following URLs: ff http://php.net/manual/en/function.require.php ff http://php.net/manual/en/function.include.php Download at www.Pin5i.Com Deployment 354 See also ff The recipe named Using multiple configurations to simplify the deployment in this chapter Using multiple configurations to simplify the deployment In some cases, it is handy to use different configuration files for different cases. For example, we can use different configuration files for the development environment and production environment. In this recipe, we will see how to choose a configuration file automatically and how to implement the configuration inheritance. Getting ready Create a fresh application by using yiic webapp. How to do it... Carry out the following steps: 1. We will assume that we are using http://example.com/ as a production URL and http://example.local/ as a development URL. Given this fact, we can choose the appropriate config from index.php as follows: // change the following paths if necessary $yii=dirname(__FILE__).'/../framework/yii.php'; if($_SERVER['HTTP_HOST']=='example.com') { $config=dirname(__FILE__).'/../protected/config/production.php'; } else { // remove the following lines when in production mode defined('YII_DEBUG') or define('YII_DEBUG',true); // specify how many levels of call stack should be shown in each log message defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3); Download at www.Pin5i.Com Chapter 13 355 $config=dirname(__FILE__).'/../protected/config/development.php'; } require_once($yii); Yii::createWebApplication($config)->run(); 2. The plan ahead is to leave all common configurations in main.php and override the environment-specific settings in development.php and production. php. Therefore, main.php stays the same. We put the following in protected/ configs/development.php as follows: array( 'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>false, ), ), 'components'=>array( 'db'=>array( 'class'=>'system.db.CDbConnection', 'connectionString'=>'mysql:host=localhost; dbname=example', 'username'=>'root', 'password'=>'', 'charset'=>'utf8', 'enableProfiling'=>true, 'enableParamLogging'=>true, ), 'log'=>array( 'class'=>'CLogRouter', 'routes'=>array( array( 'class'=>'CProfileLogRoute', ), ), ), ), ) ); Download at www.Pin5i.Com Deployment 356 3. In addition, put the following in protected/configs/production.php as follows: array( 'db'=>array( 'class'=>'system.db.CDbConnection', 'connectionString'=>'mysql:host=localhost;dbname=example', 'username'=>'example', 'password'=>'2WXyVNb4dBSEK3HW', 'charset'=>'utf8', 'schemaCachingDuration'=>60*60, ), 'cache'=>array( 'class'=>'CFileCache', ), ), ) ); 4. That is it. Now we can just upload files to the production server that runs http://example.com/ and the application will use the production.php config that inherits all settings from main.php, overrides some of them, and adds some more settings. How it works... When we create a web application instance inside of index.php, it is possible to pass a single argument to Yii::createWebApplication. This argument is a path to the application configuration file. Given this fact, we can vary the path to this file based on some kind of criteria. In our case, it is the name of the host where the application is running: if($_SERVER['HTTP_HOST']=='example.com') { $config=dirname(__FILE__).'/../protected/config/production.php'; } else if($_SERVER['HTTP_HOST']=='example.local') { Download at www.Pin5i.Com Chapter 13 357 // remove the following lines when in production mode defined('YII_DEBUG') or define('YII_DEBUG',true); // specify how many levels of call stack should be shown in each log message defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3); $config=dirname(__FILE__).'/../protected/config/development.php'; } If host equals example.com, then we use the production config. Else, we use the development config and additionally, turn on the debugging. You can use virtually anything to choose the config file. For example, you can run the application in a debug mode at the production server if there is a cookie with a specific name and value. As most of the development and production settings, such as application parameters and routes, will stay the same, we leave these in main.php. Moreover, as Yii configuration files are PHP arrays, we can use CMap::mergeArray to implement the configuration inheritance as follows: return CMap::mergeArray( require(dirname(__FILE__).'/main.php'), array(…) ); There's more... In order to learn more about how exactly config files are merged, refer to the following URL: http://www.yiiframework.com/doc/api/CMap#mergeArray See also ff The recipe named Moving configuration parts into separate files in this chapter Implementing and executing cron jobs Sometimes, an application requires some background tasks such as re-generating a sitemap or refreshing statistics. A common way to implement this is by using cron jobs. When using Yii, there are two ways to do it which are as follows: 1. Emulate the browser to call the web application controller action. 2. Use the command line command to run as a job. Download at www.Pin5i.Com Deployment 358 In this recipe, we will see how to implement both. For our recipe, we will implement writing the current timestamp into a timestamp.txt file under the protected directory. Getting ready Create a fresh application by using yiic webapp. How to do it... Carry out the following steps: 1. Create protected/controllers/CronController.php as follows: /dev/null When we use a controller in this way, we need to make sure that it is used only as a cron job. For example, we can check for a value of a specific $_GET variable. 3. Create protected/commands/CronCommand.php as follows: renderPartial("index"); } } 2. Then, we create a view named protected/views/maintenance/index.php as follows: name)?> is under maintenance

name)?> is under maintenance

We'll be back soon. If we aren't back for too long, please drop a message to params ['adminEmail']?>.

Download at www.Pin5i.Com Chapter 13 361

Meanwhile, it's a good time to get a cup of coffee, to read a book or to check email.