PHPカンファレンス福岡、PHP勉強会(東京・相模原)、Symfony Meetupで話をしてきました。
前回のエントリから1年以上間が空いてしまいましたが、PHPにおけるHTTPメッセージについての再考ということで カンファレンス・各勉強会にて話をさせていただきました。
記事やコード・発表は時系列ですと以下の通りです。
-
2015年03月 日記
- PHP - 憂鬱な希望としての PSR-7
2015年06月 Symfony Meetup
- Symfony ユーザ向け psr-7 zend-diactoros Middleware 入門
2015年06月 動作確認
- ストリーム 関連のプロジェクトの話
2015年11月 PHP勉強会@相模原
- PSR-7 - これは何だ
2016年05月 PHPカンファレンス福岡2016
- HTTPメッセージ - PHPで扱う場合の再入門
2016年05月 PHP勉強会
- HTTPメッセージ、PHPの 事情ば分かっとっと?
Symfony Meetupでのスタッフの方々、PHP勉強会@相模原を運営されたyoyaさん、PHPカンファレンス福岡のスタッフの方々、PHP勉強会@東京のスタッフの方々、ならびに発表を聴いていただいた方々、そしてまた私のPHPのコミュニティ周りに関する愚痴を月一程度で聴いてくださったt_wadaさん、ほか様々な方にお礼申し上げます。ありがとうございます。
前回のエントリはそもそもどこかしらのカンファレンスでの発表しようかなという点でのとっかかりとして書いたので、目的通り福岡にて行えたので発表行えたのでよかったかなと思います。
発表内容は、2015年のほうこそPSR-7をつけていますが、そもそも前回のエントリの末に「今はPHPとHTTPメッセージについて見つめなおす良いチャンスだ」とも自分で書いてましたので、その点を重視した内容への主張へシフトいたしました。
PHPカンファレンス福岡では、原稿テキストファイル用意してましたので、以下にそのまま貼ります。(実際のスライドは注意書きはさんだので+1ページです)
2 よろしくお願いします。させざきです。 今日は、タイムテーブルでのタイトルでは「HTTPメッセージ - PHPで扱う場合の再入門」ということで ご案内させていただいております。 まあ、今ご覧のとおり「PHP、おまえだったのか。いつもHTTPメッセージを運んでくれたのは。」 ということで、 PHPがHTTPリクエストデータをどのように受け取り、どのようにレスポンスデータを返すか。 どのように我々が開発しやすい形でのAPIが提供されていて、 どのようにデータを メモリ利用などふくめ利用していくべきか。 リクエストデータをどのように受け取り、どのようにレスポンスデータを返すか。 ひたすらこの観点に着目をしていって今日話を勧めていきたいと思います。 3 では、ちょっとだけ古い話をします。 2000年ごろの話です。 PHP 4.0よりちょっと前の頃の話をしたいと思います。 4 当時は、サーバーサイドで掲示板なりのプログラムを書くとしたら、 Perl イコール CGI というような時代でした。 5 当時のperlでのCGIスクリプトのことを思い出すために、 CGI.pmを利用したサンプルを見てみましょう。 まあ、だいたいこんな感じだったかなあというのを当時を知っている方は思い出してもらえましたかね。 ああ、若いみなさんは、ふーんこういうふうに書くんだぐらいに思っていただければいいじゃないでしょうか。 ヘッダーをこのように出力したりですね。 6 2001年当時、 PHPについての本は日本語のものだとマンモス本と言われるようなものもふくめ3,4冊ぐらいしかなかったころですが、 PHPの中の人が作ったこの本で、 PHPの作者のラスマスラードフが この本の序文にてさきほどのCGIスクリプトやCで書かれたCGIを含め、 こんな風に言及しています。 7 そして、そのラスマスが書いたサンプルはこうです。 nameをinputとして受け取るフォームで、 もしname変数があれば、それを出力 8 XSSやんけ!!! 9 さきほどのサンプルですが、 最近のPHPにしか触れてない方には、 そもそもundefined variableじゃないのと思うようなコードです。 ですが、PHPにはその昔、リクエスト値をとことん楽に扱える機能としてregister globalsやマジッククオートといったものがありました。。 10 忘れましょう。 確認ポイントとして、 2000年代初頭では、セキュリティ観点からみて ヤヴァイ APIを用意していたということです。 それゆえPHPでは、 いわゆる歴史的経緯的なものとして API上でも変更や多様な呼び方があるということを頭の片隅にいれていただけたらと思います。 11 では、そういうことならばregister globalや自動変換機能をはずしてみましょうか。 かと言ってみなさん毎度毎度リクエストメッセージを平文で処理したいですか? ・・・ もちろんいやですよね。 12 ということで、今日われわれがPHPのサーバーリクエストを取得するAPIの確認です。 スーパグローバルとしてこれらの変数や、 はたまたapache_request_headers() また、php://input入力ストーリムからの取得 も行っていますね。 13 "ポスト"の場合についてをもうちょっと掘り下げてみていきましょう。 PHP内部では、大まかにスライドのような呼び出しが行われています。 とくにサーバー、、この図ではapacheにしてますが、リクエストデータを受け取る サーバーAPI、 すなわちSAPIでの初期化時の処理としてsapi_activateにてPOST値の取得などを行っています。 SAPIというのは、apacheなどを含めて、いろんなサーバの抽象化を行うPHPのレイヤです。 14 スーパーグローバルはPHP側で用意されたレイヤにて利便性も高く利用できるようになっていき、 われわれは、その恩恵を受けているという訳です。 よかった。よかった。 。めでたし。めでた..... 15 ひー ポストサイズの制限超過に遭遇してしまいました。 こういった、エラーメッセージをみたとき、 はじめて事実に気付きます。 16 PHP、おまえだったのか。いつもpost_max_sizeからかばってくれていたのは そう、さきほどの制限超過エラーは、php.iniで設定したpost_max_sizeを超えていたためです。 17 さきほど見ていただいた処理の図にあった、 sapi_activate。 このリクエストの初期化処理にてpost_max_sizeなどの最大サイズのチェックが働いているというわけです。 18 では、サーバーリクエストをPHP内部では、サーバーソフトウェア apacheなど から受け取ったデータをどのように変換しているかポイントを確認します。 まず、制限としては、 最大サイズ・最大長などの制限を設けてる HashDosで知られるmax_input_varsの導入についてはPHP 5.3.9 からです。 もちろん、サーバソフト側でも制限設定項目はありまして、 例としては、ApacheのLimitRequestBody ディレクティブなどがあります。 そして自動変換の部分として POSTメソッドの場合の$_POSTへの変換があります。PHPのソースでのmain/SAPI.cを除くと、sapi_activate関数で sapi_read_post_dataにより取得 しているんだなってことが見て取れるかと思います。 つけ加えて、 $_SERVER変数とリクエストヘッダーについて振り返ってみましょう。 ヘッダーが"HTTP_”プリフィクスなどをつけているのは、これはPHPに限らないのですがCGIの環境変数由来です。 つねに$_SERVERというグローバル変数を参照されるとして作成されるのは非効率でしょう。 PHP 5.0からでは、Just in timeに作成されるようになりました。 19 それでもやはり、現行のPHPのAPIだけではつらいというのが場面がありますね 20 たとえば、このheaders already send、 ヘッダーは、レスポンスボディの前、 プロトコル上我々は知っっているはずですが、 いわゆる手続き型のプログラムコードにて条件分岐が複雑化していった結果、億劫な場面に遭遇します。 21 ほかにもPHPで用意されているAPI利用時には、つらいなあという所があると思います。 例えば、 スーパーグローバルを直に触るのはつらい header()を先に呼ばなきゃいけないのがつらい ファイル配列操作がつらい リクエストURIの操作がつらい といった所です。 ここで、 HTTPメッセージをデータのまとまりとして見た場合に、アプリケーション実装者が求めているものは何でしょう? 22 開発者が行いたいことは何か突き詰めていくと リクエストを受け取って、レスポンスを生成する ということです。 23 つまり、リクエストオブジェクトを受け取りレスポンスを織り込むということから プロジェクトの起動ポイントでは、このようにディスパッチのシグネチャが用意されています。 24 さきほどのディスパッチの点を踏まえ こんにち、ウェブアプリケーションで多く用いられてるであろう構成についてみると、細かい点こそあれ、 このような形になるのではないでしょうか? index.phpにフロントコントローラとよばれるようなディスパッチャの起動ポイントがあり、ルーティングにて任意のコントローラを呼ぶというような構成ですね このようにHTTPリクエスト・レスポンスをオブジェクトとして扱って、 またリクエストに基づいた処理を適切にアクションなどのモジュールを個々に呼び、レスポンスとして返却し、再びアプリケーションランナーがレスポンスを送信することで、(めくり) 25 我々は複雑になっていくHTTPメッセージ利用でのAPIコールと 真摯 にとりくめるようになった訳です。 26 しかしながら、さきほどのようなアクションを呼ぶ構成を持ってしても、 このAllowed memory sizeの問題に出くわした方はいるんじゃないでしょうか。 たとえば、このエラーに遭遇する状況としては、 大量な行数でのCSVやテンプレートエンジンを利用している場合が考えられます 27 (PHPおまえだったのかは読まない!!) いわゆる手続き型にて、echoをつなげていく問題では表出されなかったことですが、多く広まっていたHTTPメッセージコンポーネントやテンプレートエンジンでは今度は出力バッファについてさいなまれるようになりました。 なぜでしょう? 28 かつて多くの実装で見られたHTTPレスポンスでのボディの取得は、スカラー、のstringをとる形でした。 「問題あるんか?」 ええ、問題です。 29 まず、・メモリを制限なく消費する可能性という点があります。 そもそもヘッダやボディに最大長についてRFC規定ありましたでしょうか? まあ、ないんですけど。 例えばApacheの場合、LimitRequestBodyにて許可バイト数設定しています。 ・そのため、開発者はmemory sizeのエラーにそうぐしたときにレスポンスオブジェクトの返却を諦めてアクションでecho をし始めます。 もしかすると、getContent()にてコールバックを許容すればいいじゃないかと思う方もいるかも知れませんが、そうすると戻り値の方が一致しません。 ・そして次に、これはStreamリソースを利用するという発想が抜けていることを意味します。 これはHTTPクライアントライブラリの実装を確認すればstream_copy_to_streamの利用を確認もできるわけです。 もし、ちょっと意味が分からないという方は、 file_get_contents()をわけもわからず使っている可能性があります。 file_put_contentsするデータをfile_get_contents()をとりあえずしてからおこなってませんか? 30 では、PHPのレスポンスの出力はどう行われてるか、ざっくりとですが確認してみましょう。 さきほどまででの流れの図ではリクエスト取得の箇所としてrequest_startupを見ていただきました。 outputの際には、request_shut_downにて出力バッファリングのスタックから出力が行われいます。 31 出力バッファリング、ここで改めて基本的なところを振り返ってみましょう。 レスポンスボディの出力・フラッシュについてですが、 標準の設定では、echoがあれば直ちに送信や文字列すべてを出力をしません。 パフォーマンスのためにデフォルトのphp.iniではoutput_buffering = 4096 に設定されています。 ob_start() コールバック関数についてですが、 ob_start() での引数、またはphp.iniでのoutput_handlerの指定により、出力バッファの内容を操作できます。 そしてそのob_startを用いたテンプレートエンジン / Viewレンダラーでの応用です。 出力内容を文字列として取得するために、ob_start() ob_get_contents()を行っているライブラリが多く見られます。。 32 では、さきほどの問題に戻って、 stringではなくstreamを返すようにようにすればどうでしょう? 33 まず、その前に PHP開発者の多くでは、ストリームについて遠ざけてしまっているように思えますので、 簡単にここではレスポンス作成での観点にてストリームの特徴をあげますと、、、、 ・抽象化 I/O PHP 4.3から登場したresourceオブジェクト 普段のファイルシステムやhttpなどのスキーマは、デフォルトのラッパーにすぎない ・入出力ストリーム php://temp により、メモリならびにテンポラリファイルへの読み書きが行える ・ストリームフィルタ ストリームはカスタムフィルタを作成し、登録できる 34 こうして、 ストリームや出力バッファリング制御 の利用により、メモリ利用量を抑えレスポンス出力時に処理を実行することは可能でしょう。 ただし、レスポンスボディ作成時の例外(DBコネクションエラーなど)などを考慮すると一度テンポラリーに書き出しておき、そのストリームリソースを再度渡すなどの対応も考慮すべきではないでしょうか。 またPHPにてレスポンスを返却するのではなく、apacheの x-senm-fileなどを利用するという戦略の持ち方もあるでしょう。 35 (無言) 36 しかしながら、PHP開発者はHTTPメッセージコンポーネントを使いたいという場合に、頭を抱えてしまうことになります。 37 いくつかあげますと、このようなHTTPコンポーネントがあげられます。 もしかしたら、みなさんの中には、自分の愛する特定のコンポーネントを使えばいいんだ!って考えてるかたもいるかもしれませんが、それってnot invested here シンドロームとかと何が違うんでしょうか。 38 このHTTPコンポーネントが多すぎる問題、プラグインの開発なども踏まえ問題になるのですが、この 依存を抑え、リクエスト・レスポンスを扱う処理 を相互運用するには? 39 その観点から、このミドルウェアとして最近PHP界隈にて取沙汰されているアプローチがあります。 40 リクエストを受け取って、レスポンスを合成し、次の処理へ渡す つまり 41 このようにHTTPメッセージを受け取り、かつ処理をパイプできるようにするということです。 → もういちど言いますと、(40) → 39 といったミドルウェア構成ということです。 ここでは、メッセージボディにはストリームを使っているということも着目してください。 42 では、いったんPSR-7とミドルウェアについてポイントを見ましょう。 ・psr-7はHTTPメッセージの値についてのinterfaceを定義している 従来のレスポンスクラスでは、send()メソッドなんて用意していた ・サーバーリクエストには、アプリケーションでの相互運用のために attributesプロパティが用意されている これは、もしかたら不純物なものを求めるひとには邪悪に見えるかもしれません。 ・つぎに、 今は、php-figという場で新しいPSRとしてミドルウェアシグネチャの要望があがっていますが、ミドルウェアシグネチャでの議論・検討の余地があります。 赤字で書いたのは私のしけんでありますが、 従来のEventManagerを利用したプラグインとの使い分けは? HTTPクライアントでのミドルウェアはどうあるべきか? インターフェイスなどにて別途psrを定義すべきでは? という所が考えられます。 43 さて、今までストリーム利用という点で、PSR-7に触れましたが、こんな意見もあります。 PSR-7では、主にエラー処理などの対応のためにもメッセージをStreamInterfaceとして定義しているのですが、ミスリーディングな名前でもあります。 個人的には内部はstreamリソースでも名前はbufferなになになインターふぇぃす名にしとけばよかったのかな、とは思ったりしてます。 44 では、最後となりますが。 今回の話では、 HTTPリクエストに対し、どうHTTPレスポンスを返すか?データの流れはどのような工夫が必要だったか?PHP内部と周辺プロジェクトの状況を簡単に俯瞰してみました。 普段のコーディングでは内部の動作についてはあまり意識していないかも知れません。しかし、現実的な問題(アプリケーションロジック)に専念するためにも、再度振り返っていただければと思います。 45 ありがとうございました
ところで、カンファレンス福岡では「特集ページとか作られて気合入ってますね?」などとお声をかけていただいたり、私の発表後に「ガチすぎて~」のような反応いただきたましたが、、、 え、気合の入ってないもの発表してどうするの?。。。
まあそうは言ってもカンファレンス福岡・勉強会@東京では、難しいとの反応いただいておりましたのですが、発表の分かりやすさに私なりの苦心の結果ということでご理解いただければと思います。
カンファレンス福岡の話となりますが、特にスタッフの方には私の「OpenOfficeのノートってどうすればいいか分からないナリー」な所をサポートしていただいたところなど再度お礼申し上げます。また発表内容としては、資料作成時に「やっぱApacheリクエスト構造体ダンプしないとダメやな」など思って、人間とウェブの未来 - Apache内部の情報をダンプするモジュール mod_request_dumperも拝見していたので松本亮介さんの発表を聴けたので良かったです。また新原さんの「制約でコードに秩序を」の発表については、オブジェクトの発表後に「jsonSerialize
とかあるじゃないすかってツッコミは野暮?」など話もさせてもらいましたが、Zend FrameworkでのHydratorやSymfonyでのSerializerなど、ハイドレーションについて思う所はあるのですが、この話はまた今度。。
PHP勉強会@東京では、カンファレンスでのスライドにちょっと手を入れつつでの発表をいたしました。
カンファレンス福岡での質問については末に捕捉もしておりますので、カンファレンス福岡にて話を聴かれた方も確認いただければとおもいます。uzullaさんの質問の箇所の追記は勉強会@東京の方々には余計頭混乱させたかと思います。スイマセン。
@sasezaki 色々察して頂くような感じですみません。まだちゃんと伝わるか自信ないのですが、ちょっと長文なのでエントリでおこしました。https://t.co/2z3STLKxv6
— うずら (@uzulla) 26 May 2016
カンファレンス福岡・勉強会@東京の発表では、冬頃に、APIデザインケーススタディ ――Rubyの実例から学ぶ。問題に即したデザインと普遍の考え方:書籍案内|技術評論社 を読んだこともあってかHTTPメッセージについてのAPIについての考察・主張をしております。(勉強会などでの発表に講師としての説明を求める人もいるかも知れませんが、私は基本的に主張をしに行ってます。)
今回の発表では、HTTPメッセージでのデータの流れでのAPIを主軸にしていましたので、ちょっとスコープ外でした。 もちろん、$_SESSIONグロバール変数は、HTTPメッセージと密接な関係ですので、
セッションとは何か…phplibからの変遷・サーブレットとは・機密性とは・sessionstorageとは・ステートレスとは・そしてauto csrf protection とは…、phpcon でのuzulla さんの濃密な一時間基調講演、期待しております。
— PHP6 グローバルアンバサダー (@sasezaki) 27 May 2016
ところで、スライドの画像について、質問頂いたのですが
のものを主に利用させてもらいました。ありがとうございます。(有名となりすぎた「いらすとや」の画像はラクダだけ使っています。)