読者です 読者をやめる 読者になる 読者になる

日記

日々のことと、Python/Django/PHP/Laravel/nodejs などソフトウェア開発のことを書き綴ります

Laravel 5.1 入門記 その3(URL Routing 編)

Routing から再開。

HTTP Routing - Laravel - The PHP Framework For Web Artisans

Basic Routing

URL のルーティングは、 app/Http/routes.php に書いていくらしい。この route.php はApp\Providers\RouteServiceProvider が読み込むらしい。よくわかんないけど!

Laravel の基本的なルーティングは URIクロージャ(無名関数のことね) でシンプルに記述できるらしい。まずはドキュメントに載っているコードを app/Http/routes.php に追記してみます。

Route::get('/', function () {
    return 'Hello World';
});

元々あった、 '/' の定義はコメントアウトしちゃいました。 ブラウザで http://localhost:8080/ にアクセスすると、「Hello World」と表示されました\(^o^)/

シンプルだけど、実際のアプリ開発で、routes.php に処理を書いてしまうのは、ちょっと辛そう。他のクラスに委譲するパターンはそのうち出てくるんでしょう。

Registering A Route For Multiple Verbs

どうやら、get/postを同じ定義にまとめることもできるらしい。書き方はこんな感じ。

Route::match(['get', 'post'], '/', function () {
    return 'Hello World';
});

Route::match を使うのがポイントみたい。さらにどんな HTTP method でも受け取るエンドポイントを作りたいときは

Route::any('foo', function () {
    return 'Hello World';
});

Route::any を使って定義する。

さて次、次

Generating URLs To Routes

Django の reverse 関数と同じかな。

$url = url('foo');

少し重要な機能なので試してみます。route.php を以下のように書き換え

Route::get('/', function () {
    return 'Hello World. ' . url('/');
});

Route::get('/helloworld', function () {
    return 'Hello World. ' . url('/helloworld');
});

そして、 http://localhost:8080/ にアクセスすると

Hello World. http://localhost:8080

と表示され、 http://localhost:8080/helloworld にアクセスすると

404 Not Found !! orz

原因を調べてみたら、使っていた ubuntu 14.04 では Apache module の mod_rewrite が有効になっていなかった。

$ cd /etc/apache2/mods-enabled
$ sudo ln -s ../mods-avaialble/rewrite.load ./rewrite.load
$ sudo apachectl restart

以上を行って、もう一度 http://localhost:8080/helloworld にアクセスすると、今度こそ

Hello World. http://localhost:8080/helloworld

\(^o^)/\(^o^)/\(^o^)/

2行のセクションなのに、はまって長かった。次に行きます。

Route Parameters

Required Parameters

ここからは RESTful なものを実装するときに重要なやつですね。

URL上の文字を正規表現で引っかけて、クロージャに渡すことが出来るらしい。

Route::get('user/{id}', function ($id) {
    return 'User '.$id;
});

これを routes.php に追記して、 http://localhost:8080/user/tnamao にアクセスすると

User tnamao

と表示されました。 ちなみに id は必須パラメータなので user/ まででアクセスしても必須パラメータなので例外が出力されてしまいました。

続いて、複数のパラメータの場合は次のように書くみたい。

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    return 'post: ' . $postId . ' commentId: ' . $commentId;
});

これを追記して、 http://localhost:8080/posts/aaa/comments/bbb にアクセスすると

post: aaa commentId: bbb

と表示されました。これ、パラメータの順番は変えられるのかな、と思って次の設定も試してみた。

Route::get('posts2/{post}/comments/{comment}', function ($commentId, $postId) {
    return 'post: ' . $postId . ' commentId: ' . $commentId;
});

これで、 http://localhost:8080/posts/aaa/comments/bbb にアクセスしたら、

post: bbb commentId: aaa

さっきのと同じ結果を期待したのですが、残念! orz やっぱり Django / Python みたいには柔軟にいかないようです。Python の kwargs の仕組みは強いなぁ、と。

あと余談ですが、この Route parameter にはハイフンは使えないとのこと。

Optional Parameters

少し前に ちなみに id は必須パラメータなので user/ まででアクセスしても必須パラメータなので例外が出力 の解決策っぽい。

Route::get('user/{name?}', function ($name = null) {
    return $name;
});

この例の様に、? を付ければ良いらしい。早速、さっきの定義に追加して、以下の様にしてみた。

Route::get('user/{id?}', function ($id=null) {
    return 'User '.$id;
});

これで http://localhost:8080/user/ にアクセスすると、エラーが出なくなった\(^o^)/

Regular Expression Constraints

恐らくやりたいことの本命が来た。

where メソッド正規表現を使った複雑なパラメータ指定ができるらしい。

Route::get('user/{id}', function ($id) {
    return return 'User '.$id;
})
->where('id', '[0-9]+');

Route::get('user/{id}/{name}', function ($id, $name) {
    return 'User '.$id.' Name '.$name;
})
->where(['id' => '[0-9]+', 'name' => '[a-z]+']);

うーん、イマイチ感が…。ま、仕方ない。

Global Constraints

id などあちこちで定義するようなパラメータの正規表現は、Global レベルの定義をできるらしい。

RouteServiceProvider の boot 関数の中で以下のように書けばよいみたい。

public function boot(Router $router)
{
    $router->pattern('id', '[0-9]+');

    parent::boot($router);
}

これを使うには

Route::get('user/{id}', function ($id) {
    // Only called if {id} is numeric.
});

この定義で大丈夫らしい。でも、グローバルなのかローカルなのか、違いが無いのでグローバルな定義は使うときに気をつけたほうが良さそう。

Named Routes

名前付きのルーティングらしい。 Django にもあったけど、URLを逆引きするときの設定ぽい。

Route::get('user/profile', ['as' => 'profile', function () {
    //
}]);

なんか、まんまですね。恐らく使うときの本命はこっち。

Route::get('user/profile', [
    'as' => 'profile', 'uses' => 'UserController@showProfile'
]);

ここで初めて、他のクラスに処理を委譲する方法が出て来た。無名関数で処理を定義していた時はよかったけど、同じクラスを異なるURLでルーティングするときに、URLが引けない問題があるんだろうなぁ、と。

憶測なので外れているかもしれませんが…。 ただ、URLとは異なる別名指定しておくと、コードのメンテナンスは色々楽かもしれない。URLは変わるものだしね…。

Route Groups & Named Routes

ルーティング定義をグルーピングするための機能らしい。

Route::group(['as' => 'admin::'], function () {
    Route::get('dashboard', ['as' => 'dashboard', function () {
        // Route named "admin::dashboard"
    }]);
});

なるほど。機能単位でまとめるものなのね。ちょっと試してみたら、URL の階層化は定義されていないのね。 URL はどうやって取るのだろうと思って以下のコードも試してみた。

Route::group(['as' => 'admin::'], function () {
    Route::get('dashboard', ['as' => 'dashboard', function () {
        return 'dashboard. ' . url('admin::dashboard');
    }]);
});

結果は

dashboard. http://localhost:8080/admin::dashboard

なんか違う…。どうも次のセクションにやり方が書いてあるっぽい。

Generating URLs To Named Routes

名前付きでルーティング設定しておくと、URL生成とかリダイレクトが route 関数で出来ちゃうよ、と言うことらしい。

$url = route('profile');

$redirect = redirect()->route('profile');

こんな感じで出来るらしい。リダイレクトのところがちょっとよく分からないけど…。

パラメータが必要なURLは、以下の様にパラメータも与えれば URL を生成できるみたい。

Route::get('user/{id}/profile', ['as' => 'profile', function ($id) {
    //
}]);

$url = route('profile', ['id' => 1]);

では、さっき出来てなかったコードを修正してみます。

Route::group(['as' => 'admin::'], function () {
    Route::get('dashboard/{id}', ['as' => 'dashboard', function ($id) {
        return 'dashboard. ' . route('admin::dashboard', ['id' => $id]);
    }]);
});

オマケで id パラメータを塚用にしちゃった。そして、何となく予想して route 関数の一つ目の引数は admin::dashboard にしてみた。そして、ブラウザで http://localhost:8080/dashboard/1 にアクセスすると

dashboard. http://localhost:8080/dashboard/1

できたー\(^o^)/

ややこしいけど、url と route を使い分けないとダメですね。しかし、url 関数はアプリケーションの接頭辞を付けるだけの関数なんだろうか。いまのところは、イマイチ存在意義が分かっていない。

Route Groups

Route Group を使うと ルーティング属性を共有出来るらしい。middleware / namespace / route-prefix なんかが共通設定として定義できるらしい。まだ入門前の middleware とか難しいのは置いといて、route-prefix で試してみた。

Route::group([
    'prefix' => 'admin',
    'as' => 'admin::'
], function () {
    Route::get('dashboard/{id}', ['as' => 'dashboard', function ($id) {
        return 'dashboard. ' . route('admin::dashboard', ['id' => $id]);
    }]);
});

こんな感じに dashboard の定義を修正して、 'prefix' => 'admin' を追加してみた。そして、ブラウザで http://localhost:8080/admin/dashboard/1 にアクセスすると

dashboard. http://localhost:8080/admin/dashboard/1

アクセス先も変わったし、route 関数が返す URL も変わりました\(^o^)/

Route Group はうまく使えば設定を簡略化できる。でもそのためには、URL 設計をしっかりやろうってことか。URLだけじゃなくて、名前空間もかな。恐らく機能単位で名前空間は分けた方が良さそう。

CSRF Protection

ルーティングのところで CSRF に触れるのか…。

Laravel の CSRF Protection はセッションにトークンを生成して、自動的に処理が行われるらしい。トークンはセッションに保存され、フィールドとしては _token が自動的に使われる。 csrf_field helper 関数でトークンの値は取得できる。

HTML Form に設定する時は以下のいずれかの方法で実装する。

<?php echo csrf_field(); ?>

<input type="hidden" name="_token" value="<?php echo csrf_token(); ?>">

{!! csrf_field() !!}

一番最後は Blade template エンジンというのを使った時のパターンらしい。

HTTP メソッドの POST / PUT / DELETE でアクセスがあったときは、自動的に CSRF token のチェックが行われる。VerifyCsrfToken HTTP middleware が、入力とセッションのtokenをチェックしているらしい。

Excluding URIs From CSRF Protection

CSRF チェックを外すことも出来るらしい。以下のコードを参照

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;

class VerifyCsrfToken extends BaseVerifier
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'stripe/*',
    ];
}

$except に CSRF チェックを外すURLを定義するらしい。Ajax 用の API とかだったら、CSRF チェックを外すというか、Cookie で何とかしたいよね…。

と、思ったら、Ajax の場合は X-XSRF-TOKEN ヘッダーから取得して使えって書いてあるな。じゃ、何の問題もない。

Form Method Spoofing

HTTP メソッドが通常の FORM では GET / POST しかない問題への対応で、 以下の様に hidden で _method パラメータを作ってそこに DELETE / PUT / PATCH を設定するらしい。

<form action="/foo/bar" method="POST">
    <input type="hidden" name="_method" value="PUT">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
</form>

Throwing 404 Errors

404エラーを投げるときは、以下のコード

abort(404);

詳しくは、errors を見ろとのこと。

とりあえず、 routing に関してはこれくらいで、次回は Middleware に行きます。