CakePHP Contact Form with File Uploads

One of my most popular articles is howto upload files and images using CakePHP so I thought it would be a good idea to give it a little update using the latest version which is currently 2.2.5 and will be workable for the whole 2.x family.

I'll also combine the file uploading with a simple contact form which is a very common element in almost all websites and will hopefully provide you with a solid foundation to build upon in your own websites.

Database Setup

OK the first thing I'm going to do it create a new database with a table for our contact form submissions, nothing fancy just some message details and a location to the uploaded file (if supplied).

CREATE DATABASE `cakephp_2_2_5`;

USE `cakephp_2_2_5`;

CREATE TABLE IF NOT EXISTS `contacts` (
  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(255) DEFAULT NULL,  
  `email` VARCHAR(255) DEFAULT NULL,
  `message` TEXT DEFAULT NULL,
  `filename` VARCHAR(255) DEFAULT NULL,
  `created` DATETIME DEFAULT NULL,
  `modified` DATETIME DEFAULT NULL
);

Next rename and edit the database.php.default to database.php and enter in the database name along with your username and password:

public $default = array(
	'datasource' => 'Database/Mysql',
	'persistent' => false,
	'host' => 'localhost',
	'login' => 'cakephp225',
	'password' => 'cakephp225',
	'database' => 'cakephp_2_2_5',
	'prefix' => '',
	//'encoding' => 'utf8',
);

CakePHP Setup

I usually find the best way of building an app in CakePHP is to try out what our URLs will look like and then follow the instructions given to us by the system. In this case we want our contact page to be found when going to /contact in the URL.

It's telling us a ContactController could not be found so go ahead and create the app\Controller\ContactController.php file:

<?php
class ContactController extends AppController {

}

Refresh and it'll now tell you that the index is missing, again go ahead and create that:

<?php
class ContactController extends AppController {
	public function index() {

	}
}

Refresh again and it'll now complain about the missing index View, create the app\View\Contact\index.ctp View file and refresh once more to see an empty page.

Contact View

We're going to begin by building the contact form, I'm going to have a name, email, message and file fields:

<h2>Contact</h2>
<?php 
echo $this->Form->create('Contact', array('type'=>'file'));
echo $this->Form->input('name');
echo $this->Form->input('email');
echo $this->Form->input('message');
echo $this->Form->input('filename',array('type' => 'file'));
echo $this->Form->end('Submit');
?>

Quite simple and the only thing to be aware of is the extra 'type' option when creating the form to allow file uploads. Something I always forget!

Contact Model

Next up is the Contact Model which will contain our validation rules and logic for processing the uploaded file. First we're going to add some simple validation rules for our other fields to ensure they're all required and the email address is valid. I'm then going to supply multiple rules for our filename and you can see these below:

// app/Model/Contact.php
class Contact extends AppModel {
	public $name = 'Contact';
	
	public $validate = array(
		'name' => 'notEmpty',
        'email' => array(
			'rule' => 'email',
			'message' => 'Please enter a valid Email Address'
		),
		'message' => 'notEmpty',
		'filename' => array(
			// http://book.cakephp.org/2.0/en/models/data-validation.html#Validation::uploadError
			'uploadError' => array(
				'rule' => 'uploadError',
				'message' => 'Something went wrong with the file upload',
				'required' => FALSE,
				'allowEmpty' => TRUE,
			),
			// http://book.cakephp.org/2.0/en/models/data-validation.html#Validation::mimeType
			'mimeType' => array(
				'rule' => array('mimeType', array('image/gif','image/png','image/jpg','image/jpeg')),
				'message' => 'Invalid file, only images allowed',
				'required' => FALSE,
				'allowEmpty' => TRUE,
			),
			// custom callback to deal with the file upload
			'processUpload' => array(
				'rule' => 'processUpload',
				'message' => 'Something went wrong processing your file',
				'required' => FALSE,
				'allowEmpty' => TRUE,
				'last' => TRUE,
			)
		)
    );
}

The uploadError is a core validation rule and checks that the file uploaded ok, the mimeType is also a core rule and here we can specify the type of files that can be uploaded. In this example I'm only allowing images although you could simply remove the rule for all files or change it to only allow Word docs or PDFs for example. The final rule processUpload is a custom rule which corresponds to a function that we're going to create.

The method will check the file has been uploaded, build the filename according to the $uploadDir and file name, it will then move the file to our uploads directory and if successful it will replace the 'filepath' upload data with a string of the path relative to the WWW_ROOT file which is by default app/webroot

As we're actually saving the filepath instead of the posted form data we'll need some extra logic to inject the filepath into our form data, todo this I've added some beforeSave logic to check if 'filepath' information exists and if so replace the 'filename' with the path to the file form the WWW_ROOT.

/**
 * Upload Directory relative to WWW_ROOT
 * @param string
 */
public $uploadDir = 'uploads';

/**
 * Process the Upload
 * @param array $check
 * @return boolean
 */
public function processUpload($check=array()) {
	// deal with uploaded file
	if (!empty($check['filename']['tmp_name'])) {

		// check file is uploaded
		if (!is_uploaded_file($check['filename']['tmp_name'])) {
			return FALSE;
		}

		// build full filename
		$filename = WWW_ROOT . $this->uploadDir . DS . Inflector::slug(pathinfo($check['filename']['name'], PATHINFO_FILENAME)).'.'.pathinfo($check['filename']['name'], PATHINFO_EXTENSION);

		// @todo check for duplicate filename

		// try moving file
		if (!move_uploaded_file($check['filename']['tmp_name'], $filename)) {
			return FALSE;

		// file successfully uploaded
		} else {
			// save the file path relative from WWW_ROOT e.g. uploads/example_filename.jpg
			$this->data[$this->alias]['filepath'] = str_replace(DS, "/", str_replace(WWW_ROOT, "", $filename) );
		}
	}

	return TRUE;
}

/**
 * Before Save Callback
 * @param array $options
 * @return boolean
 */
public function beforeSave($options = array()) {
	// a file has been uploaded so grab the filepath
	if (!empty($this->data[$this->alias]['filepath'])) {
		$this->data[$this->alias]['filename'] = $this->data[$this->alias]['filepath'];
	}
	
	return parent::beforeSave($options);
}

As our file is optional currently the form will display an error if no file is selected, to fix this I'm going to add some logic in the beforeValidate filter that will unset the filename form field when no files have been selected.

/**
 * Before Validation
 * @param array $options
 * @return boolean
 */
public function beforeValidate($options = array()) {
	// ignore empty file - causes issues with form validation when file is empty and optional
	if (!empty($this->data[$this->alias]['filename']['error']) && $this->data[$this->alias]['filename']['error']==4 && $this->data[$this->alias]['filename']['size']==0) {
		unset($this->data[$this->alias]['filename']);
	}

	parent::beforeValidate($options);
}

Contact Controller

Next up is the actual logic to deal with the form submission, fairly simple and straight forward if you're familiar with CakePHP:

<?php
class ContactController extends AppController {

	/**
	 * Main index action
	 */
	public function index() {
		// form posted
		if ($this->request->is('post')) {
			// create
			$this->Contact->create();

			// attempt to save
			if ($this->Contact->save($this->request->data)) {
				$this->Session->setFlash('Your message has been submitted');
				$this->redirect(array('action' => 'index'));
			}
		}
	}
}

Test the form with a file upload and if things have gone to plan you should be see everything in the database along with some files in your app/webroot/uploads directory!

Added Extras

Currently if you select a file and submit without filling in anything else the form will not save as the other validation rules have not passed. Ideally we'll want to keep track of the uploaded file and display it on the form when the validation fails.

Todo that we will add in some extra Controller logic to see if a file has been submitted, this is the case if the 'filepath' data is a string and isn't empty. We're then going to attach the file path to the request data so that it's available in our View. Here's the extra Controller code:

// attempt to save
if ($this->Contact->save($this->request->data)) {
	$this->Session->setFlash('Your message has been submitted');
	$this->redirect(array('action' => 'index'));

// form validation failed
} else {
	// check if file has been uploaded, if so get the file path
	if (!empty($this->Contact->data['Contact']['filepath']) && is_string($this->Contact->data['Contact']['filepath'])) {
		$this->request->data['Contact']['filepath'] = $this->Contact->data['Contact']['filepath'];
	}
}

This is what our view file now looks like, if the 'filepath' isn't empty then it's been set in the Controller and we can display a link to the file along with a hidden form field so that it keeps track of it on each submission. If it is empty or it's not set then display our usual file form field:

<h2>Contact</h2>
<?php 
echo $this->Form->create('Contact', array('type'=>'file'));
echo $this->Form->input('name');
echo $this->Form->input('email');
echo $this->Form->input('message');
?>

<?php if (!empty($this->data['Contact']['filepath'])): ?>
	<div class="input">
		<label>Uploaded File</label>
		<?php
		echo $this->Form->input('filepath', array('type'=>'hidden'));
		echo $this->Html->link(basename($this->data['Contact']['filepath']), $this->data['Contact']['filepath']);
		?>
	</div>
<?php else: ?>
	<?php echo $this->Form->input('filename',array('type' => 'file')); ?>
<?php endif; ?>

<?php
echo $this->Form->end('Submit');
?>

Wrapping Up

Hopefully everything has made sense in this article, if not get in touch and I'll try and help you out. The full source code is available at my public repo CakePHPContactFormWithFileUpload so feel free to have a gander and correct any mistakes if you see them.

Posted on 29th January 2013
4 years, 3 months, 3 weeks, 6 days ago

comments powered by Disqus