Full CakePHP Application Part 10

Welcome to the 10th article in my series about creating a full CakePHP application from start to finish, cant believe I've managed to fill this much space with a single application but its still going and finally taking some shape and its looking pretty, pretty, pretty good (Curb your Enthusiasm Reference - Great Show).
In this article I'm going to be finishing off the header and footer of the main DVD page by adding some functionality to the form and creating some re-usable elements which grabs data from the database. I'm also going to be cleaning a few things up as and when I come across them.

Upgrading CakePHP

There have been quite a few updates to CakePHP since starting this application so its about time to upgrade. Download the latest version from the main website and overwrite the cake directory with the latest version.
That should be it, I've had a quick check to make sure that everything is working and I didn't come across any errors. Upgrading most frameworks is a very simple process but you must be careful and check that your application doesn't throw any errors or that anything being used was not depreciated.

Defining a Default Start Page

When you first load up a CakePHP application you will be met with a standard page but with most (if not all) applications you develop you will want to change the default starting page. You can do this quite easily by modifying the /app/config/routes.php file, but first have a quick read of the Routes Chapter in the Cookbook.
You can do some pretty advanced stuff with the routes file but it boils down to re-mapping urls to certain Controllers and Actions. In my case I want the default Controller/Action to be /dvds/index, so when the application loads via http://dvdcatalog/ my index() action in the dvds_controller.php will be run.
To do this I need to comment out the first "Router" line and create a new one, this will tell CakePHP to run the appropriate controller action depending on the url being requested. In this case the url will be the very top level e.g. / and will be re-routed to the dvds controller.
  1. // file: /app/config/routes.php  
  2.   
  3. // from this  
  4. Router::connect('/'array('controller' => 'pages''action' => 'display''home'));  
  5. // to this  
  6. Router::connect('/'array('controller' => 'dvds''action' => 'index'));  

Header

Ok so its finally time to implement some functionality into the header form, this is going to quite complex but I'll break it down so that things make sense. I want to be able to filter the dvds by all the options in the form e.g. by Type and Genre and i'm going to make the searches bookmarkable by creating a url to show all the filters being used.
First I need to get all the options from the database in list format ready to be used in a form select. CakePHP has this covered by using the $this->find('list') method. I'm going to add a few conditions so the options are in alphabetical order and have not been deleted. I then need to pass these to the view using the $this->set() method.
  1. // file: app/controllers/dvds_controller.php  
  2.   
  3. // get all options for form  
  4. $formats = $this->Dvd->Format->find('list'array(  
  5.     'fields' => 'id, name',   
  6.     'order'=>'Format.name',   
  7.     'conditions'=> array(  
  8.         'Format.status'=>'1'  
  9.     )  
  10. ));  
  11. $types = $this->Dvd->Type->find('list'array(  
  12.     'fields'=>'id, name',   
  13.     'order'=>'Type.name',   
  14.     'conditions'=> array(  
  15.         'Type.status'=>'1'  
  16.     )  
  17. ));  
  18. $locations = $this->Dvd->Location->find('list'array(  
  19.     'fields'=>'id, name',  
  20.     'order'=>'Location.name',   
  21.     'conditions'=> array(  
  22.         'Location.status'=>'1'  
  23.     )  
  24. ));  
  25. $genres = $this->Dvd->Genre->find('list'array(  
  26.     'fields'=>'id, name',   
  27.     'order'=>'Genre.name',  
  28.     'conditions'=> array(  
  29.         'Genre.status'=>'1'  
  30.     )  
  31. ));  
  32.   
  33. // add name to option  
  34. $formats    = array(''=>'Formats') + $formats;  
  35. $types      = array(''=>'Types') + $types;  
  36. $locations  = array(''=>'Locations') + $locations;  
  37. $genres     = array(''=>'Genres') + $genres;  
  38.   
  39. // save the dvds in a variable for the view  
  40. $this->set(compact('formats''types''locations''genres''dvds'));  
Just a quick aside about the compact() function, this is a standard PHP function and will try to find any variable being passed and creates an array with the variable as a key. This is great for passing multiple variables to the view.
Now that we have our variables in the view I need to use the form helper to create the select boxes. Using the $form->input() we can pass in the options from the controller and it will automatically create the drop down form element.
  1. // file: app/views/elements/index_header.ctp  
  2.   
  3. <?php echo $form->input('format'array(  
  4.     'label'     => '',   
  5.     'type'      => 'select',   
  6.     'options'   => $formats,  
  7.     'selected'  => $this->data['format']  
  8.     )); ?>  
In order to save the selected form data and redisplay it once the form has been submitted I've used the selected option, this means that I need to set the variables in the $this->data array if the form has not been submitted to prevent any errors.
  1. // file: app/controllers/dvds_controller.php  
  2.   
  3. // if form submitted  
  4. if (!empty($this->data)) {  
  5.   
  6. else {  
  7.     // set form options  
  8.     $this->data['format'] = '';  
  9.     $this->data['type'] = '';  
  10.     $this->data['location'] = '';  
  11.     $this->data['genre'] = '';  
  12.     $this->data['search'] = '';  
  13. }  
Once the form has been submitted I'm going to loop through all the filter options and build up a url which I can use to save the filter terms. The url will consist of /filter_term/selected_option and if more than one filter is selcted they will be added to the url string like this /format/dvd/genre/action. Once complete I'm going to redirect the page using the url so that the filter terms can be saved.
  1. // file: app/controllers/dvds_controller.php  
  2.   
  3. // if form submitted  
  4. if (!empty($this->data)) {  
  5.     // if reset button pressed redirect to index page  
  6.     if(isset($this->data['reset'])) {  
  7.         $this->redirect(array('action'=>'index'));  
  8.     }  
  9.   
  10.     // init  
  11.     $url = '';  
  12.   
  13.     // remove search key if not set  
  14.     if($this->data['search'] == '') {  
  15.         unset($this->data['search']);  
  16.     }  
  17.   
  18.     // loop through filters  
  19.     foreach($this->data as $key=>$filter) {  
  20.         // ignore submit button  
  21.         if($key != 'filter') {  
  22.             // init  
  23.             $selected = '';  
  24.   
  25.             switch($key) {  
  26.                 case 'format':  
  27.                     $selected = $formats[$filter];  
  28.                 break;  
  29.                 case 'type':  
  30.                     $selected = $types[$filter];  
  31.                 break;  
  32.                 case 'location':  
  33.                     $selected = $locations[$filter];  
  34.                 break;  
  35.                 case 'genre':  
  36.                     $selected = $genres[$filter];  
  37.                 break;  
  38.                 case 'search':  
  39.                     $selected = $filter;  
  40.                 break;  
  41.             }  
  42.             // if filter value is not empty  
  43.             if(!empty($filter)) {  
  44.                 $selected = $this->slug($selected);  
  45.                 $url .= "/$key/$selected";  
  46.             }  
  47.         }  
  48.     }  
  49.   
  50.     // redirect  
  51.     $this->redirect('/dvds/index/'.$url);  
  52. else {  
  53.   
  54. }  
Now that the filters have been set I need to create some more logic in the index action that will parse the filters and create some SQL conditions that will be used when retrieving Dvds from the database.
If any filters are active in the URL they will be stored in the $this->params['pass'] array so I need to check that this is not empty. Then I'm going to loop through the filters, grab the selected value by getting the next item in the array and then perform a switch statement based on the active filter.
In the example below I've done the format example, I've retrieved the Format from the database based on the slug in the filter and then created a SQL where statement to select only the formats in the filter. Once all the SQL has been created in the $conditions array all the Dvds are found in the db based on said conditions.
  1. // file: app/controllers/dvds_controller.php  
  2.   
  3. // if any parameters have been passed  
  4. if(!empty($this->params['pass'])) {  
  5.     // only select active dvds  
  6.     $conditions = array('Dvd.status'=>1);  
  7.   
  8.     // get params  
  9.     $params = $this->params['pass'];  
  10.     // loop  
  11.     foreach($params as $key=>$param) {  
  12.         // get the filter value  
  13.         if(isset($params[$key+1])) {  
  14.             $value = $params[$key+1];  
  15.         }  
  16.           
  17.         // switch based on filter is use  
  18.         switch($param)  
  19.         {  
  20.             case 'format':  
  21.                 // get format  
  22.                 $format = $this->Dvd->Format->find('first'array(  
  23.                     'recursive' => 0,  
  24.                     'conditions' => array(  
  25.                         'Format.slug'=>$value  
  26.                     )  
  27.                 ));  
  28.                 // set where clause  
  29.                 $conditions['Dvd.format_id'] = $format['Format']['id'];  
  30.                 // save value for form  
  31.                 $this->data['format'] = $format['Format']['id'];  
  32.             break;  
  33.             case 'type':  
  34.             break;  
  35.             case 'location':  
  36.             break;  
  37.             case 'genre':  
  38.             break;  
  39.             case 'search':  
  40.               
  41.             break;  
  42.         }  
  43.           
  44.         // get all dvds with param conditions  
  45.         $dvds = $this->Dvd->find('all'array(  
  46.             'order' => 'Dvd.name',  
  47.             'conditions' => $conditions  
  48.         ));  
  49.     }  
  50. }  
Creating these conditions are quite simple for formats, types, locations, and searches because all the information is stored in the dvds table however this is not the case for Genres because this is a HABTM association and I'm unable to filter Dvds using SQL alone.
To filter by Genre I'm going to retrieve the Genre from the database and save it for later. Once all the Dvds have been found I need a little processing to remove the Dvds that dont have the Genre selected in the Filter. This seems to be a long winded way of doing things and your right, but SQL does have its limitations and when using a HABTM association its very difficult and often impossible to use the where clause to filter your selection.
  1. // file: app/controllers/dvds_controller.php  
  2.   
  3. // get the selected genre from thr database  
  4. case 'genre':  
  5.     // get genre  
  6.     $genre = $this->Dvd->Genre->find('first'array(  
  7.         'recursive' => 0,  
  8.         'conditions' => array(  
  9.             'Genre.slug'=>$value  
  10.         )  
  11.     ));  
  12.     // save value for form  
  13.     $this->data['genre'] = $genre['Genre']['id'];  
  14. break;  
  15.   
  16. // get all dvds with param conditions  
  17. $dvds = $this->Dvd->find('all'array(  
  18.     'order' => 'Dvd.name',  
  19.     'conditions' => $conditions  
  20. ));  
  21.               
  22. // if genre filter has been set loop through all dvds and remove if the genre  
  23. // doesn't the one we have selected in the filter array  
  24. if(isset($genre)) {  
  25.     // loop through dvds  
  26.     foreach($dvds as $key=>$dvd) {  
  27.         // init  
  28.         $found = FALSE;  
  29.         // loop through genres  
  30.         foreach($dvd['Genre'as $k=>$g) {  
  31.             // if the genre id matches the filter genre no need to continue  
  32.             if($g['id'] == $genre['Genre']['id']) {  
  33.                 $found = TRUE;  
  34.                 break;  
  35.             }  
  36.         }  
  37.   
  38.         // if the genre was not found in dvds  
  39.         if(!$found) {  
  40.             // remove from list  
  41.             unset($dvds[$key]);  
  42.         }  
  43.     }  
  44. }  

Wrapping Up

Phew that was quite an article, if your still following what I'm doing here and you need any help then let me know via the comments or email and I'll do my best to help. I've finally got the filters working which include a fully bookmarkable system for saving your searches.

Source Code

The source code for this article can be downloaded using this link. If these articles are helping you out why not consider donating I can always use a beer! :)

This entry was posted in . Bookmark the permalink.

Leave a Reply