Psr7を使ってみた(というか不変オブジェクトを初めて使った感想)

  • 48
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Psr7を使ってみたので感想を書いてみます。特徴はImmutable(不変オブジェクト)になっていること。それが何なのか興味があったので、不変オブジェクトを中心に書いてみます。

参考:PSR-7 By Example

Psr7の基本

Psr7とは、PHP-FIGというグループが作っているHTTPリクエストとリスポンスの標準仕様のことです。コードはhttp-message at gitHubにて見ることができます。

細かいAPIについては、ドキュメントを参考に。必要そうなメソッドは大体入ってます。方向性としては必要最低限の仕様を作ろうとしているそうです。

不変オブジェクト

大きな違いは、Immutableなこと。不変オブジェクトになってます。withで始まるメソードは、新しいオブジェクトを返します。

echo $request->getMethod(); // 'get'
$newReq = $request->withMethod('post'); // ←ここ!
echo $newReq->getMethod(); // 'post';
echo $request->getMethod(); // 'get'

つまりwithメソッドから戻ってきたオブジェクトを受け止めてあげないと、「値が設定されてない?」というバグが出現します。

echo $request->getMethod(); // 'get'
$request->withMethod('post');
echo $request->getMethod(); // 'get'、あれ?

こういうコードです。
実際、何度か間違えましたが、簡単に問題の箇所を見つけられました。withを探せばいいのと、大体原因が推測できるからでしょう。

不変オブジェクトで便利な点

不便なのか便利なのか、すぐにはわからない不変オブジェクト。要するに、関数に渡しても中身が変わらないのが保証されます。例えば…

$mod = function($request) {
  $request = $request->withMethod('bad'); // ←意外な挙動
};

echo $request->getMethod(); // 'get'
$mod($request);
echo $request->getMethod(); // 'get'、安全!

この$mod内で$requestに何をしようとも、呼び出し元のオブジェクトは変更されません。つまり「安心して」関数を呼ぶことができるわけです。これが不変オブジェクトの良い点です。

不変オブジェクトで不便な点

不便な点は…オブジェクトを変更してもらえない点です。変更して欲しい場合は、オブジェクトを返してもらうなど、前もって考えておく必要があります。

$mod = function($request) {
  return $request->withMethod('good');
};
$request = $mod($request);
echo $request->getMethod(); // 'good'

これだけなのですが、$requestにデータを保持させてあちこち持ち運ぶ、という使うことが簡単にできなくなります。こういう使い方を意外としてたようで、最初は不便に感じました。

不変オブジェクトのパフォーマンス

何度もオブジェクトを生成して、パフォーマンスへの影響は?と心配になるところです。メーリングリストでは、withを呼び出すと24ns〜150nsぐらい時間がかかるそうです。せいぜい10回程度の使い方なら十分無視できる、とのことです。

不変オブジェクトにより思いがけないバグを減らす効果のほうが大きい、と考えられます。

不変オブジェクトの使い方

この便利なような不変オブジェクトを使う場合のコーディングについて考えてみました。なお、コードは適当です。

返り値で返す

これが基本だと思います。

例えばStrutsのような場合だと、$responseに値を入れておけば、新しい状態を取得することが可能になります。

$response = $app($request, $response);

戻り値を使う意外な利点がありました。戻り値を無視すれば変更されないし、オブジェクトを上書きすれば変更を受け入れられるしと、変更について呼び側で決定できることです。

次に呼んでもらう

Chain of Responsibility (CoR)のような設計にすることで、対応できます。

$mod = function($request, $next) {
  $request = $request->withMethod('good');
  return $next($request); // ←次を呼び出す!
}

$mod($request, function($request) {
  echo $request->getMethod(); // 'good'
});

なんのメリットも感じられないサンプルですが、とても正しい解決法と思います。

コンテナを共有する

$appやstatic(つまりグローバル)な場所を使う。PHPだとスレッドがないので、安全に使えるテクニックです。

入れ物に入れてしまう

昔からあるテクニックです。ミュータブルなオブジェクトに入れ込んでしまいます。

$mod = function($dto) {
  $dto->request = $request->withMethod('huh?');
};
$dto = new stdClass;
$dto->request = $request;
$mod($dto);
echo $dto->request->getMethod(); // 'huh?'

このコードはイメージです。

せっかくのイミュータブルの利点が消えてしまう気がしますね。

転送用のプロパティを作る

こいつも闇改造な感じがしますが…

class Request extends Psr\Http\Message\Request {
  function setData($data) {
    $this->data = $this->data->with($data); // 不変オブジェクト?
  }
  function getData() {
    return $this->data;
  }
}

このコードはイメージです。

個人的には、ちゃんと実装できればありかな?と思ってます。そもそもミュータブルな情報がなければプログラムを書く必要はありません。つまり不変な情報と変更されうる情報を明確に区別できれば問題無い、と思うのですが…

最後に

最初はメリットより不便さを感じた不変オブジェクトですが、使っているうちに良さを感じるようになってきました。何かというと…

正しい設計になる

と思ったからです。今回の$requestのようにアプリケーションの中心のような変数を不変にしたら、きちんと設計しないと動かなくなるのは想像できます。

正しい設計だと、コードが玉ねぎのようなレイヤー構造になって、オブジェクトが外側から内側へと伝わってゆきます。不変オブジェクトなので、内側のコードは外側に影響を与えず、挙動が安定します。

何もかもイミュータブルにする必要はないと思いますが、コード内で取り回すオブジェクトを不変にするメリットは大きいと感じました。