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'));?>
	<fieldset>
 		<legend><?php __('Add Upload');?></legend>
	<?php
 
		echo $form->input('alt');
		echo $form->input('filename', array('type' => 'file'));
 
	?>
	</fieldset>
<?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'));?>
	<fieldset>
 		<legend><?php __('Add Product');?></legend>
	<?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'));
	?>
	</fieldset>
<?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)

11 Responses to “Two CakePHP behaviours to extend MeioUpload”

  1. Ilia

    Tried to run your demo code, but it fails with

    Parse error: syntax error, unexpected T_STRING, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or ‘}’ in /app/models/behaviors/je_meio_upload.php on line 135

    Can anyone helps me with this?

  2. Flipflops

    Thanks

    I’ll check that out right away and fix it.

  3. Flipflops

    Very weird. I just down loaded the zip file and and installed it and it runs without any problem once I set up the database of course…

  4. Ilia

    It was my fault, my PHP was 4.4… upgrade to 5.2 fix it, thanks

  5. Flipflops

    No worries. Slightly odd error though, must be to do with the public modifiers on the class methods or something like that.

  6. unidev

    hey there!
    thanks for the great example! Could you give a brief example of how you retrieve the variant images for the product? I have managed it, but it seems very long winded and feel I may be missing a trick?
    Cheers!

  7. unidev

    Sorry, to further my query – do you even query the UploadVariants or just call the images by prepending the variant name to the Upload filename ie:

    $html->image(‘thumb.’.$product[‘Upload’][0][‘filename’]);

  8. Flipflops

    @unidev

    Yeah, you could do that, I wouldn’t say there is a real right or wrong way though. I’ve attached a couple of code fragments as to how I use it. Unfortunately though since I wrote this, I’ve done very little Cake based stuff (new jobs etc.) although I am doing some more soon so want to have another look at this all.

    The code fragments are taken from the admin section of my CMS

    Controller:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    $contain = array(
    			'UploadVariant'
    		);
     
     
     
     
     
    		$this->paginate = array(
    							'Upload'=>array(
    											'limit' => 20,
    											'contain' => array('UploadVariant.variant = "small"'),
    											'order' => array('Upload.id' => 'DESC')
    							)
    		); 
     
    		$this->data = $this->paginate($conditions);

    Helper:
    This just takes creates a thumbnail and opens a full size version.

    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
    74
    75
    76
    77
    
    <?php
     
    class UploadHelper extends AppHelper {
     
    	var $helpers = array('Html', 'Form');
     
    	var $uploadModel = 'Upload';
    	var $uploadModelVariant = 'UploadVariant';
     
    	public function showThumb(&$data, &$sub_data = null, $rel = null){
     
    		$str = '';
     
    		$dir = WWW_ROOT . $this->getRealDir($data['dir']);
    		$web_dir = '/' . $this->getWebDir($data['dir']);
     
     
     
    		if(file_exists($dir . DS . $data['filename'])) {
     
     
     
     
    			if($data['quick_type'] == 'image'){
     
     
    				if(!empty($sub_data['filename'])){
     
     
     
    					if(file_exists($dir . DS . $sub_data['filename'])){
     
    						$image = $this->Html->image($web_dir . '/' . $sub_data['filename'], array('alt' => '', 'height' => $sub_data['height'], 'height' => $sub_data['height']));
     
    						$str .= $this->Html->link($image, $web_dir . '/' . $data['filename'],array('class' => 'thickbox', 'rel' => $rel), false, false);
     
    					} else {
     
     
     
    					}
    				} else {
     
     
    				}
     
    			} else {
    				$str .= $this->Html->link($data['filename'], $web_dir. DS . $data['filename'],array('target' => '_blank'), false, false);
    			}
     
    		} else {
     
    			$str = 'Missing File';
     
    		}
     
     
    		return $this->output($str);
     
    	}
     
    	protected function getRealDir($dir){
    		$dir = r('\\', DS, $dir);
    		$dir = r('/', DS, $dir);
    		return $dir;
    	}
     
    	protected function getWebDir($dir){
    		$dir = r('\\', '/', $dir);
    		return $dir;
    	}
     
     
     
    }
     
    ?>

    View:

    1
    
    echo $upload->showThumb($var['Upload'], $var['UploadVariant'][0]);

    Thinking about it, you would usually know exactly what variant you are after so between the containable behaviour to filter the data and then possibly some use of array_key_exists() in the view you should be able to end up with some very neat code.

    Hope this helps.

    John

  9. alonso

    thx tuto, but..

    how can display the image?

    $html->image($upload[‘Upload’][‘dir’].$upload[‘Upload’][‘filename’]); ??????

  10. slvolz

    Great extension to MeioUpload behavior!

    I have gotten it to work on upload and delete. The polymorphic deletion was not working at all. At least not for the UploadVariant. So I did a custom deletion process.

    I would like to suggest that in the je_meio_upload.php behavior that at the for loop of the creation of the UploadVariant (line 495-500?) that you do not add entries for any variant named ‘normal’. This is something that meio_upload uses to overwrite the existing upload to a modified image size as per your options. There is not a normal variant actually created so there is no need to add a entry to the table for it.

  11. Flipflops

    Hi

    I’m glad that it has come in useful. Thanks for your comments, I’ll try and follow them up – about the UploadVariant.

    I use the behaviours on a couple of sites, but to tell the truth I haven’t done any new work with Cake for over a year now.

    Cheers