【CakePHP】CakePHP Advent Calendar2010(day 7) : Tips for Routes

CakePHP Advent Calendar2010 の当番が回ってきました。
本来はもっとかる~い内容でいくつもりでしたが、皆さんがっつりとやられているので、慌てて秘蔵のネタ(?)を引っ張り出すことにしました。まあとはいえ実際はそんなでもないので軽く読んでいただければ(笑)。

さて、Modelにまつわる話が多いようなので今回も違う話題にしましょう。Modelは今までTipsが出にくい仕組みの一つだったのかもしれないのですが、同じくなかなか情報として出回らないRoutes関連でも取り上げてみようと思います。

CakePHPであることを隠蔽する (CakePHP Advent Calendar 2010 2日目) でも取り上げられていましたが、CakeぽくないURLを作成するためにはRouteを変更することで自由なURLにコントローラとアクションを振り分けることができます。しかし多くの場合「/:controller/:action/*」という標準のルート指定で問題がないため、修正する機会はなかなかないのではないかと思います。そこで2ステップでRoutes活用法をご紹介します。

ステップ1:パラメータチェックをRoutesで済ませてしまう

Routesの設定は上記のようにURL変更というのも活用法のひとつかもしれませんが、もう一つとても便利な使い方があります。それは、URL中にパラメータを含む場合に、そのパラメータチェックをRoutes内で行うことができる点です。
たとえば

http://www.example.com/foo/hoge/0123456789

というURLを想定するとしましょう。fooコントローラ内のhogeアクションで1つの値を入手したい場合です。この場合、デフォルトですとコントローラ内の「$this->params[‘pass’][‘0’]」に値が入ります。そこで一般的に、次のようなコントローラを書くでしょう。


class FooController extends AppController {
	var $name = 'Foo';
	var $uses = array();
	function hoge(){
		//まずパラメータチェック
		//
		if(!preg_match('/^[0-9A-Za-z]+$/', $this->params['pass'][0])){
			$this->flash('正しくないURLです', '/');
			return;
		}
		//以下、必要な処理
		//
	}
}

しかし、Routesを活用することで、コントローラにチェックを書かずにチェックを行うことができます。「app/config/routes.php」に次のように記述します。


Router::connect('/foo/hoge/:viewerId', array('controller' => 'foo', 'action' => 'hoge'), array('viewerId' => '[0-9A-Za-z]+'));
Router::connect('/foo/hoge/*', array('controller' => 'foo', 'action' => 'error'));

この場合は$this->params[‘viewerId’]にて値を受け取ることができます。条件に満たさない「/foo/hoge」はすべてerrorアクションに流れます。Router::connect()は先着順でルーティングを決定しますので、一番最後に条件を満たさない場合のルートを書いておけば、デフォルトである「/:controller/:action/*」に流れることはありません。

メリットとしては、まずアクション内でパラメータチェックが要らなくなる点で、fat controllerのスリム化に貢献できると思います。そして不要なアクションがコントローラ内に渡ってこない点にあります。そもそもURLから渡されるパラメータはコントローラに渡ってくる時点で正しいものであるべきかと思いますので、Routesでチェックすることは理にかなっていると言えます。

ステップ2:正規表現で表せないルーティングはどうするか?

上記ルーティングは、あくまでも正規表現で表される場合であり、複雑な条件分岐を伴ったりモデルを参照したい場合などはうまく表現できません。
しかしCakePHP1.3からは「カスタムルーティング」という機能が備わり、その中でチェックしたりデフォルト値を追加したりすることができます
たとえば、既にモデルに登録されている値のみをルーティングし、登録されていない値はアクセスできないようなルーティングを表現してみましょう。まず、カスタムルーティングのコードを「app/libs」に記述します。コードを置く場所はどこでもよいのですが、Cake用のコードなのでvendorsではなくlibsがいいでしょう。ここでは「app/libs/viewer_route.php」とします。


class ViewerRoute extends CakeRoute {

	//Userモデル内のviewerIdをチェックし、userIdを返す
	//
	function __checkViewerId($id){

		$model =& ClassRegistry::init('User');
		$result = $model->find('first', array(
			'conditions' => array(
				'viewerid' => $id,
			),
		));
		if(empty($result)){
			return false;
		}
		return $result[$model->alias]['id'];
	}

	//パース処理(URL => array())
	//
	function parse($url){

		//ここはお決まりの処理
		//
		$result = parent::parse($url);
		if(empty($result)){
			return false;
		}

		//viewerIdが登録されているかをチェック
		//userIdをルーティングパラメータとして追加
		//
		if(isset($result['viewerId'])){
			$userId = $this->__checkViewerId(h($result['viewerId']));
			if($userId === false){
				return false;
			}
			$result['userId'] = $userId;
		}else{
			return false;
		}

		return $result;
	}

	//マッチ処理(array() => URL)
	//
	function match($url){

		//ここに、URL生成に必要な処理を記述する(今回はなし)
		//

		//以下お決まりの処理
		//
		$result = parent::match($url);

		return $result;
	}
}

カスタムルーティングでは、parse()とmatch()を記述する事で、それぞれ正引き(Router::connect())と逆引き(Router::url())で独自の処理を行う事ができます。それ以外のコード(上記では__checkViewerId())は独自メソッドです。
ClassRegistry::init()を用いると、ルーティング処理内でもモデルを扱うことができます。ただし、parse()の段階ではまだコントローラ等は初期化が済んでいませんから、その範囲内での処理しかできないことを留意しておきます。match()は逆引きで用いられるので、タイミング的にcontrollerの初期化が終わっているのでこの限りではありません。
次に、「app/config/routes.php」にカスタムルーティングを適用します。第3引数の連想配列の中にある「routeClass」パラメータに作成したカスタムルート名を指定します。また、このクラスは自動的に読み込まれないので、事前に「App::import()」等で読み込んでおきます。


	App::import('Lib', 'ViewerRoute');
	Router::connect('/foo/hoge/:viewerId', array('controller' => 'foo', 'action' => 'hoge'), array('viewerId' => '[0-9A-Za-z]+', 'routeClass' => 'ViewerRoute'));
	Router::connect('/foo/hoge/*', array('controller' => 'foo', 'action' => 'error'));

これで、viewerIdというパラメータで特定ユーザのみを許可するルーティングが実現できます。また、「ルーティングパラメータに存在しないパラメータを後から追加できる」ことも注目したい点です。上記サンプルでは、モデルから取得したuserIdを追加しています。このパラメータはコントローラ内で「$this->params[‘userId’]」として受け取ることができます。

CakePHPは1.2でも十分に実用的ですが、1.3になってよりいろいろなことができるようになりました。今回の「カスタムルーティング」も使い方次第では大変に便利な機能といえます。また1.2を含めて、まだまだ便利な機能がCakePHPには備わっています。当方の執筆しました「Pocket詳解 CakePHP辞典」では、1.2/1.3の違いを含めコアクラス内に収録されているプロパティ・メソッドを盛りだくさんで紹介しています。こちらのほうもぜひご利用いただけますと幸いです。

Pocket詳解 CakePHP辞典

Pocket詳解 CakePHP辞典 [書籍]

著者滝下 真玄

出版社秀和システム

出版日2010-09-27 (月)

商品カテゴリー単行本

ページ数672

ISBN4798027456

Supported by amazon Product Advertising API

次の担当は kanonji さんです。
どんなCake話が聞けるか楽しみですね!:D
ちょっと気が早いかもしれませんがMerry Xmas!!

【追記】(2010.12.07)
カスタムルーティングについての解説を若干足しました

【戯れ言】「メトリック値」とは何ですか?

社内のサーバを引き続き構築しております。

ある程度出来てきたので、普段使用しているデスクトップ(XP)からリモートで管理者側LANにも接続できるように、LANカードを増設することにしました。俗に言う「LANカード二枚差し」です。

二枚差しそのものは当然ながらうまくいき、それぞれのネットワークに接続でき、問題なく動作していたかに見えたのですが、一つ困った状況になりました。
管理者側LANは、セキュリティの観点からインターネットに接続できないように設定してあるのですが、2枚差ししてある状態でインターネットを閲覧すると、まれに管理者側の方にルーティングされてしまい、インターネットアクセスが出来なくなってしまうのです。
また、この「まれ」というのが規則性が全くなく、突然起きるため非常に厄介でした。デフォルトゲートウェイ等を一生懸命設定してみたのですが、全く変化無し。見れたり見れなかったりを繰り返すため、仕方がないので管理者側のLANカードを無効にしておいて、必要なときに有効にするしか無いと思っていました。

Googleで「二枚差し」をキーワードに入れて検索したりもしたのですが、有効と思われるものが引っかからなくて、半ば諦めていたのですが、ようやく求めていた情報を入手することが出来ました。今まで気にもとめなかったパラメータが、ここで出てくるとは!

結論を言いますと、LANカード(厳密にはTCP/IP)に「メトリック値」というものを設定することが出来、これを用いることでネットワークの優先順を決めることが出来ます。
場所は、「マイネットワーク→(変更したいネット環境)→プロパティ→(TCP/IPのプロパティ→詳細設定→自動メトリック」で、チェックボックスを取り、「インターフェイスメトリック」の入力欄に小さい値(1など)を入れると、そのネットワークの優先度を上げることが出来ます。もう一方のネットワークについても、同様にこれより大きな値を入れておくとさらに安心です。

ちなみに、このネットワーク設定は、無線LAN併用しているようなノートPCでも有効だと思います。無線LANを自動接続設定してしまっていると、有線LANに接続しても無線LANを選択される場合もあります。そこで、有線LAN側のメトリック値を設定することで、高速回線を優先的に選択することが出来るようになりますので、この値設定はかなり有効だと思います。

いやー、XPは発売当初から使っていますが、まだまだ知らないことはたくさんあるもんだなぁ…と改めて思いました。
というか、ネットワークについて無知なだけかもしれませんが(^^;;;。

【サーバ関連】心が折れています(iptables・routeの設定)

サーバ設定に行き詰まっています。

今回はNIC3枚差しで、1枚はインターネット側に、もう2枚はローカルで、そのうちの1枚は一般向け、もう1枚がサーバ管理向け、という構成でセットアップしているのですが、ローカル側でのルーティング(フォワーディング)の設定が分からず困っています。

もう少し詳しく書くと、こういった構成を考えています。

LAN1 / eth0 : 192.168.10.0/24 gateway 192.168.10.1
LAN2 / eth1 : 192.168.20.0/24 gateway 192.168.20.1
WAN  / eth2 : 10.1.1.0/29 gateway 10.1.1.1

LAN1もLAN2も、WAN側をへ通過でき、LAN1からLAN2は通過できるけどLAN2からLAN1は特定ポートが通過できないような仕組みにしたいのです。
現在iptablesは全てACCEPTしていて、pingは

○ LAN1→etc0→SV→etc2→WAN
○ LAN2→eth1→SV→etc2→WAN
× LAN1→eth0→SV→etc1→LAN2
× LAN2→eth1→SV→etc0→LAN1
という状況です。

「ルーティング」の仕組みについてしっかりと理解していないと思うのですが、このような場合「スタティックルーティング」でLAN1←→LAN2の橋渡しをするゲートウェイが必要なのではないかと思うのですが、routeの設定をいくらいろんな数値でやってもうまくいかないのです。例えば

route add -net 192.168.10.0/24 gw 192.168.20.1 dev eth1
route add -net 192.168.20.0/24 gw 192.168.10.1 dev eth0

route add -net 192.168.10.0/24 gw 192.168.20.2 dev eth1
route add -net 192.168.20.0/24 gw 192.168.10.2 dev eth0

とかやってみましたがダメです。

そもそも、セグメント越えの概念はやめたほうがいいんですかね?
相当ネットで検索していろんな設定を調べたんですが、全く分からないです。
従来通りLAN側を一緒にしてしまっても良いのですが、出来れば一般社員にSSH等を使わせたくないんですよね…
将来的にVPNとかもやりたいので、セグメントを分け出来ないとかなり困るなぁ…

どなたかネットワーク構築に詳しい方、アドバイスをいただけると嬉しいです。