アライドアーキテクツ Advent Calendar 17日目 兼
Laravel Advent Calendar 2014 17日目
の記事です
ごあいさつ
Laravel AdventCalendarの総支配人のytakeさんにQiitaのアドベントカレンダーと会社のアドベントカレンダーを一緒くたにして書いてもいいっすか?って聞いたら快諾してくれたので二つのカレンダーを兼ねるというセコいスタイルでいきます。石川さんです。どうぞよろしくお願いします。
まず、この記事は12/09時点のcommitをベースとして書いています。Laravel5は現在開発中であり記事の内容と違う部分が出てくると思います。(ちなみに、これの記事を書くために前から暖めていたサンプルアプリケーションがあったのですが、12/09時点でだいぶ内容変更が行われたために全てゼロから書き直しました。泣ける話ですね。)なお、新機能っぽいところには「★」をつけてますので、新機能だけでいいんだよという方はその辺だけかっさらっていってください。
前回の記事でもLaravel5の新機能の一つであるLaravel Elixirについて触れましたが、今回は実際にサンプルアプリケーションを作ってみつつ、色々追加された新機能について見ていきたいと思います。ちなみに今回作るサンプルアプリケーションはTodoアプリです。太古の昔から、サンプルアプリケーションといえばTodoアプリ、サンプルデータベースといえば社員表と相場が決まっているのです。
事前準備
環境構築
前回の記事を参照で。
Database
mysqlにlaravel5という名前でDatabaseを作っておきます。
Databaseを作ったらプロジェクトルートでphp artisan migrate
として、テーブルの準備をします。
ディレクトリ構成の確認
Laravel4に比べてLaravel5では大きくディレクトリ構成が異なっています。Laravel5はpsr-4に準拠した構造になり、namespaceを使用する様になっています。まぁこの辺は他の記事でも色々書いてあるので割愛します。実際にインストールしてみてください。
★Environment Detection & Environment Variables
Larvel5では環境に依存する情報はプロジェクトルート配下の.env
ファイルで管理する様になっています。
とりあえずインストール時に一緒についてくる.env.example
をリネームしましょう。
.env
1 2 3 4 5 6 7 8 9 10 11 |
APP_ENV=local APP_DEBUG=true APP_KEY=SomeRandomString DB_HOST=localhost DB_DATABASE=laravel5 DB_USERNAME=homestead DB_PASSWORD=secret CACHE_DRIVER=file SESSION_DRIVER=file |
configからこの値を参照する様に書き換えていきましょう。とりあえずDatabase部分だけ紹介します。
resources/config/database.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... 'connections' => [ 'mysql' => [ 'driver' => 'mysql', 'host' => getenv('DB_HOST'), // localhost 'database' => getenv('DB_DATABASE'), // laravel5 'username' => getenv('DB_USERNAME'), // homestead 'password' => getenv('DB_PASSWORD'), // secret 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', ], ... |
phpdotenvというライブラリを使っています。プロジェクトのどこからでもgetenvメソッドで.envファイルに指定した値を取り出す事が出来ます。上記を参考にして他の箇所も書き換えていきましょう。
とりあえずアプリ画面を開いてみる
Laravel4に比べてずいぶんとスタイリッシュな画面がありがたいお言葉と共に表示されました。ロードする度に変化するこのありがたいお言葉はIlluminate\Foundation\Inspiring
クラスで実装されています。中身を覗いてこれから一緒に”Smile, breathe, and go slowly”していきましょう。
あ、書き忘れてました。今回サンプルアプリは一旦開発環境に限定しますので、optimizeの結果は潰しておきましょう。
1 2 |
$ cd path/to/projectroot $ php artisan clear-compiled |
ユーザ認証まわり
★Spiffy Authentication
Laravel5にはデフォルトでメールアドレスとパスワードによるユーザ認証の仕組みがついています。もちろん自分で実装しても大丈夫ですよ!ちょっと中身を覗いてみます。
app/Http/Controllers/Auth/AuthController.php
1 2 3 4 5 6 7 8 9 |
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers; class AuthController extends Controller { use AuthenticatesAndRegistersUsers; } |
少し前まではここにごちゃごちゃとコントローラの実装が書いてあったんですが、traitを使う形に変更されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?php namespace Illuminate\Foundation\Auth; use Illuminate\Http\Request; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Registrar; trait AuthenticatesAndRegistersUsers { protected $auth; protected $registrar; public function __construct(Guard $auth, Registrar $registrar) { $this->auth = $auth; $this->registrar = $registrar; $this->middleware('guest', ['except' => 'getLogout']); } public function getRegister() { return view('auth.register'); } ... } |
コンストラクタインジェクションを使ってGuardとRegistrarが依存注入される仕組みになっています。早速Laravel5のContractsが使われていますね。
★Contracts
GuardにはフレームワークのデフォルトですでにIlluminate\Auth\Guard
が関係付けられています。
Registrarは、validatorメソッドとcreateメソッドを持ったinterfaceです。Concrete ClassもApp\Services\Registrar
に用意されており、App\Providers\AppServiceProvider
で以下の様に紐付けられています。
1 2 3 4 5 6 7 8 |
// App\Providers\AppServiceProvider public function register() { $this->app->bind( 'Illuminate\Contracts\Auth\Registrar', 'App\Services\Registrar' ); } |
従って、AuthControllerのコンストラクタで$authにはIlluminate\Auth\Guard
が、$registrarにはApp\Services\Registrar
がそれぞれ注入されます。
今回はこのままの形でいきますが、ユーザー認証について部分変更したい場合は、AuthControllerで必要なメソッドをオーバーライドし、必要なデータの登録方法をRegistrarクラスで実装してあげれば良いわけですね。
Viewの作成
デフォルトではViewは用意されないのですが、ここについての詳細は主題と逸れるため今回は内容を省きます。viewについてはresources/templates
配下に作ることになっていますので、AuthControllerの期待するviewを追加していけば良いです。こんな感じで。
良い感じに適当になってきました(笑)
Todoリストの実装をする
Modelを作る
ここは大してnewが無いのでさらっと。とりあえずテーブル構成は以下の様にしておき、Todoモデルをapp
に配置します。
todosテーブル
field | type | null | key | default | extra |
---|---|---|---|---|---|
id | int | NO | PRI | NULL | auto_increment |
title | varchar | NO | NULL | ||
limit | datetime | NO | NULL | ||
done | tinyint | NO | NULL | ||
user_id | tinyint | NO | MUL | NULL | |
created_at | timestamp | NO | 0000-00-00 00:00:00 | ||
updated_at | timestamp | NO | 0000-00-00 00:00:00 |
app/Todo.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Todo extends Model { protected $table = 'todos'; protected $fillable = ['title', 'limit', 'done', 'user_id']; protected $dates = ['limit']; public function user() { return $this->belongsTo('App\User'); } } |
Controllerを作る
必要そうなメソッドを考える
こんな感じの画面を作るので以下のメソッドを用意することにします。
- index => 一覧画面表示処理
- create => 新規作成画面表示処理
- store => 新規作成処理
- edit => 更新画面表示処理
- update => 更新処理
- delete => 削除処理
- check => TODOをチェックする処理
TodoController
Controllerを作ります。ここから新機能を多く使っていきます。
app/Http/Controllers/TodoController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php namespace App\Http\Controllers; class TodoController extends Controller { public function index() { } public function create() { } public function store() { } public function edit($id) { } public function update($id) { } public function delete($id) { } public function check($id) { } } |
リソースコントローラに毛が生えた様なもんですが、とりあえずアクセスパスはこんな感じで良いでしょう。
★Route Annotations
Laravel5はRoutingをAnnotationで指定する事が出来ます。詳細はytakeさんの記事で詳しく解説してくれています。
やってみよう
「TodoControllerに関するルーティングをAnnotationで指定するよ、スキャンしてね」って事をApp\Providers\RouteServiceProvider
に書きます。
1 2 3 4 5 6 7 8 9 |
class RouteServiceProvider extends ServiceProvider { protected $scan = [ 'App\Http\Controllers\TodoController', // php artisan route:scanで実行するコントローラーを指定 ]; protected $scanWhenLocal = true; // ローカル環境の場合はアクセスの度にroute:scanするよっていう設定 ... |
それではTodoControllerにRoute Annotationを書いていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
/** * @Controller(prefix="todo") * @Middleware("auth") */ class TodoController extends Controller { /** * @Get("/", as="todo.index") */ public function index() { } /** * @Get("create", as="todo.create") */ public function create() { } /** * @Post("store", as="todo.store") */ public function store() { } /** * @Get("{id}/edit", as="todo.check") */ public function edit($id) { } /** * @Post("{id}/update", as="todo.update") */ public function update($id) { } /** * @Get("{id}/delete", as="todo.delete") */ public function delete($id) { } /** * @Get("{id}/check", as="todo.check") */ public function check($id) { } } |
ここまで書いたら一度ルーティングを確認します。
1 2 |
$ php artisan route:scan $ php artisan route:list |
かいつまんで解説
クラスに指定したAnnotation
@Controller(prefix="todo")
アクセスパスのprefixにtodoをつけます。
@Middleware("auth")
Laravel4でいうFilterです。authミドルウェアが適用されます。
メソッドに指定したAnnotation
@Get("{id}/edit", as="todo.edit")
GETメソッドの指定です。Controllerアノテーションでtodoプレフィックスを指定しているので、実際のurlは”todo/11/edit”の様になります。
ここで{id}と指定したルートパラメータはメソッドの引数に指定した変数$idに渡されます。(上の例でいうと11が渡されます)
@Post("{id}/update", as="todo.update")
POSTメソッドの指定です。urlは”todo/12/update”の様になります。id部分についてはGetと同様です。
実装する
アクセスパスの準備が終わったので、中身を実装していきます。
ログイン後に使う事が前提なので、Guardをメンバーに定義することにします。
Todoを扱うコントローラーですから、Todoも定義します。
1 2 3 4 5 6 7 8 9 10 |
// TodoController protected $auth; protected $todos; public function __construct(Guard $auth, TodoRepositoryInterface $todos) { $this->auth = $auth; $this->todos = $todos; } |
説明を省きましたが、疎結合にする為にModelを直接使用せず、いわゆるリポジトリパターンの形にしています。eloquentをラップしてそこにちょろちょろと処理を生やしたものです。(機会があれば、別記事で書こうと思います)各リポジトリはinterfaceを用いているため、Concrete Classとの紐付けはAppServiceProviderで行っています。
では、indexの実装をしていきます。
★Method Injection
indexではUserに紐づくTodo一覧を表示します。ページングがしたいので、ペジネーションオブジェクトを取得することにします。この処理はUserRepositoryクラスに実装しました。
1 2 3 4 5 6 7 8 |
// App\Repositories\Eloquent\UserRepository public function getTodosPaginate($id, $perPage = 10){ return $this->find($id) ->todos() ->orderBy('limit', 'ASC') ->paginate($perPage); } |
TodoControllerはUserRepositoryをコンストラクタ引数で受けていません。indexメソッド以外ではUserRepositoryを使う機会が無いからです。Laravel4の場合、この様なケースではコンストラクタで受ける様にするか、App::make('App\Repositories\UserRepositoryInterface')
の様にするしかありませんでしたが、Laravel5ではControllerにおけるメソッドインジェクションをサポートした為、以下の様な記述が可能です。
1 2 3 4 5 6 7 8 9 10 |
// TodoController /** * @Get("/", as="todo.index") */ public function index(UserRepositoryInterface $users) { $list = $users->getTodosPaginate($this->auth->user()->id); return view('todo.index')->with(compact('list')); } |
記述がとてもシンプルになります。また、単体テストをする時にもAppにmockオブジェクトをバインドせず、単純に引数に渡せば良いだけになるので良いですね。
ちょっと脇道に逸れてみる
indexメソッドの実装は上記でおしまいですが、viewについては触れる気はなかったのですが、Laravel5になって若干bladeの仕様が変更されていたので、ついでに紹介しておきます。
★bladeで{{$hoge}}
がサニタイジングされる様になった
以前までは{{ $hoge }}
は直接出力、{{{ $hoge }}}
はHTMLエスケープを行う仕様だったはずですが、{{ $hoge }}
もエスケープされる様になりました。代わりに直接出力する場合には{!! $hoge !!}
となります。
これ、あんま語られてないけど地味にキツい変更じゃないかと思ってますw
まぁ確かにごっちゃになることがあったしわかりやすくはなったけど、下位バージョンを使っている人たちでバージョンアップする際の障壁になりそうです。「エスケープを気にしなくて良いのは小学生までだよねー」という意識が徹底されたプロダクトにおいては単純に置換すりゃいいんでしょうけども。
★FormRequest
本題に戻ります。storeメソッド(新規作成処理)はformから値を受け付けて、Todoを保存する処理を担います。ここではValidatorとRequestが一緒くたになった新機能のFormRequestを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// TodoController /** * @Post("store", as="todo.store") */ public function store(StoreRequest $request) { $data = [ 'title' => $request->get('title'), 'limit' => $request->get('limit'), 'done' => false, 'user_id' => $this->auth->user()->id ]; $this->todos->create($data); return redirect(route('todo.index')); } |
引数で指定しているStoreRequestが今回追加されたFormRequestです。どんな実装になっているかというと
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php namespace App\Http\Requests\Todo; use Illuminate\Foundation\Http\FormRequest; class StoreRequest extends FormRequest { public function rules() { return [ 'title' => 'required', 'limit' => 'required|date_format:"Y-m-d"', ]; } public function authorize() { return true; } } |
特に説明しなくても雰囲気が掴めると思いますが、バリデートルールの設定をしています。storeメソッドが呼び出されると、このStoreRequestがフィルタの役割を果たします。自動でリクエスト項目にバリデートを行い、エラーが無ければそのままstoreメソッドを呼び出し、エラーがあった場合にはフラッシュデータにメッセージをセットして呼び出し元のurlにリダイレクトします。(リダイレクト先については$redirect
もしくは$redirectRoute
、$redirectAction
で指定できます)
ちなみにauthorizeメソッドについては、ここでfalseを返すと403ページへ遷移する様になっている(デフォルトの挙動はfalseなのでオーバーライドする必要がある)のですが、イマイチ活用方法が見出せませんでした。。。誰か知っていたら教えてください。
POSTの場合にはほとんどのケースでバリデートをすることになるので、FormRequestでバリデートするのは良いアイデアだと思います。また、副次的な効果としてコントローラとバリデーターを切り離す事が出来る為、テストも容易になります。
Controller最終形
あらかた新機能に触れました。最終的にコントローラーはこんな感じになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
/** * Class TodoController * * @Controller(prefix="todo") * @Middleware("auth") * @Middleware("todo.owner", only={"check", "edit", "update", "delete"}) * @package App\Http\Controllers */ class TodoController extends Controller { /** * @var Guard */ protected $auth; /** * @var TodoRepositoryInterface */ protected $todos; /** * @param Guard $auth * @param TodoRepositoryInterface $todos */ public function __construct(Guard $auth, TodoRepositoryInterface $todos) { $this->auth = $auth; $this->todos = $todos; } /** * Todo一覧画面を表示する * * @Get("/", as="todo.index") * @param UserRepositoryInterface $users * @return Response */ public function index(UserRepositoryInterface $users) { $list = $users->getTodosPaginate($this->auth->user()->id, 5); return view('todo.index')->with(compact('list')); } /** * Todo作成画面を表示する * * @Get("create", as="todo.create") * @return Response */ public function create() { return view('todo.create'); } /** * 新しく作成したTodoを保存する * * @Post("store", as="todo.store") * @param StoreRequest $request * @return Response */ public function store(StoreRequest $request) { $data = [ 'title' => $request->get('title'), 'limit' => $request->get('limit'), 'done' => false, 'user_id' => $this->auth->user()->id ]; $this->todos->create($data); return redirect(route('todo.index')); } /** * Todo編集画面を表示する * * @Get("{id}/edit", as="todo.edit") * @param int $id * @return Response */ public function edit($id) { return view('todo.edit')->with('todo', $this->todos->find($id)); } /** * Todoを更新する * * @Post("{id}/update", as="todo.update") * @param int $id * @param UpdateRequest $request * @return Response */ public function update($id, UpdateRequest $request) { $this->todos->update($id, $request->all()); return redirect(route('todo.index')); } /** * Todoを削除する * * @Get("{id}/delete", as="todo.delete") * @param int $id * @return Response */ public function delete($id) { $this->todos->destroy($id); return redirect(route('todo.index')); } /** * Todoをチェックする * * @Get("{id}/check", as="todo.check") * @param $id * @return Response */ public function check($id){ $this->todos->toggleDone($id); return redirect(route('todo.index')); } } |
Done!非常にシンプルにコントローラを書く事が出来ました。Getメソッドでdeleteしてんじゃねーよ!という言葉が聞こえてきそうですが、一旦聞こえなかったフリをすることにします(^▽^)
★Middleware
さて、TodoControllerのRoute Annotationに@Middleware("todo.owner", only={"check", "edit", "update", "delete"})
をシレっと追加しています。
MiddlewareはLaravel4でいうところのfilterに代わるものです。todo.ownerという名前の通り、対象となるTodoレコードが認証を通過したユーザーの所有物であるかどうかをチェックします。
★New File Generators
僕はあまり使わないのですが、せっかくなので新たに追加された雛形作成機能を使ってみます。
1 |
$ php artisan make:middleware TodoOwner |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Contracts\Routing\Middleware; class TodoOwner implements Middleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { // } } |
こんな感じの雛形が生成されました。あとはこのhandleメソッドを実装していけば良いだけです。説明するよりもコードを見てもらった方が速いと思うので、以下をご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<?php namespace App\Http\Middleware; use App; use App\Repositories\TodoRepositoryInterface; use Closure; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Routing\Middleware; class TodoOwner implements Middleware { /** * @var Guard */ protected $auth; /** * @var TodoRepositoryInterface */ protected $todos; /** * @param Guard $auth * @param TodoRepositoryInterface $todos */ public function __construct(Guard $auth, TodoRepositoryInterface $todos) { $this->auth = $auth; $this->todos = $todos; } /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $id = $request->route()->getParameter('id'); $userId = $this->auth->user()->id; if (!$this->todos->isTodoOwnedByUser($id, $userId)) { App::abort(); } return $next($request); } } |
handleメソッド内でTodoレコードの所有者IDと認証済ユーザーのIDが一致するかを判定しています。問題が無ければ、クロージャを呼び出して処理を終了します。
実装完了
なんとか実装が完了しました!良い感じに新機能に触れながらサンプルアプリケーションを作ることが出来ました。開発版といえど、Core部分はほとんど固まっている感じですので、特に大きな問題も無く開発することが出来ました。
おわりに
前回の記事でLaravel Elixirにも触れましたので、あとはMultiple Filesystems(Webストレージ)、Socialite(ソーシャルログイン)、Event Annotationに触れられれば一通りLaravel5の新機能巡り完了なんですけど、まぁそのへんは他の方に任せるとしましょう(オイ
いやーLaravel5、楽しみですね!リリースされたらまたいじくり倒してやろうと思います!!長文お読みいただき、ありがとうございました!
さてさて、明日のアライドアーキテクツ Advent Calendarは弊社新進気鋭のandroidエンジニア、ちっくりーんの番で、Laravel Advent Calendar 2014はmonaye@githubさんの番です!お楽しみに!
アライドアーキテクツでVPoPをしています。おもにダイエットに関する話を書きます。たまにサービス開発において大事だと思っていることを書いたりします。