Perl Hackers Hub

第41回Plack::Middleware再入門(3)

(1)こちら⁠2)こちらから。

汎用的なPlack::Middlewareを書く

さて、いろいろなPlack::Middlewareを利用していると、CPANにはないものが欲しくなって自分で書く場面がやってきます。ただ、Plack::Middlewareの実装にはPSGIの仕様上いくらか気を付けなければいけない点があり、ゼロから汎用的なものを目指すのは大変です。ここで紹介するユーティリティを利用すると、そうした問題を避けることができます。Plack::Middlewareを作る際には、ぜひ活用しましょう。

Plack::Util::Accessor─⁠─アクセサの生成

Plack::Util::Accessorを利用することで、Class::Accessor::Fastのようなアクセサモジュールを継承することなく、アクセサを生成できます。

Plack::Middlewareは、次のようにロード時にオプションを渡すことができます。file_etagがオプションです。

enable "ETag",
    file_etag => [qw/size/];

ミドルウェア側からは、Plack::Util::Accessorで生成したアクセサによって値を簡単に取得できます。

use Plack::Util::Accessor qw/
    file_etag
/;

sub call {
    my ($self, $env) = @_;

    my $file_attr = $self->file_etag;
}

Plack::Util─⁠─Plackに関するユーティリティ

Plack::Utilは、フレームワークやPlackサーバなどを書く際に利用できる関数が集まったクラスです。Plackアプリケーションやクラスのロードユーティリティをはじめ、PlackにおけるHTTPヘッダやレスポンスボディを操作する際に便利な関数が集まっています。その中からいくつかピックアップして紹介します。

response_cb─⁠─遅延レスポンスやストリーミングに対応する

Plackのレスポンスと言えば、要素が3つのARRAYリファレンスという認識の人も多いと思います。しかし、実は遅延レスポンスやストリーミングに対応するために、コードリファレンスを返すcallback形式のレスポンスもあります。そうしたコードリファレンスのレスポンスに対応するためのユーティリティがresponse_cbです。Plack::Middlewareの中では$self->response_cbとして利用できます。

sub call {
    my ($self, $env) = @_;

    my $res = $app->($env);

    return Plack::Util::response_cb($res, sub {
        my $res = shift;
        # do something with $res;
    });
}

遅延レスポンスやストリーミングを扱わないアプリケーションの中だけであればレスポンスを直接操作しても問題ないかもしれませんが、response_cbを利用することで汎用性を持った実装ができます。

header_*関数群─⁠─HTTPヘッダを操作する

Plackレスポンスのヘッダ部分はkey-value形式で並んだARRAYリファレンスです。シンプルな構造なので直接操作してしまいそうになりますが、Plack::Utilのheader_get、header_exists、header_set、header_push、header_remove、header_iterを利用することをお勧めします。

これらの中で注意が必要なのはheader_setとheader_pushです。header_setは引数にヘッダ名と値を渡して利用しますが、すでにそのHTTPヘッダが存在すれば上書きし、なければ追加するという挙動をします。それに対してheader_pushは単純に追加するだけなので、意図せず同じヘッダが重複する可能性があります。

また、Plack::Utilのドキュメントには書かれていませんが、header_iterという関数もあります。次のようにヘッダとコールバックを渡してヘッダ全体に処理を行う場面で有用です。

Plack::Util::header_iter(
    ['Content-Type' => 'text/plain'],
    sub {
        my ($http_header_key, $http_header_value) = @_;
        # do something
    },
);

また、関数ではなくオブジェクト指向な操作ができるheadersというメソッドも用意されています。

my $headers = ['Content-Type' => 'text/plain'];
my $h = Plack::Util::headers($headers);
$h->get($key);
if ( $h->exists($key) ) {(省略)}
$h->set($key => $val);
$h->push($key => $val);
$h->remove($key);
$h->iter($code);

is_real_fh─⁠─ファイルが実体を持っているかを判定する

Plackレスポンスのボディには、実はIO::Handleのようなgetlineメソッドとcloseメソッドを備えたオブジェクトを渡すことができます。ファイルを配信する場面などでデータ全体をメモリに載せずに済むアプローチです。Plack::Util::is_real_fhは、そうしたレスポンスがファイルの実体なのかそうでないかを判定する関数です。

set_io_path─⁠─ファイルハンドルに実体のパスを設定する

ファイルの配信をアプリケーションの前段にいるlighttpdやApacheやnginxに任せるX-Sendfileというしくみがあります。アプリケーション自体による転送がなくなるので配信効率の向上が望めます。Plack::MiddlewareでX-Sendfileに対応するにはPlack::Middleware::XSendfileを利用すると簡単なのですが、このミドルウェアはレスポンスのボディにあるオブジェクトが内部ファイルパスを返すpathメソッドを持っていることを期待しています。しかし、一般的なIO::Handleのようなオブジェクトはpathメソッドを持ちません。そこでこのPlack::Util::set_io_pathを使うと、オブジェクトにpathメソッドを追加できます。

prepare_app─⁠─ロード後に一度だけ呼ばれるメソッド

Plack::Middlewareにはprepare_appというメソッドを書くことができ、ロード後に1度だけ呼ばれます。重いインスタンス生成をcallメソッドの外に追い出したり、ミドルウェアのオプションの初期値を設定したりする場面で使います。

次のコードではprepare_appでData::UUIDのインスタンス生成を行いつつ、callbackにデフォルトのコードリファレンスを設定しています。

package Plack::Middleware::Foo;
use parent qw( Plack::Middleware );
use Plack::Util::Accessor qw( uuid callback );
use Data::UUID;

sub prepare_app {
    my $self = shift;

    $self->uuid(Data::UUID->new);

    unless ($self->callback) {
        $self->callback(sub {
            my $res = shift;
            warn "status:$res->[0]\n";
        });
    }
}

sub call {
    my ($self, $env) = @_;

    $env->{psgix.uuid} = $self->uuid->create_str();

    my $res = $self->app->($env);

    $self->callback->($res);

    return $res;
}

Plack::Middlewareのテスト

最後はテストについて紹介します。

Plack::Middlewareは、基本的にPSGIアプリケーションと同じように$envを受け取ってレスポンスを返すという挙動をするものなので、テストも同様に書けます。つまり、Plack::TestやTest::WWW::Mechanize::PSGIといったモジュールを利用して書くことができます。アプリケーションとクライアントのシンプルなやりとりをテストするならPlack::Testが最適です。フォーム送信をはじめとしたコンテンツ周りのより複雑な挙動をテストするならTest::WWW::Mechanize::PSGIが便利です。

Plack::Testを使ったテスト
use Plack::Builder;
use HTTP::Request::Common;
use Test::More;
use Plack::Test;

my $app = builder {
    enable 'ETag';
    sub {[
        200,
        ['Content-Type' => 'text/plain'],
        ['OK']
    ]};
};

my $cli = sub {
    my $cb = shift;
    my $res = $cb->(GET '/');
    is $res->code, 200;
    is $res->content_type, 'text/plain';
    is $res->content, 'OK';
    is $res->header('ETag'),
            '9ce3bd4224c8c1780db56b4125ecf3f24bf748b7';
};

test_psgi $app, $cli;

done_testing;
Test::WWW::Mechanize::PSGIを使ったテスト
use Test::WWW::Mechanize::PSGI;
use Test::More;
use Plack::Builder;

my $mech = Test::WWW::Mechanize::PSGI->new(
    app => builder {
        enable 'ETag';
        sub {[
            200,
            ['Content-Type' => 'text/plain'],
            ['OK']
        ]};
    },
);

$mech->get_ok('/');
is $mech->ct, 'text/plain';
$mech->content_is('OK');
$mech->header_is(
    'ETag', '9ce3bd4224c8c1780db56b4125ecf3f24bf748b7'
);

done_testing;

Plack::Middlewareのテストは比較的パターンが決まっているので、CPANにある多種多様なPlack::Middlewareの中から、似通ったものを探してそれを参考に書くのが一番簡単かもしれません。

また、筆者がPlack::Middlewareを書くときによくやるのは、先述したPlack::Middleware::DebugLoggingを有効にして、テスト中もリクエストとレスポンスの中身を見えるようにしておくことです。これにより、テストで利用するモジュール群が内部的にリクエストやレスポンスをよしなに扱ってくれる部分も、その都度見ることができます。何か困ったことが発生してからテストコードでprintデバッグするよりもずっと効率的です。

まとめ

Plack::MiddlewareはWebサーバとアプリケーションをつなぐ層でさまざまな仕事をします。今回はそのしくみを理解するところからはじめ、代表的なモジュールの紹介、そしてPlack::Middlewareを書くときに必須のユーティリティからテストまで、できる限り具体的に解説してみました。これを機に、みなさんのプロジェクトに眠るミドルウェアがCPANizeされたり、新しく有用なミドルウェアが登場してきたりすると非常にうれしく思います。

さて、次回の執筆者は星野将さんで、テーマは「広告配信サーバの事例から見るPerlとCPANモジュールの活用」です。

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧