CakePHPの超便利なファイルアップロードプラグイン、FileBinderプラグインの使い方をまとめてみた。

CakePHPを利用している上で、一番悩むのがファイルのアップロードとその管理です。

MediaPluginという有名なファイルアップロードプラグインがありますが、高機能・多機能との引き替えに、インストール方法やその利用方法が非常にわかりづらく、さらにプラグインをバージョンアップしただけでエラーを吐いて動かなくなったりと、常時メンテナンス、するプロジェクトに導入するには少々の抵抗があります。

そこで、もっとシンプルで使いやすいファイルアップロードプラグインである、FileBinderプラグインを紹介したいと思います。

FileBinderプラグインの特徴

FileBinderプラグインは、ファイルアップロードに関して下記のような機能を持っています。

  • ファイルサイズの制限やファイルタイプの制限など、アップロードしたファイルのバリデーション
  • ファイルの保存方法を任意のディレクトリに保存するか、DBにバイナリとして保存するか選べる
  • 全てのファイルを1つのテーブルで管理できる
  • 任意のテーブルにファイルの情報を仮想のフィールドとして組み込むことができる

ファイルアップロードで必要とされるであろうほとんどの機能が実装されていて、普通に使う分であれば何一つ不自由はしません。

そして、FileBinderプラグインの特徴と言える機能が、上記でも紹介している、任意のテーブルにファイルの情報を仮想フィールドとして組み込むことができる機能です。

この機能は、特定のテーブルの検索結果に仮想のフィールドを作成し、そこにファイルの情報を埋め込むことで、あたかもそのテーブルにファイルのデータが格納されているかのように扱うことができるのです。

仮想フィールドの例

例えば下記のようなプロフィールを管理するテーブルがあったとします。

  • first_name
  • last_name
  • age
  • blood_type
  • address
  • email
  • phone

このテーブルにはプロフィール画像などを格納するフィールドはありません。
しかし、FileBinderプラグインを用いると、このテーブルの構造に全く手を付けずに、検索結果にプロフィール画像の情報を埋め込む事が可能になります。

  • first_name
  • last_name
  • age
  • blood_type
  • address
  • email
  • phone
  • profile_image
    • file_size
    • file_name
    • file_content_type
    • file_object

この機能は非常に便利です。

テーブルに手を加えなくても良いのでデータベース設計が柔軟になりますし、1つのテーブルにいくつでも仮想フィールドを加えることができるため、仕様変更が発生した場合でも簡単に対応できます。

また、Mediaプラグイン同様にファイルの情報を1つのテーブルで管理できるため、情報の管理がしやすくなるというメリットもあります。

基本的な利用の流れ

FileBinderプラグインの基本的な利用方法は、下記のようになっています。

  1. ファイルの情報を組み込みたいモデルにBindableビヘイビアを組み込む
  2. 上記のモデルにファイルアップロードに関する設定を定義
  3. 上記のモデルを利用するコントローラーにRingコンポーネントを組み込む
  4. ファイルアップロードを含んだフォームをコントローラーに送信
  5. コントローラーでbindUpメソッドを実行
  6. モデルのsaveメソッドでフォームデータを保存

ビヘイビアとコンポーネントを利用するので多少複雑に感じますが、利用し始めると全くその複雑さは感じません。

実行環境

解説は下記の環境を前提に行っています。

  • PHP5系
  • CakePHP 1.3系

CakePHPのバージョンは1.2系でも動作しそうではありますが、実際に試していないので動作保証はできません。

1.2系をご利用の場合は、コンソールからの実行コマンドが一部違ったりするため、その点は読み替えてご利用下さい。

プラグインファイルのダウンロード

下記のURLにアクセスして、最新バージョンをダウンロードして、app/plugins/ ディレクトリ以下に展開してください。

fusic/filebinder – GitHub

Attachmentモデルの作成

まずはファイル情報を格納するためのモデルである、Attachmentモデルを作成します。

プラグインのconfig/ディレクトリ以下にschema.phpファイルがありますが、そのまま実行するとバイナリデータ保存用のフィールドの型指定がCakePHPの許可する型に無いため、エラーが発生してしまいます。

そのため、下記のようにfile_objectフィールドの型をlongtextからtextなどに変更します。

class AppSchema extends CakeSchema {
    var $name = 'App';

    function before($event = array()) {
        return true;
    }

    function after($event = array()) {
    }

    var $attachments = array(
                           'id' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),
                           'model' => array('type' => 'text', 'null' => false, 'default' => NULL),
                           'model_id' => array('type' => 'integer', 'null' => false, 'default' => NULL),
                           'field_name' => array('type' => 'text', 'null' => false, 'default' => NULL),
                           'file_name' => array('type' => 'text', 'null' => false, 'default' => NULL),
                           'file_content_type' => array('type' => 'text', 'null' => false, 'default' => NULL),
                           'file_size' => array('type' => 'integer', 'null' => false, 'default' => NULL),
                           'file_object' => array('type' => 'text', 'null' => true, 'default' => NULL), // この行のタイプを text に変更
                           'created' => array('type' => 'timestamp', 'null' => true, 'default' => NULL),
                           'modified' => array('type' => 'timestamp', 'null' => true, 'default' => NULL),
                           'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)),
                           'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'InnoDB')
                           );
  }

そして、コンソールから先ほどのschema.phpを利用してテーブルを作成します。

コンソールからの実行コマンドが間違えていたのを修正しました。(2011.07.26)

cake schema create -app your_app_name -plugin filebinder -name app

データベースにファイルを保存する場合、先ほど変更したfile_objectフィールドのデータ型を、データベースのコンソールや設定画面から変更しておきましょう。

さもないと、text型では最大サイズの都合から、テキストファイルや小さな画像程度しか保存できなくなってしまいます

また、忘れずにモデルクラスも作成しておきます。
app/models/attachment.php

class Attachment extends AppModel
{
}

Bindableビヘイビアの組み込み

ファイルアップロードのキモとなるBindableビヘイビアを、ファイルの情報を結び付けたいモデルに組み込みます。

class Profile extends AppModel
{
	public $actsAs = array(
		'Filebinder.Bindable' => array(
			'model' => 'Attachment', // ファイル情報を保存するモデル名
			'filePath' => WWW_ROOT . 'img' . DS, // ファイルを保存するディレクトリ(絶対パス)
			'dbStorage' => true, // ファイルをバイナリデータとしてデータベースに保存するか
			'beforeAttach' => null, // フック関数(ファイル保存前)
			'afterAttach' => null, // フック関数(ファイル保存後)
			'withObject' => false, // 検索結果にファイルのバイナリデータを付加するか
		)
	);
}

オプションを指定しない場合、上記の値がデフォルト値として利用されます。

bindFields変数の定義

そして、ファイル情報を結び付ける仮想フィールドの設定を、$bindFieldsというメンバ変数として下記のように定義します。

class Profile extends AppModel
{
	public $actsAs = array(
		'Filebinder.Bindable' => array(
			'model' => 'Attachment',
			'filePath' => WWW_ROOT . 'img' . DS,
			'dbStorage' => true,
			'beforeAttach' => null,
			'afterAttach' => null,
			'withObject' => false,
		)
	);

	public $bindFields = array(
		array(
			'field' => 'profile_image', // ファイル情報を組み込む仮想フィールド名
			'filePath' => WWW_ROOT . 'profile_image' // ファイルを保存するディレクトリ(絶対パス)。指定されない場合、ビヘイビアの設定が利用されます。
		),
	);
}

もし、仮想フィールドを複数指定したい場合は下記のように指定します。

public $bindFields = array(
	array(
		'field' => 'file_upload_filed_name1',
		'filePath' => WWW_ROOT . 'file1'
	),
	array(
		'field' => 'file_upload_filed_name2'
		'filePath' => WWW_ROOT . 'file2'
	),
	array(
		'field' => 'file_upload_filed_name3'
	),
);

バリデーションの定義

Bindableビヘイビアにはファイルアップロード専用のバリデーションメソッドが揃っています。

checkContentType
ファイルのメタ情報を制限します。
array(
	'profile_image' => array(
		'rule' => array('checkContentType', array('image/jpeg', 'image/gif', 'image/png'))
	)
);
checkExtension
ファイルの拡張子を制限します。
array(
	'profile_image' => array(
		'rule' => array('checkExtension', array('jpg', 'gif', 'png'))
	)
);
checkFileSize
ファイルサイズを制限します。KBMBGBなどの単位が利用出来ます。
array(
	'profile_image' => array(
		'rule' => array('checkFileSize', '10MB')
	)
);
checkMinFileSize
ファイルの最小サイズを制限します。KBMBGBなどの単位が利用出来ます。
array(
	'profile_image' => array(
		'rule' => array('checkMinFileSize', '10MB')
	)
);
funcCheckFile
ユーザー関数でファイルのバリデーションを行います。
array(
	'profile_image' => array(
		'rule' => array('funcCheckFile', 'userFunction')
	)
);
notEmptyFile
ファイルの未アップロードを制限します。
array(
	'profile_image' => array(
		'rule' => array('notEmptyFile')
	)
);

必要に応じて、仮想フィールドを組み込むモデルにバリデーションを定義します。

class Profile extends AppModel
{
	public $actsAs = array(
		'Filebinder.Bindable' => array(
			'model' => 'Attachment',
			'filePath' => WWW_ROOT . 'img' . DS,
			'dbStorage' => true,
			'beforeAttach' => null,
			'afterAttach' => null,
			'withObject' => false,
		)
	);

	public $bindFields = array(
		array(
			'field' => 'profile_image',
			'filePath' => WWW_ROOT . 'profile_image'
		),
	);

	public $validate = array(
		'profile_image' => array(
			'fileSize' => array(
				'rule' => array('checkFileSize', '1MB'),
				'message' => 'ファイルサイズは1MB以内でアップロードしてください。'
			),
			'fileExt' => array(
				'rule' => array('checkExtension', array('jpg', 'gif', 'png')),
				'message' => '許可されていない拡張子を利用しています。'
			)
		)
	);
}

Ringコンポーネントの組み込み

フォームからのデータを受け取るコントローラーに、Ringコンポーネントを組み込みます。

class ProfileController extends AppController
{
	public $components = array(
		'Filebinder.Ring'
	);
}

特に設定は必要ありません。

フォームを作成

ファイルアップロードを行うフォームを作成します。
このとき、ファイルを選択する入力フォームは、先ほど定義した仮想フィールドの名前と同一にします。

フォームの内容がおかしかったのを修正しました。 (2011.07.26)

<?php echo $this->Form->create(array('type' => 'file')) ?>
<dl>
<dt>性</dt>
<dd><?php echo $this->Form->text('first_name') ?></dd>
<dt>名</dt>
<dd><?php echo $this->Form->text('last_name') ?></dd>
<dt>年齢</dt>
<dd><?php echo $this->Form->text('age') ?></dd>
<dt>血液型</dt>
<dd><?php echo $this->Form->text('bloodtype') ?></dd>
<dt>住所</dt>
<dd><?php echo $this->Form->text('address') ?></dd>
<dt>メールアドレス</dt>
<dd><?php echo $this->Form->text('email') ?></dd>
<dt>電話番号</dt>
<dd><?php echo $this->Form->text('phone') ?></dd>
<dt>プロフィール画像</dt>
<dd><?php echo $this->Form->file('profile_image') ?></dd>
</dl>
<?php echo $this->Form->end() ?>

仮想フィールドを複数作成した場合、それに対応した入力フォームを用意しましょう。

BindUpメソッドを実行し、データを保存

フォームのデータを受け取るコントローラーのアクションで、RingコンポーネントののbindUpメソッドを実行し、あと通常通りデータを保存します。

bindUpメソッドはファイルアップロードの前処理を行うメソッドで、このbindUpメソッドを実行しないとファイルが保存されません。

class ProfileController extends AppController
{
	public $components = array(
		'Filebinder.Ring'
	);

	public function add()
	{
		if (!empty($this->data)) {
			$this->Ring->bindUp();

			if (!$this->Profile->save()) {
				// 保存完了
			}
		}
	}
}

このとき、bindUpメソッドの引数には、ファイルの保存処理を行うモデル名を指定する事ができます。

このモデル名は、先ほどBindableビヘイビアを組み込んだモデルを指定しますが、省略した場合、自動的にコントローラーの$modelClass変数に定義されたモデルが利用されます。

データが登録されているか確認する

保存したモデルのデータを実際に確認してみます。

$profile = $this->Profile->read();
debug($profile);
[Profile] => Array
(
    [id] => 1
    [first_name] => 太郎
    [last_name] => 山田
    [age] => 25
    [blood_type] => A
    [address] => 東京都千代田区
    [email] => taro.yamada@test.com
    [phone] => 03-1234-5678
    [craeted] => 2011-07-20 00:00:00
    [modified] => 2011-07-20 00:00:00
    [profile_image] => Array
        (
            [id] => 1
            [model] => Profile
            [model_id] => 1
            [field_name] => profile_image
            [file_name] => sample.jpg
            [file_content_type] => image/jpeg
            [file_size] => 65536
            [created] => 2011-07-20 00:00:00
            [modified] => 2011-07-20 00:00:00
            [file_path] => /app/webroot/profile_image/Profile/1/profile_image/sample.jpg
            [bindedModel] => Attachment
        )
)

正しくファイルの情報が結びついて読み出されているのがわかりますね。

注意

この検索結果にファイルの情報を結びつける機能は、ビヘイビアのafterFindメソッドで実現しています。

CakePHPでは、ビヘイビアのaftetFindメソッドは、そのビヘイビアが組み込まれたモデルが直接検索された時にしか呼ばれません

要するに、AというモデルにhasManyで結びつけられたBというモデルがあり、そのBモデルにFileBinderビヘイビアが組み込まれているとします。

この時、Aモデルをfindした結果にBモデルの情報が存在していても、Bモデルの情報にはファイルの情報は結びついて来ません。

これは実に不便なのですが、CakePHPの仕様のため、ビヘイビアではどうすることもできません。

ビューでの使い方

取得したファイルの情報を表示するには、付属のLabelヘルパーが便利です。
Labelヘルパーには下記のような機能があります。

  • ファイル情報の配列からaタグ(リンク)を生成
  • ファイル情報の配列からimgタグを生成
  • ファイルがブラウザから参照出来ない場所にある場合でも、ファイルがダウンロード・表示できるようにURLを生成してくれる

Labelヘルパーの使い方

HTMLヘルパーに似ているので、使い方は簡単です。

ファイル情報からaタグを生成する
echo $this->Label->link($profile['profile_image']);
ファイル情報からimgタグを生成する
echo $this->Label->image($profile['profile_image']);

ファイルの削除

仮想フィールドに登録したデータを削除したい場合、下記のようなフィールドを利用します。

$this->Form->checkbox('profile_image_delete');

仮想フィールド名+_deleteという名称のフィールドが送信された場合、そのフィールドの値が真であれば、対応する仮想フィールドに結びついているファイル情報が削除されます。

とても便利なプラグイン

以上、基本的な使い方を解説しましたが、使い慣れると手放せなくなるほど便利なプラグインです。

ファイルのバージョン生成機能やキャッシュ機能などはありませんが、Mediaプラグインのように面倒なインストールや設定作業が必要ではなく、フック関数を利用することで様々な利用方法が考えられます。

是非一度、利用してみてください。