Full CakePHP 1.2 App Part 4

Full CakePHP 1.2 Application

As the title aptly suggests this is article 4 in my series of creating a full online DVD Catalog Application using CakePHP 1.2. Last time I quickly setup the Types and Locations Controller and Views so that I could start to add more data to the database.

In this article I'm going to create the DVDs controller and View files so that I can start to add / edit / delete DVDs from the database (I've gone for a Tarantino theme to begin with). This is going to be a bit more advanced because I am going to be uploading images to the server and I'm also going to use the jQuery Javascript library to progressively enhance a few forms and to stripe all the tables on the fly.

DVDs Controller and Views

First thing to do as usual is to create the Controller (dvds_controller.php) for the DVD Model along with the related Views for the actions.

<?php

/**
 * Dvds Controller
 *
 * file: /app/controllers/dvds_controller.php
 */
class DvdsController extends AppController {
	// good practice to include the name variable
	var $name = 'Dvds';

	// load any helpers used in the views
	var $helpers = array('Html', 'Form', 'Javascript', 'Misc');

	// global ratings variable
	var $ratings = array('0'=>'0', '1'=>'1', '2'=>'2', '3'=>'3', '4'=>'4', '5'=>'5', '6'=>'6', '7'=>'7', '8'=>'8', '9'=>'9', '10'=>'10');


	/**
	 * index()
	 * main index page for dvds
	 * url: /dvds/index
	 */
	function index() {

	}

	/**
	 * view()
	 * displays a single dvd and all related info
	 * url: /dvds/view/dvd_slug
	 */
	function view($slug) {

	}
	
	/**
	 * admin_index()
	 * main index page for admin users
	 * url: /admin/dvds/index
	 */
	function admin_index() {

	}
	
	/**
	 * admin_add()
	 * allows an admin to add a dvd
	 * url: /admin/dvds/add
	 */
	function admin_add() {
	
	}

	/**
	 * admin_edit()
	 * allows an admin to edit a dvd
	 * url: /admin/dvds/edit/id
	 */
	function admin_edit($id = null) {

	}
	
	/**
	 * admin_delete()
	 * allows an admin to delete a dvd
	 * url: /admin/dvds/delete/1
	 */
	function admin_delete($id = null) {

	}
}

?>

Here are the View files that I need to create:

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

DVD Index

The index() function will be similar to the past ones I've created, retrieving the DVDs from the database that are currently active and also order the DVDs in alphabetical order. CakePHP's find methods are quite advanced and allow you to pass a number of arguments that will filter and sort the data from the database. For a full rundown of the parameters you can pass check out the Models Chapter in the cookbook.

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

function index() {
	// get all dvds from database where status = 1
	$dvds = $this->Dvd->findAll("Dvd.status=1", null, "Dvd.name");

	// save the dvds in a variable for the view
	$this->set('dvds', $dvds);
}

The View is pretty similar to the previous index views with a few additions. I'm going to link to the edit page of the Format, Type and Location and I'm also going to leave out the coding for the striped table this will be accomplished with jQuery which I'll go through later in the article.

// file: /app/views/dvds/admin_index.ctp

<div class="dvds index">

	<h2>Dvds Admin Index</h2>
    <p>Currently displaying all DVDs in the application</p>

    <?php
	// check $dvds variable exists and is not empty
	if(isset($dvds) && !empty($dvds)) :
	?>

    <table>
    	<thead>
        	<tr>
				<th>Name</th>
            	<th>Format</th>
                <th>Type</th>
                <th>Location</th>
				<th>Rating</th>
                <th>Created</th>
				<th>Modified</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
        	<?php foreach($dvds as $dvd): ?>
            <tr>
				<td><?php echo $dvd['Dvd']['name']; ?></td>
				<td><?php echo $html->link($dvd['Format']['name'], array('controller'=> 'formats', 'action'=>'edit', $dvd['Format']['id'])); ?></td>
				<td><?php echo $html->link($dvd['Type']['name'], array('controller'=> 'types', 'action'=>'edit', $dvd['Type']['id'])); ?></td>
				<td><?php echo $html->link($dvd['Location']['name'], array('controller'=> 'locations', 'action'=>'edit', $dvd['Location']['id'])); ?></td>
				<td><?php echo $dvd['Dvd']['rating']; ?></td>
				<td><?php echo $dvd['Dvd']['created']; ?></td>
				<td><?php echo $dvd['Dvd']['modified']; ?></td>
				<td>
				<?php echo $html->link('Edit', array('action'=>'admin_edit', $dvd['Dvd']['id']) );?>
                <?php echo $html->link('Delete', array('action'=>'admin_delete', $dvd['Dvd']['id']), null, sprintf('Are you sure you want to delete Dvd: %s?', $dvd['Dvd']['name']));?>
				</td>
            </tr>
            <?php endforeach; ?>
        </tbody>
    </table>

    <?php
	else:
		echo 'There are currently no DVDs in the database.';
	endif;
	?>
    
    <ul class="actions">
		<li><?php echo $html->link('Add a DVD', array('action'=>'add')); ?></li>
	</ul>
</div>

DVD Admin Add

The admin_add() function will be quite complex so I'll break it down a little and first get the functionality working without a file upload. The main logic will be the same as any add function, I'll create a slug from the name, attempt to save the data and finally redirect the admin with an error message.

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

function admin_add() {
	// if the form data is not empty
	if (!empty($this->data)) {
		// initialise the Dvd model
		$this->Dvd->create();

		// create the slug
		$this->data['Dvd']['slug'] = $this->slug($this->data['Dvd']['name']);
		
		// check for a dvd with the same slug
		$dvd = $this->Dvd->find('first', array(
			'conditions' => array(
				'Dvd.slug'=>$this->data['Dvd']['slug'],
				'Dvd.status' => '1'
			)
		));

		// if slug is not taken
		if(empty($dvd)) {
			// try saving the format
			if ($this->Dvd->save($this->data)) {
				// set a flash message
				$this->Session->setFlash('The DVD has been saved', 'flash_good');

				// redirect
				$this->redirect(array('action'=>'index'));
			} else {
				// set a flash message
				$this->Session->setFlash('The DVD could not be saved. Please, try again.', 'flash_bad');
			}
		} else {
			// set a flash message
			$this->Session->setFlash('The DVD could not be saved. The Name has already been taken.', 'flash_bad');
		}
	}
	
	// find dvd options in a list format
	// new 1.2 feature, can also have 'count' and 'first'
	$formats 	= $this->Dvd->Format->find('list');
	$types 		= $this->Dvd->Type->find('list');
	$locations 	= $this->Dvd->Location->find('list');
	$ratings	= $this->ratings;

	// set the variables so they can be accessed from the view
	$this->set(compact('formats', 'types', 'locations', 'ratings'));
}

In order to select the Format, Type and Location I will need a drop down select box, CakePHP has a few very nice features that will help with creating them. First the find('list') method will automatically create an array that will be used in the form. It seems that the new 1.2 version automatically detects the type of form inputs but just to show how to create one the manual way I've included the code below. The View file will be standard and will use the Form Helper to quickly create the form inputs, CakePHP will also automatically re-fill the form if any errors occur.

// file: /app/views/dvds/admin_add.ctp

<div class="dvds form">

<?php echo $form->create('Dvd');?>
	<fieldset>
 		<legend>Add a Dvd</legend>
		<?php
		// create the form inputs
		echo $form->input('name', array('label'=>'Name: *'));
		echo $form->input('format_id', array('label'=>'Format: *', 'type'=>'select', 'options'=>$formats));
		echo $form->input('type_id', array('label'=>'Type: *', 'class'=>'type_select'));
		echo $form->input('location_id', array('label'=>'Location: *'));
		echo $form->input('rating', array('label'=>'Rating:'));
		echo $form->input('website', array('label'=>'Website URL:'));
		echo $form->input('imdb', array('label'=>'Imdb URL:'));
		echo $form->input('discs', array('label'=>'Number of Discs:', 'class'=>'tv_hide'));
		echo $form->input('episodes', array('label'=>'Number of Episodes:', 'class'=>'tv_hide'));
		?>
	</fieldset>
<?php echo $form->end('Add');?>
</div>

<ul class="actions">
    <li><?php echo $html->link('List DVDs', array('action'=>'index'));?></li>
</ul>

Like before I'm also going to setup a few validation rules so that I cannot add empty data to the database.

// file: /app/models/dvd.php

// setup form validation for dvd
var $validate = array(
	'name' => array(
		'rule' 		=> VALID_NOT_EMPTY,
		'message' 	=> 'Please enter a Dvd Name'
	),
	'format_id' => array(
		'rule'		=> 'numeric'
	),
	'type_id' => array(
		'rule'		=> 'numeric'
	),
	'location_id' => array(
		'rule'		=> 'numeric'
	)
);

Uploading Files

Once the add form is up and running I can now add the file functionality, there is just a few changes I need to make in the View and a bit more logic in the controller.

The first thing I need to do is make sure the form include the enctype attribute so that the server will save the file data, this is easily done by passing a variable in the form create() helper method:

// file: /app/views/dvds/admin_add.ctp

// this
<?php echo $form->create('Dvd', array('type'=>'file'));?>

// instead of
<?php echo $form->create('Dvd');?>

I also need to include the actual file input and this is done again by using the form helper:

// file: /app/views/dvds/admin_add.ctp

echo $form->input('File.image', array('label'=>'Image:', 'type'=>'file'));

I've used the input name File.image so that when the form is submitted the file information is stored in a seperate array to the actual DVD data, this makes it easier to upload the file and also makes sure that the validation is run on the DVD information. The form data looks like this:

Array
(
    [Dvd] => Array
        (
            [name] => testing
            [format_id] => 1
            [...] => ...
        )
    [File] => Array
        (
            [image] => Array
                (
                    [name] => desperado.jpg
                    [type] => image/jpeg
                    [tmp_name] => C:\server\tmp\phpCE.tmp
                    [error] => 0
                    [size] => 44218
                )
        )
)

Now I need to deal with the file upload in the Controller action. I've dealt with file uploads in a previous article of mine so I've copied the upload_files() function to my app_controller.php file.

I've created a private function called _upload_image() in my dvds_controller.php to process and upload the file from the form. First I check that a file has been selected for upload by checking the error variable in the file array, then I use my upload_files() function to try and upload the file to the server. If no errors exist then I can save the url if there are errors then they are saved to be displayed in the view.

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

/**
 * upload_image()
 * private function to upload a file if it exists in the form
 */
function _upload_image() {
	// init
	$image_ok = TRUE;
	
	// if a file has been added
	if($this->data['File']['image']['error'] != 4) {
		// try to upload the file
		$result = $this->upload_files('img/dvds', $this->data['File']);

		// if there are errors
		if(array_key_exists('errors', $result)) {
			// set image ok to false
			$image_ok = FALSE;
			// set the error for the view
			$this->set('errors', $result['errors']);
		} else {
			// save the url
			$this->data['Dvd']['image'] = $result['urls'][0];
		}
	}

return $image_ok;
}

I can then call this method from my admin_add() action to upload the file if it exists. The action now looks like this:

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

function admin_add() {
	// if the form data is not empty
	if (!empty($this->data)) {
		// check for image
		$image_ok = $this->_upload_image();

		// if the image was uploaded successfully
		if($image_ok) {
			// do same logic as before
		}
	}
// do same logic as before
}
Full CakePHP 1.2 Application

To display any file upload error messages I've created a helper that will contain my custom functions that I may need in a View. One such function displays error messages if its passed either a string or an array. Create a new file called misc.php in the /app/views/helpers folder and use this code to create a new function.

<?php
/**
 * MiscHelper Class
 * has a few custom functions that are useful in a view
 *
 * file: /app/views/helpers/misc.php
 */
class MiscHelper extends AppHelper {
	
	/**
	 * display_errors()
	 * displays a list of errors given an array or just a string
	 */
	function display_errors($errors) {
		//init
		$output = '';
		$temp = '';

		// if an array
		if(is_array($errors)) {
			// loop through errors
			foreach($errors as $error) {
				$temp .= "<li>{$error}</li>";
			}
		} else {
			// save error
			$temp .= "<li>{$errors}</li>";
		}

		// build up div
		$output = "<ul class='flash_bad'>{$temp}</ul>";

	return $output;
	}
}
?>

This is just a simple function that will print out any error messages in an unordered list. It can be used by calling the function in a View like this:

// file: /app/views/dvds/admin_add.ctp

// if there was an error uploading the file then display errors here
if(isset($errors)) {
	echo $misc->display_errors($errors);
}

Here's a screenshot of the final add form:

Full CakePHP 1.2 Application

DVD Admin Edit

The admin_edit() function will be very similar to the add function with a few slight differences, first I must check that the id being passed is valid and if not I'll redirect with a flash error message.

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

// if the id is null and the form data empty
if (!$id && empty($this->data)) {
	// set a flash message
	$this->Session->setFlash('Invalid Dvd', 'flash_bad');
	// redirect the admin
	$this->redirect(array('action'=>'index'));
}

This code is standard for an edit function and if you've ever used the bake script to generate your Controller code you will have something similar. Next I'm going to check if the form has been submitted, if it has then I'm going to process the form in the exact same way as the admin_add() function so I can just copy and paste that code here. If the form has not been submitted I can retreive the DVD from the database and save it to the data array.

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

function admin_edit($id = null) {
	// if the id is null and the form data empty
	if (!$id && empty($this->data)) {
		// set a flash message
		$this->Session->setFlash('Invalid Dvd', 'flash_bad');
		// redirect the admin
		$this->redirect(array('action'=>'index'));
	}

	// if the form was submitted
	if (!empty($this->data)) {
		// code from admin_add() goes here
	} else {
		// find the DVD from the database and save it in the data array
		$this->data = $this->Dvd->read(null, $id);
	}
	
	// find dvd options from database in a list
	$formats = $this->Dvd->Format->find('list');
	$types = $this->Dvd->Type->find('list');
	$locations = $this->Dvd->Location->find('list');
	$ratings = $this->ratings;
	$this->set(compact('formats','types','locations', 'ratings'));
}

In the View file for the admin_edit() function I'm going to display the current uploaded image (if one exists) and I also need to include the id of the DVD in the form. This ensures that CakePHP knows that this is an edit form and will change the correct DVD in the database. Here's a snippet of code from the Admin Edit View:

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

// include the id of the DVD as a form input
// CakePHP will automatically create this as a hidden element
echo $form->input('id');

// display image if it exists
if(!empty($this->data['Dvd']['image'])): ?>
<div class="input">
	<label>Current Image:</label>
	<img src="/<?php echo $this->data['Dvd']['image']; ?>" alt="Dvd Image" width="100" />
</div>			
<?php endif;

Here's a screenshot of the Admin Edit View:

Full CakePHP 1.2 Application

DVD Admin Delete

For the delete action I'm going to change the status of the DVD from '1' to '0' in the database, this way I dont actually delete anything and if something goes wrong I can get my data back from the database. This is know as a 'soft delete' and I'm going to use this method throughout the application.

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

// set the id of the dvd
$this->Dvd->id = $id;

// try to change status from 1 to 0
if ($this->Dvd->saveField('status', 0)) {

}

Using jQuery in CakePHP 1.2

I've been using jQuery for some time now and its a great library to use in everyday web development. The first thing to do is download it from the jQuery site. I've downloaded the packed version, which is a smaller compression version which is great for keeping downloads to a minimum.

Place the library in /app/webroot/js and open up the default layout file located at /app/views/layouts/default.ctp. If this file does not exist then you will need to copy the one located at /cake/libs/view/layouts/default.ctp into your layouts folder in your app directory. This just makes sure that your default.ctp is the one that Cake will use.

Next I need to link to the jQuery library if the Javascript Helper is active in a Controller. To create a link I can use the link() function to make the library active in the View. As well as the jquery-1.2.3.pack.js I've created a new blank file called common.js, this will contain my javascript code to run on the page.

// file: /app/views/layouts/default.ctp

// if the javscript helper is set include the javascript library
if(isset($javascript)) {
	echo $javascript->link(array('jquery-1.2.3.pack', 'common'), true);
}

When I want to use Javascript I must now load the Helper in the Controller. I've done this before with the HTML and Form Helpers I just need to add Javascript to the array like this:

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

// load any helpers used in the views
var $helpers = array('Html', 'Form', 'Javascript', 'Misc');

Here I've checked that the Javascript files are being loaded correctly using the Firebug Firefox Extension:

Full CakePHP 1.2 Application

Striping Tables with Javascript

Now that I have jQuery at my disposal I'm first going to use it to stripe the even rows in any table that I assign a class='stripe'. This is very handy because I dont need to hard code this functionality with PHP which makes my code a little cleaner.

Open up the common.js file and enter the code below. The first ready() function is the base of all jQuery code and simply executes the code when the page has fully loaded. To read more about the basics of jQuery check out the documentation on the website. I'm going to target any Table that has a 'stripe' class and select all the even rows. Once they have all been selected I'm going to add a 'altrow' class to the row, once this is done I can then use CSS to style the class with a different colour.

// file: /app/webroot/js/common.js

// when the document is ready
$(document).ready(function(){
	// stripe all the tables in the application
	$('table.stripe tr:even').addClass('altrow');
});

jQuery has extremely powerful 'selector' methods and also uses 'method chaining' to make things easy to select and manipulate. With just one line of code I can stripe any table throughout the application with a single class.

Here is the CSS I've used and below is a screenshot of the striping in action:

// file /app/webroot/css/cake.generic.css

table tr.altrow td {
	background: #ebebeb;
}
Full CakePHP 1.2 Application

Enhancing Forms

Currently the Admin Add Form displays two inputs (Number of Disks and Number of Episodes) that will only be used if the DVD is a 'TV Show'. So I'm going to use Javascript to hide the inputs if the DVD is a 'Film' and show the inputs if 'TV Show' is selected in the drop down menu.

To target these two form inputs I've added a 'tv_hide' class when creating the form like so:

// file: /app/views/dvds/admin_add.ctp

// add a class to the form input
echo $form->input('discs', array('label'=>'Number of Discs:', 'class'=>'tv_hide'));
echo $form->input('episodes', array('label'=>'Number of Episodes:', 'class'=>'tv_hide'));

This will produce the following HTML code:

<div class="input" style="display: block;">
	<label for="DvdDiscs">Number of Discs:</label>
	<input type="text" id="DvdDiscs" value="" maxlength="4" class="tv_hide" name="data[Dvd][discs]"/>
</div>

Now I need to attach a Javascript event handler to the 'Type' drop down input that will find out what type has been selected and will show or hide the form inputs accordingly.

// file: /app/webroot/js/common.js

// add event handler to type select form input
$('.type_select').change(function(){
	// get the value of the selected option
	var type = $(this).find('option:selected').text();

	// log the type for testing purposes
	console.log( type );

	// if the type is a tv show
	if(type == "TV Show") {
		// fadein the form inputs
		$('.tv_hide').parent().fadeIn();
	} else {
		// fade out and hide the form inputs
		$('.tv_hide').parent().fadeOut();
	}
});

Because I've added a class to the form input and not the surrounding div I make use of the .parent() method to select the correct div. This example also shows method chaining in action.

Its not quite finished yet because I need to accomodate for the Admin Edit Form and make sure that the form inputs are already displayed or hidden depending on the 'Type' the edited DVD has. To do this I use the same techniques as before and get the selected DVD type and if it doesn't equal 'TV Show' then I hide the form inputs with a 'tv_hide' class.

// file: /app/webroot/js/common.js

// get value of selected type
var current_type = $('.type_select option:selected').text();

// if the selected option is not 'TV Show' then hide the tv options
if(current_type != "TV Show") {
	// hide the tv elements from form
	$('.tv_hide').parent().hide();
}

Wrapping Up

I've managed to cover quite a lot of ground in this article, I've setup yet another Controller and related View files (a familiar process by now I hope) and included the ability to upload files to the server and save the URL to the database.

I've also covered including jQuery in the application and using some Javascript voodoo to stripe tables and enhance forms by showing and hiding inputs depending on a selected DVD Type. If you do have any problems with anything I've covered let me know and I'll try to sort you out.

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'll be dealing with Genres and this will include setting up the Controller and View files and delving into how CakePHP deals with the hasAndBelongsToMany relationship between DVDs and Genres. At the end of the article you will be able to add / edit / delete Genres and also assign DVDs to multiple genres so that when a Genre is viewed all the DVDs will be retrieved and displayed.

Posted on 9th May 2008
9 years, 7 months, 1 week, 2 days ago

comments powered by Disqus