PSR-7対応 HTTP/2サーバプッシュ用ミドルウェア

# 概要

HTTP/2には、HTML内に含まれるリソースに対し、クライアントが次にリクエストを送ってくることを見越して、リクエスト無しに**サーバ側から能動的に送る**サーバプッシュという機能があります。H2Oやnghttp2といったWebサーバはプリロード用のLinkヘッダを付与することでサーバプッシュを行ってくれるとのことなので、PSR-7対応のミドルウェアを書いてみました…

- <del>AdventCalendarの人たち怖いので普通に野良投稿</del>
- こういうのって`mod_mruby`でやったほうが良いの？

# 例

## ミドルウェア定義

```php:Middleware/AutoLinker.php
<?php

namespace mpyw\SampleApp\Middleware;

use \Psr\Http\Message\ServerRequestInterface;
use \Psr\Http\Message\ResponseInterface;

class AutoLinker {

    /**
     * インスタンスをそのままミドルウェアとして渡すことを想定
     *
     * @access public
     * @param  Psr\Http\Message\ServerRequestInterface $request  PSR7 request
     * @param  Psr\Http\Message\ResponseInterface      $response PSR7 response
     * @param  callable                                $next     Next middleware
     * @return Psr\Http\Message\ResponseInterface
     */
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) {
        $response = $next($request, $response);
        $server = $request->getServerParams();
        if (isset($server['SERVER_PROTOCOL']) && $server['SERVER_PROTOCOL'] === 'HTTP/2') {
            $response = static::withPreloadHeaders($response);
        }
        return $response;
    }

    /**
     * HTMLタグに応じたプリロードヘッダを付加する
     *
     * @access protected
     * @static
     * @param  Psr\Http\Message\ResponseInterface $response PSR7 response
     * @return Psr\Http\Message\ResponseInterface
     */
    protected static function withPreloadHeaders(ResponseInterface $response) {
        $dom = new \DOMDocument;
        $response->getBody()->rewind();
        @$dom->loadHTML($response->getBody()->getContents());
        $xpath = new \DOMXPath($dom);
        $selectors = [
            'style' => '//link[@rel="stylesheet"]/@href',
            'script' => '//script/@src',
            'image' => '//img/@src',
        ];
        foreach ($selectors as $as => $selector) {
            foreach ($xpath->query($selector) as $node) {
                $value = "<{$node->nodeValue}>; rel=preload; as={$as}";
                $response = $response->withAddedHeader('Link', $value);
            }
        }
        return $response;
    }

}
```

## 使用例

```php:index.php
<?php

namespace mpyw\SampleApp;

require 'vendor/autoload.php';

$container = new \Slim\Container;
$app = new \Slim\App($container);
$app->add(new Middleware\AutoLinker);
$app->get('/', function ($request, $response) {
    return $response->write('
        <!DOCTYPE html>
        <meta charset="UTF-8">
        <title>Example</title>
        <link rel="stylesheet" href="style.css">
        <img src="example.png">
        <script src="default.js"></script>
   ');
});
```

# 問題点

- クライアント側がキャッシュを保持しているにも関わらず毎回サーバプッシュでコンテンツを送信するのは無駄がある。
  - [HTTP/2 Push を Service Worker + Cache Aware Server Push で効率化したい話](http://jxck.hatenablog.com/entry/service-worker-casper)
  - [H2O - Configure > HTTP/2 Directives "http2-casper"](https://h2o.examp1e.net/configure/http2_directives.html#http2-casper)
- （静的なサーバプッシュに関しては`mod_mruby`等でやればいいが）FastCGIを使用する場合で動的なコンテンツに関して、HTML生成に時間がかかるが一部のリソースをプッシュすることが確定している場合においても、全ての処理が終わるまでサーバがプッシュを実行することが出来ない。

HTTP/2の仕様が策定されたといえ、サーバプッシュに関してはもう少し、という印象でしょうか。

# 追記

H2O作者のkazuhoさんご本人よりはてブコメントいただきました。

![スクリーンショット 2015-12-11 22.14.34.png](https://qiita-image-store.s3.amazonaws.com/0/25060/8294edbb-5def-930c-95b1-d8e2c41b27b2.png)

私なりの考察ですが

- PHPではレスポンスボディを1バイト以上出力し始めるまでレスポンスヘッダはバッファリングされ続ける。
- `HTTP/2`では`Transfer-Encoding: chunked`はサポートされていない。

ということを考慮すると、現在のPHPの構造上、実現は厳しい気がします…
（あらかじめ長めに`Content-Length`を確保しておく方法もあるが現実的ではない）

```html:こんな感じに出来たらいいなぁという理想（HTTP/1.1風の書き方）
GET /example.php HTTP/2
Host: example.com
Link: </example.css>; rel=preload; as=style
Link: </example.js>; rel=preload; as=script
Link: </example.png>; rel=preload; as=image
[... あらかじめここまでH2Oに渡しておいて長時間の処理に入る ...]
Content-Length: xxx

<!DOCTYPE html>
<title>Very Heavy Response</title>
Sorry for stupid latency, but all the following resources are preloaded!
<link rel="stylesheet" href="/example.css">
<script src="/example.js"></script>
<img src="/example.png">
```