PHP用ORM Emonkak\Ormの概要

背景

データベースを利用したWebアプリケーションをインフラストラクチャ(技術的機能)の観点から単純化すると、クライアントのリクエストを入力に、データベースに格納されたデータをCRUD(Create/Read/Update/Delete)して、その結果をレスポンスとして出力するものと定義できる。

この定義からいうと、リクエストとレスポンスの入出力を管理するのがWAF(Web Application Framework)で、データベースに格納されたデータをCRUDするのがORM(Object-relational mapping)となる。 両者はWebアプリケーションを構成するインフラストラクチャのコアであり、特にORMはドメインモデルの設計にも大きな影響を与えるのでその重要性はより高い。

そのような中、ORMといえばRuby On RailsのActive Recordに代表されるような重厚長大なものが主流だ。 一方で、ドメインモデルを特定のインフラストラクチャ(ORM)に依存することを避けるという観点からは、よりシンプルで依存性の少ないORMの方が望ましい。

ドメインの関心とインフラストラクチャの関心を適切に分離して、それぞれの関心に注力することでシンプルかつ明瞭な設計を実現する。 そのために新しいORMが必要だった。

コンセプト

Emonkak\Ormはフレームワークではなくライブラリとして設計した。

ここで言うフレームワークとライブラリの違いは以下の点にある:

ここでフレームワークとしてのORMの問題を考えてみる。

よくある例としては、ドメインモデル(エンティティ)の実装にORMが提供する基底クラスを継承しなければならないということがある。 これでは、ドメインとインフラストラクチャの関心が混ざり合い、適切に分離するのが難しい。 ORMが提供するモデルとドメインモデルを別個のものとして設計するという考えもあるが、冗長な実装になりがちでデメリットの方が大きいだろう。

制御の逆転を利用したフレームワークの難点としては、単純にわかりづらいという問題がある。 どのように機能が呼び出されるのかは、フレームワークの動作を知らなければわからないからだ。

そこで、Emonkak\Ormはあくまでインフラストラクチャの実装を助けるためのライブラリであると位置付け、ドメインの設計に影響を与えないようにした。 いわゆるリポジトリの実装に使うための一つのライブラリに過ぎないということだ。

特徴

Emonkak\Ormはクエリビルダー、リレーション、オブジェクトへのマッピングと基本的な機能しか提供しない。 データベースの抽象はEmonkak\Databaseが、結果セットのシーケンス処理はEmonkak\Enumerableと別のライブラリによって提供されるものを利用する。

  • SELECT, INSERT, UPDATE, DELETE文のクエリビルダー
  • カスタマイズ可能なマッピング動作
  • 一対一、一対多、多対多のリレーション
  • アクセスされるまで読み込みが遅延される遅延リレーション
  • PSR-6 Caching Interfaceを利用したキャッシュ付きのリレーション
  • ページネーション

使い方

※すべて開発中のものなので変更可能性がある

基本的な使い方としてはクエリビルダーを作成してクエリを構築、用途に合った実行メソッドを呼ぶ形になる。

SELECT文を発行して結果セットを取得する

クエリの実行結果を得るための結果セットを取得するには getResult() メソッドを呼び出す。 getResult() 実行時点ではまだクエリは実行されておらず、foreach で走査するなどして、実際に結果を得ようとして初めてクエリは実行される。 結果セットは Emonkak\Enumerable\EnumerableInterface を実装しているので、例えば first() メソッドを呼び出すことで一件だけ取り出すことができる。

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\Fetcher\PopoFetcher;
use Emonkak\Orm\SelectBuilder;

$users = (new SelectBuilder())
    ->from('users')
    ->getResult($pdo /** @var PDOInterface */, new PopoFetcher(User::class));

$user = (new SelectBuilder())
    ->from('users')
    ->where('user_id', '=', 1)
    ->getResult($pdo /** @var PDOInterface */, new PopoFetcher(User::class))
    ->first();

ここで PopoFetcher というのが出てきたが、これは Emonkak\Orm\Fetcher\FetcherInterface の実装クラスで、 Emonkak\Database\PDOStatementInterface からどのように値を取得するのかを決めるものだ。 PopoFetcher の場合は指定されたクラスのインスタンスを作成して、それをPOPO(Plain Old PHP Object)としてプロパティに値を代入する。 POPOとはPOJO(Plain Old Java Object)PHP版で、マジックメソッドなどを利用しないごく普通のPHPオブジェクトを表す。

POPOの実装例:

<?php

class User
{
    private $user_id;
    private $first_name;
    private $last_name;

    public function getUserId()
    {
        return $this->user_id;
    }

    public function getFirstName()
    {
        return $this->first_name;
    }

    public function getLastName()
    {
        return $this->last_name;
    }
}

FetcherInterface の別の実装としては指定されたクラスのコンストラクター連想配列を与えてインスタンスを作成する ModelFetcher と、オブジェクトにマッピングせずに連想配列をそのまま返す ArrayFetcher の実装がある。 ModelFetcherActiveRecordタイプのORMにあるようなModelクラスを利用したい場合に役立つ。

ページネーションを利用する

指定された件数ごとにページ分けされた結果セットを取得するには paginate() メソッドを呼び出す。

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\Fetcher\PopoFetcher;
use Emonkak\Orm\SelectBuilder;

$userPages = (new SelectBuilder())
    ->from('users')
    ->where('user_id', '=', 1)
    ->paginate($pdo /** @var PDOInterface */, new PopoFetcher(User::class), 100 /** @var integer ページ分けの単位 */);

$users = $userPages->at(1);  // 1ページ目を取得
$userPages->getNumItems();  // 総アイテム数を取得
$userPages->getNumPages();  // 総ページ数を取得

更新系のクエリを実行する

結果行がない更新系のクエリを実行する際は execute() メソッドを利用する。

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\UpdateBuilder;

$stmt = (new InsertBuilder())
    ->into('users', ['first_name', 'last_name'])
    ->values(['foo', 'bar'])
    ->execute($pdo /** @var PDOInterface */);

$stmt = (new UpdateBuilder())
    ->table('users')
    ->set('first_name', 'foo')
    ->set('last_name', 'bar')
    ->execute($pdo /** @var PDOInterface */);

$stmt = (new DeleteBuilder())
    ->from('users')
    ->where('user_id', '=', 1)
    ->execute($pdo /** @var PDOInterface */);

簡易クエリビルダー

Emonkak\Orm\Sql クラスはSQL文字列と、プレースホルダーにバインドする値のペアを表すクラスだが、簡易的なクエリビルダーとしても機能する。 また、 Sql オブジェクトを各種クエリビルダーのメソッドに渡すことで、クエリビルダー単体では実現できないような複雑なクエリも表現できる。

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\Fetcher\PopoFetcher;
use Emonkak\Orm\Relation\Relations;
use Emonkak\Orm\SelectBuilder;
use Emonkak\Orm\Sql;

$users = (new Sql('SELECT'))
    ->append('*')
    ->append('FROM')
    ->append('users')
    ->append('WHERE')
    ->append('user_id IN')->appendValues([1, 2, 3])
    ->getResult($pdo /** @var PDOInterface */, new PopoFetcher(User::class));

// SQLの断片を条件式に指定することもできる
$users = (new SelectBuilder())
    ->from('users')
    ->where(new Sql('user_id = ?', [1]))
    ->getResult();

リレーションを利用する

リレーションは SelectBuilder あるいは Sql クラスの with() メソッドを利用することで行う。 with() に与えるリレーションは Emonkak\Orm\Relation\Relations が提供するファクトリメソッドで作成できる。

リレーションはネスト可能で、Emonkak\Orm\Relation\RelationInterfacewith() メソッドを使うことでネストできる。

作成できるリレーションの種類としては以下のものがある:

  • Relations::oneToOne(): 一対一
  • Relations::oneToMany(): 一対多
  • Relations::manyToMany(): 多対多
  • Relations::lazyOneToOne(): 遅延一対一
  • Relations::lazyOneToMany(): 遅延一対多
  • Relations::cachedOneToOne(): キャッシュ付きの一対一
  • Relations::cachedOneToMany(): キャッシュ付きの一対多

遅延リレーションについては別記事にて詳しい記述がある。

リレーション作成時に指定するパラメータは以下ものがある:

  • $tableName: string

    リレーション先のテーブル名。

  • $relationKey: string

    リレーション先のオブジェクトを格納するプロパティ名。

  • $outerKey: string

    リレーション元のテーブルの結合対象のカラム名

  • $innerKey: string

    リレーション先のテーブルの結合対象のカラム名

  • $connection: Emonkak\Database\PDOInterface

    リレーション先のテーブルのデータを取得するためのDBコネクション。 リレーションごとに指定するので、それぞれのリレーション先のテーブルが別のDBにあっても良い。

  • $fetcher: Emonkak\Orm\Fetcher\FetcherInterface

    リレーションのクエリの結果からどのように値を取得するかを決定する FetcherInterfaceインスタンス。 リレーションごとに指定するので、全く別のクラスか、あるいは連想配列をも混在してリレーションさせることもできる。

  • $proxyFactory: ProxyManager\Factory\LazyLoadingValueHolderFactory (Optional)

    遅延リレーションを利用する場合のプロキシオブジェクトを作成するためのファクトリ。

  • $cachePool: Psr\Cache\CacheItemPoolInterface

    キャッシュ付きのリレーションを利用する場合のキャッシュプール

  • $cachePrefix: string

    キャッシュ付きのリレーションを利用する場合のキャッシュキーのプレフィックス。

  • $lifetime: integer

    キャッシュ付きのリレーションを利用する場合のキャッシュの有効期限。

  • $builder: Emonkak\Orm\SelectBuilder (Optional)

    リレーション先のテーブルからデータを取得するためのクエリを生成するクエリビルダー。 リレーションする条件を特別に指定したい場合に利用する。

以下はユーザー(User)が投稿(Post)と、それに対するコメント(Comment)を持っているというリレーションの例だ:

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\Fetcher\PopoFetcher;
use Emonkak\Orm\Relation\Relations;
use Emonkak\Orm\SelectBuilder;

$hasPosts = Relations::oneToMany(
    'posts',
    'posts',
    'user_id',
    'user_id',
    $pdo /** @var PDOInterface */,
    new PopoFetcher(Post::class)
);

$hasComments = Relations::oneToMany(
    'comments',
    'comments',
    'post_id',
    'post_id',
    $pdo /** @var PDOInterface */,
    new PopoFetcher(Comment::class)
);

$users = (new SelectBuilder())
    ->with($hasPosts->with($hasComments))
    ->from('users')
    ->where('user_id', '=', 1)
    ->getResult($pdo /** @var PDOInterface */, new PopoFetcher(User::class));

おわりに

Web開発の世界は長らくフルスタックのフレームワークが主流で、小さなライブラリを組合せてアプリケーションを構築するという文化があまりなかった。 しかし、PHPの世界ではPRR-7 HTTP message interfaceの登場以降、様々なソフトウェアが登場し、小さなものを組合せて大きなアプリケーションを作るというUnix哲学にも通ずる考えが徐々に普及してきた。

さらに近年はDDD(Domain-Driven Design)の考えが広く浸透つつあるのもあってか、アプリケーションのアーキテクチャについてさまざまな論考が出てきている。 アーキテクチャはアプケーションの構成部品をどう分割し、どう組み合せるかということで決まる。 そのような中、構成部品の組み合わせ方に制限を加えてしまうようなフレームワークは、アーキテクチャの決定にも制限を与えてしまうだろう。

そこで、アプリケーションの要件に適合するフレームワークを、小さなのライブラリの組み合わせで独自に構築するというのが一つの考えだ。 Emonkak\Ormはこの考えを元に開発されたライブラリである。

エンティティのライフサイクルに基いたクラス設計

エンティティとはその属性によってではなく識別子によって識別されるオブジェクトである。 属性が違っていても識別子が同一であればそのエンティティは同一のものと見なされる。 そのためエンティティは多くの場合、変更可能(Mutable)なオブジェクトとして表現される。 変更可能(Mutable)なオブジェクトは最初に生成されてから、それ以後に状態を変えることがある。 この状態遷移の流れがエンティティのライフサイクルである。

では、エンティティの状態はどのように変化するだろう。 それは、ドメインモデルを使って業務手順を記述したユースケースによって決まる。 ユースケースでは業務手順に基いてエンティティを作成し、状態を変更し、あるいは削除する。 これがエンティティのライフサイクルだ。

PHPにおけるエンティティの実装

エンティティはユースケースによってその状態を変えるが、ドメインモデルの一部であるエンティティをドメインのルールに反するような状態にしてはならない。 エンティティがドメインのルールに反する状態にあるというのは、バグに他ならないからだ。 実装レベルでエンティティがドメインのルールに反する状態にすることをできなくするのが望ましい。

基本的な方針としてエンティティの持つプロパティが、DBの列名と一致するように実装する。 プロパティは自由に参照・変更できないように private に隠蔽しておく。

class Person
{
    private $person_id;

    private $first_name;

    private $last_name;

    ...
}

コンストラクター

エンティティのコンストラクターprivate にして隠蔽しておく。 自由にインスタンスを作られては状態を適切に管理できないからだ。 インスタンスの生成は後述のFactory Methodによって行う。

private function __construct()
{
}

ゲッター

エンティティの属性値として、ただの文字列(String)や数値(Integer)ではドメインの関心を表現するオブジェクトして適切ではないことがある。 その場合はプロパティをそのまま返すのはでなく、ドメインモデル(値オブジェクト)を返すようにする。 そうでない場合はそのままプロパティを返却する。

public function getName()
{
    return new PersonName($this->first_name, $this->last_name);
}

セッター

単純なプロパティのセッターはすべて private にして隠蔽しておく。 自由に属性を設定できては状態を適切に管理できない。 外から状態の変更するメソッドはセッターではなくドメインに基いた振舞いとして別途適切に実装する。 セッターで設定される値を検証して値がエンティティの属性として適切ではなければ例外を投げるということもありうる。 この検証はあくまで属性レベルの検証なので、エンティティ全体の状態として正しいことを保証しているわけではない。

private function setFirstName($first_name)
{
    if ($first_name === null || $first_name === '') {
        throw new \InvalidArgumentException('名前は必ず設定する必要がある。');
    }
    $this->first_name = $first_name;
}

Factory Method

エンティティを生成する時は必ずFactory Methodを使用する。 コンストラターでいいと思うかもしれないが、PHPはメソッドのオーバーロードができないので別途Factory Methodを作った方が都合がいい。 このメソッドで作成されたエンティティの状態はドメインにおいて合法であることが保証される。

public static function create(Name $name)
{
    // first_name や last_name を持たない人間を作ることはできない
    $person = new Person();
    $person->setFirstName($name->firstName());
    $person->setLastName($name->lastName());
    return $person;
}

エンティティのライフライクルに基いた振舞い

これはドメインのルールに基いて、エンティティの状態を変更させるメソッドである。 これは対象とするドメインによって異なるので、その都度考える必要がある。 多くの場合ドメインの用語(ユビキタス言語)で動詞に対応するものが命令形で定義されることになる。

例:名前を変更する

public function changeName(Name $name)
{
    $this->setFirstName($name->firstName());
    $this->setLastName($name->lastName());
}

まとめ

業務手順を表すユースケースとは、エンティティのライフサイクルの記述に他ならない。 エンティティのライフサイクルはドメインに基いたものでなければならず、ユースケース実装に際してエンティティを不適切な状態にできるのは望ましくない。 これを回避するために、コンストラクターとセッターを隠蔽して、エンティティのライフサイクルに基いた振舞いだけを公開するといった手法を紹介した。 これによってエンティティを不適切な状態にすることができなくなり、ライフサイクルを適切に管理することができるようになった。

集約の実装とLazy Loading

エンティティの設計においてオブジェクト同士の関連(参照)は避けては通れないものだ。 エンティティ単体では不十分で、関連するエンティティ同士が結び付いて始めて意味を成す場面は往々にしてある。 そのような場合、エンティティを他のエンティティの参照を保持させる集約 として設計する。

しかし無闇に関連を持たせた集約を設計すればいいというわけではない。 関連をたくさん持った巨大な集約というのは扱いづらいし、双方向の関連は複雑さを生む。 必要最低限かつ、単方向の関連しか持たない集約が望ましい。 そのためには、エンティティの参照を保持するのではなく、その識別子(ID)を保持させることも検討すべきだ。 この時、エンティティの参照を得るには識別子をリポジトリに問い合わせればいい。

N + 1問題

集約とそれが参照する各エンティティがデータベース内に別々に格納されているとして、集約のエンティティを複数取得するケースではパフォーマンスの悪化が懸念される。 いわゆるN + 1問題がその典型的なケースだ。 N個の集約が与えられた時、集約自身を得るクエリ 1 つと、その関連を辿るクエリN を足したN + 1のクエリを発行してしまうという問題である。

この問題の解決方法は単純で、そもそもクエリを分けずに JOIN を利用して1 つのクエリにしてしまうか、IN句を利用して関連を辿るクエリを 1 つにまとめてしまえばいい。 いわゆるEager Loadingと呼ばれる方法だ。 N + 1問題が発生する方法が関連を参照するまで遅延読み込み(Lazy Loading)するのと対比して、この方法では即時読み込み(Eager Loading)するからだ。

Lazy Loadingと N + 1問題の回避

N + 1問題を回避するためにLazy Loadingは避けて常にEager Loadingすべきかというとそうとも言えない。 ユースケースによっては集約の一部の関連しか参照しない場合もあるかもしれないが、この時Lazy Loadingが利用できればクエリの発行自体を避けることができるのでパフォーマンスで有利になる。 しかしN + 1問題が起きると途端にパフォーマンスが悪化してしまうので、N + 1問題の起きないLazy Loadingの実装があれば使い勝手がいい。 つまり、エンティティごとに個別に読み込みを遅延するのではなく関連ごとに遅延すればいい。

emonkak/ormにはこの方式のLazy Loadingが実装されている。 ocramius/proxy-managerを利用した簡単な実装だ。

利用例:

<?php

use Emonkak\Orm\Relation\Relation;
use Emonkak\Orm\SelectQuery;

$users = (new SelectQuery())
    ->with(Relation::lazyOneToOne(
        Profile::class,
        'profiles',
        'profile',
        'user_id',
        'user_id',
        $pdo
    ))
    ->with(Relation::lazyOneToMany(
        Post::class,
        'posts',
        'posts',
        'user_id',
        'user_id',
        $pdo
    ))
    ->from('users')
    ->getResult($pdo, User::class);

foreach ($users as $user) {  // $users自体も実際に走査されるまでクエリの発行が遅延されている
    $users->posts;  // Postが格納されたArrayObjectのProxy Object
    $users->posts[0];  // 実際にアクセスするまでクエリの実行は遅延される
                       // 一度アクセスした時点ですべてのUserのPostが読み込まれる

    $users->profile;  // ProfileのProxyObject
    $users->profile->introduction;  // Post の時と同様にアクセスするまで読み込みは遅延される
}

おわりに

集約の読み込みにおいてLazy Loadingを利用することでクエリの発行自体を回避してパフォーマンスを向上させることができる場面があるが、N + 1問題が発生してしまうと逆にパフォーマンスが悪化してしまうので扱いづらい。 そこでN + 1問題の起きないLazy Loadingの実装があると使い勝手がいいのでemonkak/ormに実装した。

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

はじめに

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というライブラリがあります。 これは前職で関わったとあるプロダクトで利用されています。 この時はアプリケーションの要件がかなり特殊で既存のフレームワークとは適合しなかったので、独自のフレームワークを作成する必要がありました。

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

PDOにInterface欲しい

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

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

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

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

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

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

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

PHPでフィボナッチ数列を出力するソースコードを出力するプログラム

Quineみたいなものだけど、自身のソースコードを次々に書き換えて
それを新たに起動したPHPプロセスに標準入力として与えている。

fib.php

<?php
function php($input) {
    $process = proc_open('php', array(array('pipe', 'r'), array('pipe', 'w')), $pipes);
    if (is_resource($process)) {
        fwrite($pipes[0], $input);
        fclose($pipes[0]); 
    
        $output = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        proc_close($process);
    
        return $output;
    }
}
function callback($output) {
    $n = 50;
    $lines = explode("\n", trim($output));
    $count = count($lines);
    $lines[] = end($lines) + prev($lines);
    $f = __FILE__;
    $s = file_get_contents($f);
    $s = preg_replace("/__FILE__[;]/", "'$f';", $s);
    $s = preg_replace('/0\n1\n/', implode("\n", $lines), $s);
    return $count < $n ? php($s) : end($lines);
}
ob_start("callback") ?>
0
1
<?php ob_end_flush() ?>

Result

$ time php fib.php 
12586269025
php fib.php  0.51s user 0.28s system 92% cpu 0.851 total