fuelphpのファイルアップロードを使いこなすメモ
こんにちは。
開発チームのワイルド担当、まんだいです。
単純にfuelphpでファイルアップロードするのは、非常に簡単で、ドキュメントのコードをコピペすれば、だいたいは動いてくれます。
ただ、手を加えるのは、少し勝手がわからない事が多かったので、まとめてみました。
ファイルアップロードの準備(HTML + CSS + JS)
ファイルアップロードを実装する前に、HTMLなどのフロント側の素材を用意します。
まずはHTMLから。
formタグの中にinputタグですね。
<form id="form" name="form" method="post" enctype="multipart/form-data"> <input type="file" name="f" class="hidden" id="f" multiple="multiple"> <label for="f"> <span class="btn">ファイル選択</span> <span class="selected" id="f_selected">選択されていません</span> </label> </form>
続いて、CSS。
input.hidden { /* display: none;*/ } label > span.btn { display: inline-block; border: 1px solid #999; font: 13.333px arial; height: 21px; line-height: 21px; padding-left: 0.25em; padding-right: 0.25em; border-radius: 2px; background: -webkit-linear-gradient(#fcfcfc, #dcdcdc); background: linear-gradient(#fcfcfc, #dcdcdc); } label > span.btn:active { background: -webkit-linear-gradient(#dcdcdc, #fcfcfc); background: linear-gradient(#dcdcdc, #fcfcfc); } label > span.btn:hover { border: 1px solid #666; } label > span.selected { display: inline-block; font: 13.333px arial; height: 21px; line-height: 21px; } label { -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; user-select: none; }
最後はJavaScriptです。
(function(){ var f = document.getElementById('f'); var label_selected = document.getElementById(f.id + '_selected'); f.addEventListener('change', function(){ if (f.files.length == 0){ label_selected.textContent = '選択されていません'; } else if (f.files.length == 1){ label_selected.textContent = f.files[0].name; } else { label_selected.textContent = f.files.length + ' ファイル'; } }); })();
これをブラウザで表示してみると、以下のようになります。chromeがオススメです。
その理由は、表示すれば一目瞭然。
という感じで、ファイルアップロードボタンが2つ。
どちらのボタンも押せば動作します。
実体は、左の方で、右の方はlabelタグを使った模倣品ですね。
ただやりたかっただけです。
ブラウザ間でフォーム部品の表示は違いますし、ファイルアップロードボタン自体をデザインに則したものにしたいという要望もあって、こういう小技は役に立ちます。
CSS + JSで挙動や見た目もだいたい合わせてあって、今回はchromeのフォーム部品に似せてあります。
CSSでinputタグに「display: none」を追加すれば、通常のフォームと何ら変わらぬ挙動をします。
(コメントアウトしております)
大事な事としては、formタグにenctypeの属性を忘れないということでしょうか。
フロントの準備はこれで完了です。
fuelphpの実装(下準備)
実装の前に、各種設定と、モデルなどの整備を行います。
fuelphpは設定第一なので、configファイルで振る舞いを大きく変える事が可能です。
まず、アップロードに際して、アップロード用のconfigファイルを用意します。
元になるconfigファイルは、「fuel/core/config/upload.php」にありますので、これを「fuel/app/config」以下にコピーして、設定を変更していきます。
必須の設定項目は、pathですね。
保存先ですが、public以下に設定すると、ダウンロードの仕組みは簡単になりますが、個人情報が記載されたPDFなどは、URLが分かるとダウンロードし放題になってしまうので、public以下に置くのは危険です。
アクセスコントロールする必要があるなら、publicより上の階層にfilesなどのディレクトリを作成して指定する事にしましょう。
その場合、外部からアクセスする手段がなくなってしまいダウンロードできないので、オーバーヘッドが気になるところですが、ダウンロード用のURLを作成するコントローラを別途作成して対応します。
この辺りはファイルアップロードとは少し内容がズレてくるので、別の機会に記事にできればと思っています。
今回は、publicと同階層(プロジェクトルート直下)にfilesディレクトリを作成し、ファイルを保存するようにします。
また、保存するファイル名は、configファイル内の、randomizeをtrueにして、32文字のMD5ハッシュを用いるようにしました。
修正したconfig/upload.phpは以下のようになりました。
return array( 'path' => '/var/www/html/test/files', 'randomize' => true, );
次に、モデルを作成します。
保存するファイル名にハッシュ値を付けるため、元のファイル名がわからなくなるため、それを保存するのが主な役割です。
保存先のuploadsテーブルを作成するためのSQL文は以下の通りです。
CREATE TABLE `uploads` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `origin_name` varchar(256) NOT NULL, `file_name` varchar(256) NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), KEY `idx_file_name` (`file_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
このSQLを実行した後、以下のようなoilコマンドを使ってモデルを作成するのが一番手っ取り早いです。
php /path/to/oil r fromdb:model uploads
また、ORMパッケージを利用するので、config.php内のalways_loadの項目を修正しておきます。
packagesの部分をアンコメントすれば自動的にロードされるようになります。
fuelphpの実装(コントローラ)
データがポストされたときのコントローラの振る舞いを実装していきます。
コントローラに記載するソースコードは以下のようになりました。
function post_index() { $data = []; $this->template->title = 'test'; /* * randomize処理を行った結果、ファイル名は以下のようになる * [0-9a-f]{32}.[ext] * オリジナルのファイル名は別フィールドに保存するので、ファイル名はハッシュ値だけにする処理を保存前に導入する */ Upload::register('before', function(&$file) { $file['filename'] = substr($file['filename'], 0, 32); $file['path'] .= $file['filename'][0]. '/'. $file['filename'][1]. '/'; }); if (Upload::is_valid()) { Upload::save(); foreach (Upload::get_files() as $file) { Model_Upload::add($file); } } foreach (Upload::get_errors() as $file) { // エラー処理 } $this->template->content = View::forge('upload/index', $data); }
configのupload.phpで「auto_process」をtrueにした場合(デフォルト)、Upload::process()は明示的に書く必要はありません。
ドキュメントによると、「'auto_process' => true」の時に、Upload::process()を実行すると、Upload::process()が2度実行されるようです。
Upload::process()でconfigの上書きができるので、複数の箇所でファイルアップロード処理の記述がある場合は、auto_processをfalseにしておき、都度Upload::process()を実行する形が望ましいようです。
Upload::register()メソッドを使って、ファイル保存前に処理を割り込ませています。
第一引数は'validate'、'before'、'after'の中から1つを選びます。
今回は、保存前にファイル名と保存用のパスを変更するので、beforeを選択しました。
config/upload.phpでrandomizeをtrueにした場合、ファイル名は32文字のハッシュ値になりますが、拡張子がオリジナルのものが付いてきます。
完全に32文字のファイル名で統一するため、ファイル名をハッシュ値だけにする処理を入れています。
また、保存先ですが、ハッシュ値の1文字目と2文字目でディレクトリを作成してパスとして再設定しています。
アップロードするファイルの数が少ない場合は特に必要ないですが、アップロードするファイルの数が多い場合、数万ファイル格納されたりすると、読み書きのレスポンスが著しく低下します。
これに対する対応として、1ディレクトリ内のファイル数を分散させる目的で処理を追加しています。
Upload::register()内のクロージャで引数になっている$fileは、fuel/vendor/fuelphp/upload/src/File.php内にかかれているクラスが渡されており、外部からこのクラスにアクセスする手段はなさそうです。
'before'(保存前)の時点でこのクラスを編集すれば、保存パスやファイルの操作は可能になります。
$fileをダンプすると、以下のような内容になります。
Fuel\Upload\File::__set_state(array( 'container' => array ( 'element' => 'f', 'filename' => 'e9477391cbeefc40e69f7e1bc24868d2', 'name' => 'test.html', 'type' => 'text/html', 'tmp_name' => '/tmp/phpDsDmiL', 'error' => 0, 'size' => 335, 'extension' => 'html', 'basename' => 'test', 'mimetype' => 'text/html', 'path' => '/home/vagrant/workspace/test/files/e/9/', ), 'index' => 0, 'errors' => array ( ), 'config' => array ( 'langCallback' => '\\Upload::lang_callback', 'moveCallback' => NULL, 'max_size' => 0, 'max_length' => 0, 'ext_whitelist' => array ( ), 'ext_blacklist' => array ( ), 'type_whitelist' => array ( ), 'type_blacklist' => array ( ), 'mime_whitelist' => array ( ), 'mime_blacklist' => array ( ), 'prefix' => '', 'suffix' => '', 'extension' => '', 'randomize' => true, 'normalize' => false, 'normalize_separator' => '_', 'change_case' => false, 'path' => '/var/www/html/test/files', 'create_path' => true, 'path_chmod' => 511, 'file_chmod' => 438, 'auto_rename' => true, 'new_name' => false, 'overwrite' => false, ), 'isValidated' => true, 'isValid' => true, 'callbacks' => array ( 'before_validation' => array ( ), 'after_validation' => array ( ), 'before_save' => array ( 0 => Closure::__set_state(array( )), ), 'after_save' => array ( ), ), ))
ややこしいですが、Upload::get_files()で得られるファイル情報は、上記のクラスではなく、Fuel\Upload\Fileのcontainer部分だけになります。
また、Upload::get_files()の情報を変更しても、実際のファイル情報は変更できないので、アップロードされたファイルの情報は、beforeイベントで変更するしかないようです。
この辺りは、ドキュメントのregisterメソッドの項に詳しく載っています。
以上です。