atijust's blog

技術的なこととか。

Laravel4でブログチュートリアルっぽいのをやってみた - その2

Laravel4でブログチュートリアルっぽいのをやってみた - その1では、記事の一覧と表示を実装した。今回は記事の投稿を実装する。編集、削除については実装しないことにした。編集と投稿はほとんど同じだし、削除はコードが短く特に面白いところもないので。

リファクタリング

機能を実装していく前に、テストしやすいように予めリファクタリングしておくことにする。

app/controllers/PostController.php

<?php

class PostController extends BaseController
{
    public function index()
    {
        $posts = Post::all();
        return View::make('post.index', ['posts' => $posts]);
    }

    public function show($id)
    {
        $post = Post::findOrFail($id);
        return View::make('post.show', ['post' => $post]);
    }
}

前回作成したPostControllerではPostモデルのstaticメソッドを呼び出しておりテストがやりにくい。

Postモデルをモックできるようにコンストラクタで注入するようにする。

app/controllers/PostController.php

<?php

class PostController extends BaseController
{
    protected $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    public function index()
    {
        $posts = $this->post->all();
        return View::make('post.index', ['posts' => $posts]);
    }

    public function show($id)
    {
        $post = $this->post->findOrFail($id);
        return View::make('post.show', ['post' => $post]);
    }
}

コンストラクタに渡されたPostモデルをプロパティに保存しておき、アクションではそれを使うようにした。このようにしておけば、コンストラクタにダミーのインスタンスを渡すことにより、スムーズにテストを行うことができる。

アプリケーションの実行時は、LaravelのIoCコンテナによりPostモデルが渡される。IoCコンテナはリフレクションでコンストラクタのタイプヒントを調べて、対応するクラスのインスタンスを自動的に注入してくれる。なので、Postモデルを注入するコードを自前で書く必要はない。

バリデーション

記事の投稿ではユーザから入力されたデータをバリデーションする必要がある。アクションを実装するまえにバリデーションを用意しておくことにする。

バリデーションをどこに実装するべきかについては諸説あるようだが、今回はモデルにバリデーション機能を持たせる(楽だから)。

app/models/Post.php

<?php

class Post extends Eloquent
{
    protected $fillable = ['title', 'body'];
    protected $errors;

    public function validate(array $params)
    {
        $validator = Validator::make($params, [
            'title' => 'required',
            'body'  => 'required',
        ]);

        if ($validator->passes()) {
            return true;
        } else {
            $this->errors = $validator->messages();
            return false;
        }
    }

    public function errors()
    {
        return $this->errors;
    }
}

Laravelのバリデーション機能はValidatorから利用できる。titleとbodyを必須に指定している。失敗した場合はエラーメッセージをプロパティに保存しておき、モデルのerrors()メソッドで取得できるようにしておく。

ここでは自前で実装しているが、Eloquentを拡張してモデルにバリデーション機能をつけてくれるパッケージなんかもあるので、そういうのを使うのもいいかもしれない。

記事の投稿

記事を投稿するフォームを表示するgetCreateアクションと、実際に記事をDBに保存するpostCreateアクションの2つを作成する。

app/controllers/PostController.php

<?php
    public function getCreate()
    {
        return View::make('post.create');
    }

    public function postCreate()
    {
        $attrs = Input::only(['title', 'body']);

        if (!$this->post->validate($attrs)) {
            return Redirect::action('PostController@getCreate')
                ->withErrors($this->post->errors())
                ->withInput();
        }

        $this->post->create($attrs);

        return Redirect::action('PostController@index');
    }

HTTPリクエストに付随する各種情報にはInputからアクセスできる。Inputにはかゆいところに手が届く便利メソッドが色々と用意されており、ここでは指定した名前のパラメータだけを取り出せるonly()を使っている。

バリデーションが失敗したら投稿フォームにリダイレクトする。投稿フォームにエラーメッセージと前回の入力値を表示するために、withErrors()withInput()でエラーメッセージと入力値をセッションに保存している。

記事の保存にはPostモデルのcreate()を使っている。引数に渡した配列からエンティティを作成しDBにINSERTしてくれる。create()でセットできるのはモデルの$fillableに指定したものだけなので注意。create()に限らず配列で一気にエンティティに値をセットする系のメソッドは$fillableで明示的に許可されたプロパティ以外には使えない。もし許可されてないプロパティに値をセットしようとすると例外が発生する。

投稿フォームのViewを作成する。

app/views/post/create.blade.php

<!doctype html>
<html lang="ja">
<head>
  <title>Demo Blog</title>
  <meta charset="UTF-8">
</head>
<body>
<h1>記事投稿</h1>
@foreach ($errors->all() as $error)
  <p>{{{ $error }}}</p>
@endforeach
<form action="{{ action('PostController@postCreate') }}" method="post">
  {{ Form::token() }}
  <input type="text" name="title" value="{{{ Input::old('title') }}}" placeholder="タイトル"><br>
  <textarea name="body" rows="3">{{{ Input::old('body') }}}</textarea><br>
  <input type="submit" value="投稿">
</form>
</body>
</html>

アクションのリダイレクト処理においてwithErrors()でセッションに保存したエラーメッセージはViewでは$errorsで参照できる。 withInput()で保存した前回のユーザ入力はInput::old()で取得できる。

{{ Form::token() }}でフォームの隠しパラメータにCSRF対策のためのトークンをセットしている。このトークンは後述のルートフィルタでチェックする。

最後にルーティングを定義する。

app/routes.php

<?php
Route::get('/create', 'PostController@getCreate');
Route::post('/create', ['before' => 'csrf', 'uses' => 'PostController@postCreate']);

POSTの/createcsrfルートフィルタを設定している。csrfはLaravelに元から用意されているフィルタで、トークンをチェックしてくれる。

以上で記事の投稿機能は完成。組み込みサーバを立ち上げ、http://localhost:8000/createにアクセスすれば、投稿フォームが表示される。

最後に

取り敢えず、機能としてはこれで完成。Laravelにも多少馴れてきたような気がする。 テストについてはエントリをあらためて書きたいと思う。