Invalid Article.

ImageBehavior - best from database blobs and file storage

By Grzegorz Pawlik (meta)
I've always had problem with uploaded images that were stored just as files. They shouldn't go to repository and wasn't in database either. So I created an ImageBehavior to handle my problems.
It handles storage in BLOB after upload, and convenient way to retrieve this data.
The requirements are that model which acts as ImageBehavior has thise fields
`content` BLOB (MEDIUMBLOB or LONGBLOB)
`modified` DATETIME (time of actual image version)
`ext` VARCHAR(10) (extension of uploaded file)
its good to have those too (but they are not required) :
`type` varchar (50) (for mime-type)
`size` int (for size in bytes)

You'll need filecache/ folder in app/webroot/img, and set permissions so my behavior could write stuff in it.
Now, You're ready to download ImageBehavior:

Model Class:

Download code <?php 
/**
 * ImageBehavior - take best from database blobs adn file image storage
 * requires 'content' field that is a blob (mediumblob or longblob), and
 * 'ext' varchar(10) field  and
 * 'modified' datetime field
 * @author Grzegorz Pawlik
 * @version 1.0
 */
class ImageBehavior extends ModelBehavior {
   
   
/**
    * directory in which cached files will be stored
    *
    * @var string
    */
   
var $cacheSubdir 'filecache';
   
/**
    * if set to false - never check if cached file is present (nor actual)
    *
    * @var bool
    */
   
var $usecache true;
   
   function 
setup(&$Model) {
      
// no setup at this time
   
}
   
   
/**
    * Insert proper blob when standard data after upload is present
    *
    * @param object $Model
    * @return bool true
    */
   
function beforeSave(&$Model) {

      if(isset(
$Model->data[$Model->name]['file']['tmp_name']) && is_uploaded_file($Model->data[$Model->name]['file']['tmp_name'])) {
      
// podnieś wyżej parametry
      
$Model->data[$Model->name] = array_merge($Model->data[$Model->name],  $Model->data[$Model->name]['file']);
      
// przygotuj blob
      
$this->_prepareBlob($Model);
      
      
$this->_getExt($Model);
      }
      
      return 
true;
   }
   
   
/**
    * prepares blob contents 
    *
    * @param object $Model
    */
   
function _prepareBlob(&$Model) {
      
App::import('Core''File');
      
$file = new File($Model->data['Medium']['tmp_name'], false);
      
$content $this->addSlashes$file->read() );
      
$Model->data[$Model->name]['content'] = $content;
   }
   
   
/**
    * Get uploaded file extension
    *
    * @param object $Model
    */
   
function _getExt(&$Model) {
      
$file explode('.'$Model->data['Medium']['name']);
      
$ext array_pop($file);
      
$Model->data[$Model->name]['ext'] = $ext;
   }
   
   
/**
    * replace blob contents with file path
    * After reading database checks if cached file is present. If not creates it (from blob contents) and
    * returns a 'file' field with path relative to /app/webroot/img
    * 
    *
    * @param object $model
    * @param array $results
    * @param unknown_type $primary
    * @return unknown
    */
   
function afterFind(&$model$results$primary) {
      foreach(
$results as $key => $val) {
         
         
         
         
$relpath $this->cacheSubdir DS 
                 
$val[$model->name]['id'] . '_' $model->name '_' 
                 
$val[$model->name]['modified'] . '.' $val[$model->name]['ext']; 
         
$relpath str_replace( array(' '':') , '_'$relpath);
         
         
$fullpath IMAGES $relpath;
         
         if(!
file_exists($fullpath) || !$this->usecache ) {
            
file_put_contents($fullpath$this->stripSlashes($results[$key][$model->name]['content']));
         }
         
         
$results[$key][$model->name]['file'] = $relpath;
         
// remove blob from results (its messy when You want to output results in debug)
         
unset($results[$key][$model->name]['content']);
      }
      return 
$results;
   }
   
   
/**
    * add slashes (just wrapper)
    *
    * @param string $string
    * @return string with slashes
    */
   
function addSlashes($string) {
      return 
addslashes($string);
   }
   
   
/**
    * strip slashes (just wrapper)
    *
    * @param string $string
    * @return string without slashes
    */
   
function stripSlashes($string) {
      return 
stripslashes($string);
   }
}
?>

On next page - example of use

Page 2: Using ImageBehavior

Comments 956

CakePHP Team Comments Author Comments
 

Comment

1 Enter description here...

Thanks for sharing. I hate to be a bitch, but do you think you could clean up your code before posting it? Completing the php doc, removing outcommmented code and debugs and private comments shouldnt take that long :)

Also your example could be clearer. If the point is that you use html->link with the file value of the database then this should be more empathised. You suggest doing a find all for all images even if you just need one? If you just need the file field, why do you ask for the blob?
Posted Mar 1, 2009 by Alexander Morland
 

Comment

2 re

Thanks for sharing. I hate to be a bitch, but do you think you could clean up your code before posting it? Completing the php doc, removing outcommmented code and debugs and private comments shouldnt take that long :)
Ok, I'll do that as soon as I can :) I'm grateful for advices.
Also your example could be clearer. If the point is that you use html->link with the file value of the database then this should be more empathised. You suggest doing a find all for all images even if you just need one? If you just need the file field, why do you ask for the blob? I'm not sure if I understand Your question. This behavior asks for the blob only if the file is nit present or outdated.
Or maybe You refer to MediaController::index, where findAll() is present? This is just an example. It should work with find() or read() either. Did You find a bug at this point?
Posted Mar 2, 2009 by Grzegorz Pawlik
 

Comment

3 changes

Ok, I made some changes in this article. Please check it out in Your free time.
Greg
Posted Mar 2, 2009 by Grzegorz Pawlik
 

Comment

4 Behavior Update

I have made some changes to your code, including implementing file hashing instead of the old method of file naming. I also made the code cakePHP friendly. I am still working on this to make it better, I hope this helps someone.

This should work with any model you want to name it, however I decided to call it images for brevity:

Sql Table:


CREATE TABLE `images` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(75) NOT NULL default '',
  `clean_name` varchar(75) NOT NULL default '',
  `type` varchar(255) NOT NULL default '',
  `size` int(11) NOT NULL default '0',
  `data` longblob NOT NULL,
  `filehash` varchar(255) NOT NULL default '',
  `ext` varchar(255) NOT NULL default '',
  `created` datetime default NULL,
  `updated` datetime default NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;


<?php 
/**
 * ImageBehavior - To help with image CRUD
 * @author Grzegorz Pawlik 1.0
 * @author Travis Rowland (Theaxiom) 1.2
 * @version 1.2
 */
class ImageBehavior extends ModelBehavior {
    
    var 
$_defaults = array(
        
'cache_dir' => 'filecache''use_cache' => true'file_input' => 'File''tmp_name' => 'tmp_name''model_name' => 'Image''file' => 'file',
        
'ext_field' => 'ext''filehash_field' => 'filehash''name_field' => 'name''data_field' => 'data''clean_name_field' => 'clean_name'
    
);
    
    function 
setup(&$Model$config = array()) {
        if (!
is_array($config)) {
            
$config = array('type' => $config);
        }
        
$settings array_merge($this->_defaults$config);
        
$this->settings[$Model->alias] = $settings;
    }

    function 
beforeSave(&$Model) {
        
extract($this->settings[$Model->alias]);
        if(isset(
$Model->data[$Model->alias][$file_input][$tmp_name]) && is_uploaded_file($Model->data[$Model->alias][$file_input][$tmp_name])) {
            
$Model->data[$Model->alias] = array_merge($Model->data[$Model->alias],  $Model->data[$Model->alias][$file_input]);
            
App::import('Core''File');
            
$file = new File($Model->data[$Model->alias][$tmp_name], false);
            
$Model->data[$Model->alias][$data_field] = addslashes$file->read() );
            
$Model->data[$Model->alias][$filehash_field] = md5($Model->data[$Model->alias][$name_field] . $Model->data[$Model->alias][$data_field]);
            
$file explode('.'$Model->data[$Model->alias][$name_field]);
            
$Model->data[$Model->alias][$ext_field] = array_pop($file);
            
$Model->data[$Model->alias][$clean_name_field] = array_pop($file);
        }
        
        return 
true;
    }

    function 
afterFind(&$Model$results$primary) {
        
extract($this->settings[$Model->alias]);
        
        foreach(
$results as $key => $val) {
            if (isset(
$val[$model_name])) {
                
$relpath $cache_dir DS $val[$model_name][$filehash_field] . '.' $val[$model_name][$ext_field];
                
$fullpath IMAGES $relpath;
                
                if(!
file_exists($fullpath) || !$use_cache) {
                    
file_put_contents($fullpathstripslashes($results[$key][$model_name][$data_field]));
                }
                
                
$results[$key][$model_name][$file] = $relpath;
                unset(
$results[$key][$model_name][$data_field]);
            }
        }

        return 
$results;
    }
}
?>

You can now call it from other models recursively (1 level) by adding the following code to the other model:


    var $actsAs = array('Image');

You get the image url from within the other model by requesting the "file" field.

Example view code:


<?php echo $html->image($otherModel['Image']['file'], array('height'=>'100''width'=>'100')); ?>

images_controller.php


<?php 
class ImagesController extends AppController {

    var 
$name 'Images';
    var 
$helpers = array('Html''Form');
    
    function 
index() {
        
$this->set('images'$this->Image->findAll());
    }
    
    function 
add() {
        if (!empty(
$this->data)) {
            
$this->Image->create();
            if(
$this->Image->save($this->data)) {
                
$this->Session->setFlash(__('Image upload successful'true));
                
$this->redirect(array('action' => 'index'));
            } else {
                
$this->Session->setFlash(__('Error on Image upload. Please try again.'));    
            }
        }
    }
    
    function 
edit($id null) {
        if (!
$id && empty($this->data)) {
            
$this->Session->setFlash(__('Invalid Id'true));
            
$this->redirect(array('action' => 'index'));
        }
        if (!empty(
$this->data)) {
            if(
$this->Image->save($this->data)) {
                
$this->Session->setFlash(__('Image upload successful'true));
                
$this->redirect(array('action' => 'index'));
            } else {
                
$this->Session->setFlash(__('Error on Image upload. Please try again.'));    
            }
        }
        if (empty(
$this->data)) {
            
$this->data $this->Image->read(null$id);
        }
    }
}
?>

image model (image.php)


<?php 
class Image extends AppModel {
    var 
$name 'Image';
    var 
$actsAs = array('Image');
}
?>

add/edit.ctp


<?php echo $form->create('Image', array('action' => 'add''type' => 'file'));
    echo 
$form->file('File');
    echo 
$form->submit('Upload');
echo 
$form->end(); ?>
Posted Mar 11, 2009 by Travis Rowland
 

Comment

5 see above

see above
Posted Mar 11, 2009 by Travis Rowland
 

Comment

6 Thanks for help

That looks good... I need some cake approach course. Maybe I could get to Berlin this summer ;)
Posted Mar 27, 2009 by Grzegorz Pawlik
 

Question

7 About blobs

Wouldn't it be better to save blobs in separate table? That would also save the problem with quering blobs with every select...

thanks.
Posted Apr 3, 2009 by RT
 

Comment

8 Thank you!

Thank you so much, I can't believe I didn't think of that! I was trying to figure out a way to do that, thank you so much you are a life saver. I will modify this behavior and post an update soon. Thanks again!

Wouldn't it be better to save blobs in separate table? That would also save the problem with quering blobs with every select...

thanks.
Posted Apr 3, 2009 by Travis Rowland
 

Comment

9 More ideas

Thank you so much, I can't believe I didn't think of that! I was trying to figure out a way to do that, thank you so much you are a life saver. I will modify this behavior and post an update soon. Thanks again!
You welcome :)

Of course to be production ready it may need some more polish:
- default image if field is empty
- some kind of size management (max size allowed or resizing or splitting big images)
- different sizes of the same image (as an option)

I will try to work it out some time after Easter, but I am new to Cake so it may take some time...

Thanks for the code.
Posted Apr 7, 2009 by RT
 

Comment

10 You should look into PHPThumb integration

I haven't set it up before, but PHPThumb has settings that will pull images from a database instead of the file system. It seems like the behavior for loading them in combined with PHPThumb for outputting the image paths directly in img src tags would be a pretty slick combination.
Posted May 29, 2009 by Barry
 

Question

11 associated model

Great Behaviour!
In my setup the "images" model "belongsTo" another model (and the other model "hasOne" image, respectively").

Saving the other model nothing is saved in the images table. Any suggestions ? Does this behaviour work only if the corresponding model is saved directly ($this->Image->save() ) or should function calling associated models too (e.g $this->othermodel->save() )? I'm using the 1.2 version. Thanks !
Posted Jan 25, 2010 by Teo Mat
 

Comment

12 windows-hack

hey!
great behavior!
had problems in the beginning because it was interfering with some other behaviors...but nevertheless, while bugfixing and testing on windows i discovered that you have to use following short hack to got it running on windows (for the v1.2 of the ImageBehavior):

simply use

$results[$key][$model_name][$file] = str_replace('\\','/', $relpath); //WindowsHack

instead of

$results[$key][$model_name][$file] = $relpath;

Posted Jun 18, 2010 by Flow
 

Question

13 habtm - please help


Notice (8): Undefined index: ext [APP/models/behaviors/image.php, line 44]
Notice (8): Undefined index: ext [APP/models/behaviors/image.php, line 44]


    [Image] => Array
        (
            [0] => Array
                (
                    [id] => 1
                    [name] => scope.jpg
                    [clean_name] => scope
                    [type] => image/jpeg
                     => 58085
                    [data] => ����\0JFIF\0\0H\0H\0\0��\0....lotsofrawfiledata...�+��@h�7�y���c�����Er��
                    [filehash] => 21bcfa51e3e15ebdee2b8d9c544189bf
                    [ext] => jpg
                    [created] => 2010-06-24 16:41:27
                    [updated] => 2010-06-24 16:41:27
                    [ImagesKit] => Array
                        (
                            [id] => 1
                            [kit_id] => 1
                            [image_id] => 1
                        )

                )

            [file] => filecache/.
        )

)

Posted Jun 24, 2010 by Nic
 

Comment

14 best

I think that the best form of storage would be in the encrypted form on an external peripheral


Cremation Urns
Posted Jul 18, 2010 by bad anooj
 

Comment

15 ed

When I have rationalized the realistic way to solve that over time that it starts to blend three years ago. car shipping Yes, it will be evaluated with a presentation.
Posted Jul 22, 2010 by dan kaylee