のらぬこの日常を描く

ノージャンルのお役立ち情報やアニメとゲームの話、ソフトウェア開発に関する話などを中心としたブログです。

PHPでフレームワークを使わずURLのルーティングをいい感じにやる

どうものらぬこです。

PHPでWebアプリを書くなら、SlimとかCakePHP等のフレームワークを使うのがおそらく一般的なのかと思います。

ところが、既存の環境を間借りして、そこにアプリを置くような場合等、PHPのバージョンが古い等の問題により、なかなかそうもいかない場合もあります。

先日やったお仕事で、既存のシステムの一部リプレイスをしたかったのですが、PHPのバージョンが古すぎてフレームワークが導入できず、また、ヘッダ、フッタ、サイドバーなどはSmartyのtemplateで書かれているのですが、そいつらは流用したいということがありました。

そんなわけで、僕がとった方針は、ロジック部分はPHPべた書きでゴリゴリ書いて、テンプレートエンジンだけSmartyを使う形で開発しました。

さて、例として、以下のようなURLパターンに対して機能を実装していくとします。

/search
/categories
/catgegory/4
/prefecture/10/category/2
/city/2011/category/1
/railway/20/stations
....

これらのURLを適切に解釈し、それぞれの機能に対応した Controller を呼び出し、パラメータ値に対応したModelをロードしていい感じに加工した後、適切なViewTemplateを呼び出すような仕組みは、何らかのフレームワークを使えばたいてい備わっています。

しかし、べた書きで書くような場合には、これらを自前で実装する必要があります。

Webを漁ってみたのですが、正規表現使った力業や、'/' で explode した後でひたすら条件分岐で頑張るなど、再利用性もメンテナンス性もまるでなさそうな方法しか見つからなかったので、URLパターンをSlim frameworkの Router みたいな記法で書ける URLマッチングメソッドを自分で書いてみました。

<?php
function url_matcher(
    $inputUrl, $matchUrl, &$resultParams) {
    if (substr($inputUrl, -1) === '/') {
        $inputUrl = substr($inputUrl, 0, -1);
    }
    if (substr($matchUrl, -1) === '/') {
        $matchUrl = substr($matchUrl, -1);
    }

    $resultParams = array();
    $inputUrlParts = explode('/', $inputUrl);
    $matchUrlParts = explode('/', $matchUrl);
    if ($inputUrl === '' && $matchUrl === '') {
        return true;
    }

    if (strpos($inputUrl, '/') !== 0 ||
        strpos($matchUrl, '/') !== 0) {
        return false;
    }
    if (count($inputUrlParts) !== count($matchUrlParts)) {
        return false;
    }

    for ($index = 1; $index < count($inputUrlParts); $index++) {
        $path = 
            preg_replace(
                '/^\{([^}]+)\}$/', '\1', $matchUrlParts[$index], 1, $replacedCount);

        if ($replacedCount === 0) {
            if ($inputUrlParts[$index] === '' ||
                $inputUrlParts[$index] !== $matchUrlParts[$index]) {
                // path変数の重複によりエラー
                return false;
            }
        } else if ($replacedCount === 1) {
            if (isset($pathParams[$index]) ||
                $inputUrlParts[$index] === '') {
                return false;
            } else {
                $resultParams[$path] = $inputUrlParts[$index];
            }
        } else {
            // ルールに一致しない
            return false;
        }
    }
    return true;
}

使い方はこんな感じです。

<?php
function routing($method, $inputUrl) {
    $pathParams = null;
    if ($method === 'GET') {
        if (url_matcher($inputUrl, '/search'), $pathParams) {
            // 処理
        } else if (url_matcher($inputUrl, '/category/{categoryID}', $pathParams)) {
            $categoryId = $pathParams['categoryID'];
            // 処理
        } else if (url_matcher($inputUrl, '/prefecture/{prefectureID}/category/{categoryID}', $pathParams) {
            $prefectureId = $pathParams['prefectureID'];
            $categoryId = $pathParams['categoryID'];
            // 処理
        } else if {
            ....
        }
    } else if ($method === 'POST') {
        ....
    } else {
        // page not found処理
        exit;
    }
}

こちら、PHP5.2、PHP7.1で動作確認済です。

適用する環境がせめてPHP5.3?5.4?以上であれば、url_matcher に function オブジェクトを渡すようにする等、もう少しやりようはあったのですが、このソースを動かす環境が PHP5.2とかいうまことに残念な環境であったため、こんな形になっております。

余談ですが、url_matcher で urlパターンにマッチした時の処理ですが、僕は以下のようなController もどきのクラスを作っています。

<?php
abstract class Controller {
    protected $smarty = null;
    protected $mathod = null;
    protected $pathParams = null;
    protected $requestParams = null;
    public Controller($smarty, $method, $pathParams, $requestParams) {
        $this->smarty = $smarty;
        $this->method = $method;
        $this->pathParams = $pathParams;
        $this->requestParams = $requestParams;
    }
    abstract public function createModel();
    abstract public function getTemplateFileName();
}

class MyController extends Controller {
    public function createModel() {
        ....
        $this->smarty->assign(...);
        $this->smarty->assign(...);
    } 

    public function getTemplateFileName() {
        return $smarty->getTemplateDirectory().'/hogehoge.tpl';
    }
}

まず、一致したURLルール毎に、Controllerの適切なサブクラスを作成し、以下のようなコードを書けば、一応MVCっぽい感じに実装できると思います。

<?php
$controller->createModel();
$smarty->display($controller->getTemplateFileName());

パラメータのバリデーションチェック用のメソッドを用意したり、controller内の共通処理をserviceクラスを作ってそっちに移譲するなど、もう少しいろいろ頑張れば、ソースの見通しも良くなると思います。

ということで今回は、PHPで開発しているけど様々な事情でフレームワークが使えない方向け、簡易URLマッチング関数を実装してみたお話でした。

この記事が、どなたかのお役に立っていただければ幸いです。