WEB+DB Press 86 に PHP での画像処理についての記事を書きました
4/23 発売の WEB+DB Press Vol.86 に PHP での画像処理についての記事を書きました。 画像をこねくり回すという内容ではなく、Web アプリで画像を扱うときに気にするべきことを広く浅くカバーしようという内容です。基本的な Imagick (imagemagick) の使い方から、有名な最適化 Tips、それからジョブキューを活用した負荷対策や、サムネイルの動的生成などについて実際に pixiv で使われている技術をベースに解説しています。PHP に限らずこれから画像を扱うシステムを作ろうとしているけれど、ちょっと自信がないなあという人にはおすすめの内容です。是非手に取ってみてください。
- 作者: 結城洋志,沖元謙治,足永拓郎,林健太郎,大竹智也,内田誠悟,伊藤直也,中山裕司,hiroki.o,泉水翔吾,佐藤太一,高橋俊幸,西尾泰和,舘野祐一,中島聡,橋本翔,はまちや2,竹原,麻植泰輔,WEB+DB PRESS編集部
- 出版社/メーカー: 技術評論社
- 発売日: 2015/04/23
- メディア: 大型本
- この商品を含むブログを見る
PHP のトレイトに気をつける
普段 Scala でトレイトを使いまくってるけれども PHP にも 5.4 からトレイトが入った。
trait の良いところは多重継承のできない言語で多重継承っぽいことができることだ。 use, use とつけていけば、いくらでも追加できる。DRY に書けてよいことだ。
対して悪いところはいとも簡単に複雑で暗黙的な依存関係が生まれることだ。
例えばこんなの
<?php trait Greeting { public function say() { if ($this->location == 'ja') { echo 'こんにちは' . PHP_EOL; } else { echo 'Hello' . PHP_EOL; } } } class Location { } class US extends Location { use Greeting; private $location = 'us'; } class Japan extends Location { use Greeting; private $location = 'ja'; } $us = new US(); $us->say(); $ja = new Japan(); $ja->say();
US、Japan ともに Location クラスを継承している。 それぞれに挨拶をするメソッドを追加したいけれどもこれは継承で実装するのが適切だろうか? よくわからない。よし、トレイトを作ろう。という経緯で上のコードは書かれた、という設定。
このコードのどこが微妙か。それは Greeting トレイト が US, Japan クラスのフィールドに依存していることだ。しかも暗黙的に。
こういうコードは読みにくい。Greeting トレイト だけを読んでも $this->location
ってなに?という気持ちになるし、US、Japan クラスだけを読んでも、まさかその private フィールドに Greeting トレイトが依存しているとは思うまい。
これだけでもひどいが、もう少しやりすぎ感を出してみよう。
<?php trait Greeting { public function say() { if ($this->location == 'ja') { echo 'こんにちは' . PHP_EOL; } else { echo 'Hello' . PHP_EOL; } } } trait JapanLocation { private $location = 'ja'; } class Japan { use JapanLocation; use Greeting; } $ja = new Japan(); $ja->say();
Japan クラスは実装を持っていない。Greeting と JapanLocation を mix-in してやることでオブジェクトを合成している。Greeting は Japan に mix-in されているが、それだけでは動作しない、JapanLocation も一緒に mix-in してやることで初めて動作する。わかりづらい。Greeting トレイトを使うきは Location トレイトも mix-in してくださいね!ということはドキュメントにしっかり書こう。あと JapanLocation の $location
は PhpStorm が未使用の変数として警告してくるけど消しちゃだめだよ、これは一見意味のない変数に見えるけれど、実は Greeting メソッドと一緒に mix-in するときに使われるんだ、ってこともドキュメントにちゃんと書いておかなきゃね。
とまあ、そんなのはやりすぎに見えて説得力がないけれど、 trait を使いまくるとこういうところに行き着くだろう。 それに実を言うとこれは Scala ではよく見るパターンだ。
trait Location { val location: String } trait JapanLocation extends Location { val location: String = "ja" } trait Greeting { self: Location => def say(): Unit = { if (location == "ja") { println("こんにちは") } else { println("Hello") } } } object Japan extends JapanLocation with Greeting
このコードの何が PHP と違うのかと言うと、次の1行にある。
trait Greeting { self: Location =>
self: Location =>
という記述は self-type annotation
と呼ばれ、
この Greeting トレイトは Location 型でもありますよ。つまり Location 型のクラスやトレイトにのみ mix-in できますよ、という意味になる。使いかたを間違えるとコンパイルエラーになる。
このように Scala ではクラス間の関係を明示することができるし、しなければコンパイルが通らない。ドキュメントなどではなく、コードとして表現できる。Scala 最高だった。
Scala ではこのように小さな trait を組み合わせて大きなオブジェクトを合成するパターンは、ケーキ作りになぞらえて、Cake Pattern と呼ばれる。Cake Pattern を使うと実装の一部となっている trait を差し替えることができるのでコンパイル時 DI のような使われ方もしている。
とは言え、いくら型の制約があると言っても、やりすぎると当然コードは読みづらくなるし、コンパイルが遅くなるとか、パス依存型が絡んでややこしくなるなどといった問題はあるので、最近では「Cake Patternはアンチパターンだ」とする流れもある。
話がずれた。Scala の話じゃない。PHP ではどうすれば良いか。そんなのは簡単、というか今まで(<PHP5.3)どおりやれば良い。
<?php class Greeting { public static function say($location) { if ($location == 'ja') { echo 'こんにちは' . PHP_EOL; } else { echo 'Hello' . PHP_EOL; } } }
結局これだけでよかった。この実装はシンプルだ。 このファイルだけ見れば良いし、呼び出し元から見ても say は場所に依存するんだな、ということがわかる。 このクラス単体で動作し、テストも書きやすい。リファクタリングも PhpStorm で自動的にできる。
トレイトの使いどころ
フレームワーク側で便利な trait を提供したりするのはそこまで悪くはないと思う。ドキュメントなりサンプルコードなりがしっかりあって、ユーザーの前提知識となっていれば良い。(例えば Ruby の Enumerable みたいなの)
それから古いフレームワークのコードを見ていて、継承関係がやたら複雑で、トレイトがあったらもっとマシになるなあと思うことはあるので、多重継承がない言語で多重継承っぽいことをやりたい、というところでは使えば良い。そういうものなので。
それ以外の、今までスタティックメソッドやら委譲・集約とかで済んでたものをトレイトにする、というのはやらないほうがよい。
まとめ
トレイトは柔軟で便利ではあるけれども、特に PHP のように型の制約のない言語で使用すると、 複雑で暗黙的なオブジェクト間の関係を生み出しコードはひどく読みづらくなる。 密結合で壊れやすく、変更が難しいコードになる。
トレイトはどうしても多重継承したい、という場面で使う。しかしそもそもを言えば「継承より委譲・集約を選ぶ」べきである。
暗黙的な知識を要求するトレイトを使うときはきちんとドキュメントを書くなり知識の共有を行うなどする。
結局は trait が悪いというよりは書き方の問題であるけれども、trait が意図せずにクラス間の結合を生み出す危険性が高いことは確か。話がややこしくなるから Ruby の module には触れなかったけど、同じような辛い場面には遭遇する。trait をたくさん使いたかったら Scala を書けば良い。
WEB+DB Press 82 に PHP の記事を書きました
- 作者: 山口徹,Jxck,佐々木大輔,横路隆,加来純一,山本伶,大平武志,米川健一,坂本登史文,若原祥正,和久田龍,平栗遵宜,伊藤直也,佐藤太一,高橋俊幸,海野弘成,五嶋壮晃,佐藤歩,吉村総一郎,橋本翔,舘野祐一,中島聡,渡邊恵太,はまちや2,竹原,河合宜文,WEB+DB PRESS編集部
- 出版社/メーカー: 技術評論社
- 発売日: 2014/08/23
- メディア: 大型本
- この商品を含むブログを見る
8月23日発売の WEB+DB Press 82 で PHP の記事を書きました。
大規模な PHP プロジェクトをどうやって安全に、というか事故らずに運用していくかという視点で書きました。まあそんなの無理なんで、内容としてはかなり保守的に見えるかもしれません。イケイケ PHP プログラマは嫌がるのかな、わからん、とか思って書きました。
大規模になってくると、もう PHP がどうとかそういう大変さよりは、負荷が...ストレージが...コネクションが...とかのパフォーマンス問題や、あっ、PCサイトバグってる、よし直した、あっ、iPhone アプリ...し、死んでる!!みたいなマルチデバイス対応の辛さ、などなどばかりで、あまり PHP 関係ないんじゃないかな、と思うんですが、そう思って原稿書いてたら「なんか PHP の連載なのに PHP のこと書いてないですね」「ですよねー!!」となり、頑張って PHP のことも書きました。
特徴的な点としては、PhpStorm について詳しめに書いていることでしょうか。それから最近の弊社開発チームでの取り組みなどについても少し書いてあって面白いと思います。
ところで、書いてる途中、お腹痛いなーと思ってたら腸に穴が空いていました。お尻から血が噴き出して、死ぬのかな?って思いました。やっぱ PHP 体に悪いのかな...ちなみにまだ治ってません。
医者「(腸の写真を見ながら)ほらここ穴あいてる」自分「なるほどー」
— Toshiyuki Takahashi (@tototoshi) 2014, 7月 3
ということで、WEB+DB Press 82 買ってください。10 冊買っても内視鏡検査より安い。なんてこった。
PHP の file_get_contents は get どころか post も put も delete も upload もできる
stream_context_create と組み合わせて使います。
手元でてきとーに動かしてた REST API とかで試してます。
get
普通ですね。
<?php $content = json_decode(file_get_contents("http://localhost:5000/api/note/161"));
post
<?php $context = stream_context_create( array( 'http' => array( 'method'=> 'POST', 'header'=> 'Content-type: application/json; charset=UTF-8', 'content' => json_encode( array( 'title' => 'file_get_contents で POST', 'raw' => "file_get_contents で POST\nPHP すごい...\n" ) ) ) ) ); file_get_contents('http://localhost:5000/api/note', false, $context);
なるほど〜。file_get_contents は file_post_contents だったのか〜。
put
<?php $context = stream_context_create( array( 'http' => array( 'method'=> 'PUT', 'header'=> 'Content-type: application/json; charset=UTF-8', 'content' => json_encode( array( 'id' => 162, 'title' => 'file_get_contents で PUT', 'raw' => "file_get_contents で PUT\nPHP すごい...\n" ) ) ) ) ); file_get_contents('http://localhost:5000/api/note/162', false, $context);
なるほど〜。file_get_contents は file_put_contents だったのか〜。
delete
<?php $context = stream_context_create( array( 'http' => array( 'method'=> 'DELETE' ) ) ); file_get_contents('http://localhost:5000/api/note/162', false, $context);
なるほど〜。file_get_contents は file_delete_contents だったのか〜。
upload
<?php $upload_content = file_get_contents('upfile.txt'); $context = stream_context_create( array( 'http' => array( 'method'=> 'POST', 'header'=> 'Content-Type: multipart/form-data; boundary=-PHP_FILE_GET_CONTENTS', 'content' => "---PHP_FILE_GET_CONTENTS Content-Disposition: form-data; name=\"upfile\"; filename=\"upfile.txt\" Content-Type: text/plain {$upload_content} ---PHP_FILE_GET_CONTENTS " ) ) ); file_get_contents("http://localhost:5001/test_upload.php", false, $context);
なるほど〜。file_get_contents は file_upload_contents だったのか〜。
ひとこと
無理すんなや。
composer で依存関係から特定のライブラリを除外する
Maven でのこれ
<dependencies> <dependency> <groupId>sample.ProjectA</groupId> <artifactId>Project-A</artifactId> <version>1.0</version> <scope>compile</scope> <exclusions> <exclusion> <!-- declare the exclusion here --> <groupId>sample.ProjectB</groupId> <artifactId>Project-B</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
composer にはこの依存関係から特定のライブラリを除外する機能はない。まだ実装していないのではなく方針として入れるつもりはなさそう。composer 開発者はこの機能を不要と思っているっぽい。
Exclude packages · Issue #1549 · composer/composer
理想をいうと微妙なところだけど、実際こういう問題はよく起きるからカバーするべきじゃないかな。composer 開発者はそういう経験ないのだろうか。PHP はフルスタックでそれのみで完結するようなフレームワークが多く、依存関係のツリーが深くならないから dependency hell が起きづらいと勝手に理解しておく。
一応、問題自体は除外したいモジュールを provide に指定すれば解決する。すでに提供されているのだから composer で持ってくる必要はないよね、ということ。
{ "require": { "foo": "1.0.0" }, "provide": { "bar": "*" } }
ただし、これだと問題は解決するものの意味としては全く違うから気持ち悪い。設定が json だからコメントも書けない。
まとめ
- composer で exclude したいときは provide を使う
- json で設定ファイルはきびしい
+n month の罠
PHP Advent Calendar 2013 - Qiita の 23 日目です。
strtotime の話をします。
+n month の罠
日時を扱うのは大抵どの言語でも難しいです。PHPでももちろんそう。
PHP で日付の足し算をするのには strtotime を使います。
echo date('Y-m-d', strtotime('2013-01-31 +1 day')) . PHP_EOL; //=> 2013-02-01
strtotime はすごく便利ですが、うかつに月の計算をすると予期しない結果が返ってきます。
echo date('Y-m-d', strtotime('2013-01-31 +1 month')) . PHP_EOL; //=> 2013-03-03 echo date('Y-m-d', strtotime('2013-02-28 +1 month')) . PHP_EOL; //=> 2013-03-28 echo date('Y-m-d', strtotime('2013-03-31 +1 month')) . PHP_EOL; //=> 2013-05-01
1月の1ヶ月後が3月、3月の1ヶ月後が5月になっています。これは一体。。。
nヶ月後っていつなのか
冷静に考えてみると、月の日数って月によって違うし、nヶ月後の日付って人間の意思なしには決められないものなんですね。
1月31日の1ヶ月後はいつなのか、30日後にしたいとき、31日後にしたいとき、2月の月末にしたいとき、いろいろあると思います。
ちゃんと +n month する
この +n month の罠にハマらないために、n月 +1 month が n+1 月となることを期待して +n month するときは月初を起点にすることを心がけましょう。
echo date('Y-m-d', strtotime('2013-01-01 +3 month')) . PHP_EOL; //=> 2013-02-01 echo date('Y-m-d', strtotime('2013-02-01 +3 month')) . PHP_EOL; //=> 2013-03-01 echo date('Y-m-d', strtotime('2013-03-01 +3 month')) . PHP_EOL; //=> 2013-04-01
なぜ、これだとうまくいくのか。PHP は +1 month するとき、現在月の日数を足し算するからです。うるう年とかも考慮されてるのでご安心を。
echo date('Y-m-d', strtotime('2013-01-31 +1 month')) . PHP_EOL; //=> 2013-03-03 echo date('Y-m-d', strtotime('2013-02-28 +1 month')) . PHP_EOL; //=> 2013-03-28 echo date('Y-m-d', strtotime('2013-03-31 +1 month')) . PHP_EOL; //=> 2013-05-01 echo date('Y-m-d', strtotime('2013-01-31 +31 day')) . PHP_EOL; //=> 2013-03-03 echo date('Y-m-d', strtotime('2013-02-28 +28 day')) . PHP_EOL; //=> 2013-03-28 echo date('Y-m-d', strtotime('2013-03-31 +31 day')) . PHP_EOL; //=> 2013-05-01
とまあこの件に関しては PHP の仕様がおかしいとはあまり思わないんですが、にしても PHP の日付・時刻周りはひどすぎですね。なんか使いやすいライブラリないのかな。
代入文の左辺右辺を書く順番を逆にする
例えば
$a = array();
とか書くときに
代入される側の $a のほうを先に書くのは自然な流れではない。
array() のほうを先に書きたい。というか思わずそっちから書き始めちゃう。
だからこういうコマンド便利じゃないかと思った。
(defun my-php-super-semicolon () (interactive) (insert ";") (beginning-of-line) (indent-for-tab-command) (insert "$ = "); (backward-char 3)) (define-key php-mode-map (kbd "C-;") 'my-php-super-semicolon) ;; array() ここまで書いてCtrl + ; を押す ;; $◯ = array(); ◯のとこにカーソルが飛ぶ ;; $a = array; 代入文が書きやすくなるんじゃね?