読者です 読者をやめる 読者になる 読者になる

PSR-7とPSR-15を使ったWebアプリケーション開発

php

はじめに

PSR-7(HTTP Message)が承認されてからしばらく経ちますが、現在はこれを使った様々なライブラリ・フレームワークが登場しています。 これによって特定のライブラリ・フレームワークにロックインされずに、Webアプリケーションを実装できる道程が見えてきました。

しかし、PSR-7はあくまでHTTPメッセージのインターフェイスを提供するもので、リクエストを受け取ってレスポンスを返す流れを抽象化するものではありません。 これはHTTPミドルウェアと呼称されますが、そのインターフェイスはそれぞれの実装でまちまちです。 そこで、これを抽象化するPSR-15(HTTP Middleware)が提案されています。

ミドルウェアは大まかにダブルパスのミドルウェアと、シングパスのミドルウェアに分けることができます。 PSR-15は現在の所シングルパスのシグネチャを採用しています。

シングルパスのミドルウェアシグネチャ

function (ServerRequestInterface $request, callable $next): ResponseInterface;

ダブルパスのミドルウェアシグネチャ

function (ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface;

PSR-15のインターフェイスを提供するhttp-interop/http-middlewareにシングルパスを採用する理由があります。 ダブルパスのミドルウェアには事前にレスポンスを与える必要がありますが、そのレスポンスが利用可能なものかどうかを保証できないことが問題だと判断し、シングルパスを採用したようです。

The most severe is that passing an empty response has no guarantees that the response is in a usable state. This is further exacerbated by the fact that a middleware may modify the response before passing it for further dispatching.

残念ながら、この2つのインターフェイスには相互運用性がないので、アダプターを介してシグネチャを統一しても同じ動作を保証できません。 しかしこれは過渡期の問題で、恐らくはPSR-15の承認とともにどちらかに統一されることで解決に向かうはずです。

実装方針

本稿ではPSR-15を利用して簡単なサンプルとしてのWebアプリケーションを実装することを目的とします。 アプリケーションの設計については様々な手法がありますが、今回はDDD(Domain-driven design)を採用することを念頭に置いて進めていきます(サンプルなのでドメインと言っても実体があまりないですが)。

実装にあたっては既存のフレームワークを使うという選択肢もありますが、ここでは単機能なライブラリを組合わせて疎結合な独自のマイクロフレームワークを構築し、その上でアプリケーションを実装します。 これはあえてフルスタックフレームワークを避けたということでもあります。

フルスタックフレームワークは機能が豊富で便利なのは確かですが、注意深く設計をしないとドメインが特定のインフラストラクチャに依存する形になってしまいがちです。 さらには、複雑なドメインとアプリケーションの要件次第では、フレームワークの機能も十分に生かすことができず、かえって足枷になってしまうこともあります(フレームワークに汚いハックをして無理矢理要件を満たすなど)。 これはフレームワークが汎用のものである所以です。

ドメインは特定のインフラストラクチャから完全に分離されており、それ単体で動作するのが理想的です。 そうすると、ドメインへの影響は一切なしに、フレームワークを乗り換えることだってできます。 フレームワークもアプリケーションの要件に完全に合致したものがあればミスマッチもありません。

そこで、クリーンアーキテクチャを採用した、独自のマイクロフレームワークを作成するという道を選択しました。 幸いにして、そのための部品(ライブラリ)は揃いつつあります(揃えました)。 クリーンアーキテクチャではフレームワークに依存するレイヤーはこの図にある「Interface Adapters」と「Frameworks & Drivers」の部分だけです。

f:id:emonkak:20161209010829j:plain

https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

仕様

作成するのはアカウント登録、ログイン、ログアウトがあるだけのごく単純なアプリケーションです。

  • ユーザーはサービスを利用するためのアカウントを作成(サインアップ)しなければならない
  • アカウント作成にはメールアドレスとパスワードが必要
  • アカウントが作成されると自動的にログインされる
  • ログインに成功すると「Hello World!」と表示される
  • ログインしたアカウントはログアウトすることができる
  • ログインするにはアカウント作成時に入力したメールアドレスとパスワードが必要

実装

今回実装したアプリケーションのソースは以下にあります。

https://github.com/emonkak/php-http-app-skeleton

使用ライブラリ

いくつか自作のライブラリも含まれますが、それぞれ詳細については追って記事にしたいと思います。

ディレクトリ構造

Laravelの構成を参考にしています。

パッケージ構成

  • App\Adapters

    HTTP、コンソール(CLI)、データベースなどの外部インターフェイスへのアダプターが格納されます。 本来であればデータベースにアクセスするリポジトリの実装はこちらに配置するのが望ましいですが、今回はインターフェイス定義を省略して簡略化するために App\Domain に配置しています。

  • App\UseCases

    アプリケーションのユースケースが格納されます。 今回の場合はアカウントの認証と作成のためのサービスが格納されています。

  • App\Domain

    アプリケーションが対象とする問題領域であるドメインの実装が格納されます。 パッケージ直下には実装は格納せずに、集約ルート(ルートエンティティ)ごとにサブパッケージを定義します。 今回はアカウントのサブパッケージのみが格納されています。

  • App\Supports

    他の各レイヤーの実装を支援するためのユーティリティが格納されます。

エントリーポイント

まずはサーバーのエントリーポイントとなるスクリプトを見てみます。

最初に、リクエストからレスポンスを生成するための Application クラスのインスタンスを別ファイルから require しています。 この Applicationインスタンスにリクエストを与えて、生成されたレスポンスを送信するというのが全体の流れです。

<?php
// public/index.php

use Zend\Diactoros\Response\SapiEmitter;
use Zend\Diactoros\ServerRequestFactory;

// アプリケーションのインスタンスを読み込む
$app = require __DIR__ . '/../bootstrap/http.php';

// グローバル変数からリクエストを生成
$request = ServerRequestFactory::fromGlobals();

// リクエストからレスポンスを生成
$response = $app->handle($request);

// レスポンスを送信する
(new SapiEmitter())->emit($response);

アプリケーションの初期化

Application の初期化は bootstrap/http.php で行なわれます。 ここではオートローダーを読み込み環境変数の設定をした上で、各種ミドルウェアの登録を行っています。

以下は bootstrap/http.php の内容です:

<?php

require __DIR__ . '/../vendor/autoload.php';

// .envから環境変数を読み込み
(new Dotenv\Dotenv(__DIR__ . '/../'))->load();

if (getenv('APP_DEBUG')) {
    // デバッグモードならsymfony/debugによるエラー画面を表示する
    Symfony\Component\Debug\Debug::enable();
}

$app = new App\Adapters\Http\Application(realpath(__DIR__ . '/../'));

// 以降ミドルウェアの設定
// ミドルウェアは追加された順番通りに実行される

// POST時に_methodパラメータをPOSTすることでメソッドをオーバーライドする
$app->pipe(
    (new Middlewares\MethodOverride())
        ->post(['PATCH', 'PUT', 'DELETE'])
        ->parsedBodyParameter('_method')
);

// セッションを開始する
$app->register(App\Adapters\Http\Middlewares\SessionStarter::class);

// アカウントを認証する認証
$app->register(App\Adapters\Http\Middlewares\Authenticator::class);

// テンプレートに設定される共通の変数($request, $uri, $session, $flashes)を設定する
$app->register(App\Adapters\Http\Middlewares\ViewSharedVariables::class);

// ルーティングの結果から処理を設定されたハンドラに移譲する
$app->registerDispatcher();

// エラーのロギングを実行する
$app->registerErrorHandler();

if (!getenv('APP_DEBUG')) {
    // Dispatcherが処理できなかった時に最後に実行されるミドルウェア
    // 404ぺージが表示される
    $app->register(App\Adapters\Http\Middlewares\ErrorPage::class);

    // 同様の実装をエラー発生時にも実行されるように登録する
    // catchされない例外があればここでハンドリングされてエラーページが作成される
    $app->registerOnError(App\Adapters\Http\Middlewares\ErrorPage::class);
}

return $app;

DIコンテナの生成

Applicationインスタンスが生成される時に、コンストラクターでは bootstrap/container.phprequire してPSR-11の ContainerInterface の実装が生成されます。 要求されるのは ContainerInterfaceインスタンスなので、好きなDIコンテナの実装を利用することができます。 今回は自作の emonkak/di を使っています。

このコンテナにはアプリケーションに必要な各種インスタンスが登録されます。 環境の違いによって異なる設定を利用したい場合は、.env によって設定される環境変数を利用して切り替えます。

以下はDBのコネクションをコンテナに登録している部分のソースです:

<?php

$container->factory(PDOInterface::class, function() {
    return new PDO(
        sprintf(
            '%s:host=%s;port=%d;dbname=%s',
            getenv('DB_CONNECTION'),
            getenv('DB_HOST'),
            getenv('DB_PORT'),
            getenv('DB_DATABASE')
        ),
        getenv('DB_USERNAME'),
        getenv('DB_PASSWORD'),
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        ]
    );
})->in(SingletonScope::getInstance());

本サンプルでは設定値を管理するための専用の仕組みは得に設けていませんが、ほとんどの場合この環境変数を使う方法で事足りるはずです。

仮に環境によって特別な設定をしたい場合についても、以下のように if で分岐してしまえば対応できます。 多くの設定は環境によらず共通のはずで(環境ごとの差異が大きすぎるのは問題)、異なる部分だけをif文で分岐するというのが重複も発生せずシンプルでいいと思ってます。

<?php

if (getenv('APP_ENV') === 'production') {
    // production settings
} else {
    // other settings
}

ルーターの生成

ルーターはURLのパスに応じて、リクエストをハンドリングするクラス(ハンドラー)を決定するためのものです。

ルーターの生成は、パスに応じた処理を実行するための Dispatcher ミドルウェアの登録時に bootstrap/router.phprequire することで行われます。 ここで要求されるのは emonkak/router で定義されたインターフェイスである RouterInterface の実装です。

以下は bootstrap/router.php の内容です:

<?php

use App\Adapters\Http\Handlers;
use Emonkak\Router\TrieRouterBuilder;

return (new TrieRouterBuilder())
    ->get('/', Handlers\Index::class)
    ->get('/api/ping', Handlers\Api\Ping::class)
    ->get('/accounts/sign_up', Handlers\Accounts\SignUp::class)
    ->post('/accounts', Handlers\Accounts\Create::class)
    ->get('/sessions/login', Handlers\Sessions\Login::class)
    ->post('/sessions', Handlers\Sessions\Create::class)
    ->delete('/sessions', Handlers\Sessions\Delete::class)
    ->build();

これは単純にルーティングの内容を書き下しているだけですが、ハンドラーの命名に一定の法則を設けることで、クラスの一覧を取得して自動的にルーティングを定義するという方法も考えられます。 さらに生成したルーティングの内容をシリアライズした上でキャッシュすることで、性能的にも有利になります。 ルーティング数の多い大規模アプリケーションであれば、このような方法も検討もするといいかもしれません。

実装のイメージとしては以下のようなものです:

<?php

if (file_exsits(__DIR__ . '/../storage/router.cache.php')) {
    return unserialize(file_get_contents('/../storage/router.cache.php'));
}

$builder = new TrieRouterBuilder();

foreach (RoutingResolver::resolve(__DIR__ . '/../src/Adapters/Http/Handlers') as list($method, $path, $handler)) {
    $builder->route($method, $path, $handler);
}

file_put_contents(__DIR__ . '/../storage/router.cache.php', serialize($builder->build()));

return $router;

ハンドラーの実装

リクエストをハンドリングしてレスポンスを返すクラスをハンドラーと呼んでいます。 あらゆるPSR-15のミドルウェアはハンドラーとして利用できます。

その他、1つのクラスで複数のエンドポイントを取り扱う、コントローラースタイルのクラスをハンドラーとして登録することもできます。 コントローラーを登録する時はルーターにクラス名とメソッド名のペアを指定します。 指定されたクラスのメソッドは ServerMiddlewareInterfaceprocess() メソッドと同じシグネチャであることを期待されます。

コントローラーの例:

<?php

class ExampleController
{
    public function index(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        ...
    }

    public function create(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        ...
    }

    ...
}

http-interop/http-middleware に言及がありますが、PSR-15のシングルパスのミドルウェアはレスポンスの生成をミドルウェア自身が行うので、レスポンスの実装に対する依存が起きてしまいます。

Some have argued that passing the response helps ensure dependency inversion. While it is true that it helps avoid depending on a specific implementation of HTTP messages, the problem can also be resolved by injecting factories into the middleware to create HTTP message objects, or by injecting empty message instances. With the creation of HTTP Factories in PSR-17, a standard approach to handling dependency inversion is possible.

確かにPSR-17を使うことで特定の実装に依存することは回避できるのですが、ちょっと使い勝手が良くありません。 例えばJSONのレスポンスを返す時にContent-Typeを適切に設定してデータを json_encode() を設定しなければならないのはなかなかに面倒です。

そこでもっと簡単にレスポンスオブジェクトを生成できる Respondable トレイトを作成しました。

<?php

trait Respondable
{
    public function html($html, $statusCode = 200, array $headers = [])
    {
        return new HtmlResponse($html, $statusCode, $headers);
    }

    public function json($data, $statusCode = 200, array $headers = [])
    {
        return new JsonResponse($data, $statusCode, $headers);
    }

    public function redirect($uri, $statusCode = 302)
    {
        return new RedirectResponse($uri, $statusCode);
    }

    ...
}

これらのメソッドをハンドラーにミックスインして呼び出すことで、直接特定のレスポンスの実装に依存することはなくなりました。

おわりに

PSR-7とPSR-15を使ってマイクロフレームワークを作成して、簡単なサンプルアプリケーションという試みをしてみました。 機能としては不足している点はありますが、この成果は現実のアプリケーションに十分に適用できるものだと思います。

実は、このような試みをしたのは2度目で、その時の成果として emonkak/wafというライブラリがあります。 これは前職で関わったとあるプロダクトで利用されています。 この時はアプリケーションの要件がかなり特殊で既存のフレームワークとは適合しなかったので、独自のフレームワークを作成する必要がありました。

本稿では説明不足な点も多々ありますが、ソースはシンプルで読み易いはずなので、興味があれば読んで頂ければと思います。

株式会社インフィニットループを退職しました

work

2011年10月から約五年間務めていました株式会社インフィニットループを退職しました。

今の会社を離れて新しいことをやりたくなったというのがその理由です。 別の言い方をするのであれば、会社から与えられる仕事について不満があったというのが正直な所です。

インフィニットループソーシャルゲームの受託開発を主としていますので、開発の人員の割合もこれに関するものが最も多いです。 そのため社員にはゲーム好きが多く集り、自社で開発したゲームについても熱心なプレイヤーが社内にもいます。

私自身については、ゲームがとにかく好きで暇さえあればゲームをやっていた時代もありましたが、今はほとんどやらなくなって十年以上が経ちます。 そのきっかけは何だったかというと、自分はゲームをやることよりも物語を楽しむことが好きだと気付いたのが始まりです。 物語を楽しむのであれば、私にとってゲーム性の部分は不要で、ビジュアルノベルで良かったし、さらには小説やアニメで良かったということです。

そんな中、ゲーム好きが集まる社内では疎外感を感じることもありましたし、ソーシャルゲームの開発というのも自分として決してやりたい仕事ではありませんでした。 しかしながら、今の時代プログラマーのキャリアとしてソーシャルゲームの開発に携わるというのは重要だと考えていたので、仕事として全力で取り組みはしました。 ですが同時にこれをずっと続けていくのは耐えられないとも思いました。

一方で、幸いにして自分は、会社からある程度好きなことをやっていいという裁量を与えられていました。 自ら新しい仕事を提案することもできましたし、嫌な仕事は断わって別のことをやらせてもらうこともできました。 実際、インフィニットループでの五年間でソーシャルゲームの開発に携わったのはごく僅かで、検定アプリだったり、勤怠管理だったり、某PBWだったりと他にも様々な開発に参加してきました。 結果的に、iOS/Androidネイティブクライアント、Unityクライアント、Windowsアプリケーション、JavaScriptのSPA、サーバーサイドではPHPとNodejs、などと様々な環境を経験することができました。

退職に至る話し合いの折も、いくつか自社企画の下で新しい仕事の提案を頂きました。 しかし、自分としてそれをこの会社でやる意味を見出せませんでした。 本気でそれを作りたいという会社としての強い意思が感じられなかったからです。

まずもって、ある企画を確固とした意思をもってサービスとして世に出すというのは、相当な熱量が必要になるのではないでしょうか。 それが受託中心の会社におけるマインドの問題もあって、難しいのではないかと思います。

最後に、インフィニットループでの様々な経験が今の自分の糧になっているのは間違いなく、得難い経験をさせて頂いた会社にはとても感謝しています。入社のころから考えると、会社の規模が二倍、三倍になって環境が大きく変わっていったのは、とても刺激的で興味深い体験でした。

これからは先の仕事は何も決まっていませんが、とりあえずは無職を満喫しようと思います。

時間計算における丸め

本稿では時間計算についての丸め操作を考える。 最初に丸めの定義は以下とする。

丸め(まるめ)とは、与えられた数値を、ある一定の丸め幅の整数倍の数値に置き換えることである。 https://ja.wikipedia.org/wiki/%E7%AB%AF%E6%95%B0%E5%87%A6%E7%90%86

丸めの対象

時間計算における丸めの対象は瞬間(Instant)あるいは日時(LocalDateTime)とする。

瞬間 とはタイムゾーン(場所)によらず一意な時間である。瞬間は一般にUTC時間で表される。 一方、日時 は特定の瞬間を表すものではなく、タイムゾーンによってその日時を表す瞬間は異なる。 瞬間日時タイムゾーンが与えられれば相互に変換することができる。

丸めの単位

時間計算における丸めの単位は継続時間(Duration)とする。 継続時間とは、長さを表す時間の概念で秒で表されるものとする(実際の実装ではナノ秒)。

ここでまず単純な例を挙げると:

  • 2001/02/03 01:02:03 を 10分(600秒) を単位に切り捨てる → 2001/02/03 01:00:00
  • 2001/02/03 01:02:03 を 10分(600秒) を単位に切り上げる → 2001/02/03 01:10:00

まず丸め処理をするためには、丸めの対象となる日時を数値に変換する必要がある。 この例だと丸めの単位が一日の長さの約数なので、日を無視して時間だけに注力すれば良い。 SI単位系を用いて計算すると 01:02:03(1 * 60 * 60) + (2 * 60) + 3 = 3723(秒) となるので、ここから 3723 % 600 = 123(秒) のように余りを求めてそれを足し引きすればいい。

これは自明は結果だが、以下のケースだとどうなるか:

  • 2001/02/03 01:02:03 を 7分 を単位に切り捨てる → ?

この丸めは一般に定義できない。 なぜなら7分(420秒)というのは一日の長さの約数ではないからだ。 一日の長さははSI単位系24 * 60 * 60 = 86400(秒) なので 86400 / 420 = 205.714286... のように割り切ることはできない。 すると日を含めた日時を数値に変換して余りを求めなければならないが、それはどの瞬間を起点とするか、1年の長さが何秒なのかによって結果が異なってしまう。 これは 紀年法と暦を定義しないと数値に変換することはできない ことを意味する。

実装

型レベル数値リテラルを使って型レベルFizzBuzz

型レベルFizzBuzz(及び、type familyにおけるガードの書き方) - claustrophobiaを見て自分も型レベルFizzBuzz書いてみました。 GHC 7.8以降で動作します。

Natを受け取るとFBkindを持つ型を返すFizzBuzz型族と、FBに対するsingletonになるSFB型を定義して、 あとは型からsingletonを生成してそれをprintしています。

SNumberT 1
SNumberT 2
SFizzT
SNumberT 4
SBuzzT
SFizzT
SNumberT 7
SNumberT 8
SFizzT
SBuzzT
SNumberT 11
SFizzT
SNumberT 13
SNumberT 14
SFizzBuzzT
SNumberT 16

TypeScript 0.95でコンパイルできないジェネリックメソッドのコード

typescript

0.91ではコンパイルできた以下のコードが0.95ではコンパイルできなくなっている。バグ?

interface IFoo {
    f<T>(): T;
}   

class Foo implements IFoo {
    f<T>(): T {
        return null;
    }
}   

エラーメッセージ:

/Users/emon/Desktop/test.ts(5,7): error TS2137: Class Foo declares interface IFoo but does not implement it:
        Types of property 'f' of types 'Foo' and 'IFoo' are incompatible:
                Call signatures of types '<T>() => T' and '<T>() => T' are incompatible.

追記:Issuesに上がってた。

Interface declaration with generics not working in TypeScript 0.9.5

PDOにInterface欲しい

programing php

PHPの色々なフレームワークでDatabase Driverのラッパーが量産されているのが不毛に思った。 ここに共通化されたインターフェイスがあればフレームワークの違いを気にせず色々できるのになと。

そこで、PSR-3のLoggerのように標準化できないのかなと思って調べてみると、DoctrinのDBALが目に付いた。

ソースを読んでみると、DBALのConnectionインターフェイスはPDOのサブセットになっている。 そうすると、PDOに対する実装であるPDOConnectionConnectionに対して実装を宣言するだけで済むわけだ。

PDOをインターフェイスとして定義することで、PDOの機能をそのまま同じインターフェイスで使うことができるし、PDOを使う時には不要な抽象化レイヤーを挟まないのでシンプルでいいなと。 PDOは事実上の標準なので多くの場合これで必要十分だと思う。

しかしながら、DBALにはクエリビルダーなど不要な機能も含まれているしdoctrin/commonへの依存もある。Connection部分だけ抜きだしたpackageがあればいいのだけど、今の所ないようだった。

ということでPDOのサブセットとしてのインターフェイスだけを提供するcomposerのpackageを作りました。

https://github.com/emonkak/pdo-interface

VimScriptでIteratorを作ってメソッドチェインで処理する

programing vim

業務でPHPを使っていてarray系の関数の酷さに耐えかねて作ってしまったUnderbar.phpですが、 VimScriptでもIteratorを作ってメソッドチェインで処理したくなってきますね!

ということで書きました。特に実用性はありません。