ImageBehavior - best from database blobs and file storage

by GrzegorzPawlik
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:

<?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


In my example I'll use this table:

CREATE TABLE IF NOT EXISTS `media` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(50) NOT NULL,
  `ext` varchar(10) NOT NULL,
  `content` longblob NOT NULL,
  `size` int(11) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `type` varchar(20) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM;

Model Class:

<?php 
class Medium extends AppModel {

    var 
$name 'Medium';
   var 
$actsAs = array('Image');
}

?>

Controller Class:

<?php 

class MediaController extends AppController {

    var 
$name 'Media';
    var 
$helpers = array('Html''Form');
    
    function 
index() {
      
      
$this->set('media'$this->Medium->findAll());
      
    }
    
    function 
add() {
       if(!empty(
$this->data)) {
          
$this->Medium->save($this->data);
       }
    }
    
}

?>

In add example You can see how files are stored in database. Model expects file in ModelName.file field.

add.ctp view:

View Template:


<?php 
   
echo $form->create(
      array(
'url' => array(
                           
'controller' => 'media',
                           
'action'    => 'add'
                     
),
            
'enctype' => 'multipart/form-data'
            
)
   ); 
?>

<?php echo $form->file('Medium.file'); ?>

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

In index action one can see how data are retrieved. Despite that images are stored as blobs, we can use them like ordinary files.

index.ctp:

View Template:


<?php foreach($media as $medium): ?>
   <?php echo $html->image($medium['Medium']['file']); ?>
<?php 
endforeach; ?>


When You upload file it assumes, that it will be Model.file field.
When it's ok - it just stores a BLOB in a database (with additional data like extension and other stuff).

But magic happens when You try to retrieve data from Model acting as Image.
* It checks /filecache folder for cached files. If one is found - it just removes content field from results, ant place there 'file' field with relative path to cached file.

If none is fond or found file is older than contents in DB (according to `modified` field) it creates such file, and acts as described above (*).

So You work with those images like with ordinary files, but when You export database, You export files too.

Additionally You can force distant future expire header in .htaccess file in filecache/ so any client browser will cache it until it's changed in database.

Potential problems:
When You insert image file in text content in database (by BBCode for example), and the image is changes - contents will remain outdated.

Probably blob field shouldn't be retrieved in every find, only when a cached file isn't present or outdated. That's stuff to fix in future versions.

1 | 2 | 3

Report

More on Behaviors

Tags

Advertising

Comments

  • pirat3 posted on 06/24/10 03:37:04 PM

    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
                        [size] => 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/.
            )

    )

  • flowmotion posted on 06/18/10 04:54:34 PM
    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;

  • teox76 posted on 01/25/10 06:39:02 PM
    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 !
  • brightball posted on 05/29/09 01:43:43 PM
    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.
  • moors posted on 04/03/09 11:41:39 AM
    Wouldn't it be better to save blobs in separate table? That would also save the problem with quering blobs with every select...

    thanks.
    • Theaxiom posted on 04/03/09 06:10:21 PM
      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.
      • moors posted on 04/07/09 04:07:01 AM
        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.
  • GrzegorzPawlik posted on 03/27/09 08:59:39 AM
    That looks good... I need some cake approach course. Maybe I could get to Berlin this summer ;)
  • Theaxiom posted on 03/11/09 01:10:50 PM
    see above
  • Theaxiom posted on 03/11/09 01:04:24 PM
    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(); ?>
  • alkemann posted on 03/01/09 05:57:04 AM
    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?
    • GrzegorzPawlik posted on 03/02/09 08:07:47 AM
      Ok, I made some changes in this article. Please check it out in Your free time.
      Greg
    • GrzegorzPawlik posted on 03/02/09 04:31:56 AM
      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?
login to post a comment.