PHP - 憂鬱な希望としての PSR-7

tl;dr PSR-7は普段PHPにてHTTPメッセージを扱うインターフェイスとしてそこそこ十分に機能する。メインユースケースの8割は満たすだろうが、PHPのポテンシャルの5割にも満たないかもしれない。だがそれで良い。

「今年は PSR-7 が来る」

つい先日、PHP-FIGのHTTPメッセージ用インターフェイスに関するPSR(PHP Standard Recommendations)のステータスがレビュー段階に入った。

https://github.com/php-fig/fig-standards/blob/master/index.md

そこでこのエントリではPSR-7のインターフェイスが実際のフレームワークとアプリケーション間での利用の際に上手く機能するかについて考察する。なお、OOPとしての正しさについては深く言及しない(ヘッダーについてのデメテルの法則や、イミュータブル性などだ。)。 PSR-7の仕様は、現在以下のプロポーザルディレクトリ下に置かれてる。可決されれば、acceptedフォルダへ移動されるだろう。

もしあなたがPSR-7についての議論の過程を知りたいなら、PHP-FIGの該当のMLを参照してほしい。ただし、正直言うと私はMLの内容はほとんど追っていない。なぜかって? 言えないね。。

また、いくつかのPSR-7についての記事をあらかじめ紹介する。

共有インターフェイスは喜びか悲しみか

PHPにおけるインターフェイス

今更だがPHPにはインターフェイスがある。

現在活発に機能追加について議論が進んでるPHPではあるが、近い将来インターフェイスの立ち位置が変わるかもしれない。(残念ながら Anthony Ferrara が1年ほど前にRFCに出した PHP: rfc:protocol_type_hinting は立ち消えとなったようだ。 そうそう、mwopは Zend Framework 2 についてGoスタイルのインターフェイスもサポートしていることについてMLで述べてたね。/ 参考 ZF開発者向けMLの過去ログ )。

ここでは現行 PHP5 の インターフェイスについて主に扱っていこう。

では、フレームワーク・ライブラリをまたいだインターフェイスについて考えてみよう。3年半ほど前に初代PHPSpecの開発者でもあるpadraicは lsmithの考察 に反応する形で 以下のコラムを書いている。

共有インターフェイスの話とは別にフレームワーク間での"依存"の話というとpmjonesのDe-Coupledについての主張など、10年代においてオブジェクトの取り扱いについてはPHP開発者でも重要な関心事だ。

そろそろ PHP-FIG についてひとこと言っておくか

PHP-FIGについて断わっておくと、一部にはPHP-FIGは標準化団体なので開発者はPSRに強制的に従うべきだと考えてる人が昨今いるようだが、個人的にはなぜそんな息苦さを強いろうとしてるのか理解できない。そもそもなぜそのルールはあるのだろうか?

PHPフレームワークのあらまし

まず、PHP-FIG誕生前のフレームワークについての状況を俯瞰しよう。単純なリリース時期については、pmjonesが先月年表を公開していたのでそちらを参照いただきたい。なお、日本語コミュニティ周辺に特化したものは私の方で個別に作成した。 もちろん、リリース時期を確認したところで何もインパクト度合が分からない。Zend Frameworkについては4年ほどまえに記事を書いたが、あなたの"お好きな"フレームワークについてはあなたが一番ご存知のはずだ。(これ以上のことは、個人の影響度合いの話なので、 PEARmojavi, PHP第一世代フレームワークブームがあったということだけ踏まえよう。)

PHP-FIG そして PSR-0

PSR-0

今ならクラスオートローディングについての恩恵についてはみんな十分に理解してるだろう。(5年ほど前ならXフレームワークの仕組みを使うとクラス読み込みが楽という主張の記事も見られたのだが。)

PHP-FIG は、PHP Framework In Group の略で、誕生は6年前に遡る。誕生期の個人的な印象は、「まさにフレームワーク間の相互運用のための最低限の取決め」それだけだった。もっと正直な感想としては「PEAR由来のクラスローディングにみんな沿ってくれる」だった。もしかしたらこの記事を読んでる人には、開始当初はnews.php.net内にてやりとりが交わされていたのを記憶にある方もいるだろう。

クラスローディングについてとPSR-0の重要性については 2011年の私の発表 で口酸っぱく行っている。当時の発表時の反応としてはPSR-0についてはあまり知られてなかったが、composerの普及などもあり割と知れ渡っているだろう(この発表当時は、composerはcreate-project的な役割のツールとして作成されるだけだと思っていたので、現在の普及のされ方は予想もしなかったが。)

PSR-1,2

PSR-1,2 は 基本コーディング規約とコーディングガイドについてのルールだ。

PSR-0の項で述べた通り私はPSRの相互運用の最低限の取り組みについてのみ感銘を受けているだけなので、これが本当にフレームワーク間の相互運用に本当に必要なのか今でも懐疑的だ。皮肉的立場としてPHP-FG(figではない/FGだ)の存在もあるように。。この記事ではインターフェイスについて着目したいので、PSR-2について確認したがInterfaceサフィックスについての命名規約はないようだ。

PSR-3

PSR-3 ロガーインターフェイスはmonolog/composerの作者であり、Symfonyの開発メンバでもあるSeldaek(Jordi Boggiano)が推し進めたものだ。PSRで初めてインターフェイス定義(コードファイル)を必要とするものが誕生した。

日本語での紹介は、hirakuさんが発表記事を書いている。

上記スライドでは、Zend\Logが対応していないという嘆きがあるが、そもそもZend Framework2(ZF2)では後方互換性の観点からPSR-3は導入しない。このLoggerインターフェイスについてmwopのブログエントリがあるのそちらを見てもらおう。

ちなみにmwopはこの記事執筆時期にPHP-FIGのメンバから抜け代わりにpadraicbがZF側メンバとしてPHP-FIGに参画している。(あれちょっとまってよ。padraicってPEARが特権的なものになったの批判してなかったっけ?PHP-FIGは??..おっと置いとこう)

PSR-3のロガーインターフェイスとZF2でのロガーインターフェイスの大きな違いはここだ。

『Why must the $context be an array -- why won't any Traversable or ArrayAccess object work?』

PSR-3を初めて知った時の自分の反応は、「おいおいなんでarrayでタイプヒンティングしてんだよ?」だった。しかし、そこは本当に気に掛けるべきだったんだろうか?いやいや、chobieさんの ArraySerializable interface がacceptされてれば考えはとっくに変わってた?..いやそれとも。。

キャッシュインターフェイスの泥沼

PSR-3にてインターフェイスを定義する際のちょっとした問題点については分かっていただけただろう。そもそもロギングレベルについてはRFC5424という指針があったからこそ、議論は紛糾せず受け入れられたとも考えられる。紛糾?そうロガーよりも先にキャッシュについては先に提案があったがまだ決着は見えていない。

この終わりの見えないCacheインターフェイスについてはircmaxellが「An Open Letter To PHP-FIG」というタイトルにて、次のような提言を行っている。

http://blog.ircmaxell.com/2014/10/an-open-letter-to-php-fig.html

50%の問題を解決するAPIを標準化しろとの主張がある。あえて冒頭では「メインユースケースの8割は満たすだろうが、PHPのポテンシャルの5割」と書いたが、PSR-7では風呂敷を広げないように努めているようにも思う。

PHPとHTTP

ここから先はPHPとHTTPについて立ち位置を確認していこう。

PHPとHTTPの密接な関係

PHPはサーバーサイドでの稼働を強く意識した言語だ。いやいやそもそもLAMPの名のもとに Apacheモジュールでの組み込みにて00年代前半に広まっていったのはご承知の通りだろう。 そしてSAPI (Server Application Programming Interface)にCLIが導入されたPHPのヴァージョンは4.3だ。 SAPI? そう思われた方はPHPアーキテクチャについて、moriyoshiさんの過去のスライドの図が分かりやすいのでそちらを参照いただきたい(9-13ページの箇所が該当)。ほかには、do_akiさんのSAPIについての発表資料も参考になる。

PHPの処理原則としてHTTPが背後にある。 Zeev曰く、「全てがリクエストの後に真っ更になる」が最もたる例だろう。

また、例えばPHPソースコードでは$_FILESの扱うコードがmainフォルダにあるのもPHPがサーバーサイド言語たるところだろう。プログラムファイル名もそのままrfc1867.cだ。さらに言うとファイルポスト最大値 upload_max_filesize については以下の該当コードに処理がある。

https://github.com/php/php-src/blob/PHP-5.6/main/rfc1867.c#L1036

PHPでリクエストを扱う。レスポンスを返す。

PHPは、Webアプリケーションを作りやすい言語として良くも悪くもよく知られている。レスポンス文字列には、HTMLをそのままだったり言語構造のechoを使えばよい。ヘッダーを設定する際にはheader関数をコールすればよい。ただし、すべての実際の 出力の前にコールする必要があるっていうのはマニュアルに覚えておいてって言ってるから皆知ってるよね?(え、エラーメッセージを何十回も見ることによって覚えたって?)。 リクエストを扱う際には、スーパーグローバルによってパースされた値を取得することができる。ちなみに、スーパーグローバルが導入されたのはバージョン4.1.0だ。(PHP3のころから使い始めた人は一体どんな苦労をしてたんだろう?)しかしながら、グローバル変数は一般に好ましくない実践だと考えられている。 (独自考察?) いやいや、スーパーグローバルを使っていてもテストは書けるんだ。ええじゃないか!Really? ..と、ここまで書かなくても多くの人には、HTTPメッセージクラスが必要とされた動機が分かっていただけただろうか?

それでは次に、いくつかのHTTPメッセージクラス/インターフェイスについて確認していこう。

これまでのフレームワーク・ライブラリでのHTTPメッセージクラス

Zend Framework 1

ZF1のRequest/Responseはクソッタレな実装の一例だ。(今ではそう見えてしまう)。 2つほどダメな点を挙げよう。 - getActionName()などControllerに特化したメソッドが用意されてしまっている。 - HTTPクライアントのReponseクラスは、Controller_Requestとはインターフェイスが別物。

なおHTTPクライアントコンポーネントにRequestクラスがないが、これはPEARのHTTP_RequestやHTTP_Clientにも同様のことが言える) http://framework.zend.com/svn/framework/standard/tags/release-1.0.0/library/Zend/Controller/Request/Abstract.php http://framework.zend.com/svn/framework/standard/tags/release-1.10.0/library/Zend/Controller/Request/

しかしながら、かつてはこの一連のRequestクラスにはUtilなメソッドがあったり、おかしてしまいそうな過ちについて知見が積み重なっていったこと など、存在意義・メリットがあったことだけは注記しておきたい。

pecl_http version 1

PHPにそもそもHTTPメッセージクラスが提供されている可能性はなかったのだろうか?そんな疑問に答えるのが、pecl_httpとの長い長いコア行きの話となる。少なくともその議題は2008年まで遡る。

 https://marc.info/?l=php-internals&m=122210992911766

extension上の名前ではhttpだが、紛らわしいのでpecl_httpという名称も使われている。

http://php.net/manual/ja/book.http.php

上記マニュアルを見ればクラス構成が分かるだろう。そう、RequestクラスとReponseクラスを備えていたのだ。ただし、HTTPメッセージに類しそうなHttpMessageクラスはそれらの基底クラスとはなってない。また、昔話をしてしまうとPEARのメンバが参画していたOrchestra.ioが(おっと、今ではこのドメインはengineyardに飛ぶようだ)始動し始めた前後で開発されていたREST用フレームワークfrapiでは、pecl_httpが必要だった。 なお、pecl_httpの現行ヴァージョンは2だが、ドキュメントは作者のmike(m6w6)のサイトにある。

さてもう1回確認しよう。Add pecl_http to the core? No! ← oh.

Symfony 2

ことHttpFoundationについては、Symfony(c)fabpotのDrupalへのアプローチなどもあり、今日様々なプロジェクトで使われている。(どのプロジェクトで利用されているかは、Packanalystを確認するとよい)。また、BrowserKitについてはGuzzleやBehat/Minkなどにても使われている。個人的には、BrowserKitコンポーネントのRequest/ResponseがHttpFoundationと別個に作成されているのが、ZF1の過ちを繰り返してるように見えて仕方がないが。

reactphp / amphp

node.jsのブームを受け登場したのが、ReactPHPだ。ReactPHP自体はEvent-driven, non-blocking I/O with PHPがコンセプトなプロジェクトだが、HTTPサーバーの実装としてstream_socket_serverを利用したプロジェクトとしては一番有名だろう。該当のコード行はここだ

なおReactPHPとは別にrdlowreyを中心としたメンバーによるamphpというプロジェクトがあることもご紹介しておきたい。

appserver.io

appserverはpthreadsを用いたマルチスレッドサーバーだ。HTTPメッセージの実装について現在は appserver-io/http で行われているようだ。

StackPHP, conduit

HTTPメッセージが共有化されるようになるとどのような利点が生まれるだろう。いや、アプリケーションをシンプルに保っていくにはどうすればいいだろう。StackPHPは、その疑問への回答を差し示している。StackPHPはSilexの開発者としても知られるigorwほかの開発者が立ち上げたプロジェクトだ。よって当然のことながらHTTPメッセージインターフェイスにはSymfony HttpFoundationのそれが採用されている。StackPHPはRackの影響を受けている。

StackPHPとは別にmwpoは Node.jsで用いられるSencha Connect をもとにphly/conduitを作成している。

もし、PHPにてPSR-7が普及すればmwopが夢見ているように一つのアプリケーション内でモジュールごとに各フレームワークを導入することも可能だろう。(アプリケーションのランナーをミドルウェアに移行すればよい。)未来予測をする気はないのでミドルウェアについてはここまでとする。

View・テンプレートエンジン / ストリーム / Output buffer

ここまででフレームワーク・ライブラリのHTTPメッセージについての取り組みを見てきた。

もっとも、今日PHPでアプリケーションを作成する側はレスポンスを返す際はMVCでのViewを意識している。今日のフレームワークを利用したアプリケーションでは、コントローラ・アクション内で、View変数・Viewモデルを生成しフレームワーク側へ返却したり、Responseオブジェクトを返却するのがよく見られる傾向だろう。そしてまた、ViewテンプレートとしてPHPそのものを利用したりテンプレートエンジンを利用している。

View機構としてコード量の少ない例を見てみよう。どうやってレスポンスボディを生成しているだろう。

ほかにも有名なView機構・テンプレートエンジンのほとんどは、ob_get_contents()を利用し一旦コントローラ・レスポンスへと押し込めている。 ...これは必要なのだろうか? レンダーの際に、ob_get_contents()の結果をメモリに格納することの目的は以下が考えられる。 - 実際の出力に対する動作検証/UnitTest - レイアウト機能やPartialによる柔軟性 - 内部的なキャッシュ

しかし、メモリ格納には巨大なレスポンスサイズでさえメモリに貯めてしまうという懸念点がある。 以下のサンプルコードで確認しよう。

template.phtml

<h1><?php echo htmlspecialchars($title) ?></h1>
 <div class="main">
 <?php foreach (range('a', 'z') as $c) : ?>
   <?php echo htmlspecialchars(str_repeat($c, 1024 * 1024)) ?>
 <?php endforeach; ?>
 </div>
render.php
<?php
require_once DIR.'/vendor/autoload.php';
$view = new Spindle\View('template.phtml');
$view->title = 'title';
echo $view->render();

$ php -d memory_limit=6M render.php
(中略)
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

Fatal error: Allowed memory size of 6291456 bytes exhausted (tried to allocate 2097153 bytes) in /vagrant/tmp/spindle/template.phtml on line 4

oh...Spindle\Viewを槍玉にあげてしまったが、ob_get_contents()を使ってる限りほとんどのフレームワーク・ライブラリはメモリを食っていくということだ。 え、HTMLでここまでデカいレスポンスボディは作らないって?

数百MBのCSVも画像ファイルもどうやってレスポンスボディとして表現する?PSR-7としての回答はストリームの利用だ。

Stream入門

あらためてPHPのストリームについて振り返ろう。hnwさんが5年前に懸念してるようにPHPのストリームの知名度は低いように感じる。 いまさら改めて コミ をしなくてもいんだろうけど。。streamについてはElizabeth Smithのスライドがとてもよくまとまっている。

Viewでの考察

さし戻って、View・テンプレートエンジンでストリームの恩恵にあずかれるだろうか?spindle-viewをもとに簡単なViewコンポーネントを作成してみた。レイアウト処理については、includeをstackに積む形にすることで対応している。

https://github.com/struggle-for-php/SfpStreamView/blob/0.0.2/src/SfpStreamView/View.php

template.phtml

<h1><?php echo htmlspecialchars($title) ?></h1>
<div class="main">
 <?php foreach (range('a', 'z') as $c) : ?>
   <?php echo htmlspecialchars(str_repeat($c, 1024 * 1024)); ?>
   <?php ob_flush(); ?>
 <?php endforeach; ?>
</div>

render.php

<?php
require_once DIR.'/vendor/autoload.php';
$view = new SfpStreamView\View(DIR);
$view->title = 'title';
$temp = fopen("php://temp/maxmemory:". 1  1024  1024, 'wb+');
$fp = $view->render('template.phtml', $temp);
rewind($fp);
fpassthru($fp);

やった動くぞー。やったぞー。俺はmemory_limitの限界を超えたんだ!。(↑のコードを見て、お前ob_flush入れてるやんけ!とツッコミがあるだろうが、fpassthruされるまで出力されないという所でご理解いただきたい。)

まあ、正直なところある程度複雑化されたものをStreamとして表現するのも難しい場合があるだろう。テンプレートエンジンの場合にはプリコンパイル型など事前に単純なechoの積み重ねを生成するようにするなど回避策もありうる。しかしながらストリームの利用によって、メモリ制約解決の道筋は見えてきた。 これで、コントローラ・アクション内でHTMLも数百MBのCSVも画像ファイルも統一されたレスポンスオブジェクトとして返せるんだ!

ほんとに?

ここで実際にレスポンスをsend()する側の phly/http に含まれるServerクラスの実装を見てみよう。

echo $response->getBody(); https://github.com/phly/http/blob/0.11.0/src/Server.php#L177

? ここで私は以下のような質問を投げてみた。

f:id:sasezaki:20150307190839p:plain

https://github.com/phly/http/issues/38

あ!そうですか。ということで、ここからはPHPの出力制御部分について確認していこう。

Output Buffer

PHPのアウトプットバッファリングについては、PHP5.6のリリースマネージャJulien Pauliの昨年12月のブログ記事が大変秀逸なのでそちらを参照してほしい。

またマニュアルでも書かれている通り、実際のPHPディストリビューションでのCLIではないSAPIの場合は output_bufferingの値が 4096 バイトということに注意を払ってもらいたい。

fpassthru()の場合、jpauliの記事でソースが指示されたphp_output_opには↓のように掘っていけるのだろう。

ここら辺については、また後でデバッガで確認しようとは思う。

前項ではfpassthru(あるいはstream_copy_to_stream)などストリーム全体を出力することに固執して見てしまっていたが、Content-Rangeの利用などもあるわけだ。ここらへんについては、prolicのzf2用モジュールがきめ細かい対応を行っている。

繰り替えし言うと、SymfonyBinaryFileResponseのようにコンテンツ形式によってフレームワーク呼び出し側がレスポンスオブジェクトを明示的に指定してフレームワーク側へ伝えるというのはまだまだ有効だろうということだ。

そして、フレームワークへ(?)

実は、mwop自体は4年ほど前にfig-libraryというプロジェクトを作りfabpotやAgaviのリーダーへ打診していた過去がある。当時のことについて記憶が確かならPSR-0が発表された程度で現在のようなレビュー・ステータスは確立されていなかったが。

その後、bebeliのHTTPクライアントの提案の時期を経て、前述したようにmtdowlingのdraftをmwopが引き継いでいくことになる。今年2月には割と唐突にZend Framework 3 のリリース時期について発表され、PSR-7の採用・ミドルウェアアプローチについても言及された。(ZFの開発についてはgithu・IRC・MLにて主に行われるが、PSR-7を採用しようか話題は出てたのかな?割と皆空気読み取っててちょっとIRCで話した程度だと思うけれど)

また、guzzleがリポジトリpsr7 を作成し マイクロフレームワークとして有名な Slim の次期バージョン 3.0でもPSR-7が採用されることが発表された。(ちなみにSlimの開発者codeguyはPHP Right wayの著者としても有名で、オライリーからは Modern PHP が発行されたばかりだ。)

このまま一斉に各フレームワークがPSR-7を採用することにより未来は薔薇色?。実際の普及率には、有名フレームワークの次期バージョンでの採用動向次第だろう。そもそもなぜPSR-7は成立したのか利用者側が理解していなければ意味がない。またOOP銀の弾丸ではないのだし、採用した/されただけで満足していはいけない。

最後に

PSR-7はPHPにとって"まっとう"なインターフェイスで、共有化されていることによるメリットもある。が、このインターフェス(ツール)を使うことにこだわり過ぎるのもおかしい。今はPHPとHTTPメッセージについて見つめなおす良いチャンスだ。

【追記- 2015/03/06 23:56】SAPIについてのdo_akiさんの資料の箇所追記