Using a polymorphic attachment model for file storageΒΆ
In some cases you will want to store multiple file uploads for multiple
models, but will not want to use multiple tables because your database
is normalized. For example, we might have a Post
model that can have
many images for a gallery, and a Message
model that has many videos.
In this case, we would use an Attachment
model:
Post hasMany Attachment
We could use the following database schema for the Attachment
model:
CREATE table attachments (
`id` int(10) unsigned NOT NULL auto_increment,
`model` varchar(20) NOT NULL,
`foreign_key` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`attachment` varchar(255) NOT NULL,
`dir` varchar(255) DEFAULT NULL,
`type` varchar(255) DEFAULT NULL,
`size` int(11) DEFAULT 0,
`active` tinyint(1) DEFAULT 1,
PRIMARY KEY (`id`)
);
Our attachment records would thus be able to have a name and be activated or deactivated on the fly. The schema is simply an example, and such functionality would need to be implemented within your application.
Once the attachments
table has been created, we would create the
following model:
<?php
class Attachment extends AppModel {
public $actsAs = array(
'Upload.Upload' => array(
'attachment' => array(
'thumbnailSizes' => array(
'xvga' => '1024x768',
'vga' => '640x480',
'thumb' => '80x80',
),
),
),
);
public $belongsTo = array(
'Post' => array(
'className' => 'Post',
'foreignKey' => 'foreign_key',
),
'Message' => array(
'className' => 'Message',
'foreignKey' => 'foreign_key',
),
);
}
?>
We would also need to create a valid inverse relationship in the
Post
model:
<?php
class Post extends AppModel {
public $hasMany = array(
'Image' => array(
'className' => 'Attachment',
'foreignKey' => 'foreign_key',
'conditions' => array(
'Image.model' => 'Post',
),
),
);
}
?>
The key thing to note here is the Post
model has some conditions on
the relationship to the Attachment
model, where the Image.model
has to be Post
. Remember to set the model
field to Post
, or
whatever model it is you’d like to attach it to, otherwise you may get
incorrect relationship results when performing find queries.
We would also need a similar relationship in our Message
model:
<?php
class Message extends AppModel {
public $hasMany = array(
'Video' => array(
'className' => 'Attachment',
'foreignKey' => 'foreign_key',
'conditions' => array(
'Video.model' => 'Message',
),
),
);
}
?>
Now that we have our models setup, we should create the proper actions in our controllers. To keep this short, we shall only document the Post model:
<?php
class PostsController extends AppController {
/* the rest of your controller here */
public function add() {
if ($this->request->is('post')) {
try {
$this->Post->createWithAttachments($this->request->data);
$this->Session->setFlash(__('The message has been saved'));
} catch (Exception $e) {
$this->Session->setFlash($e->getMessage());
}
}
}
}
?>
In the above example, we are calling our custom
createWithAttachments
method on the Post
model. This will allow
us to unify the Post creation logic together in one place. That method
is outlined below:
<?php
class Post extends AppModel {
/* the rest of your model here */
public function createWithAttachments($data) {
// Sanitize your images before adding them
$images = array();
if (!empty($data['Image'][0])) {
foreach ($data['Image'] as $i => $image) {
if (is_array($data['Image'][$i])) {
// Force setting the `model` field to this model
$image['model'] = 'Post';
// Unset the foreign_key if the user tries to specify it
if (isset($image['foreign_key'])) {
unset($image['foreign_key']);
}
$images[] = $image;
}
}
}
$data['Image'] = $images;
// Try to save the data using Model::saveAll()
$this->create();
if ($this->saveAll($data)) {
return true;
}
// Throw an exception for the controller
throw new Exception(__("This post could not be saved. Please try again"));
}
}
?>
The above model method will:
- Ensure we only try to save valid images
- Force the foreign_key to be unspecified. This will allow saveAll to properly associate it
- Force the model field to
Post
Now that this is set, we just need a view for our controller. A sample
view for View/Posts/add.ctp
is as follows (fields not necessary for
the example are omitted):
<?php
echo $this->Form->create('Post', array('type' => 'file'));
echo $this->Form->input('Image.0.attachment', array('type' => 'file', 'label' => 'Image'));
echo $this->Form->input('Image.0.model', array('type' => 'hidden', 'value' => 'Post'));
echo $this->Form->end(__('Add'));
?>
The one important thing you’ll notice is that I am not referring to the
Attachment
model as Attachment
, but rather as Image
; when I
initially specified the $hasMany
relationship between an
Attachment
and a Post
, I aliased Attachment
to Image
.
This is necessary for cases where many of your Polymorphic models may be
related to each other, as a type of hint to the CakePHP ORM to
properly reference model data.
I’m also using Model.{n}.field
notation, which would allow you to
add multiple attachment records to the Post. This is necessary for
$hasMany
relationships, which we are using for this example.
Once you have all the above in place, you’ll have a working Polymorphic upload!
Please note that this is not the only way to represent file uploads, but it is documented here for reference.