Uploading Files and Images with CakePHP

22nd February 2008 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.

Buy Me a Beer for Helping You Out

Back to Home Page

Comments

Sliv (27/03/2008 - 08:08)

Thanks very much for this article - I'm looking forward to trying this out.

h0p (02/04/2008 - 23:10)

your pa function already exists globally as pr. eg) pr($this->data);

James (04/04/2008 - 11:28)

Ahh good stuff, better than creating my own function for all my projects. Thanks!

Luke (10/04/2008 - 08:35)

Also, pr() is mega, but check out debug() with Cake as well - nice feature!

A nice tip for image uploads is to use the Fsckeditor also - which has its own builtin image uploader that easily works with cake if you are building any kind of CMS style backend.

James (10/04/2008 - 10:53)

Cheers Luke,

I'll look into debug() and Fckeditor when I get the chance, although I try to stay away from the big wysiwyg editors because they produce messy code but that was a while back so they may have improved.

James

Eddie (12/05/2008 - 21:45)

Thanks for going over each section of your code. I found it very helpful.

Markus (14/05/2008 - 10:01)

Hello. Is it somehow possible, if i upload two different files (pdf/image), to store those two files in different folders?

best regards markus

James (14/05/2008 - 10:43)

@Markus: I would inspect the mime type of the file and then change the folder url depending on the type. This is pretty easy to do by looking in the $file['type'] variable and change the folder between images and pdf's.

Let me know how you get on. James

PandoO (16/05/2008 - 16:28)

Thank's a lot for sharing that kind of informations. It mades 3 weeks i am learning cakephp, and all developer blogs are really helpfull therefore to me ! Great !

Pentla (26/06/2008 - 14:50)

I suggest to use Pear and its Net_FTP class as vendor. http://pear.php.net/package/Net_FTP

Okto Silaban (06/09/2008 - 23:01)

In this line :

# // if file was uploaded ok
# if(array_key_exists('urls', $fileOK) {

I think it should be :
if(array_key_exists('urls', $fileOK)) {

:)

Btw, where is the location of the img/files exactly?

WWW_ROOT/app/img/files or WWW_ROOT/img/files ?

Cause I get an error message :
mkdir() [function.mkdir]: No such file or directory

James (07/09/2008 - 10:09)

@Okto: Thanks for that, the code has been fixed. The WWW_ROOT should be pointing to the "/app/webroot/" folder so the files will be uploaded to the "/app/webroot/img/files/ dir.

Faxxxe (16/09/2008 - 16:06)

Nice! Thank you. And wbat about deleting? There should be deleted files first and db items later... am I right?

James (17/09/2008 - 11:10)

@Faxxxe: hey no problem, next is really upto you. Leave them on the server or try deleting from the database first and then if successful remove the actual file.

Josey (30/09/2008 - 07:39)

Brilliant, i got this to work in a matter of minutes.
I implemented this with a jquery multiple uploads script and looped though the results to have it save each upload to its own row in the database table. Here is what I entered into the add method of my uploads controller:

if (!empty($this->data)) {
$fileOK = $this->uploadFiles('upload', $this->data['File']);

// upload the file to the server

// if file was uploaded ok
if(array_key_exists('urls', $fileOK)) {
// save the url in the form data

$howMany = count($fileOK['urls']);
$successes = 0;
for($i = 0; $i < $howMany; $i++) {
$this->Upload->create();
$this->data['Upload']['image_url'] = $fileOK['urls'][$i];
if ($this->Upload->saveAll($this->data)) {
$successes++;
} else {
}
}
if($successes == $howMany){
$this->flash(__($successes. ' Files were Uploaded.', true), array('action'=>'index'));
}
}
}

It loops through the uploads and then flashes how many were uploaded successfully.

James (03/10/2008 - 00:57)

@Josey: Hey thanks for commenting and glad you got it working.

Fernando Aires (06/10/2008 - 16:56)

Hi, What the program you use for format your code in your posts?

Thank's

James (07/10/2008 - 02:28)

@Fernando: Hi, I use a javascript syntax highlighter which can be found here

http://code.google.com/p/syntaxhighlighter/

pszemsza (08/11/2008 - 15:58)

Hi,
solution you presented didn't work for me. The content of $this->data['File'] is only:

Array
(
[image] => Dolphin_triangle_mesh.png
)

However, after adding "enctype='multipart/form-data'" to

tag everything works fine. I'm not sure whether it's a bug or rather I'm missing something, but AFAIR there must be this 'enctype' attribute to images upload work correctly, isn't that?

Anyway, thanks for great tutorials, I find them pretty interesting.

Regards

James (10/11/2008 - 04:51)

@pszemsza: Hey thanks for commenting, your right the "enctype" should be included in the form for file uploads and thanks for pointing it out.

Add Your Comment

Recently Watched Films

Mr Brooks Rec Pathology Diary of the Dead

TV Shows

The Shield Flight of the Conchords

Site by James Fairhurst 2008, all rights reserved and all that malarky