Uploading Files and Images with CakePHP

When starting out with CakePHP I found it to be a very steep learning curve, getting used to MVC along with learning all of CakePHP's magic methods was quite a lot of infomation to assimilate. Using a framework can sometimes lead you down the path of becoming a lazy programmer who expects everything to be done in an abstracted way and that was my thought process when dealing with file uploads. I expected CakePHP to do everything for me and for a time I was a little stuck on how to go about uploading and dealing with files.

Then stupidly I realised that Cake was a PHP framework and I could deal with file uploads like any other file upload I've been working with on normal web projects. In this article I'm going to continue our previous blog application by adding a file upload facility. Each post will now have the option of uploading an image which will be stored on the server and the url of that file will be stored in the database so we can easily access it when a post is viewed.

Continuing the Application

Continuing from our previous application we are going to be modifying the 'add' view to insert our file form field and our 'posts_controller' to deal with the file and move it to a folder in the webroot. First we need to add the 'image_url' field to the database so here's a quick SQL statement that will do that for you.

ALTER TABLE `posts` ADD `image_url` VARCHAR( 255 ) NOT NULL AFTER `body`;

Creating the Form

When creating the file input area we can either use the CakePHP helper or code it up manually. It doesn't really matter which so I'll quickly go through both ways. An important note is that we will first need to add the enctype value to the form (enctype="multipart/form-data"), if this is not done you will not see any file data and trust me its a common problem cos i've done it far too many times and nearly blown a fuse figuring out why my file isn't uploading.

Using the CakePHP html helper we can quickly create a form input field, looking in the manual and a.p.i it takes 3 arguments. The first is the fieldname of the input and this is where the file information will be saved, the second is an array of html attributes that you may want to apply to the input e.g. an id or class and the 3rd argument is a return value which we needn't worry about. In our 'add' view we need to add the file input like this:

echo $form->labelTag('File/image', 'Image');
echo $html->file('File/image');

When viewing the source of the html page the form helper produces this code:

<label for="FileImage">Image</label>
<input type="file" name="data[File][image]" id="FileImage" />

If you dont want to use the html helper then we can simply add the above source code straight into our view, the name attribute of the file input is consistant with cake naming conventions and it will give us access to the file information in the posts_controller by looking into the data array ($this->data['File']).

File data

To test that the form is working I've created a small function in the 'app_controller.php' file that outputs an array to the screen inside pre tags, this is then available to any controller by calling $this->pa(), to check the form i've outputted the contents of the $this->data array.

// prints out an array
function pa($arr) {
	echo '<pre>';
	print_r($arr);
	echo '< /pre>';
}
// output of $this->pa($this->data);
Array
(
    [Post] => Array
        (
            [title] => Test Title
            [body] => Test body text
        )
    [Tag] => Array
        (
            [Tag] => Array
                (
                    [0] => 1
                )
        )
    [File] => Array
        (
            [image] => Array
                (
                    [name] => 15_zamri.jpg
                    [type] => image/jpeg
                    [tmp_name] => C:\server\tmp\php5A.tmp
                    [error] => 0
                    [size] => 56759
                )
        )
)

The 'Post' and 'Tag' information is saved in the array along with the 'File' information that we can use to save the file to a folder of our choosing on the server.

Dealing with the Upload

Now I'm going to create a function in '/app/app_controller.php' that will take the file form data and upload it to a folder that we specify. This is so that the function will be available to all the controllers in the application by calling "$this->uploadFiles()", this function will take 3 arguments, the 1st will be the path of the directory from the root of the application, the 2nd will be an array of file data from the form and the 3rd will be an optional argument and will create a subfolder that the file will go in. This will be useful if we want to create a seperate folder for each of our blog posts.

I've posted the source code for my upload function below and its heavily commented so you should be able to go through and understand whats going on fairly easily. Its a standard php file upload script that creates the upload directory, checks the file type is ok, trys to upload the file and if a file with the same name already exists it will create a new unique filename. To use the function in our 'posts_controller.php' we need to pass in a directory name and the file form data like this:

// upload the file to the server
$fileOK = $this->uploadFiles('img/files', $this->data['File']);

The upload directory here is set to 'img/files' and this relates to 'http://cakephp/img/files/' this way you dont need to insert the full directory name including the base url. If we have a look at the returned array we will see this:

Array
(
	[urls] => Array
		(
			[0] => img/files/15_zamri.jpg
		)

)

Our function will return either the url of the filename or an error message explaining what went wrong. If the file upload was successful we need to add the image url to the form data so that when we try to save the post the 'image_url' field will contain our uploaded file url.

// if file was uploaded ok
if(array_key_exists('urls', $fileOK)) {
	// save the url in the form data
	$this->data['Post']['image_url'] = $fileOK['urls'][0];
}

The image url is now available in the Post 'view' file by accessing $post['Post']['image_url'] and we need to do a simple check to see if its not empty before displaying the image:

// if the post has an image display it
if(!empty($post['Post']['image_url'])) {
	$url = $post['Post']['image_url'];
	echo '<div class="uploaded_image">';
	echo '<img src="/$url" alt="Post Image" />';
	echo '</div>';
}

Hopefully if everything went according to plan you should now be able to upload an image with your blog post. Below is the complete source code for the uploadFiles function, you can also download the full application source code at the end of the article.

/**
 * uploads files to the server
 * @params:
 *		$folder 	= the folder to upload the files e.g. 'img/files'
 *		$formdata 	= the array containing the form files
 *		$itemId 	= id of the item (optional) will create a new sub folder
 * @return:
 *		will return an array with the success of each file upload
 */
function uploadFiles($folder, $formdata, $itemId = null) {
	// setup dir names absolute and relative
	$folder_url = WWW_ROOT.$folder;
	$rel_url = $folder;
	
	// create the folder if it does not exist
	if(!is_dir($folder_url)) {
		mkdir($folder_url);
	}
		
	// if itemId is set create an item folder
	if($itemId) {
		// set new absolute folder
		$folder_url = WWW_ROOT.$folder.'/'.$itemId; 
		// set new relative folder
		$rel_url = $folder.'/'.$itemId;
		// create directory
		if(!is_dir($folder_url)) {
			mkdir($folder_url);
		}
	}
	
	// list of permitted file types, this is only images but documents can be added
	$permitted = array('image/gif','image/jpeg','image/pjpeg','image/png');
	
	// loop through and deal with the files
	foreach($formdata as $file) {
		// replace spaces with underscores
		$filename = str_replace(' ', '_', $file['name']);
		// assume filetype is false
		$typeOK = false;
		// check filetype is ok
		foreach($permitted as $type) {
			if($type == $file['type']) {
				$typeOK = true;
				break;
			}
		}
		
		// if file type ok upload the file
		if($typeOK) {
			// switch based on error code
			switch($file['error']) {
				case 0:
					// check filename already exists
					if(!file_exists($folder_url.'/'.$filename)) {
						// create full filename
						$full_url = $folder_url.'/'.$filename;
						$url = $rel_url.'/'.$filename;
						// upload the file
						$success = move_uploaded_file($file['tmp_name'], $url);
					} else {
						// create unique filename and upload file
						ini_set('date.timezone', 'Europe/London');
						$now = date('Y-m-d-His');
						$full_url = $folder_url.'/'.$now.$filename;
						$url = $rel_url.'/'.$now.$filename;
						$success = move_uploaded_file($file['tmp_name'], $url);
					}
					// if upload was successful
					if($success) {
						// save the url of the file
						$result['urls'][] = $url;
					} else {
						$result['errors'][] = "Error uploaded $filename. Please try again.";
					}
					break;
				case 3:
					// an error occured
					$result['errors'][] = "Error uploading $filename. Please try again.";
					break;
				default:
					// an error occured
					$result['errors'][] = "System error uploading $filename. Contact webmaster.";
					break;
			}
		} elseif($file['error'] == 4) {
			// no file was selected for upload
			$result['nofiles'][] = "No file Selected";
		} else {
			// unacceptable file type
			$result['errors'][] = "$filename cannot be uploaded. Acceptable file types: gif, jpg, png.";
		}
	}
return $result;
}

Multiple Files

The ability to upload multiple files at once is also a breeze thanks to the uploadFiles() function that we created earlier, its been coded to loop through all the images in the 'File' array and upload them all one after another. If we added another file input to our form and named it 'image1' like this:

echo $form->labelTag('File/image1', 'Image1');
echo $html->file('File/image1');

Our 'File' array in $this->data will now contain all the details for the 2 files, here's a print out using our $this->pa() function that was created earlier:

[File] => Array
(
	[image] => Array
	(
		[name] => ahashakeheartbreak.jpg
		[type] => image/jpeg
		[tmp_name] => C:\server\tmp\php93.tmp
		[error] => 0
		[size] => 1586
		)
	[image1] => Array
	(
		[name] => aweekendinthecity.jpg
		[type] => image/jpeg
		[tmp_name] => C:\server\tmp\php94.tmp
		[error] => 0
		[size] => 2777
	)
)

And if we inspect the results from our uploadFiles() function we get the urls of both images in one nice array:

Array
(
	[urls] => Array
	(
		[0] => img/files/ahashakeheartbreak.jpg
		[1] => img/files/aweekendinthecity.jpg
	)
)

How good is that?! With quite a simple upload files function we can easily add this to any CakePHP application and get up and running in no time.

Wrapping Up

Hmmm this article has turned into a bit of beast but hopefully its been broken down into managable chunks, at first i thought uploading files with CakePHP would be different than normal PHP but alas its not which is a good thing so you can just use your normal file upload function, pop it in the 'app_controller' file and its available in all your controllers. Give it a try on your local server and if you've got any problems just send me an email or comment on the post and i'll try and help out.

I'm going on a Ski Trip to France on Saturday so i wont be around for a week, so expect another post in two weeks time. I think I'm going to cover using search engine friendly 'url slugs' instead of post id's in my next article so check back soon.

Source Code

Here is the source code of the sample application built using CakePHP which includes the file upload code that i've described in this article. You should be able to use it on your local server by adding in your database information, an exported SQL file is also included in the zip file so you can quickly create a database.

Posted on 22nd February 2008
6 years, 1 month, 4 weeks ago