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.
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) {
- }
- }
- ?>
- /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);
- }
- // 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'));
- }
- // 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>
- // 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');?>
- // file: /app/views/dvds/admin_add.ctp
- echo $form->input('File.image', array('label'=>'Image:', 'type'=>'file'));
- 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
- )
- )
- )
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;
- }
- // 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
- }
- <?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;
- }
- }
- ?>
- // 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);
- }
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'));
- }
- // 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'));
- }
- // 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;
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);
- }
- // file: /app/controllers/dvds_controller.php
- // load any helpers used in the views
- var $helpers = array('Html', 'Form', 'Javascript', 'Misc');
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');
- });
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;
- }
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'));
- <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>
- // 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();
- }
- });
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.