uzullaがブログ

uzullaがブログです。

ルータ等を書く時、DSLな条件に名前付きパラメタを指定し、いいかんじに値を取得するやつ

マイクロフレームワークとかでよくあるやつですけど、

<?php
get('/hoge/:id/', 'functionName');

みたいにしてDSLっぽくルーターにルートを登録するやつ、…のマッチさせる部分。
まあ普段なにも考えないでWAF書いてると意識する必要ないんですけど、自分でルーターを書く必要がでたので、調べて考えた。

まあ、URLのマッチなんて、ある意味正規表現ベタってかいて全然OKなんだけど、それにしても普通どうやってるのか知りたかったので

なんか、ぱっと最初有名WAFとかのコードみたらいきなり「ウッ機圧が高い…!」ってな感じでめまいがしたけど、ピザたべた後に順番にやってみたら、割と単純に理解できました。

ピザは偉大だ。

まずどういう処理になっているのか

基本的に、指定されたパターン"/home/:id/"を正規表現になおしてマッチしています。
一工夫することで、名前付きキャプチャの正規表現に変換しています。

正規表現にマッチしたら、(本エントリにはかいてませんが)callableで関数コールして処理をよび、引数としてマッチしたパラメータをくっつけてあげているかんじです。

"/home/:id/"をどうやって正規表現にしているか

いくつかのWAFのコードを参考にしたところ、短く書けばこういうコードになりました

<?php
$pattern = '/test/:id/';
$regexPattern = preg_replace_callback(
    '#:([\w]+)#',
    function($m){
        return '(?P<'.$m[1].'>[^/]+)';
    },
    $pattern
);

// 生成された正規表現
var_dump($regexPattern);
// string(20) "/test/(?P<id>[^/]+)/"

// 実際に入力してマッチさせてみる
$input = '/test/hoge/';
$is_match = preg_match("#".$regexPattern."#", $input, $matches);

//結果
var_dump($is_match);
// int(1)
var_dump($matches);
// array(3) {
//   [0] =>
//   string(11) "/test/hoge/"
//   'id' =>
//   string(4) "hoge"
//   [1] =>
//   string(4) "hoge"
// }

実際のwafでは末尾の/があってもなくても許すとか、それ以後をワイルドカードにするとか、オプショナル(省略可能にする)とかやっているみたいですけど、まあそういうのは上のにはない。

補足的な説明

<?php
$regexPattern = preg_replace_callback(
〜
);

preg_replace_callbackは第一引数に正規表現を指定し、第三引数の文字列を処理して、マッチした要素を第二引数のcallableに「マッチ全体」と「キャプチャ」を配列で渡す。
返値は、ここでは正規表現の文字列をくみたてている。

<?php
    '#:([\w]+)#',

第一引数の正規表現が'/〜/'ではなく'#〜#'となっているのは、URLにはもともと/が多くつかわれる(そして#は渡されてこない)ので可読性のためにかえているだけで、普通によくあるテクニックのアレです。

<?php
    function($m){
        return '(?P<' . $m[1] . '>[^/]+)';
    },

第二引数は、「(?P<id>[^/]+)」このような文字列を返す無名関数である。後でつかいやすいように、正規表現の名前つきサブパターンにしているかんじ。
http://www.php.net/manual/ja/function.preg-match.php
今回はマッチ全体はいらないし、キャプチャされた文字列(上の例だと「id」)しかいらないので$m[1]しかみていない。
勿論この第二引数はcallableならなんでもいいので、このように無名関数にする必要はない。

第三引数は入力しているPATH情報である。

補足的な説明2

<?php
$is_match = preg_match("#".$regexPattern."#", $input, $matches);

先ほど生成した$regexPatternはパターンのデリミタが省略されているので、前後に#をつけている、これは前述の通り使われていない文字をひっぱってきただけで深い意味はない。
$is_matchを見て、この入力がこのルートにマッチしたか判定する事ができる。

var_dump($matches);
 array(3) {
   [0] =>
   string(11) "/test/hoge/"
   'id' =>
   string(4) "hoge"
   [1] =>
   string(4) "hoge"
 }

$matches配列には、マッチした結果がはいる、名前付きサブパターン(名前付きキャプチャ)をつかっているので、idというキー名があり、後でつかいやすい。

0にはマッチ全体が、そして名前付きサブパターンをつかっても通常通りキャプチャを順番に保存する1などには値がはいる。

まとめ

まあ、こういうの常識のように皆さん書いてるとおもうんですけど、ゆるふわPHPerかつ老害だと「ルートみたいなのがほしいの?自分で正規表現でかけばいいじゃん。DSLとか遅いんじゃないの?」とかいいだして非常に老害なので、ちゃんとそれっぽい感じで書けるようにしたいなとおもいました。


あと、このサンプルは非常に素朴なので実際使うにはもうすこし整備が必要だし、本当はそこまでかきたかったけど、なんかまだ風邪気味です。


まあ、自分で書くとかダルい…と思う人はそもそも普通に定番のWAFをつかうと思うんですが、
それとしても、最近はGitHubとかPackagistとか見てると三日に一個位はルーターのライブラリがあがってくるので(それもどうなのか?)良さそうなのをみつくろいましょう。

参考にした資料で、最後までとじなかったタブ

Slim Framework \Slim\Route
https://github.com/codeguy/Slim/blob/master/Slim/Route.php

Rails-like PHP url router
http://blog.sosedoff.com/2009/09/20/rails-like-php-url-router/