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

日記

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

Laravel 5.1 入門記 その5(Controller 編)

今回はコントローラ。なんか基盤部分の話から一個レイヤーが上がった感じ。

laravel.com

Introduction

単一の routes.php ファイルにロジックを書くわけでは無く、Controller クラスに処理を分解して委譲できる。一般的には Controller クラスは app/Http/Controllers ディレクトリに配置していきます。

機能単位でディレクトリを分けたいような分けたくないような…。

Basic Controllers

Laravel の全ての Controller クラスは base class を継承して実装するべきらしい。

と言うことで、簡単なコントローラを作ってみる。

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class SampleController extends Controller
{
    public function showProfile($id)
    {
        return 'display user profile of id:'.$id;
    }
}

ドキュメントに載っていたサンプルは余計なものが付いてたので、ちょっと変えてみた。 さらに routes.php にも定義を追加。

Route::get('sample/{id}', 'SampleController@showProfile');

コントローラクラス名@メソッドを指定するようです。これでブラウザから http://localhost:8080/sample/tnamao にアクセスすると

display user profile of id:tnamao

簡単だった\(^o^)/

Controllers & Namespaces

routes.php に指定する Controller クラスの名前空間は、 App\Http\Controllers をルートとして読み込みを行うらしい。(very important と書いてあるので超重要なんでしょう)

App\Http\Controllers は暗黙的に使われるので、App\Http\Controllers\Photos\AdminController を指定するときは、以下の様に Photos から指定するみたい。

Route::get('foo', 'Photos\AdminController@method');

Naming Controller Routes

Controller クラスを routes.php に設定した時にもルーティングに名前を付けることができる。

Route::get('foo', ['uses' => 'FooController@method', 'as' => 'name']);

Route::get の第二引数に、uses と as で設定を行うらしい。 クラス名@メソッド からも URL を逆引きできるけど、その時に as で指定した別名が有効なものになるらしい。ちょっと試してみます。

まず、routes.php を書き換えして、同一のクラス名@メソッド名でルーティング設定を行います。

Route::get('sample/{id}', ['uses' => 'SampleController@showProfile', 'as' => 'sample']);
Route::get('example/{id}', ['uses' => 'SampleController@showProfile', 'as' => 'example']);

続いて SampleController クラスを書き換えます。

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class SampleController extends Controller
{
    public function showProfile($id)
    {
        return route('sample', ['id' => $id]).' '.route('example', ['id' => $id]);
    }
}

これで http://localhost:8080/sample/tnamao にアクセスすると

http://localhost:8080/sample/tnamao http://localhost:8080/example/tnamao

クラス名@メソッド名のルーティングも名前を付ければ、複数のルーティングがあっても問題無し。んでは、action 関数を使って、クラス名@メソッド名からURLを引いた時はどうなるでしょう?

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class SampleController extends Controller
{
    public function showProfile($id)
    {
        return action('SampleController@showProfile', ['id' => $id]);
    }
}

これでブラウザで表示すると…

http://localhost:8080/example/tnamao

何故か example 側が表示された。routes.php で順序が後ろ側が優先されているのかな。routes.php を編集

Route::get('example/{id}', ['uses' => 'SampleController@showProfile', 'as' => 'example']);
Route::get('sample/{id}', ['uses' => 'SampleController@showProfile', 'as' => 'sample']);

sample / example を入れ替えしてみました。そしてブラウザでアクセスすると

http://localhost:8080/sample/tnamao

やっぱり後に定義されたものが優先されてしまっているみたい。本格的に実装するときは別名を付けて実装していくのが、無難そう。

Controller Middleware

Controller クラスのルーティングの場合は、以下の設定で middleware が適用できる。

Route::get('profile', [
    'middleware' => 'auth',
    'uses' => 'UserController@showProfile'
]);

まぁ、これは別に対して変わらない感じ。宣言的に書くだけじゃ無く、プログラム上で設定することも出来るようで、サンプルが載っていた。

class UserController extends Controller
{
    /**
     * Instantiate a new UserController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');

        $this->middleware('log', ['only' => ['fooAction', 'barAction']]);

        $this->middleware('subscribed', ['except' => ['fooAction', 'barAction']]);
    }
}

こんな感じで、コンストラクタで定義を行うらしい。 むしろ、この log とかの引数を routes.php 上で渡す方法を知りたい…。

RESTful Resource Controllers

RESTful な Controller も artisan で作るらしい。早速作ってみる。

$ php ./artisan make:controller PhotoController
Controller created successfully.

app/Http/Controllers 配下に PhotoController.php が生成されます。routes.php に RESTful な Controller を設定する場合は、以下の様にするらしい。

Route::resource('photo', 'PhotoController');

Route::resource!! それ用の機能まで用意されているのか…。 この定義を行うとスタブに用意されたメソッドへのルーティングが自動的に適用されるらしい。暗黙的に設定されるのは、以下の内容になっているみたい。

Verb Path Action Route Name
GET /photo index photo.index
GET /photo/create create photo.create
POST /photo store photo.store
GET /photo/{photo} show photo.show
GET /photo/{photo}/edit edit photo.edit
PUT/PATCH /photo/{photo} update photo.update
DELETE /photo/{photo} destroy photo.destroy

実装予定の無い物は、ルーティングから外したり、逆に指定したものだけをルーティングしたりできる。

Route::resource('photo', 'PhotoController',
                ['only' => ['index', 'show']]);

Route::resource('photo', 'PhotoController',
                ['except' => ['create', 'store', 'update', 'destroy']]);

デフォルトのルーティング名が嫌というときは、 names を指定していくことで上書きも可能

Route::resource('photo', 'PhotoController',
                ['names' => ['create' => 'photo.build']]);

んー、がっちり楽するためのルールが決まって居るので、この RESTful Controller を使って実装するか悩ましいところだなぁ。

Nested Resources

入れ子のリソース?なんのこっちゃ。

と思ったら、RESTful を意識した URL でありがちな tnamao ユーザの 100 番目のコメントをURL上で表す時に使うものらしい。ルーティングを以下の様に設定すると入れ子という扱いになるみたいだけど、色々定義が決まっているんだなぁ。

Route::resource('photos.comments', 'PhotoCommentController');

これで定義は完了。 photos.comments と書くことで自動的に Nested となり、実際の URL は photos/{photos}/comments/{comments} という形に。

これに対応するメソッドは、以下の様な定義にします。

public function show($photoId, $commentId)

Supplementing Resource Controllers

If it becomes necessary to add additional routes to a resource controller beyond the default resource routes, ...

英語が難しかったけど、Resource Controller を登録するときに、Route::resource 以外でルーティングを登録するときは、Route::resource よりも前に Resouce::get などを定義する必要があるらしい。Route::resource を先に定義してしまうと、想定外の動きをしてしまうとのこと。

Route::get('photos/popular', 'PhotoController@method');

Route::resource('photos', 'PhotoController');

これがサンプルとして載っていた例。恐らく photos/popular は、PhotoController のルーティングと接頭辞が同じ photos/ となってしまうから、Route::resource よりも前に登録しないと Route::get のルーティングにリクエストが委譲されないってことなのかな。

Implicit Controllers

ルールに則って実装すると、暗黙的にルーティングされる Controller クラスらしい。

使わないからスルーで!

Dependency Injection & Controllers

ここで、重要なのが来た。 DI とか言葉を聞くのは何年ぶりだろう…。

意味は分かるから端折ると、Laravel の Controller は IoC コンテナで管理されていて、Controller に対して依存するクラスインスタンスを設定してくれる。Laravel がサポートしている Injection は Constructor Injection と Method Injection でどちらも type-hint で実現している。

それぞれの例を載せておくと、まず Constructor Injection は、Controller クラスのコンストラクタに定義して以下の様に定義する。

public function __construct(UserRepository $users)

続いて、Method Injection 。これも Controller クラスのメソッドとして定義する。

public function store(Request $request)

このメソッドyou may also type-hint dependencies on your controller's action methods とあったので、setter/getter 的なメソッドにはインジェクションできず、ルーティングされているメソッドが対象なんだと思う。

ちなみにルーティングしたときにパラメータを取る設定したときは以下の様な定義に変わるらしい。

Route::put('user/{id}', 'UserController@update');

ルーティングがこうだとしたら

public function update(Request $request, $id)

こうなる。

Route Caching

Controller based route だけを使用していれば、 Route Cache が使えて、ルーティングの登録が100倍速くなるかもしれないらしい!しかし Controller based route じゃないものって、 Closure route ってことなんだろうか?

キャッシュを作るのは簡単で以下のコマンドを流すだけ。

php ./artisan route:cache

ただ、route.php に変更が入っても、キャッシュが優先されてしまうため、キャッシュを再構成する必要があるらしい。開発の終わりに実行して作っておくのが良いらしい。キャッシュを消去する時は以下のコマンド

php ./artisan route:clear

恐らく git-flow を使っている場合は、 release ブランチで作っておくのが良いのだろうなぁ。でも release ブランチには作るけど、 develop ブランチには取り込まないのは手軽に出来なかった気がするなぁ…。

色々謎は多いけど、 Controller のセクションまでは終わり\(^o^)/ 次は Requests のセクションに入っていきます。