CakePHP3.4が PSR-7に対応していた話(と、ミドルウェアの話)
夏は暑いので、むやみに遊びに出かけたりしないで冷房の効いたオフィスでいつもどおり仕事をしている kagata です。
さて、今回は CakePHP3.4でおこなわれた仕様変更のお話です。本稿執筆中に CakePHP3.5がリリースされてしまってどうにも機を逸した感がありますが、気を取り直して進めます。
例題:HTTP レスポンスにヘッダを追加する
CakePHP3.4での仕様変更を端的に示す例として、次のようなお題を考えます。
CakePHP アプリケーションが返す HTTP レスポンスに、ヘッダ My-Foo: bar
を追加してみましょう。
CakePHP3.3まで
従来は、破壊的メソッド Cake\Http\Response::header()
で次のようにしていました。
// コントローラで
$this->response->header('My-Foo', 'bar');
CakePHP3.4から
これからは、非破壊のメソッド Cake\Http\Response::withHeader()
で次のようにします。
// コントローラで
$this->response = $this->response->withHeader('My-Foo', 'bar');
従来の Cake\Http\Response::header()
も後方互換のため残されていますが、非推奨となりました。
この変更で本質的なのは、メソッドを呼び出したレスポンスオブジェクト自体が書き換わる方式から、メソッドの返り値を $this->response
に戻してやる方式になった、という点です。
ははん、メソッドの名前がなんか変わったんだな、と下のようにやってしまうと、追加したつもりのヘッダが追加されていない、という不具合に遭遇して苦しむことになります(苦しみました)。
// コントローラで
$this->response->withHeader('My-Foo', 'bar'); // $this->response は変更されない!
CakePHP3.4の仕様変更と PSR-7
メソッドを呼ぶだけでよかったものが代入文の形をとるようになるというのは、一見すると非効率なようにも思えます。書くべきコードが長くなるだけでなく、処理のオーバーヘッドも気になります。
この変更の意図が垣間見える記述が、公式ドキュメントにありました。
オブジェクトをその場で更新する既存のメソッドは非推奨になり、 PSR-7 規格にある不変オブジェクトパターンに従ったメソッドに取って代わられています。
今回の変更のカギは PSR-7にあることがわかります。
PSR-7とは
PSR 全体については、以前 PSR-2について書いた際に少し紹介しました。
PSR-2に準拠した PHP コードを書いてみて気づいたこと | バシャログ。
PSR とは、PHP アプリケーションの開発にかかわるさまざまな規約について、著名な PHP フレームワークやアプリケーションの開発者が PHP-FIG なる団体に集ってとりまとめているものです。この記事では PSR は7まであるとしていましたが、2017年8月現在では草稿を含め17まで増えています。
そして、その中の背番号7では "HTTP message interfaces" すなわち HTTP のリクエストやレスポンスを取り扱うオブジェクトのインターフェイスを定めています。
PSR-7: HTTP message interfaces - PHP-FIG
そのインターフェイスは概念だけのものではなく、実際に PHP のインターフェイスとして具体的なコードが提供されています。PSR-7 に準拠する際には、このインターフェイスの実装クラスを書くことになる、というわけです。
実際、CakePHP3の HTTP レスポンスクラスは、PSR-7 の提供するインターフェイスの実装クラスとなっています。
namespace Cake\Http;
// 中略
use Psr\Http\Message\ResponseInterface;
// 中略
class Response implements ResponseInterface
{
PSR-7 の不変オブジェクトとミドルウェア
PSR-7 が定める HTTP メッセージオブジェクトには、いずれも immutable すなわち不変であるという特徴があります。これが前述の、 $this->response->withHeader()
の戻り値をわざわざ $this->response
に入れ直してやらないといけないという話につながります。
一見すると、まどろっこしい設計にも見えます。しかし、メッセージオブジェクトが不変であることによりミドルウェアオブジェクトの設計がきれいになる、というメリットがありそうです。
ここでいうミドルウェアとは、Web アプリケーション本体に渡ってくる HTTP リクエストに前処理を加えたり、Web アプリケーション本体が投げ返す HTTP レスポンスに後処理を加えたりするオブジェクトのことです。LAMP の世界で「ミドルウェア」というと Apache とか MySQL をイメージしてしまいますが、この文脈では違うものを指しています。
特に CakePHP3 とミドルウェアについて、くわしくは以下の記事が参考になります。
ミドルウェアはしばしば、アプリケーションの外側を包む層のようなものとしてイメージされます。また、ミドルウェアは1枚の層ではなく、あるアプリケーションをある機能を持つミドルウェアがラップして、さらにその上から別の機能を持つミドルウェアがくるまって…と責務ごとに複数の層をなします。
このとき、ひとつのメッセージオブジェクトをグローバル変数のごとくミドルウェアのすべての層が共有してしまうよりも、層ごとにローカルなメッセージオブジェクトを新たに生成して次に渡す形にしたほうが、各層を疎結合に保ち、ミドルウェアの責務をきれいに切り分けやすい…と考えられます。
なお、PHP のミドルウェアについては PSR-15で規約を策定中です。
fig-standards/middleware.md at master · php-fig/fig-standards
まとめ
CakePHP3.4における Cake\Http\Response
の仕様変更から、新しい仕様の基礎となった PSR-7が定める immutable な HTTP メッセージオブジェクトのインターフェイス、さらにそれとかかわりの深いミドルウェアオブジェクトの概要までご紹介しました。
すぐに役立つ Tips としては、従来の $this->response->header();
を $this->resonse = $this->response->withHeader();
に書き換える、というだけの話ではあります。ただ、そこから垣間見えるミドルウェアの存在は、PHP で書く Web アプリケーションのアーキテクチャを変えてくれそうで、なかなか新鮮です。ミドルウェアについては今回は駆け足の説明だけでしたが、おもしろい使い方があればおいおい紹介していきたいところです。