Tuesday, April 1, 2014

How to create custom CGridView in Yii

Yii is an extremely powerful PHP framework, which  helps you to create complete web applications within a short period of time. It has a broad set of built-in tools that make developers' life easier. But... and there's always a but, easier doesn't necessarily mean that you don't have to code any longer. There are some situations when you can't solve the problem clearly by using Yii's tool-set, and you need to rely on your own coding knowledge, or you should use PHP and Yii's tool-set combined. Remember that PHP frameworks can never replace the entire programming process, but provide great tools for faster development.

Drawing a matrix on Michael Knight way
I ran into a similar case few days ago when I had to create a custom matrix view based on database entries. I wanted to create a grid but the data stored in the database table was in a completely different format that I needed. If you are an experienced Yii developer, you know that using a CGridView can help you out to create a view of table entries instantly, but only in the case if you have to represent data in the same arrangement as it has been saved in the database. If you need to create a custom X and Y axes then the trouble really begins.

Problem to be solved

In my project I had to create a matrix view of number of dishes ordered on given days. I stored data in a database table which had the following structure:

CREATE TABLE IF NOT EXISTS `ff_order` (

  `letter` varchar(5) COLLATE utf8_hungarian_ci NOT NULL',
  `date` date NOT NULL COMMENT,
  `no_dishes` int(11) NOT NULL COMMENT,
  `creation_dt` datetime NOT NULL COMMENT,
  `modification_dt` datetime NOT NULL COMMENT,
  `user_id` int(11) NOT NULL COMMENT,
  PRIMARY KEY (`date`,`letter`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_hungarian_ci;

If I would have handed over the content of this table to my view by using the default data provider, the result would have looked like this :

Normal data provider view
Normal data provider view
But I was trying to achieve something like this instead:


Matrix view using the same data set
Matrix view using the same data set
This cannot be implemented by using basic parameters of Yii's CGridView, but not impossible.

If the grid can only display the data in a form that is provided by the data provider, let's change our data provider to compile the proper format that we need for our CGridView.

The theoretical part

CGridView represents data received through a data provider. So, the good news is if we can put together a data provider that represent our data in the form that we need, the only thing that we need to do is just pass it to our view file to display. Luckily, the necessary data can be prepared in not only one but several ways in Yii. I'll mention two techniques here which can be done with Yii and will further explain only one of them via an example, but I'm sure that there are many other solutions.

If you have ever generated a skeleton code with Yii's built in code generator called Gii and you have taken a look at any of your controllers you could see that it defined at least one data provider which most likely was a CActiveDataProvider. This data provider provides data in terms of ActiveRecord objects. It uses the AR CActiveRecord::findAll method to retrieve the data from database. In other words, retrieves all of your records from the database. That's it. This provider was created with the intention of this single task. That's why it has its limitations.  Don't get me wrong! It's useful in 90% of the cases but sometimes you need more flexibility in your project, just like I had in this particular case.

So what can we do? How can we get data from our database in the required form? Fortunately, Yii comes to our aid with other data provider types like CArrayDataProvider and CSqlDataProvider. Both of them are capable to compile data for your view. While the first one is used to prepare data for display based on an array that you pass to it, the second is capable to retrieve data from your database based on any SQL query. Got it? So, for example if you create a multidimensional array where you arrange your data as you want to see in the view, you can pass it to a CArrayDataProvider, which transmits it to your view. To put together this array you can utilize PHP's versatility. Same is true for CSqlDataProvider but it requires an SQL statement with which you query the data from the database as required. Or you can use them combined: retrieve the data from the database and arrange it as you want to see in the grid view. So easy!

CSqlDataProvider way

Now that we know what we're dealing with we can start to implement our matrix. I'll introduce the SQL data provider in detail here through a complete example.

I already mentioned my table structure above, so here is the view action of my controller, which prepares the data in the required format and passes it to the relevant view through a data provider:

public function actionView() {
// Get number of items for pagination
 $count=Yii::app()->db->createCommand('SELECT COUNT(DISTINCT(letter))
            FROM ff_order')->queryScalar();

 // Get dates from the table
 $sql_getLastDates = "SELECT distinct(date) as 'id'
       FROM ff_order
       ORDER BY date ASC";

 $connection = Yii::app()->db;
 $command = $connection->createCommand($sql_getLastDates);
 $dataArray = $command->queryAll();

 // Prepare CASE statements based on the matrix will be created
 $sql_case = '';
 foreach ($dataArray as $row){
  foreach ($row as $key=>$val) {
   $day = date('l', strtotime($val));
   $sql_case .= "SUM((CASE WHEN date='".$val."' THEN `no_dishes` END)) '".$day."',";
  }
 }

 // We need to cut the trailing comma of the last case
 // since our SQL query structure will be incorrect
 $final_case = rtrim($sql_case, ",");

 // Put together the final query
 $sql_getMatrix = "SELECT letter as 'id', ".$final_case."  
       FROM ff_order
       GROUP BY id";

 $sqlProvider = new CSqlDataProvider($sql_getMatrix, array(
      'totalItemCount'=>$count,
      'pagination'=>array(
        'pageSize'=>$count,
      ),
     ));
 
 $this->render('view',array(
     'dataProvider'=>$sqlProvider,
 ));
}

Let me explain the code step by step in detail. First I run a query to figure out how much menu items I have in the table. I'll use this number as item number for my pagination in the data provider as I want to represent all data on the very same page. If you check my data provider I pass this parameter as pagination number. In this way I can make sure that my data will fit to one page regardless of the number of records in the database table.

In the next few steps I prepare the SQL query whose result will be handed over to the data provider. So, our next task is to put together the following SQL query which responsible to draw the matrix:

SELECT letter as 'id',
 SUM((CASE WHEN date='2014/04/07' THEN `no_dishes` END)) '2014/04/07',
 SUM((CASE WHEN date='2014/04/08' THEN `no_dishes` END)) '2014/04/08',
 SUM((CASE WHEN date='2014/04/09' THEN `no_dishes` END)) '2014/04/09',
 SUM((CASE WHEN date='2014/04/10' THEN `no_dishes` END)) '2014/04/10',
 SUM((CASE WHEN date='2014/04/11' THEN `no_dishes` END)) '2014/04/11',
 SUM((CASE WHEN date='2014/04/12' THEN `no_dishes` END)) '2014/04/12',
 SUM((CASE WHEN date='2014/04/13' THEN `no_dishes` END)) '2014/04/13'  
FROM ff_order
GROUP BY id";

You can see that it consists of as many SQL CASE statements as many dates we have in our database table. Each date represents a coulmn with the summary of dishes orederd by clients. This SQL query can provide the matrix that we finally need but we can't hardcode the values into the query becasue those are changing all the time. Instead we have to prepare this query dinamically using PHP's felxibility. Here's the code how to generate the SQL CASE statements in PHP:

// Prepare CASE statements based on the matrix will be created
  
  $sql_case = '';
  foreach ($dataArray as $row){
   foreach ($row as $key=>$val) {
    $day = date('l', strtotime($val));
    $sql_case .= "SUM((CASE WHEN date='".$val."' THEN `no_dishes` END)) '".$day."',";
   }

This code snippet generates all the CASE statements for us. As I have to show only one week of data for this particular project I use name of days as column names. To find out name of a day based on date I use strtotime PHP function. The only thing remained is to stitch together the final SQL query and fire it via a CSqlDataprovider:

  // We need to cut the trailing comma of the last case
  // since our SQL query structure will be incorrect
  $final_case = rtrim($sql_case, ",");
  
  $sql_getMatrix = "SELECT letter as 'id', ".$final_case."  
        FROM ff_order
        GROUP BY id";
  
  $sqlProvider = new CSqlDataProvider($sql_getMatrix, array(
       'totalItemCount'=>$count,
       'pagination'=>array(
         'pageSize'=>$count,
       ),
      ));
As you probably noticed I cut off the last comma of the CASE statement before I merged it with the final SQL query otherwise it would have been syntactically incorrect. Finally we load the result to a variable, and that variable is rendered for the view as shown in the code of my complete action above.

Lastly we represent our data by using a simple GridView. I use Hungarian equivalents of English day names so my grid looks something like this:

$this->widget('bootstrap.widgets.TbGridView', array(
   'dataProvider'=>$dataProvider,
   'columns'=>array(
     array(
       'name' => 'Menü',
       'value' => '$data[\'id\']',
     ),
     array(
       'name' => 'Hétfő',
       'value' => '$data[\'Monday\']',
     ),
     array(
       'name' => 'Kedd',
       'value' => '$data[\'Tuesday\']',
     ),
     array(
       'name' => 'Szerda',
       'value' => '$data[\'Wednesday\']',
     ),
     array(
       'name' => 'Csütörtök',
       'value' => '$data[\'Thursday\']',
     ),
     array(
       'name' => 'Péntek',
       'value' => '$data[\'Friday\']',
     ),
     array(
       'name' => 'Szombat',
       'value' => '$data[\'Saturday\']',
     ),
     array(
       'name' => 'Vasárnap',
       'value' => '$data[\'Sunday\']',
     ),
   )
 ));
That's all! I hope you found this article interesting and useful. Feel free to contact me or comment on this post if you have any question on the subject.

Sunday, March 30, 2014

How to re-install Eclipse 3.8 IDE with Yiiclipse and RSE on Ubuntu

Preliminaries


Maybe I could re-install Eclipse today...
Maybe I could re-install Eclipse today...
I struggled with this all day long. Just moved to Ubuntu 14.04 LTS which completely messed up my perfect Eclipse setup. All this happened in a middle of an important project of course. Feel free to call me an idiot since a normal person would never do that (and I already swore that I don't do so in the future) but I would have never guessed that it takes me one day (!!!) to restore back to the original state.

Before I upgraded my Ubuntu version from 13.10 to 14.04 LTS I have installed Eclipse from Ubuntu's Software Center. Since then I have installed Yiiclipse and RSE (Remote System Explorer) plugins for Eclipse as I work in PHP and I prefer to use cloud directly instead of sync the code all the time. After the OS upgrade I got the following error messages in Eclipse and I couldn't open my RSE project at all.

Editor could not be initialized.
java.lang.IllegalArgumentException:at org.eclipse.core.runtime.Assert.isLegal(Assert.java:63)at org.eclipse.core.runtime.Assert.isLegal(Assert.java:47)...

I tried to remove and add the project again to project explorer but I kept getting the error messages. This was the time when I decided to re-install the above mentioned two plugins to see if it helps. It didn't. I tried to get help from several forums with no luck, thus I decided to re-install the complete Eclipse IDE. It took me almost one day to figure out how can I completely get rid of Eclipse and all of its plugins, and then install a new working instance. That's why I thought that it would worth to share my experiences in a blog post, perhaps others can learn from it, and it's also a good thing to note down all the steps I did to avoid a similar case in the future.

Step by step guide to re-install Eclipse


Few things that you need to know


Removing Eclipse through Ubuntu's Software Center doesn't do the trick since all your previously installed plugins and all of your settings stay on your PC in different system folders and you need to remove these manually. Your workspace must be also deleted (WARNING: Be sure to make a backup of your projects before you remove workspace folder!!!), since Eclipse RSE plugin stores your connection information there. If you don't remove these and you re-install Eclipse only it will conflict with plugins of your previous installation. So let's see how to avoid this.

Removing your previous Eclipse installation


1.) Remove eclipse via Ubuntu Software Center. (If you installed Eclipse manually you can skip this step.)

Remove Eclipse via Software Center
Remove Eclipse via Software Center

2.) Remove all remnants of Eclipse manually. For this fire up a terminal by pressing Ctrl+Alt+T and issue the following command:


$ gksu nautilus

A window will pop up where you need to authenticate with your root account. Once it's done a file manager will start. Navigate to your system drive and search for all files which include "eclipse".


Search for files containing "eclipse" on your system drive
Search for files containing "eclipse" on your system drive
Once the search query finished, select (Ctrl+A) and delete all of them.

3.) Delete your Eclipse workspace folder also. WARNING: Make sure to create a backup for all of your existing projects which reside in this folder!!! Eclipse stores some metadata information and some temporary files for your RSE connection in your workspace. By removing this folder we can make sure that it won't make any conflict with your new Eclipse installation. If you did everything right so far then you don't even have a piece of Eclipse on your PC now. So let's start to make a new Eclipse setup with Yii and RSE support.

4.) Open up Software Center and install Eclipse again. Search for Eclipse, and choose Eclipse Integrated Development Environment from the result list.


Install Eclipse IDE from Software Center
Install Eclipse IDE from Software Center
Once the installation finished you have a brand new Eclipse 3.8 instance on your system. To check if it starts up correctly, launch it from Unitiy dash.

Start Eclipse from Unity
Start Eclipse from Unity

5.) Install DLTK for Eclipse. DLTK (Dynamic Languages Toolkit) is comprised of a set of extensible frameworks designed to reduce the complexity of building full featured development environments for dynamic languages such as PHP and Perl. This plugin is a prerequisite of Yiiclispe. You can install it from Eclipse's 3.8 Indigo repository (http://download.eclipse.org/releases/indigo/). Do to so, go to Help > Install New Software menu option. This will open up a new window. From here select the Indigo repository from the Work with: drop-down box. If you can't find it in the drop-down list, add it manually by clicking on the Add button on the right-hand side.

Once the repository is selected wait until Eclipse loads the content of it, then type dynamic into the search box. It will take a while till Eclipse filters the result. Tick Dynamic Languages Toolkit - Core Frameworks and Dynamic Languages Toolkit - Remote Development Support under General Purpose Tools category.

Installing DLTK plugin for Eclipse
Installing DLTK plugin for Eclipse

After you selected the two plugins click Next and accept the agreement, then finish the installation.

6.) Install Remote System Explorer for Eclipse. RSE is a perspective and toolkit in Eclipse Workbench, that allows you to connect and work with a variety of remote systems. With the predefined plug-ins, you can look at remote file systems, transfer files between hosts, do remote search, execute commands and work with processes. To install it for Eclipse go to Help > Install New Software menu option and choose Indigo repository again. Type remote into the search box and wait until Eclipse filters the results.

Install RSE for Eclipse
Install RSE for Eclipse

Tick Remote System Explorer - End-User Runtime and Remote System Explorer - User Actions plugins and install both of them.

7.) Install Yiiclipse plugin for Eclipse. Yiiclipse leverages productivity and speed of Yii application development. It provides a great integrity with Eclipse. To install it go to Help > Install New Software menu option but instead of choosing Indigo repository, this time we need to add the update site of Yiiclipse (http://yiiclipse.maziarz.org/updatesite/STABLE/). Please refer to section no. 5 if you don't know how to add new software update site to Eclipse. 

Yiiclipse installation for Eclipse
Yiiclipse installation for Eclipse

That's it! If you completed these steps you have a working Eclipse 3.8 environment with RSE and Yii support. As a next step you can start to set up your remote system connection via the RSE plugin and then create a new remote Yii project or load and existing one.