忍者ブログ

STEP UP BLOG

Home > ブログ > php

[PR]

×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。

readfile()での巨大ファイルの出力

むかーしむかし、あるところにおじいさんとおばあさんが住んでいました。
おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。
おばあさんが川で洗濯をしていると、どんぶらこ、どんぶらこ、と大きなCSVファイルが流れてきました。
それはそれは大きく見事なCSVファイルだったので、おばあさんはダウンロードしようとしました。
しかし、ダウンロードしようとしたらブラウザが真っ白になってしまいました。
おやおや、困ったものだとおばあさんは途方に暮れました。
・・・・・・


というわけで巨大なファイルをダウンロード出来なかったという話です。
あらかじめ大きなファイルだということは認識していたので、
サーバ側PHPのファイル出力にはreadfile()を用いています。

http://php.net/manual/ja/function.readfile.php
注意: readfile() 自体にはメモリに関する問題はなく、 巨大なファイルを送ってもかまいません。out of memoryエラーが出る場合は、 ob_get_level() で出力バッファリングを無効にしてください。

マニュアルの注意書きにも、巨大なファイルを送ってもかまわないと書いてあります。
しかし、実際はエラー。
エラーログを確認するとメモリーのエラーです。
おやおや、とマニュアルの注意書きの"out of memoryエラーが出る場合は、 ob_get_level() で出力バッファリングを無効にしてください。"がなんだか怪しそうです。
試しにソースコードのファイル出力部分でob_get_level()を実行してみると、2と出ました。

http://php.net/manual/ja/function.ob-get-level.php
返り値 ¶ 出力バッファリングハンドラのネストレベルを返します。 バッファリングがアクティブでない場合はゼロを返します。

によると、この関数は出力バッファリングのネストレベルを返し、無効の場合は0を返すとのこと。
ということは、readfile()で巨大なファイルを出力するためにはob_get_level()が0でないといけない。
出力バッファリングを無効にするためにはob_end_clean()を実行すればよいとのことで、
実行してob_get_level()で確認したら0になるわけではなく2から1減ってネストレベルが1になっただけ。
デクリメントかい。
ob_get_level()が0になるまでob_end_clean()を実行します。
while (ob_get_clean()) {
    ob_end_clean();
}

このようにして出力バッファリングを無効にした後にreadfile()することで、
やっと巨大ファイルのダウンロードに成功しました。
めでたしめでたし。
PR

PHPの参照渡しにハマる

ゲームに課金しすぎて依存症になるといった意味ではなくハマったので備忘録がてらブログに書こうと思います。
仕事柄PHPを書く機会が多く、というよりもほとんどPHPしか書いていないのですが、
そうなってくるとなんでもPHPで書きたくなってきます。
これシェルで書いたほうがいいのでは? という処理でも、
とりあえずPHPで出来るか考えてしまいます。
挙句の果てに、シェルコマンド便利だからPHPからシェル叩きましょうみたいな思考になっていき、本日の本題に入ってくるわけです。
PHPで外部プログラムを実行する手段としてはexec()やsystem()があります。
先日私が使ったのはexec()で、そのときにハマった箇所を再現出来そうな例を以下に示します。
<?php

exec('ls .', $output);

foreach ($output as $file) {
    exec('wc -m '.$file, $wc);
    echo ($wc[0]."\n");
}

やりたいこととしてはファイルの一覧を取ってきて、ファイル毎に文字数を出力するというものです。
exec('ls .', $output);

の第2引数に実行結果が配列で格納されます。
で、実行すると、、、
$ php wc.php 
8 a.txt
8 a.txt
8 a.txt
8 a.txt

なんだか同じファイルの文字数が出力されますね。。
少し変えて、
<?php

exec('ls .', $output);

foreach ($output as $file) {
    exec('wc -m '.$file, $wc);
    echo ($file.':'.$wc[0]."\n");
}

としてみると、
$ php wc.php 
a.txt:8 a.txt
b.txt:8 a.txt
c.txt:8 a.txt
wc.php:8 a.txt

wcはループでちゃんと実行されているっぽいけど、結果の出力がおかしい??
ここまできたらvar_dump()しちゃいましょう。
<?php

exec('ls .', $output);

foreach ($output as $file) {
    exec('wc -m '.$file, $wc);
    var_dump($wc);
}

えい!!
$ php wc.php 
array(1) {
  [0]=>
  string(7) "8 a.txt"
}
array(2) {
  [0]=>
  string(7) "8 a.txt"
  [1]=>
  string(8) "12 b.txt"
}
array(3) {
  [0]=>
  string(7) "8 a.txt"
  [1]=>
  string(8) "12 b.txt"
  [2]=>
  string(7) "4 c.txt"
}
array(4) {
  [0]=>
  string(7) "8 a.txt"
  [1]=>
  string(8) "12 b.txt"
  [2]=>
  string(7) "4 c.txt"
  [3]=>
  string(10) "192 wc.php"
}

配列に実行結果が追加されている。。
そうか。参照渡しだからこうなっているのか。。
というところで原点に立ち戻ってマニュアルを確認してみると、

http://php.net/manual/ja/function.exec.php
配列に既に何らかの要素が 含まれる場合は、exec() は配列の最後に追加されることに注意してください。関数が要素を追加することを望まないのなら、それが exec() に渡される前に、配列の unset() を呼び出してください。

パラメータの章でちゃんと注意がされていますね。。。
unset()しなさいという話。
というわけで最終的には以下のようなソースになって希望通りの動きとなりました。
<?php

exec('ls .', $output);

foreach ($output as $file) {
    unset($wc);
    exec('wc -m '.$file, $wc);
    echo ($wc[0]."\n");
}

$ php wc.php 
8 a.txt
12 b.txt
4 c.txt
190 wc.php

結論としては、
・まずはマニュアルをしっかり読みましょう。
・参照渡しはバグが入り込みやすいので、なるべく使わないにこしたことはないですよね。

ログ取ったら接続元が全部同じIPだったよ(号泣)

PHPで独自にロギング機能を実装して、いざ本番リリースしたらタイトルのような不具合が発生しました。
原因はロードバランサを通していたからでした。
$_SERVER['REMOTE_ADDR']

から接続元IPを取っていたところを
$_SERVER['HTTP_X_FORWARDED_FOR']

が送られてくる場合はそちらを取るようにしました。
というより、まずはリリース前にサーバ構成を確認しましょう。。

PHPの保守性を高めるためのタイプヒンティング

PHPは語弊を恐れずに言えば、なんでもできる言語です。
変数にはどんな値でも入れられます。
例えばC言語で
int x = 1;
x = "こんにちは";
はコンパイルエラーが出ます。しかしPHPは、
$x = 1
$x = array(1,2,3)
でもエラーが出ません。
つまり変数の中に何が入っていても文句は言われないのです。

そんなPHPで関数に何か値が渡されてきたとします。
引数で渡された値が文字列なのか配列なのか、よくやるチェックに
function f($x)
{
  if (is_array($x)) {
     // 配列用の処理
  } else {
     // 配列以外の処理
  }
}

みたいなのがあると思います。
チェックしないで$x[0]とかすると、$xが文字列でもエラーは出ません(文字列の1文字目が返される)。
これは正常実行されるので、その後でこれが元のエラーが出ても原因がわかりづらい。
そこでタイプヒンティングです。
function f(array $x)
{
  // 配列の処理
}

とすることで、引数として配列が必ず渡ってくるものと宣言します。
配列が来なかったらエラーが発生します。
これで予測不可能なエラーから予測可能なエラーになりました(^^)b
タイプヒンティングにはクラスも指定できるので、特定のクラスで渡されてこなかったらエラーみたいなのもできます。
後から読む人もこれで、どんな型の値が渡されてくる関数かある程度予測でき保守性も上がるのでどんどん活用していきましょう。


まあ、基本的に配列型かスカラー型のどちらでも送られてくる可能性のある関数は実装上よろしくないなと思っています。。

LaravelでPHPUnitを使う

2014/10/23現在、ComposerでLaravelをインストールするとPHPUnitは入っていません。
既に環境にPHPUnitがインストールされていればよいのですが、そうでない場合は一手間かかります。
まずはcomposer.jsonを編集します。
"require-dev": {
  "phpunit/phpunit": "4.3.*",
  "phpunit/dbunit": ">=1.2",
  "mockery/mockery": "dev-master@dev"
},
こちらを追加しましょう。mockeryはモックのライブラリです。
$ composer update
これで上記のライブラリがインストールされます。
その後、vendor/binをPATHに追加することでPHPUnitが使えるはずです。
$ phpunit --version
PHPUnit 4.3.4 by Sebastian Bergmann.
Laravelでは、テストをapp/tests以下に置きます。
既にサンプルのテストケースがあると思うので、それに合わせて作ってみるのが手っ取り早いです。
http://laravel4.kore1server.com/docs/testing
DBを使う場合は以下URLを参考にしました。
http://qiita.com/ngmy/items/c1487991d48ddba9688d
テスト時はapp/config/testingの設定が使われます。
自分は開発とは別にテスト用のDBを用意して、 app/config/testing/database.php に記述しました。
それほど戸惑うことなくテスト実行までできたかなという印象です。
モックの使い方はまたこんど。。

PAGE TOP