Full CakePHP 1.2 App 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.

// file: /app/config/routes.php

// from this
Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
// to this
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.

// file: app/controllers/dvds_controller.php

// get all options for form
$formats = $this->Dvd->Format->find('list', array(
	'fields' => 'id, name', 
	'order'=>'Format.name', 
	'conditions'=> array(
		'Format.status'=>'1'
	)
));
$types = $this->Dvd->Type->find('list', array(
	'fields'=>'id, name', 
	'order'=>'Type.name', 
	'conditions'=> array(
		'Type.status'=>'1'
	)
));
$locations = $this->Dvd->Location->find('list', array(
	'fields'=>'id, name',
	'order'=>'Location.name', 
	'conditions'=> array(
		'Location.status'=>'1'
	)
));
$genres = $this->Dvd->Genre->find('list', array(
	'fields'=>'id, name', 
	'order'=>'Genre.name',
	'conditions'=> array(
		'Genre.status'=>'1'
	)
));

// add name to option
$formats 	= array(''=>'Formats') + $formats;
$types	 	= array(''=>'Types') + $types;
$locations 	= array(''=>'Locations') + $locations;
$genres 	= array(''=>'Genres') + $genres;

// save the dvds in a variable for the view
$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.

// file: app/views/elements/index_header.ctp

<?php echo $form->input('format', array(
	'label'		=> '', 
	'type'		=> 'select', 
	'options'	=> $formats,
	'selected'	=> $this->data['format']
	)); ?>

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.

// file: app/controllers/dvds_controller.php

// if form submitted
if (!empty($this->data)) {

} else {
	// set form options
	$this->data['format'] = '';
	$this->data['type'] = '';
	$this->data['location'] = '';
	$this->data['genre'] = '';
	$this->data['search'] = '';
}

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.

// file: app/controllers/dvds_controller.php

// if form submitted
if (!empty($this->data)) {
	// if reset button pressed redirect to index page
	if(isset($this->data['reset'])) {
		$this->redirect(array('action'=>'index'));
	}

	// init
	$url = '';

	// remove search key if not set
	if($this->data['search'] == '') {
		unset($this->data['search']);
	}

	// loop through filters
	foreach($this->data as $key=>$filter) {
		// ignore submit button
		if($key != 'filter') {
			// init
			$selected = '';

			switch($key) {
				case 'format':
					$selected = $formats[$filter];
				break;
				case 'type':
					$selected = $types[$filter];
				break;
				case 'location':
					$selected = $locations[$filter];
				break;
				case 'genre':
					$selected = $genres[$filter];
				break;
				case 'search':
					$selected = $filter;
				break;
			}
			// if filter value is not empty
			if(!empty($filter)) {
				$selected = $this->slug($selected);
				$url .= "/$key/$selected";
			}
		}
	}

	// redirect
	$this->redirect('/dvds/index/'.$url);
} else {

}

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.

// file: app/controllers/dvds_controller.php

// if any parameters have been passed
if(!empty($this->params['pass'])) {
	// only select active dvds
	$conditions = array('Dvd.status'=>1);

	// get params
	$params = $this->params['pass'];
	// loop
	foreach($params as $key=>$param) {
		// get the filter value
		if(isset($params[$key+1])) {
			$value = $params[$key+1];
		}
		
		// switch based on filter is use
		switch($param)
		{
			case 'format':
				// get format
				$format = $this->Dvd->Format->find('first', array(
					'recursive' => 0,
					'conditions' => array(
						'Format.slug'=>$value
					)
				));
				// set where clause
				$conditions['Dvd.format_id'] = $format['Format']['id'];
				// save value for form
				$this->data['format'] = $format['Format']['id'];
			break;
			case 'type':
			break;
			case 'location':
			break;
			case 'genre':
			break;
			case 'search':
			
			break;
		}
		
		// get all dvds with param conditions
		$dvds = $this->Dvd->find('all', array(
			'order'	=> 'Dvd.name',
			'conditions' => $conditions
		));
	}
}

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.

// file: app/controllers/dvds_controller.php

// get the selected genre from thr database
case 'genre':
	// get genre
	$genre = $this->Dvd->Genre->find('first', array(
		'recursive' => 0,
		'conditions' => array(
			'Genre.slug'=>$value
		)
	));
	// save value for form
	$this->data['genre'] = $genre['Genre']['id'];
break;

// get all dvds with param conditions
$dvds = $this->Dvd->find('all', array(
	'order'	=> 'Dvd.name',
	'conditions' => $conditions
));
			
// if genre filter has been set loop through all dvds and remove if the genre
// doesn't the one we have selected in the filter array
if(isset($genre)) {
	// loop through dvds
	foreach($dvds as $key=>$dvd) {
		// init
		$found = FALSE;
		// loop through genres
		foreach($dvd['Genre'] as $k=>$g) {
			// if the genre id matches the filter genre no need to continue
			if($g['id'] == $genre['Genre']['id']) {
				$found = TRUE;
				break;
			}
		}

		// if the genre was not found in dvds
		if(!$found) {
			// remove from list
			unset($dvds[$key]);
		}
	}
}

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! :)

Next Article

In my next article I'm finally going to finish the Footer of the main page and code up a few more tweaks to the site. This will include an image processing script that will automatically resize images so they are reduced in filesize and will display nice and crisp.

Posted on 8th July 2008
8 years, 11 months, 2 weeks, 5 days ago

comments powered by Disqus