Full CakePHP 1.2 App Part 5

DVD Catalog CakePHP Application

Welcome to the 5th Article in the series about creating a full CakePHP 1.2 Application, last time I setup the DVD Controller and View files and I'm now able to start adding DVDs to the system. As a side note I've put the application online at dvd.jamesfairhurst.co.uk so you can have a poke around and see how the application is shaping up.

In this Article I'm going to be dealing with the Genres section of the application so I can start to add / edit / delete Genres from the database. The relationship between DVDs and Genres is a HasAndBelongsToMany (HABTM) which means that a single DVD can have multiple Genres and a Genre can have multiple DVDs. I'm going to be looking at how CakePHP deals with this type of association and how to manually create these associations when saving related data.

Genres

As well as adding, editing and deleting Genres I'm also going to allow the DVDs Controller to add a Genre if it doesn't already exist. I first saw this type of functionality in Wordpress where you can create tags on the fly by inputting all the tags separated by a comma e.g "tag 1, tag 2, tag 3". I quite like this little feature so I'm going to implement it in my admin_add and admin_edit actions in the dvds_controller so that I can simply type all the Genres in a form input and have the controller process all the information and add new ones if required.

Create the genres_controller.php with the skeleton actions along with the following view files:

  • /app/views/genres/index.ctp
  • /app/views/genres/view.ctp
  • /app/views/genres/admin_index.ctp
  • /app/views/genres/admin_add.ctp
  • /app/views/genres/admin_edit.ctp

Genres Index

DVD Catalog CakePHP Application

The index() and admin_index() actions are going to include the same functionality as the previous index() actions that I've created. I'm going to retrieve the Genres from the database (where the status equals and order by the Genre name), save them for the view and finally display the genres in a table.

// file: /app/controllers/genres_controller.php

function index() {
	// dont get related info
	$this->Genre->recursive = 0;
	// get genres from db and save for view
	$this->set('genres', $this->Genre->findAll("Genre.status=1", null, "Genre.name"));
}

Genres View

DVD Catalog CakePHP Application

The view() action will attempt to find the Genre in the database given the Genre's slug, if one is found then the Genre is saved for the View. As you can see I've also listed the DVDs that has been assigned to that Genre, in the final application I'm not going to display them in a simple table.

// file: /app/controllers/genres_controller.php

// standard error checking has been removed
function view($slug = null) {
	// find genre in database
	$genre = $this->Genre->findBySlug($slug);

	// if genre has been found
	if(!empty($genre)) {
		// set the genre for the view
		$this->set('genre', $genre);
	}
}

Adding Genres

DVD Catalog CakePHP Application

The admin_add() action will first check that a Genre with the same name doesn't already exist and then add the Genre to the database. This is a standard add action so I'm not going to go through the process again, if your stuck then go back through my other articles.

Editing Genres

DVD Catalog CakePHP Application

The admin_edit() action will redisplay the Genre in a form and will allow the admin the change the Genre. Again the Controller will check that the new Genre name is not already taken in the database. I'm also going to display a list of all the DVDs that have been assigned to the Genre with a link to edit it. The DVDs are automatically fetched from the database when getting the Genre so I can loop through them like this:

// file: /app/views/genres/admin_edit.ctp

<?php foreach($this->data['Dvd'] as $dvd): ?>
<tr>
	<td><?php echo $dvd['name']; ?></td>
	<td><?php echo $html->link('Edit', array('action'=>'admin_edit','controller'=>'dvds', $dvd['id']) );?></td>
</tr>
<?php endforeach; ?>

DVDs and Genres

As I mentioned previously the DVD-Genre association is a hasAndBelongsToMany (HABTM) so if your new to this I suggest looking in the cookbook at the related chapter just so you have an idea of whats involved. I'm going to modify the admin_add() and admin_edit() actions of the DVDs Controller and related Views so that I can insert new Genres when creating a new DVD.

The only thing that needs adding to the View files is a new form input:

echo $form->input('genres', array('label'=>'Genres:', 'type'=>'text'));

CakePHP has a great way of dealing with and saving HABTM associations, and its all handled behind the scenes when a Model->save() is called. That is if your data has been formatted correctly.

// sample $this->data array
Array
(
    [Dvd] => Array
        (
            [format_id] => 1
            [type_id] => 1
            [location_id] => 1
            [name] => aaa
			[...] => ...
        )
    [Genre] => Array
        (
            [Genre] => Array
                (
                    [0] => 1
                    [1] => 2
                )
        )
)

Here the Genres which have an id of '1' and '2' will be associated with the newly created DVD.

In the admin_add() action of the DVDs controller I'm going to create a new private function that will process the Genres from the form, if the Genre doesn't exist create it and finally create the associations between the DVD and the Genres.

In the DVDs Controller I'm going to create a new private function that will take the Genres from the Add DVD form, split each Genre at a comma sign, check to see if the Genre exists in the database. If it doesn't then create a new one and save the insert id, if one already exists grab the id of the Genre. Finally the function will return an array of Genre Id's that will be used to associate the Genre to the DVD.

// file: /app/controllers/dvds_controller.php

/**
 * _parse_genres()
 * will parse a string of genres and get the id from the db,
 * if genre doesn't exist this function will create it
 */
function _parse_genres($genres = null) {
	// variable to save genre data array
	$data = array();

	// explode the genres sepearated by a comma
	$explode = explode(',', $genres);

	// if the explode array is not empty
	if(!empty($explode)) {
		// loop through exploded genres
		foreach($explode as $genre) {
			// remove leading/trailing spaces
			$genre = trim($genre);

			// if the genre is not empty
			if(!empty($genre)) {
				// find the genre in the db
				$db_genre = $this->Dvd->Genre->find('first', array(
					'conditions' => array(
						'Genre.name' 	=> $genre,
						'Genre.status'	=> '1'
					)
				));

				// if a genre was found
				if(!empty($db_genre)) {
					// save the genre id
					$data[] = $db_genre['Genre']['id'];
				} else {
					// create a new genre
					$save = array('Genre'=>array(
						'name'	=> $genre,
						'slug'	=> $this->slug($genre),
						'status'=> 1
					));
					
					// create model
					// has to be done when adding multiple items in a row
					$this->Dvd->Genre->create();

					// save the new genre
					$saveOK = $this->Dvd->Genre->save($save);

					// if save was successful
					if($saveOK) {
						// last insert id
						$data[] = $this->Dvd->Genre->getLastInsertID();
					}
				}
			}
		}
	}

return array('Genre' => $data);
}

// will return an array like this
Array
(
    [Genre] => Array
        (
            [0] => 23
            [1] => 24
            [2] => 25
        )
)

I can then use this function in my admin_add() and admin_edit() actions to add the Genre information to the form data array just before it is saved to the database:

// file: /app/controllers/dvds_controller.php

// save the genres to the form data array
$this->data['Genre'] = $this->_parse_genres($this->data['Dvd']['genres']);

// try saving the dvd
if($this->Dvd->save($this->data)) {
// set flash and redirect
}

I've also created another small private function to take an array of Genres from the database and return a string of genres seperated by comma's. This will be used in the view() and admin_edit() actions to display the Genres.

// file: /app/controllers/dvds_controller.php

/**
 * _create_genre_string()
 * will create a string of genres seperated by a single comma e.g. genre1, genre2, genre3, 
 */
function _create_genre_string($genres) {
	// init
	$genre_string = '';

	// if genres array not empty
	if(!empty($genres)) {
		// loop through genres and save name as a string
		foreach($genres as $g) {
			$genre_string .= $g['name'].", ";
		}
	}

return $genre_string;
}

Here's a screenshot of the Edit DVD page with the Genre's at the bottom:

DVD Catalog CakePHP Application

Deleting Genres

When deleting a Genre from the database I must also remove the association between any DVDs that may be attached. This is problematic because I've chosen to use a "soft delete" approach when deleting items from the database.

What I'm going to do instead is change the status of the Genre to '0' meaning it is not active and then manually delete the association using a custom SQL statement. This is quite simple as I just need to delete everything with the Id of the Genre that is being deleted.

// file: /app/controllers/genres_controller.php

/**
 * admin_delete()
 * allow an admin to delete a genre
 * url: admin/genres/delete/1
 */
function admin_delete($id = null) {
	// check genre is valid and exists
	$genre = $this->_check_genre($id);

	// set the id of the genre
	$this->Genre->id = $id;

	// try to change status from 1 to 0
	if ($this->Genre->saveField('status', 0)) {
		// delete the genre-dvd association from the join table
		// create the sql statement to remove association
		$sql = "DELETE FROM `dvds_genres` WHERE genre_id={$id}";

		// run the sql query
		$this->Genre->query($sql);

		// set flash message
		$this->Session->setFlash('The Genre was successfully deleted.', 'flash_good');
	} else {
		// set flash message
		$this->Session->setFlash('The Genre could not be deleted. Please try again.', 'flash_bad');
	}

	// redirect
	$this->redirect(array('action'=>'index'));
}

Wrapping Up

The application is finally taking shape with the addition of Genres, which can be added in both the Genres Controller as well as the DVDs Controller. The Has And Belongs To Many association is quite complex and difficult to get your head around if your new but keeping reading the manual and experiment with it in your applications. If you have any problems with anything in the series then drop me an email.

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 the next article I'm going to lock down all my admin actions so that you will need a username and password to add, edit and delete anything in the application. To do this I'm going to setup a new username and password in the database and use the beforeFilter() to check that a user is logged in before giving access to any admin functions.

Posted on 16th May 2008
6 years, 7 months, 3 days ago

comments powered by Disqus