ImageBehavior - best from database blobs and file storage
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:
On next page - example of use
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.








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/.
)
)
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;
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 !
thanks.
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.
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($fullpath, stripslashes($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(); ?>
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?
Greg
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?
Comments are closed for articles over a year old