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

Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

Amazon Dash Button にみるドメイン特化クラスのヒント

この投稿は、PHP Advent Calendar 2016 - Qiita の 7 日目です。

DDD のようなドメインを意識した開発手法でなくても、ドメインコンテキストで必要な操作のみを実装したクラスを作ってみましょう話です。

Amazon Dash Button

Amazon Dash Button は、ボタンが一つだけあるデバイスです。このボタンを押すと、あらかじめ決められた商品の注文が Amazon に送信され、商品が配送されるというシンプルなものです。

サントリー天然水 Dash Button

このデバイスは、「商品を届けて欲しい」というユースケースに対して、ボタンを押すというシンプルなインターフェイスを実装しています。単にボタンを押すだけなので、その裏側でどのようにして実現されているかを知らずとも、誰もが利用できます。違う言い方をすれば、ボタンを押す(商品を注文する)ことしかできないので、操作方法を覚える必要もなく、想定外の使い方もやりようがありません。*1

このように、ドメインやそのユースケース、関心事に特化したインターフェイスを用意すれば、そのコンテキストの中では迷うことなく、自然に理解でき、扱い方を間違えることもありません。

ドメイン特化クラス

「ユーザに現在の年齢を尋ねる」というユースケースの実装を考えてみます。

User クラスとして実装したのが、下記です。

現在の年齢を算出するには、生年月日が必要となるので、コンストラクタで渡しています。生年月日は、cakephp/chronos パッケージの Date クラスで表現しています。

年齢を尋ねるには、age メソッドを利用します。

<?php
namespace Acme\Generic;

use Cake\Chronos\Date;

class User
{
    /**
     * @var Date
     */
    private $dateOfBirth;

    /**
     * @param Date $dateOfBirth
     */
    public function __construct(Date $dateOfBirth)
    {
        $this->dateOfBirth = $dateOfBirth;
    }

    /**
     * @return int
     */
    public function age(): int
    {
        return $this->dateOfBirth->age;
    }
}

このコードを実行するのが、下記です。生年月日をコンストラクタを渡して、age メソッドを呼ぶだけなのでシンプルです。

<?php
// 現在日を設定
// Chronos::setTestNow(Chronos::create(2016, 12, 8, 12, 34, 56));

$user = new User(Date::create(2000, 1, 1));
echo $user->age(), PHP_EOL; // 16

このままでも動作は問題は無いのですが、気になるのは、Date クラスです。これは、日付を扱うクラスなので、挙動としては問題ありません。ですが、このクラスは汎用クラスなので、ドメインのコンテキストで扱う生年月日ではありません。プロパティ名を見れば、生年月日であることは想像できますが、より明示するために生年月日を示すクラスを実装します。

生年月日を示したのが、下記の DateOfBirth クラスです。内部表現としては、Date クラスを利用しているので、ただのラッパーのようにも見えます。ここで大事なのは、Date クラスのインスタンスを内包して、DateOfBirth クラスでは、年齢を算出する age メソッドのみを実装しているという点です。これにより、DateOfBirth クラスが担うのは、年齢を算出するだけということが分かります。

<?php
namespace Acme\Domain;

use Cake\Chronos\Date;

class DateOfBirth
{
    /**
     * @var Date
     */
    private $date;

    /**
     * @param Date $date
     */
    public function __construct(Date $date)
    {
        $this->date = $date;
    }

    /**
     * @return int
     */
    public function age(): int
    {
        return $this->date->age;
    }
}

DateOfBirth クラスを使った User クラスは下記です。コンストラクタの型宣言が変わっているだけなのですが、Date クラスに比べると生年月日であることが読み取れます。もちろん、User クラス内で生年月日を操作する際も age メソッドしかできないので、間違いようがありません。

もし、配送日などの別の日付クラスが存在したとしても、誤って User クラスに与えることができません。要求されている仕様が分かりやすいだけでなく、誤った引数に対するガードとしても有効です。

<?php
namespace Acme\Domain;

class User
{
    /**
     * @var DateOfBirth
     */
    private $dateOfBirth;

    /**
     * @param DateOfBirth $dateOfBirth
     */
    public function __construct(DateOfBirth $dateOfBirth)
    {
        $this->dateOfBirth = $dateOfBirth;
    }

    /**
     * @return int
     */
    public function age(): int
    {
        return $this->dateOfBirth->age();
    }
}

ドメイン特化クラス実装のポイント

このようなドメインに特化したクラスを実装するには色々な方法がありますが、いくつかポイントを書きます。

操作を必要最低限に絞る

PHP には、数多くの組み込みクラスやライブラリ、フレームワークがあります。一般的なデータ型を示すためのクラスは探せば見つかると思うので、多くの場合、それをベースとして利用するでしょう。

その場合、継承ではなく上記の DateOfBirth クラスのように委譲を使って実装します。

例えば、DateOfBirth クラスを Date クラスを継承して実装したとします。この時、DateOfBirth クラスは、Date クラスの public / protected メソッドを受け継ぐので、Date クラスと同様の操作が可能です。Date クラスには、日付の加算、減算といったメソッドがありますが、このコンテキストの生年月日に関してはそういった操作は不要です。不要なだけでなく、うっかり使ってしまうと想定外の動作を招く場合もあります。

こうした違いは、IDE で開発すると明確です。委譲版 DateOfBirth は、age メソッドのみ補完候補となります。一方、継承版では、age メソッドに加えて、Date クラスのメソッドが多数出現します。一つしかボタンが無いのと、たくさんのボタンがあるのと、どちらが利用者として分かりやすいかは一目瞭然です。

  • 委譲版 DateOfBirth

f:id:shin1x1:20161207135012p:plain

  • 継承版 DateOfBirth

f:id:shin1x1:20161207135019p:plain

対象に特化する(汎用性を求めない)

上記、継承が使われる理由として、DRY を意識しすぎるため、継承で実装の再利用を行いたいというものがあります。

例えば、このシステムに配送日といったモデルがあったとしましょう。生年月日も配送日も日付を示すものなので、Date クラスを基底クラスとし、それぞれ継承したとします。しかし、生年月日と配送日を同種のものとして扱う場面というのは、現実世界ではあまり思い当たりません。

ドメインに特化したクラスの実装では、汎用的に実装する必要はなく、対象にのみフォーカスして実装します。

これは、一般的な汎用クラスとは指向している方向が異なります。あくまでドメインに特化したクラスなので、汎用性を考える必要はありません。*2

もし、実装を共有したいという目的であれば、共通処理を行う別クラスを実装して、そこに委譲するか、トレイトにまとめるという手法を取ると良いでしょう。

一つの概念を表す小さなクラスを作る

今回の生年月日のように、一つの概念のみを示すクラスを作るようにします。凝集度が高い小さなクラスを作ることで、実装しやすく、使いやすく、テストしやすいクラスになります。

こうしたクラスは取り回しが楽なので、どのようなアーキテクチャであっても、取り入れることができます。特に、DDD のようなドメインを意識したアーキテクチャでなくても、一部だけでも、一つの概念だけでも、簡単に取り込むことができます。

実際の開発では、ValueObject(値オブジェクト)として実装するところから始めると良いです。

コンテキストによって異なる

DDD でも「境界付けられたコンテキスト」があるように、コンテキストというのは重要な概念です。

同じ「生年月日」でも、本エントリで扱う「生年月日」と別のシステムで扱う「生年月日」では、あるべき振る舞いが異なるかもしれません。今回は年齢の算出のみを実装しましたが、「今日が誕生日かどうか」「今月が誕生月か」といった振る舞いが必要となる場合もあるでしょう。むしろ、この2つのみが関心事であれば、「年齢を算出する」は不要かもしれません。

このように、同じ用語、概念であっても、コンテキストによって求められるものは異なります。つまり、生年月日とはこうあるべきといった汎用の生年月日クラスを実装したとしても、それがそのまま適用できるかどうかは、コンテキストによって異なるということです。

ユースケース

こうしたドメインに特化したクラスを少しづつ導入したい場合は、ValueObject として導入するのが実装するのが良いでしょう。ValueObject にすれば、操作が限定できるだけでなく、オブジェクトの不変条件を内包したり、内部表現をカプセル化することができます。

例えば、下記のようなものが考えられます。

  • 日付(日付ライブラリは高機能なわり、ドメインで要求されるものは一部。)
  • 数値、特に演算が行われるもの(金額、ポイントなど。演算の限定、制約条件の付与。)
  • 要求仕様が明確なデータ(商品コードなど。)

それ以外でも、サービスのように状態を保持しない処理もドメイン特化クラスで実装しやすい箇所です。

例えば、暗号処理なら、実際の暗号化や複合処理は別クラスで実装し、後はユースケース別のクラスからこれらのクラスを委譲で利用して、暗号化や複合を行うという場合に使えます。暗号処理は、暗号化と複合をペアで行う場合が多いので、ベアごとにクラスを分けると、ここで処理で暗号化したものは、あそこで複合しているというのが分かりやすくなります。

さいごに

こうしたドメインに特化したクラスを作るというのは、一見面倒なように見えますが、やってみると面白いものです。

生年月日も、汎用日付オブジェクトで表現すれば、それで終わりです。一方、生年月日クラスを実装すると、生年月日にどのような振る舞いが求められているかを考えることになります。このような視点で仕様を見ていけば、一つ一つの用語にも関心がいき、より深くドメインを理解して、コードに表現するようになります。

これは、教条的なものではなく、むしろ感覚に近いもので、こうしたプラクティスを重ねていくことで、身についていくもののように思います。

ドメインに必要な部品を少しづつ作っていき、それを組み合わせて構築していくという作業は、安心感もあり、とても楽しい作業です。身近なところから、取り組んでみてはどうでしょうか。

blog.shin1x1.com

*1:誤って押して、想定外の注文が飛ぶことはありますが

*2:ドメインコンテキスト内で汎用性を持たせるということは有り得ます。

「DDD パターンを活用した Laravel アプリケーション開発」を Laravel Osaka 2016 で発表しました。

2016/10/19 に大阪で開催された Laravel Osaka 2016 にて、「DDD パターンを活用した Laravel アプリケーション開発」を発表しました。

f:id:shin1x1:20161110215814j:plain

会場の MOTEX さん。巨大スクリーンが 2 面あり、話しやすい環境でした。

発表資料

Laravel の具体的なテクニックに比べると抽象的な内容なので、どれだけ伝えられるか思案したのですが、聞いて頂いた方からのフィードバックや参加者アンケートでも概ね良い評価を頂けたので安心しました。

ValueObject については、さらに掘り下げて話せるテーマなので、これ単体でもまた話してみたいです。

いつも blog や資料を参考にさせて頂いている増田さんに tweet して頂きました。インターネットっていいですね :)

質疑応答

発表の後の質疑応答では、多数の質問がありました。質問に対して的確な答えができたかは別にして、その場で今話した内容について意見交換ができるのは楽しいですね。

さらに、イベント終了後の懇親会や、後日、別の場でも発表についての質問や意見があり、そこから議論に発展することもありました。

話したことに対するリアクションが貰えるのは嬉しい限りで、発表者冥利に付きますね。

質問頂いた内容は、ざっと下記のようなものでした。(発表後の質疑応答以外のものも含む)

  • DDD をはじめるには、どこから手を付ければ良いか?
  • ValueObject を多数作った際のパフォーマンスについて
  • デメテルの法則について
  • なぜ、あえて Laravel を使っているのか?
  • リポジトリパターンと Eloquent について
  • 遅延評価クエリをどう組み込むか
  • 例外の使い方

今回の発表内容は、Laravel に関する内容は一部のみで、大筋はどういったフレームワーク、言語でも関連することなので、色々な人が自分なりの解釈で考えやすい題材であったのが一つ理由としてあるでしょう。

また、各技術の紹介や解説の発表に比べ、それらをどういった考え方で、いかに組み合わせるかというものは少ないように思います。

システム開発を行うならば、誰もが日々頭を捻りながら行っている行為なのですが、こういった話が勉強会などで表に出ることはあまり多くありません。

ドメインやコンテキストがそれぞれ異なるので、他人に話しにくいというのはありますが、今回のようにある程度抽象化すれば、みんなで考えられる題材になるので、もっと色々な人がこういった話をすると良いですね。(私はぜひ聞きたいです!)

さいごに

Laravel Osaka 2016 は、おそらく国内での Laravel 単体のイベントとしては最も大きい規模だったと思います。 第 1 回目ということで、関西だけでなく、東京や福岡からスピーカーも参加者も集まっていて、どのセッションも興味深い内容でした。

スタッフのみなさん、楽しいイベントをありがとうございました!

「正規表現再入門」を PHP カンファレンス 2016 で発表してきました

2016/11/03 に開催された PHP カンファレンス 2016 にて、「正規表現再入門」を発表してきました。

資料

speakerdeck.com

togetter.com

内容は、正規表現のマッチングの動き、量指定子のマッチングパターン、バックトラックやパフォーマンスへの影響についてです。

下記のエントリを下地にして、マッチングの動きを分かりやすく伝えることを意識してお話しました。 blog.shin1x1.com

参加された方からは、「分かりやすかった」といった好意的なフィードバックを頂けたので、発表して良かったです。

これから正規表現を学ぶにしても、マッチングがどのように行われるかをざっくりと知っておけば、正規表現を書く際にも動きが想像できるので、この発表がお役に立てると嬉しいです。

偶然、同じ時間帯に発表があった徳丸さんのセッションでも正規表現に触れられていたようで、Twitter の TL が微妙に交錯しているのが面白かったです :)

SQL と似た感覚

下地となったエントリでも書いたのですが、正規表現と SQL はやはり似ているなあと感じます。 懇親会でも、発表を聞いて頂いた方との会話で話題になりました。

宣言的に書く言語は、記述しやすく理解しやすいのですが、どのように実行されるかはブラックボックスになります。 もちろん、それは利点(How ではなく、What に集中できる)なので、実行エンジンが良しなにやってくれる時は全く持って問題ありません。

ただ、今回のバックトラック爆発のように仕組み上発生しうる挙動に関しては、利用側がそれを意識して書く必要があります。

特に、正規表現はバリデーションで外部から来た値をそのまま扱う場面でよく利用されるため、問題があればすぐに影響を受けることになります。

SQL を書く時にデータベースエンジンの動きをざっくりでも意識するように、正規表現でも意識すると良いですね。

さいごに

今年は、午後最後のセッションで、途中にサイン会などもあったので、t_wada さんのセッションだけ参加しました。

このセッションは素晴らしくて、同じように参加した人からも大絶賛でした。 内容はもちろんのこと、構成や説明など伝え方がとても参考になりました。自分の発表にも取り入れられる要素があったので、今後に生かしていきたいです。

懇親会やその後の N 次会でも、色々な方とお話ができて、楽しい時間でした。毎年そうですが、蒲田の PHP カンファレンスは最高ですね!

運営のみなさん、今年も楽しいイベントをありがとうございました。

パフォーマンスを意識して正規表現を書く

正規表現を書く際、どのようなパターンにマッチさせるか、どこをキャプチャするかという視点で記述することはあっても、パフォーマンスを考えて記述するというのはある程度知っている人でなければ忘れがちな視点です。

このエントリでは、バックトラックをメインに正規表現がパフォーマンスに及ぼす挙動について見ていきます。

対象の正規表現エンジン

ここでは、従来型 NFA を対象としています。具体的には、PHP の preg_ 関数で利用している PCRE や mb_ereg 関数が利用している鬼車です。Perl や Ruby、Python、Java、.NET でも従来型 NFA を採用しているので、似た挙動となるでしょう。

「従来型 NFA」や「バックトラック」などの用語については、「詳説 正規表現 第3版」のものを用いています。

バックトラックによるマッチ探査

正規表現エンジンでは、指定された文字列が、パターンにマッチするかどうかを判別する際、記述された正規表現で取り得るマッチパターンが見つかるように何度もマッチングを行います。

例えば、\d+\d という正規表現があり、これに 123 という文字列がマッチするか preg_match 関数でチェックします。

<?php
var_dump(preg_match('/\d+\d/', '123'));

これを実行すると、\d+ の部分は、文字列 123 にマッチします *1。しかし、次の \d にマッチする文字列が無いので、一つ前のパターン(\d+)に戻ります。次は、\d+12 にマッチさせます。次の \d は、3 にマッチするので、これで全体のマッチが成功します。

下記では、この流れを示しています。()\d+ に、{}\d がマッチした箇所で、[]が現在マッチを試みている文字になります。

* (123)[]  <--- `\d+`はマッチするが、`\d`がマッチしないので、`\d+`へ戻る
* (12){3}  <--- `\d+`も`\d`もマッチする

正規表現は、前から順に適用されていくのですが、後続のパターンがマッチしない場合に一つ前のパターンに戻って、別のマッチ方法を試行するのをバックトラック(backtracking)と呼びます。バックトラックは、正規表現に限らず、正しい解を探るためのアルゴリズムです。詳細は、下記で。

https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%83%E3%82%AF%E3%83%88%E3%83%A9%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0

「マッチしない」を確定させるのは遠い道程

次は、マッチが失敗するパターンとして、正規表現 \d+\d[^\d]123 という文字列にマッチさせます。

はじめに \d+ は文字列 123 にマッチします。次の \d にマッチしない(文字列が無い)ので、バックトラックが発生します。次は、\d+は文字列 12 に、\d は文字列 3 にマッチします。最後の [^\d] にマッチしない(文字列が無い)ので、バックトラックが発生します。今度は、\d+ を 文字列 1 に、\d を 文字列 2 にマッチさせますが、[^\d] と 文字列 3 はマッチしません。

次は、文字列の先頭を 1 文字進めて 23 に対してのマッチを試みますが、こちらもマッチには成功しません。さらに、文字列 3 に対するマッチも失敗し、最終的には全てのパターンで失敗します。

下記では、この流れを示しています。()\d+ に、{}\d がマッチした箇所で、[]が現在マッチを試みている文字になります。

* (123)[]    <--- `\d+`はマッチするが、`\d`がマッチしないので、バックトラック
* (12){3}[]  <--- `\d+`と`\d`がマッチするが、`[^\d]`がマッチしないので、バックトラック
* (1){2}[3]  <--- `\d+`と`\d`がマッチするが、`[^\d]`がマッチしないので、バックトラック
* 1(23)[]    <--- 1文字進めてマッチング開始。`\d+`はマッチするが、`\d`がマッチしないので、バックトラック
* 1(2){3}[]  <--- `\d+`と`\d`がマッチするが、`[^\d]`がマッチしないので、バックトラック
* 12(3)[]    <--- 1文字進めてマッチング開始。`\d+`はマッチするが、`\d`がマッチしないので、バックトラック
* 123[]      <--- 1文字進めてマッチング開始。`\d+`がマッチせずに終了

このように、「マッチしない」と結論付けるために、指定した正規表現で取り得る全てのマッチングを行います。この挙動が場合によっては、パフォーマンスに大きな影響を及ぼします。

爆発するマッチング

マッチさせるまで(マッチしないことを確定させるため)、あらゆるマッチングを行うがゆえに正規表現や文字列によっては、マッチングの組み合わせパターンが膨大になる可能性があります。

マッチングが膨大になる様を確認するために、regex101.com というサイトを利用します。このサイトでは、正規表現と文字列を指定すると、正規表現エンジンがどのようにマッチングを行っていくかを表示してくれます。

regex101.com

例えば、上記の例(\d+\d[^\d])であれば、下のような表示となります。これを見ると、文字列 123 に対して、14 steps がかかっていることが分かります。

f:id:shin1x1:20160817160205p:plain

ここで文字列を 123456789 という 9 文字に増やすと、91 steps となります。

マッチングが膨大になる場合を見るために、(\d*\d+)+\d[^\d] という正規表現を利用します。これは、前述の \d+\d[^\d] とマッチする文字列は同じです。このパターンを前述の 9 文字にマッチさせると、なんと 33,805 steps になりました!

https://regex101.com/r/eB0dM5/1

このようにわずか 9 文字の文字列に対するマッチングでも正規表現の書き方によって、マッチングの組み合わせが膨大に膨らむことが分かります。より複雑な正規表現や与えられる文字列が長い場合は、パフォーマンスへの影響が出てきます。

ReDosという攻撃

こうした正規表現エンジンの特性を利用した攻撃が ReDos です。ReDos については、下記の大垣さんのエントリが参考になります。

ReDoSの回避 | yohgaki's blog

特に量指定子(*?+{n,m})を入れ子にしたり、同じ文字クラスを重ねたりすると組み合わせパターンが多くなるので、こうした問題が起こる可能性があります。

末尾にあるスペースにマッチ

上記の ReDos とは別のパターンですが、単純な正規表現でも DoS を引き起こす場合があります。

stackoverflow.com では、文字列前後のスペースを除去する ^[\s\u200c]+|[\s\u200c]+$ という正規表現に対して 20,000 文字のスペースが含まれた文字列が送信されたために、上記のマッチングの組み合わせが膨大になり、34 分間アクセス不能になるという事態がありました。

Stack Exchange Network Status — Outage Postmortem - July 20, 2016

StackExchangeが攻撃されたReDoSの効果 | yohgaki's blog

大垣さんのサイトでも実証されていますが、Stack Overflow の事例を参考に文字列末尾にあるスペースを正規表現でマッチさせて取り除くという処理を見てみます。

まずは、正規表現を単純化して、\s+$ を利用し、文字列は a(半角スペース 3 文字)にマッチさせる場合で動きを見ます。

この場合、下記のようにマッチングを行なわれます。半角スペースが見えるように _ としています。人間が見れば、文字列の末尾が a なので、どのようにマッチングさせても成功しないのは明白なのですが、正規表現エンジンでは、前から順に愚直に評価していきます。

* (___)[a] <--- `\s+`はマッチするが、`$`がマッチしないのでバックトラック
* (__)[_]a <--- `\s+`はマッチするが、`$`がマッチしないのでバックトラック
* (_)[_]_a <--- `\s+`はマッチするが、`$`がマッチしないのでバックトラック
* _(__)[a] <--- 1文字進めてマッチング。`\s+`はマッチするが、`$`がマッチしないのでバックトラック
* _(_)[_]a <--- `\s+`はマッチするが、`$`がマッチしないのでバックトラック
* __(_)a <--- 1文字進めてマッチング。`\s+`はマッチするが、`$`がマッチしないのでバックトラック
* ___[a] <--- 1文字進めてマッチング。`\s+`がマッチしないのでバックトラック
* ___a[] <--- 1文字進めてマッチング。`\s+`がマッチせずに終了

スペースが 3 文字ではなく、20,000 文字となれば、それだけ多くのバックトラックが発生するため、処理に時間がかかるようになります。

パフォーマンスを計測するために下記のコードを実行してみました。それぞれのスペースの最後には a が付いているのでマッチングは失敗します。

<?php
function benchmark($title, callable $target) {
    echo '# ' . $title, PHP_EOL;
    $start = microtime(true);
    $target();
    echo microtime(true) - $start, PHP_EOL;
    echo PHP_EOL;
}

$strings = [];
foreach ([20, 200, 2000, 20000, 200000] as $no) {
    $string = str_repeat(' ', $no) . 'a';
    benchmark('preg_replace' . $no, function () use ($string) {
        return preg_replace('/\s+$/', '', $string);
    });
}

このコードを PHP 5.6 と 7.0 で実行した結果が以下です。2,000文字あたりから処理速度が低下していき、20,000文字、200,000文字で急激に処理時間がかかっています。なお、5.6 より 7.0 の方が文字数が多い場合は処理が速いので、何かしらの最適化が行われているのかもしれません。

  • PHP 5.6.24
文字長 実行時間
20文字 0.05ms
200文字 0.35ms
2,000文字 7.2ms
20,000文字 2,438ms
200,000文字 233,513ms
  • PHP 7.0.9
文字長 実行時間
20文字 0.4ms
200文字 0.2ms
2,000文字 5.8ms
20,000文字 498ms
200,000文字 50,352ms

正規表現を改良

文字列末尾のスペースにマッチングさせる正規表現を改善するために \s++$ に変えてみます。元の最大量指定子(\s+)を絶対最大量指定子(\s++)に変更しただけです。

これでベンチマークを取ると下記のようになりました。両バージョンとも処理速度が改善しており、200,000 文字で見ると、PHP 5.6 で 18 倍、PHP 7.0 で 4 倍と大きな改善となっています。変更後は、PHP 5.6 と PHP 7.0 でほぼ互角になっているのも興味深い点です。

このように正規表現の書き方一つで、大きくパフォーマンスが変わる場合があります。

  • PHP 5.6.24
文字長 実行時間
20文字 0.17ms
200文字 0.041ms
2,000文字 1.91ms
20,000文字 172ms
200,000文字 12,879ms
  • PHP 7.0.9
文字長 実行時間
20文字 0.26ms
200文字 0.047ms
2,000文字 1.29ms
20,000文字 123ms
200,000文字 12,544ms

絶対最大量指定子を使った場合( aへのマッチング)は、下記のようなマッチングを行います。絶対最大量指定子は、貪欲なマッチングとなり、かつマッチした文字列をアトミックに扱います。これにより、バックトラックの発生が抑えられるのでマッチング回数を抑制できます。これは、アトミックグループ((?>))でも同様の効果が得られます。

* (___)[a] <--- `\s++`はマッチするが、`$`がマッチしないのでバックトラック
* _(__)[a] <--- 1文字進めてマッチング。`\s++`はマッチするが、`$`がマッチしないのでバックトラック
* __(_)a   <--- 1文字進めてマッチング。`\s++`はマッチするが、`$`がマッチしないのでバックトラック
* ___[a]   <--- 1文字進めてマッチング。`\s++`がマッチしないのでバックトラック
* ___a[]   <--- 1文字進めてマッチング。`\s++`がマッチせずに終了

今回計測したPHPコードは、下記です。

regex_trim.php · GitHub

あえて正規表現を避ける

PHP でこの処理を単純に考えれば、ltrim関数で簡単に実装できます。

ltrim関数を使うと、200,000文字のスペース + a の場合でも、わずか 1ms 以下でした。

stackoverflow.com では、正規表現を使うことをやめ、文字列処理(substring関数)に取り替えたようです。

さいごに

正規表現の書き方や文字列が、パフォーマンスに影響を与えることを見てきました。

マッチングパターンのみに注力して記述した正規表現が、正規表現エンジンによって、上手い具合にチューニングされて動作すれば良いのですが、最適化が上手く働かないケースでは、その挙動を把握した対処を行わないとパフォーマンスの問題を抱えることになります。

このあたりは、SQL にも似たものを感じしますね。

正規表現は、Web アプリケーションにおいてはバリデーションで利用されることが多く、外部の値をいきなり正規表現にかけてチェックするということもあります。バリデーションは成功しないが、DoS となるような値を投げられて、パフォーマンスが低下したり、サービスが停止させられる場合も考えられます。

正規表現のパターンを考慮する、文字列長をチェックしておく、場合によっては正規表現を使わない、など対処法は考えられるが、まずは正規表現がパフォーマンス上の問題になりうるということを認識することが第一歩になるでしょう。

参考

正規表現エンジンの挙動については、下記の 2 冊がとても参考になります。どちらかというと、「詳説 正規表現」は正規表現を利用する側、「正規表現技術入門」は正規表現エンジンを実装する側から正規表現を見た本となっています。どちらも内部の動きを分かりやすく解説しているので、本エントリのような内容をより知りたい方には特におすすめです。

どちらも Kindle 版が無いので、電子版を購入する場合は、オライリー社サイト、技術評論社サイトで購入します。

詳説 正規表現 第3版

詳説 正規表現 第3版

  • 作者: Jeffrey E.F. Friedl,株式会社ロングテール,長尾高弘
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/04/26
  • メディア: 大型本
  • 購入: 24人 クリック: 754回
  • この商品を含むブログ (85件) を見る

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

*1:これは、PCRE の量指定子が最大量指定子なためです。最小量指定子や絶対最大指定子を指定したり、正規表現エンジンタイプが異なればマッチの仕方は変わります。

より実践的なDDD本「.NETのエンタープライズアプリケーションアーキテクチャ第2版 」

.NETのエンタープライズアプリケーションアーキテクチャ 第2版 (マイクロソフト公式解説書)

DDD 関連の書籍といえば、Eric Evans の DDD 本( エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践) )や、Vaughn Vernon の IDDD 本( 実践ドメイン駆動設計 (Object Oriented SELECTION) )が有名です。

DDDで登場する実装パターンや周辺知識をより実践的に解説しているのが、「.NETのエンタープライズアプリケーションアーキテクチャ 第2版 (マイクロソフト公式解説書)」(naa4e)です。

.NET 以外にも大いに通じる内容

本書は、.NET の名を冠しており、サンプルコードは、C# で記述されています。また、利用するライブラリも .NET 周辺のものであったり、解説で登場する RDBMS も、Web 系でよく登場する MySQL や PostgreSQL よりも、SQL Server や Oracle がメインとなっています。

しかし、.NET 固有の話はそれだけです。.NET 固有の内容は、枝葉の部分であり、メインの内容は、プラットフォームやプログラミング言語に関わらずとても参考になるものでした。

日頃、.NET 界隈にいない人間としては、正直、マイクロソフト公式解説書としての体裁になっているがゆえに避けてしまう人がいるのが、勿体無いとさえ感じる本でした。

DDDをガラスケースから日々実践するプラクティスへ

DDD 本、IDDD 本と本書を比べると、より実践に寄せた内容となっており、DDD をより身近な普段の開発に適用するために解説されています。

実際の開発では、上手くパターンにあてはめられなかったり、妥協してしまう場面もあるのですが、そのあたりも考慮に入れられています(トランザクションスクリプトも場面によっては有用なパターンである、など)。本書の表現を借りると、ヒーローがいなくても、一般の開発者が DDD 取り組む方法を示した本とも言えます。

それぞれ執筆された時期が異なる(DDD 本(2003年)、IDDD 本(2013年)、本書(2014年))ので、理論から実践、さらに実践した上で現実的な落し込みといった具合に、より現実的な内容に推移しているのが興味深いです。

私は、DDD 本、IDDD 本、本書の順序で読んだのですが、本書が一番理解しやすいと感じました。*1

本書の内容

本書の目次は下記です。

現代の設計手法や開発手法、OOPの基礎(SOLID原則など)やテスト手法からはじまり、DDDで利用されるパターンの解説に入っていきます。

本書で興味深いのは、第6章のプレゼンテーション層がドメインアーキテクチャのすぐ後に来ている点と、CQRS と イベントソーシング(ES)に多くのページが割かれている点です。

■第1部 基礎
第1章 現代のアーキテクトとアーキテクチャ
第2章 成功のための設計
第3章 ソフトウェアの設計原則
第4章 高品質なソフトウェアの作成

■第2部 アーキテクチャの考案
第5章 ドメインアーキテクチャの発見
第6章 プレゼンテーション層
第7章 伝説のビジネス層

■第3部 サポートアーキテクチャ
第8章 ドメインモデルの紹介
第9章 ドメインモデルの実装
第10章 CQRS の紹介
第11章 CQRS の実装
第12章 イベントソーシングの紹介
第13章 イベントソーシングの実装

■第4部 インフラストラクチャ
第14章 永続化レイヤー

ユーザエクスペリエンス(UX)ファースト

ソフトウェアの世界では、バックエンドから取り組むのが慣例となっていました。私たちの多くは、プレゼンテーションを システムのそれほど高尚な部分だと見ておらず、ビジネス層をデータアクセス層が完成してから取り組めばよい、という程度に考えていました。しかし、システムの複雑さを問わず、プレゼンテーションとバックエンドが等しく必要であることに疑問の余地はありません。

Webアプリケーションでもサーバサイドをメインにやっていると、プレゼンテーションの重要性は認識はしていても、重点をサーバ側に置いてしまいがちでした。

しかし、プレゼンテーションはユーザが直接システムに触れるインタフェースであり、ここからシステムのユースケースやタスクを見ていくというのは当然のことです。

自分が設計する際は、プレゼンテーションももちろん考えますが、どちらかと言えば、サーバサイドの設計を主に考えていた節があるので、ハッとさせられました。

CQRS を身近なパターンに

CQRSは、ソフトウェアアーキテクチャにおけるコロンブスの卵です ── すなわち、自明ではない問題に対する驚くほど明確でそつの ないソリューションです。開発者とアーキテクトは、DDDとDomain Modelの階層化アーキテクチャを理解し、正当化し、機械化すること に何年も費やしてきました。特に複雑なビジネスドメインでは、クエリスタックとコマンドスタックの両方をドメインモデルで実現する ことがきわめて難しいという問題と格闘してきました。 CQRSにより、すべてが一夜にして変わってしまい、複雑なビジネスドメインがはるかに管理しやすくなりました。

IDDD 本では、やや特殊なパターンとして解説されていた CQRS が、多くの場合に適用できる身近なパターンとして解説されています。

このあたりは、私もドメインモデルからクエリを分離して、クエリについては、DTOを返すリポジトリを作って実装した経験から、わりと使い勝手が良いパターンではないかという印象を持っていました。

本文では、単なる CRUD パターンでも、CQRS にしてコマンドとクエリの関心事を分けるのは意味があると書かれており、CQRS に興味がある人には参考になる内容でしょう。

イベントソーシング(ES)と似たものは、30年以上前からある

先に挙げたアプリケーションはどれも数十年前から存在しているもので、COBOLやVisual Basic 6でかかれたものさえあります。つまり、イベントソーシングなんて仰々しい名前が付いていても、決してソフトウェアの新しい概念の到来を告げるものではないのです。

新たな名前が付いたものが出てくると、身構えてしまいますが、実は従来からあるものに名前が付いてパターン化したものというのも良くあります。

その最たるものが、GoF でお馴染みのデザインパターンでしょう。イベントソーシングもこうした以前からあるものをパターン化したものです。

現実世界での出来事では、発生した事象が消えてなくなることはなく*2、事象をイベントとして残していくというのは特にユーザからすると理解しやすいものです。

例えば、商品購入を記録する場合、購入情報のドメインモデルを保存し、ステータスが変わる度にそれを書き換えます。そして、イベントを補足情報としてログに記録するというのは良くあるパターンです。

イベントソーシングでは、これを逆転し、イベントを記録する方を主とします。そして、保存したドメインモデルは、あくまでもスナップショットとして扱います。イベントには、必要な情報が全て記録されているので、原理的にはそれらを順に適用していけば、どの時点のドメインモデルでも再現できるからです。

実際にイベントソーシングを実装するとなると、パフォーマンスの問題は無視できないのですが、ここについても実装のアイデアが解説されています。

さいごに

DDD 本を読んで、自分たちが取り組んでいるプロジェクトに DDD を実践してみたい人や、実際に取り組んでみたもののまだしっくり来ない人に手にとって見てもらいたい本です。

ユーモアを交えて、平易な表現で書かれており、理解しやすいので、これから DDD を学ぶ人や OOP の基礎を見直す人にも良さそうです。

実装で具体的なイメージを持った状態で、理論に立ち返るとより理解が深まるので、この本から DDD 本や IDDD 本に戻るのも良いですね。

なお、この本を読んだ後に、Amazon でリコメンドされた「C#実践開発手法 (マイクロソフト公式解説書)」も気になる内容だったので、こちらも読んでみたいと思います。

.NETのエンタープライズアプリケーションアーキテクチャ第2版 .NETを例にしたアプリケーション設計原則

.NETのエンタープライズアプリケーションアーキテクチャ第2版 .NETを例にしたアプリケーション設計原則

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

実践ドメイン駆動設計 (Object Oriented SELECTION)

実践ドメイン駆動設計 (Object Oriented SELECTION)

*1:もちろん、それは前 2 冊を読んで、多少なりとも実践したからこそというのはあります。

*2:購入商品をキャンセルしても、購入した事実は消えない

PHPにおけるhttpoxyの対応

HTTP リクエストに任意の値をセットすることで、Web アプリケーションからの HTTP 通信を傍受したり、中間者攻撃(Man-in-the-Middle)を可能にする脆弱性が見つかっています。

専用サイト

httpoxyという名前が付けられ、専用サイトが立ち上がっています。詳細は、このサイトが詳しいです。

httpoxy.org

攻撃内容

  • アプリケーションからHTTP通信を行う際に、環境変数HTTP_PROXYの値を、HTTPプロキシとして見るライブラリがある。
  • HTTPリクエストにProxyヘッダを付けられると、環境変数HTTP_PROXYにその値がセットされる。(これは、CGIの仕様)
  • つまり、任意のプロキシを外部から指定できてしまうので、通信内容の傍受や偽装ができてしまう。

対象となる PHP アプリケーション

  • HTTP リクエストを受けて動作する PHP アプリケーション
  • アプリケーションから、HTTP 通信を行うもの
  • 利用している HTTP 通信を行うパッケージなりライブラリが、環境変数 HTTP_PROXY の値を HTTP プロキシとして扱う場合

対応方法

1 or 2 のいずれかの方法で対応します。

1. httpdサーバ(アプリケーション以前で)Proxyヘッダを落とす。

  • nginx

www.nginx.com

  • Apache(httpd)

https://www.apache.org/security/asf-httpoxy-response.txt

冒頭の専用サイトでは、Varnish や HAProxy などの対応方法も記載されている。

2. 環境変数HTTP_PROXYをHTTPプロキシとして利用しないようにする。

  • Guzzle

Guzzle がこれに該当していたので、修正版がリリースされている。Guzzle に依存するパッケージでは、6.2.1以上を使う。

Guzzle の対応コミットは以下。SAPIを確認して、cli以外では、HTTP_PROXYを利用しないようになっている。

github.com

Drupal が、Guzzle を利用しており、composer.json の依存バージョンを ~6.2 に変更している。Guzzle を利用したアプリケーションならこの対応が参考になる。

github.com

  • curl

curl は、10年以上前からこの問題を認識しており、大文字のHTTP_PROXYは見ないようにしていた。

Linux では、環境変数は case sensitive なので、HTTP_PROXYhttp_proxy は別ものとなる。

番外. PHP で、HTTP_PROXYの値を落とす対応はオススメしない

下記のようなPHPコードでの対応は、効果が無い *1 ので、上記 2 つでの対応が望ましい。

$_SERVER['HTTP_PROXY'] = ''; // 環境変数には効果無し
putenv('HTTP_PROXY=');       // ヘッダから来た値には無効
  • Using unset($_SERVER['HTTP_PROXY']) does not affect the value returned from getenv(), so is not an effective mitigation
  • Using putenv('HTTP_PROXY=') does not work either (to be precise: it only works if that value is coming from an actual environment variable rather than a header – so, it cannot be used for mitigation) https://httpoxy.org

追記1(2016/07/20)

getenv() は、SAPI ごとにハンドラを登録することができ、定義されていれば、そちらが優先される仕組みになっています。 github.com

例えば、php-fpm であれば、php-fpm 実行中は、fcgi env から値を取得するようになっています。 github.com

追記2(2016/07/20)

PHP 本体には、すでに修正コミットが入っています。

github.com

次のリリース(下記を見る限りは、2016/07/21)に含まれています。

github.com

追記3(2016/07/20)

RHEL / CentOS では、すでに httpd パッケージに修正版が出ています。Proxyヘッダの値を、環境変数HTTP_PROXYの値としてスクリプトに渡さなくなります。

CVE-2016-5387 - Red Hat Customer Portal

CentOS alert CESA-2016:1421 (httpd) [LWN.net] CentOS alert CESA-2016:1421 (httpd) [LWN.net] CentOS alert CESA-2016:1422 (httpd) [LWN.net]

追記4(2016/07/21)

httpoxy 対応版の PHP 7.0.9、5.6.24、5.5.38 がリリースされました。

http://jp2.php.net/downloads.php#v7.0.9

http://jp2.php.net/downloads.php#v5.6.24

http://jp2.php.net/downloads.php#v5.5.38

参考

blog.ichikaway.com

HTTPoxy - CGI "HTTP_PROXY" variable name clash - Red Hat Customer Portal

*1:$_SERVER['HTTP_PROXY']の値をプロキシとするライブラリがあれば効果はあるが...

「ざっくり分かる WordPress サイトのチューニング」を WordCamp 2016 で発表してきました。

2016/07/09、10 に大阪大学 豊中キャンパスで開催された WordCamp Kansai 2016 にて、WordPress サイトのチューニングについて発表しました。

f:id:shin1x1:20160711095229j:plain:w500 https://twitter.com/digitalcube/status/751693759121731584

発表資料

今回は、PHP(WordPress)エンジニアでなくても何か役立ててもらえるようにチューニングの具体的な手法というよりは、実際に手を動かすエンジニアと意思疎通がスムーズになるように基本的な考え方をメインにお話しました。

「推測するな、計測せよ」という格言があるとおり、チューニングにおいても、まず大事なのは「計測する」ということです。計測せずにやみくもにチューニングを行っているケースを見聞きするので、ここからはじめる内容にしました。

後半は、キャッシュを中心に紹介したのですが、最後の Varnish Cache に興味を持った人が多かったのが印象的でした。エンジニアメインの会場なら、こちらをメインに構成しても良いですね。

以前、Varnish について書いたエントリがあるので、参考まで。 http://www.1x1.jp/blog/2013/12/varnish-cache.html

さいごに

昨年に続き、お声がけ頂き、2年連続の登壇となりました。

前回は、β2 の PHP 7 上で WordPress を動かす内容だったのですが、今では、PHP 7 を本番環境で利用しており、1 年でも状況の変化は大きいなと感じたりもしました。

www.1x1.jp

今回も発表の機会を頂き、ありがとうございました。

参考

今回の内容に関連して、参考になるリンクは下記です。