ポリモーフィックな添付ファイルモデルを使ったファイルの保存
複数のモデルで複数のファイルのアップロードを保存したいが、データベースが正規化されているので複数の テーブルは使いたくない、という場合があります。たとえば、ギャラリー用に多数の画像を持てる Post モデルと、多数の動画を持つ Message モデルがあるとします。このような場合は Attachment モデルを 使います。
Post hasMany Attachment
Attachment モデルには、次のようなデータベーススキーマを使えます。
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`)
);これで、添付ファイルのレコードは名前を持ち、その場で有効・無効を切り替えられるようになります。 このスキーマは単なる例であり、こうした機能はアプリケーションの中で実装する必要があります。
attachments テーブルを作成したら、次のモデルを作成します。
class Attachment extends AppModel
{
public $actsAs = [
'Upload.Upload' => [
'attachment' => [
'thumbnailSizes' => [
'xvga' => '1024x768',
'vga' => '640x480',
'thumb' => '80x80',
],
],
],
];
public $belongsTo = [
'Post' => [
'className' => 'Post',
'foreignKey' => 'foreign_key',
],
'Message' => [
'className' => 'Message',
'foreignKey' => 'foreign_key',
],
];
}Post モデルにも、正しい逆方向のリレーションを作成する必要があります。
class Post extends AppModel
{
public $hasMany = [
'Image' => [
'className' => 'Attachment',
'foreignKey' => 'foreign_key',
'conditions' => [
'Image.model' => 'Post',
],
],
];
}ここで重要なのは、Post モデルの Attachment モデルへのリレーションに、Image.model が Post でなければならないという条件が付いていることです。model フィールドには Post、または添付先にしたい モデルを忘れずに設定してください。そうしないと、find のクエリを実行したときに、正しくない リレーションの結果が返されることがあります。
Message モデルにも同じようなリレーションが必要です。
class Message extends AppModel
{
public $hasMany = [
'Video' => [
'className' => 'Attachment',
'foreignKey' => 'foreign_key',
'conditions' => [
'Video.model' => 'Message',
],
],
];
}モデルの準備ができたので、コントローラーに適切なアクションを作成します。簡潔にするために、 Post モデルについてだけ説明します。
class PostsController extends AppController
{
public $components = ['Flash'];
/* the rest of your controller here */
public function add()
{
if ($this->request->is('post')) {
try {
$this->Post->createWithAttachments($this->request->data);
$this->Flash->success(__('The post has been saved'));
} catch (Exception $e) {
$this->Flash->error($e->getMessage());
}
}
}
}上の例では、Post モデルの独自の createWithAttachments メソッドを呼び出しています。これで、Post を 作成する処理を 1 か所にまとめられます。このメソッドは次のとおりです。
class Post extends AppModel
{
/* the rest of your model here */
public function createWithAttachments($data)
{
// Sanitize your images before adding them
$images = [];
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'));
}
}上のモデルのメソッドは次のことを行います。
- 有効な画像だけを保存しようとするようにします
- foreign_key が指定されていない状態にします。これで saveAll が正しく関連付けられます
- model フィールドを
Postにします
これで準備ができたので、あとはコントローラーのビューが必要です。templates/Posts/add.ctp (レガシーレイアウトでは app/View/Posts/add.ctp) のビューの例は次のとおりです (この例に必要のない フィールドは省略しています)。
<?php
echo $this->Form->create('Post', ['type' => 'file']);
echo $this->Form->input('Image.0.attachment', ['type' => 'file', 'label' => 'Image']);
echo $this->Form->input('Image.0.model', ['type' => 'hidden', 'value' => 'Post']);
echo $this->Form->end(__('Add'));1 つ重要な点として、Attachment モデルを Attachment ではなく Image として参照していることに 気付くでしょう。最初に Attachment と Post の間の $hasMany リレーションを指定したときに、 Attachment に Image というエイリアスを付けました。これは、多くのポリモーフィックなモデルが互いに 関連している場合に、CakePHP の ORM がモデルのデータを正しく参照するための一種の ヒント として 必要です。
また、Model.{n}.field という記法を使っているので、Post に複数の添付ファイルのレコードを追加できます。 これは、この例で使っている $hasMany リレーションに必要です。
ここまでの準備がすべて整えば、ポリモーフィックなアップロードが動作します。
これはファイルのアップロードを表現する唯一の方法ではありませんが、参考としてここに記載しています。
