FuelPHPで×画像○ファイルを出力する方法 ※追記x3

2012 年 1 月 26 日
※追記3
そもそもv1.1のCHANGELOG.mdに
* The `$this->response` Response object is now deprecated. Your action methods should return either a string, View object, ViewModel object or a Response object.
と書いてありました…。
ドキュメントについても、v1.1devから反映されているようです。
https://github.com/fuel/docs/blob/1.1/develop/classes/response.html
というか、v1.1-rc1の同梱ドキュメントを見たら、めちゃめちゃ例が増えとります。捗るぞという感じです。ただし↑の通り、v1.1devで変わっているところもあるので注意が必要ですね。

で、今回グダグダとあれこれやっていたもともとの出発点は、「HTTPレスポンスは$this->requestのお仕事」という前提に立ってしまったところ。これは間違いではないんでしょうけども、ある意味間違い。
FuelPHPのやろうとしていることや拡張していこうとする方向を見誤っていた…という感想です。
ちょっとおおげさですけど。

※追記2
ダウンロードさせようとしているんだから
$this->response->set_header('Content-Type',$mime);
のところは
$mime='application/octet-stream'; $this->response->set_header('Content-Type',$mime);
だったな、と思って追記しようとしていたところ、kenjisさんからさらに「ダウンロードだったらFile::download();が使えるよ」と教えてもらいました。たびたびありがとうございます!
これだと
\File::download($filename_full,$filename);
でOKですね。
必要なヘッダは徹底的に積まれているので、忘れることもなくなります(忘れるので!)。
mimeはデフォルトで'application/octet-stream'になるので、それ以外にしたい場合は
$mime='application/pdf'; \File::download($filename_full,$filename,$mime);
というように書けます。
File::download()が実行された時点で$filename_fullが実在しない場合にはFileAccessExceptionが発生するようなので、事前にチェックしておいても良いし、
try{ \File::download($filename_full,$filename); }catch(\FileAccessException $e){ //例外処理 }
とすることもできると思います。

全く同じやりとりが2ヶ月ほど前に公式フォーラムでありました(笑)
FuelPHP › Forums | General | How to send a file as download

Imageクラス・Fileクラスについては別途まとめた方が良い予感。
ImageMagick使う場合の検証等々含め…。

※追記
kenjisさんから 「Image::output();でできるよ」と教えてもらいました。
ありがとうございます!おっしゃる通り。画像を表示させるということであれば
\Image::load($filename_full); ->output('jpeg');
で終了です。2行。
しかも↓冒頭で書いた「ログインしているユーザーにだけ画像を表示する」というようなパターンや、表示する直前に生成する、というような場合でもこれでOKなわけです。

…で、全くもって私の書き方が悪かったところなわけですが、念頭にあったのは最後段の「ファイル出力全般に〜」というところだったんですね。おまけのように書いてしまっていますが。
だからまわりくどいことをやろうとしている形になっていたわけなのです…。
で、さらに言うと↓で、検証せずに避けた第2引数無しの\File::read();ですが、やっぱりそのまま使えるように見えるんですね。
となると、画像以外のファイルも含めた書き方というのは、
$this->response->set_header('Content-Type',$mime); \File::read($filename_full);
でOK、ということになるんじゃないかと思います。
強制的にDLさせる場合も、
$this->response->set_header('Content-Type',$mime) ->set_header('Content-Disposition','attachment;filename="$filename"'); \File::read($filename_full);
でDLされました。

ということで下記は旅の記録としてのみ残しておきます。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

ファイルとして存在する画像をそのまま表示するのではなく、たとえばログインしているユーザーにだけ画像を表示する、というような場面は結構あるわけでして、そのような場合にFuelPHPでどうやって実現するか、というのが今回のお題です。

単純に考えると、controllerの中で
(前略) header("Content-type:image/jpeg"); readfile($filename);
とすればいいんじゃないのかと思うわけですが、表示されない場合があるようなので、とりあえずはFuelPHPのお作法にのっとった書き方を探ってみようというわけです。

さてそもそも、画面出力する場合のFuelPHPのお作法とは、データを渡したResponseクラスをreturnする、という形になるかと思います。たとえばHTMLの場合こんな感じになっているはずです。
return Response::forge(ViewModel::forge('hogehoge'));
ということは、Responseクラスあたりを見ていけばなにかありそうな気がします。
Responseクラスのドキュメントはこんな感じです。
http://docs.fuelphp.com/classes/response.html

例がResponse::だったり$this->responseだったり、Requestクラスにresponseメソッドというものがあったり、
http://docs.fuelphp.com/classes/request.html#/method_response
どうなっているのかぱっと見よくわかりません。
しかしRequestクラスのドキュメント冒頭にこのような説明があります。
The Request class processes URI requests. It is used by Fuel in index.php to process the users URI request, and you need it to generate requests in an HMVC context.
URLがルーティングされた時点で自動的もしくは暗黙的に生成されるということことなのでしょうかね。
確かにWebアプリのお仕事は基本的にリクエストとレスポンスの間を作ることでしょうし。

さて脱線しましたが、とにかくResponseクラスをきっちり使えばいいのかな?という目星をつけたわけです。
今回の場合は、前段で処理が入っているので、$this->responseで続けてみます。
header("Content-type:image/jpeg");
$this->response->set_header('Content-Type','image/jpeg');
にします。HTTPレスポンスはヘッダ+本体になるわけなので、あとは
readfile($filename);
ができればいいわけです。
ふたたび先ほどのHTMLを返す場合を見てみると
return Response::forge(ViewModel::forge('hogehoge'));
となっていました。
core/response.phpの中を見ると、forgeメソッドの中身はこんな感じ。
public static function forge($body = null, $status = 200, array $headers = array()) { return new static($body, $status, $headers); }
第一引数に表示する中身を入れればよさそうなのですが、ちょっと下を見ていくとbodyメソッドなるものがあります。
/** * Sets (or returns) the body for the response * * @param string The response content * @return $this|string */ public function body($value = false) { if ($value === false) { return $this->body; } $this->body = $value; return $this; }
「@param string」と書かれているのが若干気になりますが、こっちで試してみます。
$this->response->set_header('Content-Type','image/jpeg'); $this->response->body('hogehoge'); var_dump($this->response);
この結果はこうなるので
object(Fuel\Core\Response)#12 (3) { ["status"]=> int(200) ["headers"]=> array(1) { ["Content-Type"]=> string(10) "image/jpeg" } ["body"]=> string(8) "hogehoge" }
ひとまずこのやりかたをキープします。
ではあとは$this->response->body()に画像のデータを乗せればいいはずです。 File::read();あたりが使えそうですね。
Reads a file and returns it ($as_string == true) or adds it to the output ($as_string == false).
ということは、File::read($filename);としてしまうとその場でバイナリデータをぶちまけてしまうので、
$body = \File::read($filename, true);
とすることになります。

これまでの記述をまとめるとこういうことになるかと思います。
$file = \File::read($filename_full, true); $this->response->set_header('Content-Type','image/jpeg'); $this->response->body($file); return $this->response;

これでサクっと画像が表示されました。

試してはいませんが、$this->response->set_header()にimage/jpeg以外のContent-typeを入れたり、Content-Disposition: attachmentあたりを追加して強制DLさせたりとすれば、ファイル出力全般に同じ方法がとれるだろうと思います。