Two CakePHP behaviours to extend MeioUpload

I’ve been using the the Meio Upload Behaviour for a while as its a very handy piece of code, but as is so often the case it doesn’t do quite what I need. In the past I’ve got round this by hacking together a kind of supporting framework in app_controller and scattered about in my models. But the other day I came across the newest version of the code by Juan Basso on Github and decided to dump my mess of spaghetti code and start from scratch.
Aims
The idea is that each model can have its own uploads with their own defaults, for example:

  • Products might need to generate images at 3 different sizes with the smallest zoom cropped.
  • News items might have 2 image sizes but also need to upload a PDF press release.

All of the uploaded files can be viewed and managed centrally from the Upload model.
The changes I wanted to make were as follows:

  1. Use a single Uploads table attached to multiple models (using the Polymorphic behaviour)
  2. The ability to do multiple uploads at once
  3. Rename my uploaded files
  4. Have an UploadVariants model / table with meta information about the thumbnails
  5. Be able to use more of the PHPThumb image options and use them on a per thumbnail basis.

Rather than just take the behaviour and start writing on top of it, I decided to extend the original behaviour (yes you can do this in CakePHP – its just easy to get absorbed in the framework and forget it) and then create an additional behaviour that manages the multiple uploads.
The underlying requirements are quite simple:

(Note 09/07/2009 you don’t actually need the Polymorphic Behaviour, unless you want to do fancy things with your finds)
And my two new behaviours:

At the end of the article is a link to let you download a zipped working CakePHP app with everything in place
The models
First things first. The database tables on which my models are based.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
MySQL Backup
Source Host: localhost
Source Server Version: 5.0.51b-community-nt
Source Database: je_meio
Date: 2009/06/29 15:39:15
*/
 
SET FOREIGN_KEY_CHECKS=0;
#----------------------------
# Table structure for products
#----------------------------
DROP TABLE IF EXISTS products;
CREATE TABLE `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
`created_by` int(11) DEFAULT NULL,
`modified_by` int(11) DEFAULT NULL,
`created_name` varchar(255) DEFAULT NULL,
`modified_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
 
#----------------------------
# Table structure for upload_variants
#----------------------------
DROP TABLE IF EXISTS upload_variants;
CREATE TABLE `upload_variants` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`upload_id` int(11) DEFAULT NULL,
`variant` varchar(255) DEFAULT NULL,
`filename` varchar(255) DEFAULT NULL,
`quick_type` varchar(50) DEFAULT NULL,
`width` int(4) DEFAULT NULL,
`height` int(4) DEFAULT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
`created_by` int(11) DEFAULT NULL,
`modified_by` int(11) DEFAULT NULL,
`created_name` varchar(255) DEFAULT NULL,
`modified_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
 
#----------------------------
# Table structure for uploads
#----------------------------
DROP TABLE IF EXISTS uploads;
CREATE TABLE `uploads` (
`id` int(8) UNSIGNED NOT NULL AUTO_INCREMENT,
`class` varchar(255) DEFAULT 'Upload',
`foreign_id` int(11) DEFAULT NULL,
`alt` varchar(255) DEFAULT NULL,
`filename` varchar(255) DEFAULT NULL,
`dir` varchar(255) DEFAULT NULL,
`mimetype` varchar(255) DEFAULT NULL,
`quick_type` varchar(50) DEFAULT NULL,
`filesize` int(11) UNSIGNED DEFAULT NULL,
`position` int(11) DEFAULT '0',
`height` int(11) DEFAULT NULL,
`width` int(11) DEFAULT NULL,
`created` datetime DEFAULT NULL,
`processed` tinyint(1) DEFAULT '0',
`modified` datetime DEFAULT NULL,
`created_by` int(11) DEFAULT NULL,
`modified_by` int(11) DEFAULT NULL,
`created_name` varchar(255) DEFAULT NULL,
`modified_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `class` (`class`,`foreign_id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Upload extends AppModel {
 
var $name = 'Upload';
 
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $hasMany = array(
'UploadVariant' => array(
'className' => 'UploadVariant',
'foreignKey' => 'upload_id',
'dependent' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
)
);
 
var $actsAs = array(
'Polymorphic',
'JeMeioUpload' => array(
'filename' => array(
'dir' => 'files/uploads',
'create_directory' => true,
'max_size' => 2097152,
'max_dimension' => 'w',
'thumbnailQuality' => 90,
'useImageMagick' => false,
'imageMagickPath' => '/usr/bin/convert',
'allowed_mime' => array( 'image/gif', 'image/jpeg', 'image/pjpeg', 'image/png'),
'allowed_ext' => array('.jpg', '.jpeg', '.png', '.gif'),
'thumbsizes' => array(
'small' => array('width' => 90, 'height' => 90),
'medium' => array('width' => 220, 'height' => 220),
'large' => array('width' => 800, 'height' => 600)
),
'default_class' => 'Upload',
'random_filename' => true
)
)
);
 
}

In the Upload model, just include the Polymorphic behaviour and JeMeioUpload instead of MeioUpload. You could actually use the same defaults as the MeioUpload behaviour itself but there are two additional defaults default_class’ => Upload’, and random_filename’ => true. These both refer to renaming files. As the behaviour can be used to manage uploads from multiple models in a single table it allows you to set the default Upload model. You can also set whether or not you want the files to have the meaningful part replaced with a random string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UploadVariant extends AppModel {
 
var $name = 'UploadVariant';
 
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $belongsTo = array(
'Upload' => array(
'className' => 'Upload',
'foreignKey' => 'upload_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
 
}

The UploadVariant Model is just as it was baked.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Product extends AppModel {
 
var $name = 'Product';
 
var $validate = array(
'title' => array('notempty')
);
 
var $hasMany = array(
'Upload' => array(
'className' => 'Upload',
'foreignKey' => 'foreign_id',
'conditions' => array('Upload.class' => 'Product'),
'dependent' => true,
'order' => 'Upload.position ASC'
),
 
);
 
var $actsAs = array(
'JeMeioUploadPolymorphic'
);
 
 
 
var $jeMeioUploadParams = array(
'filename' => array(
'dir' => 'files/uploads/',
'create_directory' => true,
'allowed_mime' => array('image/jpeg', 'image/pjpeg', 'image/png', 'image/gif'),
'allowed_ext' => array('.jpg', '.jpeg', '.png', '.gif'),
'thumbsizes' => array(
'small' => array('width'=>60, 'height'=>60, 'image_options' => array('zc' => 1)),
'large' => array('width'=>800, 'height'=>400, 'image_options' => array('zc' => 0)),
'shop' => array('width'=>265, 'height'=>265, 'image_options' => array('zc' => 0))
),
'random_filename' => false
)
);
 
}

Product is a test model set up to demonstrate the multiple uploads and the Polymorphic association to the Upload model. In this case the meaningful part of the filenames will not be replaced with a random string and the ’small’ thumbnail will be zoom cropped. To find out more about image_options you will need to check the PHPThumb component and the documentation PHPThumb itself (play about and experiment) – what you can and can’t do will also be effected by whether or not you have ImageMagick on your server.
The Controllers
You don’t need to make any changes to your controllers at all.
The Views
There is nothing complicated in the views at all. Just remember to set them up properly for uploads.
The uploads/add.ctp

1
2
3
4
5
6
7
8
9
10
11
<?php echo $form->create('Upload', array('type' => 'file'));?>

<?php __('Add Upload');?>
<?php
 
echo $form->input('alt');
echo $form->input('filename', array('type' => 'file'));
 
?>

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

The products/add.ctp
This view allows up to 3 uploads to be stored in the associated Upload model.

1
2
3
4
5
6
7
8
9
10
11
<?php echo $form->create('Product', array('type' => 'file'));?>

<?php __('Add Product');?>
<?php
echo $form->input('title');
echo $form->input('Upload.0.filename', array('type' => 'file'));
echo $form->input('Upload.1.filename', array('type' => 'file'));
echo $form->input('Upload.3.filename', array('type' => 'file'));
?>

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

Summary
The one part I’m not terribly happy with at the moment is error messages returning to the views in the Polymorphic associations (e.g. Product example). At the moment if an image or upload can’t be processed then there is no warning or error message. At the moment this is compromise I’m willing to live with.
I could certainly build some kind of component scaffold or something to go in the controllers to return an error message but I think the gain in responsiveness would probably be outweighed by the added complexity. Right now I can’t figure out an elegant way to get error messages from back to the views – if anybody has any suggestions please let me know.
(Note if you are uploading straight into the Upload model then the error messages are unaffected)
There is also the problem of what do you do when in a situation like the following: If you add a new Product successfully but the associated upload has problems? Ideally you would want to redirect to the edit view for that Product but there seems to be no easy way of doing this from a Model / Behaviour.
One solution that I have implemented in the past is to create dedicated views for uploading – so if you do hack your hacks are easy to manage.
I’ve baked a quick and dirty demo that you can download here. Its so basic that it doesn’t even show the uploaded images!
Todo List

  • A helper to generate things like uploads fields complete with delete checkboxes and thumbnail images.
  • Deleting the via the Polymorphic association (don’t think this works right now)