Technote

by sizuhiko

複雑な正規表現を簡単に作れるようにした

この記事は先日の正規表現を簡単に作るにはの続編になります。

前回、RFC3986のような複雑な正規表現をVerbal Expressionの既存実装でやろうとすると、うまくできないという問題に直面しました。 そこでRubyで実装されていたHEXPRESSをPHPへ移植しようという流れです。

PHP版Hexpressを作った

さっそく移植してみました。

hexpress

PSR-2の対応が実施中なのですが、Ruby側の機能とテストはすべて移植し、PHP版でいくつか機能追加をしています。

どんな感じか

READMEに書いてあるとおりインストールはcomposer経由でできます。

composer require sizuhiko/hexpress

具体的な利用方法は以下のサンプルのとおりです。

use Hexpress\Hexpress;

$pattern = (new Hexpress())
    ->start("http")
    ->maybe("s")
    ->with("://")
    ->maybe(function($hex) { $hex->words()->with("."); })
    ->find(function($hex) { $hex->matching(function($hex) {$hex->word()->with("-");})->many(); })
    ->has(".")
    ->either(["com", "org"])
    ->maybe("/")
    ->ending();

echo $pattern;             #=> "^https?\:\/\/(?:(?:\w)+\.)?([\w\-]+)\.(?:com|org)\/?$"
echo $pattern->toRegExp(); #=> "/^https?\:\/\/(?:(?:\w)+\.)?([\w\-]+)\.(?:com|org)\/?$/"

Hexpressクラスのインスタンスを生成して、メソッドチェインで正規表現を組み立てます。 ネストしたい場合は、コールバック関数を指定すると、新しいHexpressのインスタンスがパラメータで渡ってくるので、 それに対してメソッドチェインでつないていきます。

Hexpress__toString()を実装しており、文字列表現を取得すると指定された定義を展開します。 実際に正規表現として利用するにはtoRegExp()を呼び出すと、左右にデリミタを挿入した文字列が取得できるので、preg_matchなどの関数で利用可能になります。 例えば以下のような利用方法です。

preg_match("http://example.com/", $pattern->toRegExp());

複雑な正規表現は定義できたのか?

できました!!

テストコードとしてRFC3986Test.phpを作成しました。 実装は結構長いので、こちらに添付することは省略しますが、RFCの定義に書いてあるとおりそのままメソッドをつないでいくだけで、 RFCに対応した正規表現を構築することができました。(※IPv6の定義は未実装です、ご了承ください)

PHPUnitのデータプロバイダexampleUriProviderでマッチさせるパターンをftp,www,mail,news,tel,telnet,urnについて定義してすべて通過しています。

PHP版では、マッチするときfindメソッドで名前付きサブパターンを利用できるようにしました。 以下の例のように、findの第二引数にportを指定できます。

    private function port()
    {
        return (new Hexpress())->has(':')->find(function ($hex) { $hex->digits(); }, 'port');
    }

上記を含むパターンをpreg_matchで処理すると、マッチング結果に名前付きで値を取得できるようになります。

preg_match("http://example.com:80/", $pattern->toRegExp(), $matches);
echo $matches['port'];   #=> '80'

どうなのか?

たとえば今回作成したRFC3986の定義を正規表現としてダンプすると、以下のようになります。

/^(?P<scheme>[a-zA-Z](?:[a-zA-Z0-9\+\-\.])*)\:(?P<hierPart>(?:\/\/(?P<authority>(?:(?P<userinfo>(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:))*)@)?(?P<host>(?:(?:0-9|1-90-9|10-90-9|20-40-9|250-5)\.(?:0-9|1-90-9|10-90-9|20-40-9|250-5)\.(?:0-9|1-90-9|10-90-9|20-40-9|250-5)\.(?:0-9|1-90-9|10-90-9|20-40-9|250-5)|(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]))+))(?:\:(?P<port>(?:\d)+))?)(?P<pathAbempty>(?:\/(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))*)*)|(?P<pathAbsolute>\/(?:(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))+(?:\/(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))*)+)?)|(?P<pathRootless>(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))+(?:\/(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))*)*)|(?P<pathEmpty>^(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))))(?:\?(?P<query>(?:(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@)|\/|\?))*))?(?:#(?P<fragment>(?:(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@)|\/|\?))*))?$/

私はいきなりこれを空手形で実装することはできそうにないので、今回hexpressを移植して良かったなと思っています。 何か複雑な正規表現を定義したい場合などに役立てば幸いです。

さいごに

日本PHPカンファレンス2015が10/3(土)に行われます。 当日のプログラムも公開され、私はトラック3で10:50から11:20まで「Composerではじめるアプリケーション開発」というセッションを担当します。 他の番組が強力なため私の会場は比較的空いているのではないかと思うので、もしご都合がつく方はよろしくお願いします。

正規表現を簡単に作るには

皆さんは正規表現好きですか?そして得意ですか? 私は好きですが、得意とは言えません。

VerbalExpressionという選択肢

そこでVervalExpressionという正規表現を簡単に組むことができる仕組みがあります。

http://verbalexpressions.github.io/

サイトに「Regular Expressions made easy」と書いてあるように、簡単に正規表現が作れることを表明しています。 様々な言語にポートされていますが、もちろんPHP版もあります。

https://github.com/VerbalExpressions/PHPVerbalExpressions

サイトのサンプルにも書いてあるとおり、以下のようなURLにマッチする正規表現が記述できます。

use VerbalExpressions\PHPVerbalExpressions\VerbalExpressions;

$regex = new VerbalExpressions;
$regex  ->startOfLine()
        ->then("http")
        ->maybe("s")
        ->then("://")
        ->maybe("www.")
        ->anythingBut(" ")
        ->endOfLine();

echo $regex->getRegex() ."\n";
if (preg_match($regex, 'http://github.com')) {
    echo "valid url\n";
} else {
    echo "invalud url\n";
}

1つ目のechoの結果は /^(?:http)(?:s)?(?:\:\/\/)(?:www\.)?(?:[^ ]*)$/m で、2つ目のechoは valid url を表示します。 VerbalExpressionsクラスに toString メソッドが実装されているので、 preg_match 関数でそのまま使えます。

複雑なことはできるか?

例えばRFC3986に書いてあるURLパターンをマッチさせようとすると、どうなるでしょうか? 一旦スキーマ部分だけ記述してみます。

$rfc3986 = new VerbalExpressions;
// scheme
$scheme = new VerbalExpressions;
$scheme->add("http")->maybe("s")->_or("ftp");

$rfc3986->startOfLine()
        ->add($scheme)
        ->add("://");
echo $rfc3986->getRegex() ."\n";

結果は /^(?:\(\?\:http\)\(\?\:s\)\?\)\|\(\?\:ftp)(?:\:\/\/)/m のようになってしまい、期待通りではありません。 この実装は入れ子には対応していないようです。

もう1つのVerbalExpression実装

PHPにはもう1つ別のVerbalExpression実装があります。

https://github.com/markwilson/VerbalExpressionsPhp

こちらの実装はREADMEに入れ子について記述されているように、入れ子の対応はされているようです。 では早速RFC3986の定義を試してみましょう。

$rfc3986 = new VerbalExpression;
// scheme
$scheme = new VerbalExpression;
$scheme->then("http", false)->maybe("s", false)->orPipe("ftp", false);

$rfc3986->startOfLine()
        ->find($scheme)
        ->find("://")
        ->endOfLine();
echo $rfc3986->compile() ."\n";

結果は ^((?:http)(?:s)?()|()(ftp))(\:\/\/)$ のようになってしまい、まぁ不正ではないのですが、かなり無駄があります。

現時点結局のところ

どちらの実装も簡単なパターンをやるときには良いのですが、ちょっと複雑なパターンを実装しようと思うと微妙です。 大体、簡単なパターンはそのまま正規表現書けば良いじゃん… という話ですしね。

目指したいところ

RubyのVerbalExpressions実装には HEXPRESS があります。 これはVerbalExpressionsよりさらに便利なヘルパーを備えて Human Expressions, a human way to define regular expressions という標語のとおりより簡単に実装できるように見えます。

これをPHPに移植して使えるようにしようというのが、直近やろうとしていることです。

さいごに

日本PHPカンファレンス2015が10/3(土)に行われます。 私もスピーカーとして登壇しますので、もしご都合がつく方はよろしくお願いします。

htmldayのイベントに参加してきた

htmldayとは、日本全国でWebに関する多数のイベントを同じ日(6/13)に開催することで、日本のWebを一層盛り上げようという「お祭り」です。(毎年6月の第2土曜日に開催しています。)

ということで、以下のイベントに参加してきました。

当日の講演資料

まとめサイト ができました。ありがたやー。

サービスワーカーが今後のフロントエンドを支える

Google I/Oからの流れもあると思うのですが、サービスワーカー(ServiceWorker)に関連する発表が3連続になったりと、これから取り組まないといけない技術だと思います。 SafariとIEのサポートがまだなのですが、IEに関してはEdgeがChrome相当なので、MSではそちらに期待し、Appleはそのうち対応してくれるのではないかと期待しています。

サービスワーカーとはブラウザにインストールできるタスクで、アプリケーションのバックグラウンドで動作するもので、例えばPush通知とかキャッシュとかオフラインモードの実装なんかをするのに、とても役立つ技術です。

サービスワーカーに関しての詳しい内容はググるとたくさん出てくるので、それらを参照してください。

気になるキーワード

発表の中で気になったキーワードと、関連URLを紹介します。

  • Cache API
    • サービスワーカーの紹介記事の中で「Service Worker のインストール」以降にキャッシュを使う例が出ています
  • Fetch API
  • Push API
  • Lovefield
    • Indexed Databaseに対してSQLライクにアクセスできるようにしたライブラリ、Googleが開発した
  • Babel
    • ES6のコードをES5形式にコンパイルしてクロスブラウザで動作できるようにする
    • gulpとの相性が良いので便利
  • VORLON.JS
    • ブラウザをリモートでバッグするツール。マイクロソフトが開発している
    • スマートフォンのエミュレータを使ったときDOMやJavaScriptのデバッグが難しいが、それをリモートデバッグできるようにする(昔も似た技術はあった気がする)
  • manifoldjs
    • HTML5アプリケーションを各プラットフォーム用のアプリケーションに変換するツール
    • Windowsストアアプリ、Androidアプリ、iOSアプリ、Chromeアプリ、FirefoxOSアプリに変換できる

Polymer1.0がキタ

そしてPolymerな訳です。私はPolymer大好きで0.5をけっこう弄っていたのですが、1.0は互換性ないということで、個人的な利用にとどめておいて良かったと思っています。

PolymerはWebコンポーネントを実装するための仕組みにフォーカスしているので、いわゆるMVCフレームワークとは違います。 今流行のReact.jsとはかぶる部分が多いと思っています。

Polymerが適用されている実例としては

  • Atavist
  • salesforce
  • vaadin
  • youtube

そして今回のGoogle I/Oサイトということだそうです。

で、Polymerを簡単に始めるためにはPolymer Starter Kitというものがあります。 これを使うと、ブラウザで簡単にPolymerを使ったサイトのディレクトリ構成やファイルの配置、gulpのビルドファイルなどを初期構築できます。 Yeoman的な役割ですね。

Googleが提唱するMaterial designをベースにPolymerでサイトを作ると、あらゆるデバイスに最適なGoogle I/Oのようなサイトを作れるので、サイトを例としてPolymerを使ってみるのが良さそうです。

Polymerの注意点としては、ShadowDOMの実装がアレということで、Polymer1.0ではShady DOMという疑似ShadowDOMを実装しています。ShadowDOMと思ってるとそうではないのです。

最後に

ServiceWorker、ES6、Polymerあたりの技術は今後重要になってくると思っているので、何か作りたいなーと思う収穫多きイベントでした。

あと懇親会が2度(イベントが午後の部と、夜の部にわかれていたので)あって、おいしいお酒とたくさんのビールに囲まれて幸せでした。 スポンサー様、協賛ありがとうございました!

htmlday 2015 のイベントTシャツをジャンケン大会、最後の最後、ほんとの最後の1枚でGETしました!ありがたやー。早速着てこのBlogを書いています。

CakeFest2015に参加してきた

今年はCakePHP生誕10周年ということもあり、かねてよりアニバーサリーCakeFestとも言われていました。

開催場所は世界一の都市ニューヨークです。 私個人でも初のニューヨークです。

今年の第一印象

初参加が多い。CakeFestでは毎年会う馴染みの人もいるのですが、今年は特に初参加が多かったようです。 参加者が多い。さすが10周年というべきか、ニューヨークなのでアメリカの人が多かったようです。

印象に残ったセッション

昨年までと同様に、CakePHPに関する主なセッションはコアチームから、その他Webに関するトピックをCFPで通ったスピーカーからという流れでした。 今年はコミュニティマネージャのJamesが急に来れなくなってしまい、イベントの仕切りに不安を覚えるスタートだったのですが、Larryが八面六臂の働きで素晴らしいCakeFestになりました。LarryがLTのタイムキーパーやったり、抽選会の司会やったり、それはそれは大活躍でした。

コアチームの中でも

についてはCakePHPを利用する人には一読を薦める内容です。

Mark Storyのセッション

とは言えMarkのスライドはいつもの通りお題しか書いていないので、少し詳細にふれておきます。

  • CakePHP3の開発期間3年はとても長かった
  • 周辺の変化
    • PHPのバージョンも変わったし、機能も変わった
      • 3年前のPHPは5.4.4(Released: 14 June 2012)あたりです。
      • 現在は5.6.9で、その間にはジェネレータや可変引数などが導入されました。
    • 他のフレームワークの流れも変わった
  • CakePHP3は2ヶ月で8万ダウンロードされた。すごいね。
  • 今後のバージョンの話(詳しくはgithubのロードマップを見てね)
    • PSR-7対応は3.2の予定
  • 大きく変わったところ
    • Mailers:メール送信がこれまでよりも簡単になる
    • CLI:出力フォーマットの機能が増える。例えばプログレスバーとか簡単に出せるようになる
    • ORM:Joseが明日話すけど、関連のロードとかマッチングクエリが書けるようになるよ
    • ElasticSearch:ORMと似た呼び出し方法が使えるようになる
  • 今後注力していくところ
    • プラグインのリリース
    • 3.x系の開発
      • これにより2系については3からのバックポートが中心となる
      • 3.x内での互換性は重視している
    • PHP7

Jose Lorenzo Rodriguezのセッション

Joseのセッションはコード多めで、解説だけではわかりづらいと思うのでサンプルコードが提供されています。

サンプルコード::github

スライドの最初に出てくる3つのタイプのORM

  • いらっとする
  • おもちゃみたいな
  • 流行の(通の)
  • すばらしい

もちろんCakePHP3のORMはすばらしいものだよ。という流れで掴みはOKな展開。ちなみにQ&AでDoctrineは?という質問に「Hipster」と即座に回答していました。 ちなみに日本だとAgileはアジャイルと発音するけど、Joseのセッションではずっとエイジールと聞こえていた。

スライドやサンプルを見てもらえると、CakePHP3のORMがかなり強力になっているのがわかると思います。 特に気になったのは

  • タイプヒント
  • カスタムファインダー
  • 集合検索
  • 多重階層の集合検索
  • 他のDBへのアソシエーション定義
  • バーチャルフィールド

あたりです。サンプルを使って実際に動かしてみるのが良いですね。まだ全部試せていないけど、CakePHP3もくもく会で少しずつ試せたら良いなーと思っています。

10周年

ノベルティが久々にたくさんあった。きっとシカゴ以来。

で、私はそのシカゴから参加して6回目の参加となりました。 最後の抽選会でシカゴに参加した人、というときに私含め3人しか立たなかった(コアチームは除く)。さらにそのうち1人はマリアーノです。

アニバーサリーということで最後の目玉商品は来年のCakeFest招待券!!。なんと交通費込み(上限あるけど)。 当選者は

奥山さん!!!、来年はVIP待遇ですなー。来年はドイツらしいですよ。

感謝

10周年ということで、ここ数年は安藤さんと私の2人だったのを、もっと多くのCakePHPユーザにCakeFestに参加して欲しいと思い、様々なところでくどいぐらい誘いました(ご迷惑だった皆様すみません)。結果、私含め5人が日本から参加ということで、とても楽しかったです。

安藤さんの紹介で、pivotal lab.にも訪問できたし、ピザ屋さんも(すごい)美味しかったです。

また、日本でCakeRadioGaGaの中継を受け取ってくれたCo-Edoの田中さん、中継を見てくれた皆様、ありがとうございました。 別途報告会(もくもく会の中で?)、できたら良いなーと思っています。

そのほかGoogle社の前で写真撮ったり、チェルシーマーケットでおみやげ買ったり、MOMA行ったり、ちゃんとニューヨークも楽しむことができました。

最後に、CakeRadioGaGaの中継動画を。そういえばMarkとJoseに今年もメッセージちょうだいと言っていたのに、すっかり忘れたのは内緒です…

CakePHP3 のアプリケーションを Behat でテストする

CakePHP3の変更点として大きく取り上げられるのが、モデル層の変更でしょう。 しかしそれ以上に私たちが受けられる恩恵で大きいのが、PSR-2の採択です。

CakePHP3 is fully adopt PSR-2

例えばCakePHP2で単体テストを実行するときは、以下のようにcakeコマンドを使って実行する必要がありました。

Console/cake test app Model/Articles

cakeコマンド内でPHPUnitへの依存関係を解決し、PHPUnitからCakePHPのクラスが参照可能になるように作られていました。

ところがCakePHP3からは、以下のようにphpunitコマンドを使って実行します。

vendor/bin/phpunit

PSR-0のオートロードに対応したことで、PHPUnitからCakePHP3のクラスが参照可能になるのです。

はじめてみよう

同様の理由で、CakePHP2のアプリケーションをBehatでテストしたい場合は、私が作成したBdd Pluginを使ってBehatのステップ記述からCakePHPのクラスを参照可能になるようにしていました。

しかしBehatにおいても直接実行したステップ定義から、CakePHP3のクラスが参照可能になるのです。

CakePHP3のアプリケーションをどのようにBehatからアクセスするのか、CakePHP3のブログチュートリアルを例にサンプルアプリを作成しました。

cakephp3-bdd-example

サンプルアプリケーションは以前記事にもしたCakeboxを使って構築しました。 またサンプルアプリケーションの実行にもCakeboxを使うと簡単に実行環境を構築することができます。

サンプルアプリケーションのGithubページに書いてあるとおりの手順で進むことができます。 本ブログでは日本語で補足します。

必要なアプリケーションのインストール

以下のアプリケーションをホストOSにインストールします。

  • VirtualBox
  • Vagrant
  • Cakebox

詳しくはCakebox を使ってCakePHP3アプリケーションを作ってみようの記事を参照してください。

サンプルアプリケーションのインストール

CakeboxのゲストOSにログインして、cakeboxコマンドでアプリケーションをインストールします。

localhost:cakebox $ vagrant ssh
Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-24-generic x86_64)

vagrant@cakebox $ cakebox application add blog-tutorial.app --source https://github.com/sizuhiko/cakephp3-bdd-example.git --webroot blog-tutorial.app

すると、以下のように表示されます。

Creating application http://blog-tutorial.app

Configuring installer
Creating installation directory
Git installing user specified application sources
Creating virtual host
* Successfully created PHP-FPM virtual host
Creating databases
* Successfully created main database
* Successfully created test database
Configuring permissions
Updating configuration files
Application created using:
  database => blog-tutorial_app
  framework_human => user specified
  framework_short => custom
  installation_method => git
  path => /home/vagrant/Apps/blog-tutorial.app
  source => https://github.com/sizuhiko/cakephp3-bdd-example.git
  url => blog-tutorial.app
  webroot => blog-tutorial.app
Please note:
  => Configuration files are not automatically updated for user specified applications.
  => Make sure to manually update your database credentials, plugins, etc.

Remember to update your hosts file with: 10.33.10.10 http://blog-tutorial.app

Installation completed successfully

新規アプリケーションの構築と同じように、データベースやNginxの設定ファイルも生成してくれるので、すぐにアプリケーションを実行できる環境が整います。

あとはアプリケーションのルートディレクトリに移動して、不足しているディレクトリを作ってcomposerでライブラリをインストールします。

vagrant@cakebox $ cd Apps/blog-tutorial.app
vagrant@cakebox:~/Apps/blog-tutorial.app$ mkdir tmp 
vagrant@cakebox:~/Apps/blog-tutorial.app$ mkdir logs
vagrant@cakebox:~/Apps/blog-tutorial.app$ cp config/app.default.php config/app.php
vagrant@cakebox:~/Apps/blog-tutorial.app$ composer install 

サンプルアプリケーションの環境設定

データベース接続設定の変更

config/app.phpのデータベース接続設定をCakeboxで生成された内容に変更します。 以下のとおりusernamedatabaseの部分のみ変更します(それ以外はそのまま)。

    'Datasources' => [
        'default' => [
            // 省略
            'username' => 'cakebox',
            'database' => 'blog-tutorial_app',
            // 省略
        ],
        'test' => [
            // 省略
            'username' => 'cakebox',
            'database' => 'test_blog-tutorial_app',
            // 省略
        ],
ホストOSのhostsファイルの変更

ホストOSのhostsファイルに指示されたように 10.33.10.10 blog-tutorial.app の行を追加します。

Cakebox環境のチューニング

Cakeboxのデフォルト設定ではBehatを使ってアプリケーションをテストしようとすると、いくつか動かない箇所があったので、設定値をチューニングします。 まずボックスファイルのメモリを2048Mにアップします(デフォルトは1024M)。 次にxdebug.iniのxdebug.maxnestinglevelの値を調整します。READMEでは500を設定しています。もう少し値は小さくても大丈夫かもしれないですが、とりあえず500あれば大丈夫です。

具体的な設定例は、githubのREADMEを参照してください。

Webサーバの設定

Behatからアプリケーションをテストするときは、ブラウザから通常操作するのと同じようにWebサーバを通過します。 そのため、アプリケーションが通常操作としてアクセスされたのか、Behatのテストでアクセスされたのかを識別して環境を切り替えてあげないと、データベースのデータがテストによって変更するので、通常操作のデータが失われてしまいます。

このあたりの話(理由や手法)は、過去に何度か記事にしていたり、書籍CakePHPで学ぶ継続的インテグレーションでも詳しく解説していますので、CakePHP2の内容ですが、一度手に取ってみてください。

で、このサンプルはnginx用の設定ファイルをblog-tutorial.app.testというファイルで用意しておいたので、これをCakeboxのnginxの設定ディレクトリにコピーして再起動するだけで大丈夫です。

環境切り替え用にblog-tutorial.app.testというホスト名でアクセスされたら、nginxで環境変数CAKE_ENVtestという文字列を設定するようにしています。 CakePHP3のアプリケーションではconfig/bootstrap.phpで環境変数の設定値を見てDBの接続先がtestになるように設定します。

if (getenv('CAKE_ENV') === 'test') {
    ConnectionManager::alias('test', 'default');
}

CakePHP3ではConnectionManagerのaliasという機能でdefaultへ接続しようとしたときに、実際はtestの接続内容を参照するように設定することができるので、この機能を利用し、間違ってdefaultのテーブルが書き変わらないようにしています。

より詳しい手順や、設定ファイルの内容はgithubのREADMEや設定ファイルを参照してください。

Behatから参照可能なホスト名としてblog-tutorial.app.testをCakeboxのVM側の/etc/hostsに追加します。

データベースのマイグレーション

データベースの生成はマイグレーションコマンドで一発です。

bin/cake migrations migrate

Behatのテストを実行する

ここまで設定できれば、後はテストを実行するだけです。

vagrant@cakebox:~/Apps/blog-tutorial.app$ vendor/bin/behat

おそらくすべてグリーンで成功するはずです。 うまくいかなかったら、お気軽にgithubのissueに日本語で書いてください。

どうやったのか?

まずCakePHP3で最初に注目したのは、単体テストがPHPUnitのコマンドから実行できるようになっていたことです。 これは過去に外部の様々なツールやアプリケーションとCakePHPを結合するときに一番悩んでいたところでした。

PHPUnitからCakePHP3にどのように連動しているのか?を調べることから始めました。 PHPUnitは実行すると、カレントディレクトリのphpunit.xml(もしくはphpunit.xml.dist)を参照します。

PHPUnitがCakePHP3を呼び出す仕組みを知る

CakePHP3ではアプリケーションスケルトンを生成すると、ルートディレクトリにphpunit.xml.distが生成されます。

<!-- phpunit.xml.dist -->
<phpunit
    colors="true"
    processIsolation="false"
    stopOnFailure="false"
    syntaxCheck="false"
    bootstrap="./tests/bootstrap.php"  // (1)
    >
    <php>
        <ini name="memory_limit" value="-1"/>
        <ini name="apc.enable_cli" value="1"/>
    </php>

    <!-- Add any additional test suites you want to run here -->
    <testsuites>
        <testsuite name="App Test Suite">
            <directory>./tests/TestCase</directory>
        </testsuite>
        <!-- Add plugin test suites here. -->
    </testsuites>

    <!-- Setup a listener for fixtures (2) -->
    <listeners>
        <listener
        class="\Cake\TestSuite\Fixture\FixtureInjector"
        file="./vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php">
            <arguments>
                <object class="\Cake\TestSuite\Fixture\FixtureManager" />
            </arguments>
        </listener>
    </listeners>
</phpunit>

このファイルを読むと、2つ重要な箇所があるのに気がつきます。

まず(1)の bootstrap="./tests/bootstrap.php" という部分。 bootstrap属性にはPHPUnitが実行されるとき呼び出されるPHPコードを指定することができます。 ここからCakePHP3アプリケーションをテスト用にロードする場合、このファイルを呼び出せば外部ツールからCakePHP3が操作できるようになることがわかります。

実はこのファイルを実際に見てみると、以下の1行しかありません。

require dirname(__DIR__) . '/config/bootstrap.php';

テストとは関係なく、アプリケーションのconfig/bootstrap.phpをロードしています。 おそらく将来テストに関する何か差分が必要になったときに、テスト側にだけ変更が発生すると思うので、テスト用にCakePHP3をロードする場合はtests/bootstrap.phpをロードしておいた方が良いでしょう。

次に(2)のリスナー設定です。 PHPUnitのリスナーはPHPUnitのフックポイントでコールバックされる処理を記述できるクラスです。 CakePHP3ではフィクスチャ(DBのテストテーブルとデータを準備する仕組み)を投入するのに利用しています。 以下のようにFixtureInjectorクラスのstartTestとendTestでテストケース開始/終了ごとにフィクスチャのロードとアンロードが対応するようになっています。

class FixtureInjector implements PHPUnit_Framework_TestListener
{

    /**
     * Adds fixtures to a test case when it starts.
     *
     * @param \PHPUnit_Framework_Test $test The test case
     * @return void
     */
    public function startTest(PHPUnit_Framework_Test $test)
    {
        $test->fixtureManager = $this->_fixtureManager;
        if ($test instanceof TestCase) {
            $this->_fixtureManager->fixturize($test);
            $this->_fixtureManager->load($test);
        }
    }

    /**
     * Unloads fixtures from the test case.
     *
     * @param \PHPUnit_Framework_Test $test The test case
     * @param float $time current time
     * @return void
     */
    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        if ($test instanceof TestCase) {
            $this->_fixtureManager->unload($test);
        }
    }

}

BehatからCakePHP3を呼び出す仕組みに流用する

ここまでの内容が外部ツールからCakePHP3のアプリケーションをテストするのに重要な部分です。 PHPUnitがCakePHP3を呼び出すのと同じようにする仕組みをBehatのFeatureContextクラスに用意します。

features/bootstrap/FeatureContext.phpというBehatが読み込むファイルに記述します。 CakePHP2とBDDプラグインによるインテグレーションではBehatのバージョンが2系でしたが、CakePHP3との連携では最新の3系を利用しています。 Behat3からはBehat1系、2系で利用していたファイル構成と異なっています。従来、support/bootstrap.phpsupport/hooks.phpあたりに書いていたコードはすべてContextクラス内に記述することになります。

Behat3からはFeatureContextにブートストラップ記述を、それ以外のコンテキストは用途に応じて別のコンテキストクラスに分割する方がスマートに記述できそうです。 Behat2では複数のコンテキストクラスを使う場合、FeatureContextでインクルードしないといけなかったのですが、Behat3ではbehat.yml上で記述できるのでより簡単になっています。

class FeatureContext implements Context, SnippetAcceptingContext
{
    public function __construct()
    {
        require_once dirname(dirname(__DIR__)) . '/tests/bootstrap.php'; // (1)

        // Always connect test database
        ConnectionManager::alias('test', 'default'); // (2)

        Fabricate::config(function($config) { // (3)
            $config->adaptor = new CakeFabricateAdaptor([
                CakeFabricateAdaptor::OPTION_FILTER_KEY => true,
                CakeFabricateAdaptor::OPTION_VALIDATE   => false
            ]);
        });

        $this->fixtureInjector = new FixtureInjector(new FixtureManager()); //(4)
        $this->fixture = new BddAllFixture();
    }
}
  • (1)は、phpunit.xmlのbootstrapと同様にCakePHP3のtests/bootstrap.phpを呼び出します。
  • (2)は、Behatのステップ定義からテストデータを投入するときに、testの接続設定を参照するようにエイリアスを設定します。
  • (3)は、テストデータジェネレータFabricateの初期設定です。FabricateもCakePHP3対応されています。
  • (4)は、phpunit.xmlのリスナー部分を模して、Behatのシナリオ毎にフィクスチャが動くようにFixtureInjectorのインスタンスを生成しています。

BehatからCakePHP3のフィクスチャを利用する

(4)で書いたとおり、FixtureInjectorのインスタンスを生成したので、Behatのフックポイントを使ってシナリオ開始時にフィクスチャをロードし、シナリオ終了時にフィクスチャをアンロードするようにします。

    /** @BeforeScenario */
     public function beforeScenario(BeforeScenarioScope $scope)
     {
        $this->fixtureInjector->startTest($this->fixture);
     }

     /** @AfterScenario */
     public function afterScenario(AfterScenarioScope $scope)
     {
        $this->fixtureInjector->endTest($this->fixture, time());
     }

実際にフィクスチャを利用するためには、$this->fixtureのクラスがCakePHP3のTestCaseでなければならないので、$fixturesという利用するフィクスチャファイルの配列を定義しただけのクラスを用意してFixtureInjectorに渡すようにします。

class BddAllFixture extends TestCase {
    public $fixtures = [
        'Categories' => 'app.categories',
        'Articles'   => 'app.articles',
        'Users'      => 'app.users',
        'Categories' => 'app.categories'
    ];
}

このあたりの話も、書籍CakePHPで学ぶ継続的インテグレーションでも詳しく解説していますので、CakePHP2の内容ですが、一度手に取ってみてください。 CakePHP3になって、メソッドやクラスが一部変わりましたが、BehatとCakePHPをインテグレーションするためにおさえておかないといけないポイントはほとんど変わっていません。

後はBehat3のドキュメント、CakePHP3のドキュメントを見ながら進めていくと、エンド to エンドのテストが容易に記述できるようになります。

さいごに

GithubのREADMEに書いた内容をすべて日本語にした訳ではないのですが、要所をかいつまんで重要な部分を解説しました。 より詳しい内容などはREADMEを見ていただければと思います。

また、BDDプラグインのサンプルアプリにはあった、日本語のシナリオや、JavaScriptを使ったテストなど、Behat3になって大きく変わってはいませんが、サンプルアプリケーションに少しずつ載せられたらなぁと思っています。 何かうまく動かないなどあれば、気軽にGithubのissueに投稿お願いします(日本語でOKです)。