リクエスト振分け

リクエストの振分け

MVCでは、CにあたるControllerクラスより処理が始まると考えられます。
しかし「MVCの概念」のところで述べたとおり、PHPの仕組み上、本当の処理の基点はどうやってもクラス化することが出来ません。
ではどうやってこれを実現しているのでしょうか。

肝になる考え方は、全てのURLを一旦ある特定のphpファイルに集中させるという事です。

通常であればトップページはindex.php、掲示板であればbbs.phpなど、ページごとにphpファイルを作ります。
そうではなく、何か一つのphpファイル、例えばindex.phpなどに決めて、それに付加するパラメーターで呼び出すクラスを切り分けるのです。

一番単純なのは、例えば掲示板なら、
http://www.aiueo.com/index.php?p=bbs

アンケートページなら、
http://www.aiueo.com/index.php?p=enquete

みたいにして、index.phpでは$_GET['p']を参照し、それに応じた処理クラスを呼び出すという感じです。
とりあえずはこれでもMVCは実現できます。

しかしこれだとURLとして見栄えがあまり綺麗じゃありません。全部のページがindex.phpっていうのもなんか変な感じがします。

そこでapacheのリライト機能というのを利用する手があります。apacheとはWEBサーバーソフトウェアの代表です。apacheやリライトについては多くのサイトで説明されているのでそちらを見ていただくとして、
簡単に説明すると、リライトとはURLの偽装です。
リクエストされたURLに対し、実際のアクセス先それとは別のところへ導く仕組みです。

これを利用し、全てのリクエストを共通のフロントPHPへ集中させるのです。
例えばこうです。
リクエストされたURLが、
http://www.aiueo.com/bbs/
だとします。

普通に見ればドキュメントルート下のbbsディレクトリの中のindex.phpがリクエスト先になります。
しかしこれを、URLに関わらず全てのリクエストがドキュメントルート直下のindex.phpに処理が入るように操作します。
これを実現するのがapacheのリライト機能です。

で、PHPの環境変数に$_SERVERという、リクエスト情報が入っている物がありますが、その中の$_SERVER['REQUEST_URI']を利用します。
$_SERVER['REQUEST_URI']には、リクエストされたurlのドメイン以降のパスが入っています。
例えば上記のURLであれば、"/bbs/"になります。

index.phpではこの$_SERVER['REQUEST_URI']の値を参照し、その内容によって呼び出す処理クラスを振り分ける処理を行います。

index.php?p=bbsと結果的には同じです。

振分けクラスの作成

まずはURLをもとに処理の振分けを行うクラスを設計します。
index.phpに直接振り分けのロジックを書いてもいいのですが、折角だからクラス化して汎用的に利用できるようにします。

以下、apacheのリライト機能を利用した場合で説明します。
利用しない場合は、$_SERVER['REQUEST_URI']のかわりに$_GETを利用すると考えてください。

前述の通り、$_SERVER['REQUEST_URI']の値を取得し、これを"/"で区切ったものをパラメータとして使用します。

1つ目のパラメータを「コントローラー」と呼ぶことにします。そして、このパラメータと名前の一致するコントローラークラスを特定します。そしてそのクラスのインスタンスを生成します。

次に、2つ目のパラメータを「アクション」と呼ぶことにします。そして、コントローラークラスの中に存在する、「アクション」パラメータと名前の一致するメソッドを実行します。

コントローラークラスの設計単位は自由ですが、通常はサイトのページ単位です。
例えば通販サイトであれば、商品情報ページ、カートページなどです。

「アクション」というのは要はコントローラークラスのメソッドなわけですが、あるページの機能単位、またはユーザーの操作に対する動作単位と考えてもらえばいいと思います。
商品情報ページを例にすると、商品一覧、商品詳細情報などが考えられます。
その単位にメソッドをつくり、URLと直結させるわけです。

例えば、

http://www.xxx.com/product/list/ → 商品一覧ページ
http://www.xxx.com/product/info/ → 商品詳細情報ページ
http://www.xxx.com/cart/view → カート内容表示ページ

などのような感じです。

コントローラーの振り分け

$_SERVER['REQUEST_URI']の値をもとにコントローラーを振り分ける方法ですが、ごく単純に考えると以下のような処理が思い浮かびます。

Dispatcher.php
<?php

class Dispatcher
{
    public function dispatch()
    {
        $params = array();
        if ('' != $_SERVER['REQUEST_URI']) {
            // パラメーターを"/"で分割
            $params = explode('/', $_SERVER['REQUEST_URI']);
        }
        
        // 1番目のパラメーターをコントローラーとして取得
        $controller = 'index';
        if (0 < count($params)) {
            $controller = $params[0];
        }
        
        // パラメータより取得したコントローラー名によりクラス振分け
        $controllerInstance = null;
        switch ($controller) {
            case 'index': 
                $controllerInstance = new IndexController();
                break;
            case 'product': 
                $controllerInstance = new ProductController();
                break;
            case 'cart': 
                $controllerInstance = new CartController();
                break;
            default:
                header("HTTP/1.0 404 Not Found");
                exit;
                break;
        }
    }
}

?>

クラス名はなんでもいいですが、ここでは「割り当てる」などの意味を持つdispatchから、Dispatcherというクラス名にします。

単純にパラメータより取得したコントローラー名を基にswitchで振分けを行うというものです。サイトとして必要となるクラスを全てcaseで列挙するわけです。存在しないコントローラー名がURLでリクエストされた場合は404 NOT FOUNDが表示されるようにします。

とても単純な仕組みです。しかしこうすると、サイトごとにこのクラスを用意する必要が出てきます。
例えば通販サイトであれば、
商品ぺージに対してはProductController、
カートページに対してはCartController、
会員情報ページに対してはCustomerController、
などになると思います。

同じ人が、今度は新たに個人的なサイトとして、お料理教室のサイトを作ったとしましょう。
レシピページの表示を行うRecipeController、
コミュニケーション用の掲示板としてBbsController、
相互リンクページのLinkController
などが考えられると思います。

こちらのサイトでもDispatcherではswitchにより各コントローラーへの振分けを行うわけですが、通販サイトとは全くかかわりがないため、switchの内容も当然異なります。なので、システムごとにDispatcherを用意する必要が出てきます。

できればDispatcher自体は汎用的なクラスにしたいので、Dispatcherは抽象クラス(抽象クラスの詳細はこちら)などにして、振分けを行うswitchの部分だけ別メソッドとして抽象メソッド化し、各システムではDispatcherを継承して振分けメソッドだけを実装するような形にすれば、汎用的といえます。

<?php

abstract class Dispatcher
{
    public function dispatch()
    {
        // パラメーター取得(末尾の / は削除)
        $param = ereg_replace('/?$', '', $_GET['param']);
        $params = array();
        if ('' != $param) {
            // パラメーターを / で分割
            $params = explode('/', $param);
        }
        
        // 1番目のパラメーターをコントローラーとして取得
        $controller = 'index';
        if (0 < count($params)) {
            $controller = $params[0];
        }
        
        // パラメータより取得したコントローラー名によりクラス振分け
        $controllerInstance = $this->dispatchController($controller)
        if (null == $controllerInstance) {
            header("HTTP/1.0 404 Not Found");
            exit;
        }
        (以下略)
    }
    
    // 振分け処理を抽象化
    abstract protected function dispatchController($controller);
}

// 通販サイト振分け処理クラス
class TuuhanDispatcher extends Dispatcher
{
    // 振分け処理メソッドを実装
    protected function dispatchController($controller)
    {
        $controllerInstance = null;
        switch ($controller) {
            case 'list': 
                $controllerInstance = new ListController();
                break;
            case 'detail': 
                $controllerInstance = new DetailController();
                break;
            case 'cart': 
                $controllerInstance = new CartController();
                break;
            case 'customer': 
                $controllerInstance = new CustomerController();
                break;
        }
        return $controllerInstance;
    }
}

// お料理サイト振分け処理クラス
class CockingDispatcher extends Dispatcher
{
    // 振分け処理メソッドを実装
    protected function dispatchController($controller)
    {
        $controllerInstance = null;
        switch ($controller) {
            case 'recipe': 
                $controllerInstance = new RecipeController();
                break;
            case 'bbs': 
                $controllerInstance = new BbsController();
                break;
            case 'link': 
                $controllerInstance = new LinkController();
                break;
        }
        return $controllerInstance;
    }
}

?>

ですがやはり、コントローラーを追加するたびに、
Dispatcherの方にも振分け処理に追加を行わないことに変わりはありません。
できればswitchを使わずに勝手に振り分けて欲しいものです。

汎用的な振分け処理クラス

実は完全な汎用振分け処理クラスを作る事は可能です。
それにはコントローラークラスの名前付けの規則化と、
PHPのある性質を利用します。

PHPはクラス名に変数が使えるという性質があります。
例えば、

$controller = new IndexController();

これは以下のようにすることが可能です。

$className = 'IndexController';
$controller = new $className();

これをどう利用するのか見てみましょう。

Dispatcher.php


class Dispatcher
{
    private $sysRoot;
    
    public function setSystemRoot($path)
    {
        $this->sysRoot = rtrim($path, '/');
    }

    public function dispatch()
    {
        // パラメーター取得(末尾の / は削除)
        $param = ereg_replace('/?$', '', $_SERVER['REQUEST_URI']);
        
        $params = array();
        if ('' != $param) {
            // パラメーターを / で分割
            $params = explode('/', $param);
        }
        
        // 1番目のパラメーターをコントローラーとして取得
        $controller = "index";
        if (0 < count($params)) {
            $controller = $params[0];
        }
        
        // パラメータより取得したコントローラー名によりクラス振分け
        $className = ucfirst(strtolower($controller)) . 'Controller';
        
        // クラスファイル読込
        require_once $this->sysRoot . '/controllers/' . $className . '.php';
        
        // クラスインスタンス生成
        $controllerInstance = new $className();


パラメータより取得したコントローラー名の先頭文字のみ大文字にして、
末尾に'Controller'を付けた物をコントローラークラス名としてインスタンスを生成します。
まあ実際のところ、クラスの呼び出しに大文字小文字は関係ないので、頭文字のみ大文字にするのは念のためです。

この方法で振分け処理を行う場合、
URLで指定するコントローラー名とクラス名は一致させる必要があります。
そしてコントローラークラスには必ず末尾に'Controller'をつける、などの決め事を行います。
つまりクラス名に規則性を持たせることで、自動振分けを実現するのです。

アクションの振り分け

アクションという表現をしますが、要はメソッドです。
コントローラークラスが特定できたら、今度はそのクラスのどのメソッドを実行するかを特定します。

それには2番目のパラメーターを利用します。

Dispatcher.php

        // クラスインスタンス生成
        $controllerInstance = new $className();

        // 2番目のパラメーターをコントローラーとして取得
        $action= 'index';
        if (1 < count($params)) {
            $action= $params[1];
        }        

        // アクションメソッドを実行
        $actionMethod = $action . 'Action';
        $controllerInstance->$actionMethod();
    }
}

先ほどのdispatchメソッドの続きです。

クラスインスタンスの生成のときと同様に、
メソッドの呼び出しの場合も、メソッド名には変数が使用できます。

後はコントローラークラスを作るだけです。

基点スクリプト

Dispatcherクラスが出来ましたが、
あくまでクラスですので、このクラスのインスタンスを生成してdispatchメソッドを実行するスクリプトが必要です。

これこそが今回のMVCの仕組みの中で唯一、クラスではないスクリプトになります。
apacheのリライトにより全てのリクエストがこのスクリプトに集中します。

ファイル名は何でもいいですが、ここではindex.phpとします。

index.php
<?php

require_once '/home/www/public_html/mvc/Dispatcher.php';

$dispatcher = new Dispatcher();
$dispatcher->setSystemRoot('/home/www/public_html/tuuhan');
$dispatcher->dispatch();

?>

これだけのことです。

Dispatcherはどんなシステムでも利用できるようにしたいので、
最低限、システムのルートディレクトリくらいは設定できるようにしておきます。