こんにちは、山本です。

前回やった、「Laravel」チュートリアルの続きです。

レイアウトとビューの作成

まず、説明ですが

  • .blade.php拡張子はビューを表示するときにBladeテンプレートエンジンを使用する。
  • BladeテンプレートエンジンはLaravel同時のエンジンのもよう。

とのことです。もっとも、既にログイン周りでテンプレートを扱っているんですが。

ビュー

レイアウトは既に対応済みなので、タスクのテンプレートを作成します。

// resources/views/tasks/index.blade.php

@extends('layouts.app')

@section('content')

    <!-- Bootstrapの定形コード… -->

    <div class="panel-body">
        <!-- バリデーションエラーの表示 -->
        @include('common.errors')

        <!-- 新タスクフォーム -->
        <form action="/task" method="POST" class="form-horizontal">
            {{ csrf_field() }}

            <!-- タスク名 -->
            <div class="form-group">
                <label for="task" class="col-sm-3 control-label">Task</label>

                <div class="col-sm-6">
                    <input type="text" name="name" id="task-name" class="form-control">
                </div>
            </div>

            <!-- タスク追加ボタン -->
            <div class="form-group">
                <div class="col-sm-offset-3 col-sm-6">
                    <button type="submit" class="btn btn-default">
                        <i class="fa fa-plus"></i> タスク追加
                    </button>
                </div>
            </div>
        </form>
    </div>

    <!-- TODO: 現在のタスク -->
@endsection
public function index(Request $request)
{
    return view('tasks.index');
}

テンプレートの中身を見る限り、先にレイアウトが読み込まれるのではなく、viewで指定したテンプレートが読み込まれ、そのテンプレートからレイアウトが読み込まれるようです。

view での指定は、「.」で階層を指定しているようでした。
これ、毎回テンプレートを手動で指定する必要があるんですかね?

バリデーション

nameが必須で、255文字以下である
バリデーションを行うようです。

public function store(Request $request)
{
    $this->validate($request, [
        'name' => 'required|max:255',
    ]);
}

そして、エラーテンプレートを作成します。

// resources/views/common/errors.blade.php

@if (count($errors) > 0)
    <!-- フォームのエラーリスト -->
    <div class="alert alert-danger">
        <strong>おや?何かがおかしいようです!</strong>

        <br><br>

        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

エラーが出るようにタスクを追加すると

002

バリデーションが正常に動作しています。

あと、なぜリダイレクトされているのか疑問だったのですが

バリデーションが失敗したかとか、リダイレクトとかを自分で行う必要さえありません。指定したルールのバリデーションに失敗したら、ユーザーを自動的に直前のページヘリダイレクトし、エラーも自動的にセッションへフラッシュデーターとして保存されます!

とあり、そこは自動で行ってくれるようです。

また、使用できるバリデーションルール色々と柔軟な印象です。
バリデーションルール

以下のような、依存関係のある項目の内容によって必須の制御を行うことも出来るようでした。

required_if
required_unless
required_with
required_with_all

それ以外にも拡張がやりやすそうな作りになっていました。
もっとも、コントローラーに直接バリデーションを書くのではなく、分離したいところですが。

タスク作成

タスクを登録できるようにします。

public function store(Request $request)
{
    $this->validate($request, [
        'name' => 'required|max:255',
    ]);

    $request->user()->tasks()->create([
        'name' => $request->name,
    ]);

    return redirect('/tasks');
}
mysql> select * from tasks;
+----+---------+------+---------------------+---------------------+
| id | user_id | name | created_at          | updated_at          |
+----+---------+------+---------------------+---------------------+
|  1 |       1 | test | 2016-04-27 05:40:58 | 2016-04-27 05:40:58 |
+----+---------+------+---------------------+---------------------+

これで作れるようになったんですが、作成のところがちょっと良くわからないですね。

tasksからcreateなので、save無しで保存が出来るということですかね。

よく分からないので、モデルのドキュメントを読んでみました。

どうも、これが前に出てきた複数代入というもののようです。
以下のように、一つづつではなく一度に保存までを行うもののようでした。

      $task          = new Task;
      $task->name    = 'name';
      $task->user_id = $request->user()->id;
      $task->save();

他にも、firstOrCreate、firstOrNewというのもありました。

なんでも、引数の値のレコードがあれば、そのモデルを返し、無ければそのモデルを生成して返すものらしいです。
CreateかNewの違いは、実際にレコードを作るかの違いらしいのですが、、、使いどころがよく分からないですね。

既存タスク表示

今度は登録したデータの一覧表示です。

public function index(Request $request)
{
    $tasks = Task::where('user_id', $request->user()->id)->get();

    return view('tasks.index', [
        'tasks' => $tasks,
    ]);
}

これで、タスクの一覧をテンプレートに渡すんのだと思うのですが、なんでwhereで値を取ってきているんでしょう?
前に、$request->user()->tasks()がありましたが、これで取るのではないんですかね?

$request->user()->tasks()->getResults();

一応、これでwhereの結果と同じ値が取れるようですが。。。

依存注入

リポジトリーの作成

説明した通り、Taskモデルとの全アクセスロジックを持つ、TaskRepositoryを定義しましょう
とのことです。

まず、appにRepositoriesディレクトリを作りTaskRepositoryを作成します。

LaravelのappフォルダーはPSR-4オートロード規約に従いオートロードされますので、必要に応じていくらでも追加のフォルダを作成できます。
らしいので、オートロードしたいファイルは全てappの下に設置すればいいわけですね。

// vi app/Repositories/TaskRepository.php
namespace App\Repositories;

use App\User;
use App\Task;

class TaskRepository
{
    /**
     * 指定ユーザーの全タスク取得
     *
     * @param  User  $user
     * @return Collection
     */
    public function forUser(User $user)
    {
        return Task::where('user_id', $user->id)
                    ->orderBy('created_at', 'asc')
                    ->get();
    }
}
//vi app/Http/Controllers/TaskController.php
    protected $tasks;

    public function __construct(TaskRepository $tasks)
    {
        $this->middleware('auth');

        $this->tasks = $tasks;
    }

    public function index(Request $request)
    {
        return view('tasks.index', [
            'tasks' => $this->tasks->forUser($request->user()),
        ]);
    }

リポジトリを作って、依存性を注入します。

@extends('layouts.app')

@section('content')

    <!-- Bootstrapの定形コード… -->

    <div class="panel-body">
        <!-- バリデーションエラーの表示 -->
        @include('common.errors')

        <!-- 新タスクフォーム -->
        <form action="/task" method="POST" class="form-horizontal">
            {{ csrf_field() }}

            <!-- タスク名 -->
            <div class="form-group">
                <label for="task" class="col-sm-3 control-label">Task</label>

                <div class="col-sm-6">
                    <input type="text" name="name" id="task-name" class="form-control">
                </div>
            </div>

            <!-- タスク追加ボタン -->
            <div class="form-group">
                <div class="col-sm-offset-3 col-sm-6">
                    <button type="submit" class="btn btn-default">
                        <i class="fa fa-plus"></i> タスク追加
                    </button>
                </div>
            </div>
        </form>
    </div>

    <!-- 現在のタスク -->
    @if (count($tasks) > 0)
        <div class="panel panel-default">
            <div class="panel-heading">
                現在のタスク
            </div>

            <div class="panel-body">
                <table class="table table-striped task-table">

                    <!-- テーブルヘッダー -->
                    <thead>
                        <th>Task</th>
                        <th>&nbsp;</th>
                    </thead>

                    <!-- テーブルボディー -->
                    <tbody>
                        @foreach ($tasks as $task)
                            <tr>
                                <!-- タスク名 -->
                                <td class="table-text">
                                    <div>{{ $task->name }}</div>
                                </td>

                                <td>
                                    <!-- TODO: 削除ボタン -->
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            </div>
        </div>
    @endif
@endsection

テンプレート調整行い、一覧が表示されるようになりました。

003

タスク削除

削除ボタンの追加

<tr>
    <!-- Task Name -->
    <td class="table-text">
        <div>{{ $task->name }}</div>
    </td>

    <!-- Delete Button -->
    <td>
        <form action="/task/{{ $task->id }}" method="POST">
            {{ csrf_field() }}
            {{ method_field('DELETE') }}

            <button>Delete Task</button>
        </form>
    </td>
</tr>

で、削除ボタンが追加されます。

method_fieldは、引数でmethodをオーバーライドするようです。

ルートモデル結合

Route::delete('/task/{task}', 'TaskController@destroy');
public function destroy(Request $request, $taskId)
{
    echo $taskId; //{task}
}

なんでも、今のルーティング定義では{task}の値しかこないので、拡張してtaskモデルが渡るようにというのが目的のようです。

その前に、疑問なんですが

Route::delete('/task/{task1}/{task2}', 'TaskController@destroy');

とルーティングがあり「/task/1/2」にDELETEで来た場合

public function destroy(Request $request, $task2, $task1)
{
    echo $task1;
    echo $task2;
}

$task1と$task2の値はどうなるのか?

結果は

$task1 //2
$task2 //1

となるようです。
なんか、嫌な挙動ですね。

なんとか、順番に影響しないように方法がないものかドキュメント見てみましょう。

Route::delete('/task/{task1}/{task2}', 'TaskController@destroy')
  ->where(['task1' => '[0-9]+', 'task2' => '[\d]+'])
  ->name('task_delete');

とりあえず、パラメータを正規表現で縛る方法は見つかりました。
また、ルーティングに名前をつける方法もありました。

これで、URLが変わっても影響しないように出来ますね。

route('task_delete', ['task1' => 1, 'task2' => 2]);

と、他の機能は見つかるんですが順番については変えられないようです。
これは、URLを直書きではなくrouteを使えってことですかね。

さて、ルーティングを戻してモデル結合を進めます。

bootメソッドにモデルの指定を追加して

//vi app/Providers/RouteServiceProvider.php
$router->model('task', 'App\Task');
    public function destroy(Request $request, Task $task)

とすると、タスクモデルが渡るようです。

因みに、今回はプライマリーキーなんでいいんですが、それ以外のキーで結合したい場合もあると思います。

$router->bind('task', function($value) {
    return App\Task::where('name', $value)->first();
});

このようにすると、別のカラム、今回の例ではnameで結合されることが出来るようです。

認可

これでTaskインスタンスはdestroyメソッドへ注入できました。
しかし、認証済みのユーザーが指定したタスクを「所有」している保証はありません。
たとえば他のユーザーのタスクを削除するために/tasks/{task}のURLへランダムなタスクIDを渡すことで、悪意のあるリクエストを仕込むことが可能です。
そこでルートに注入されたTaskインスタンスが実際に認証済みユーザーが所有していることを確認するため、Laravelの認可機能を使う必要があります。

認可とは何だ?と思いましたが、自分のタスク以外を消せないように制限をしようってことですね。
アクション内で、手動でやるものかとも思っていましたが、Laravelでは「ポリシー」を使って行うとのことです。

まず、ポリシーをartisanで作成します。

php artisan make:policy TaskPolicy

生成されたポリシーに、処理を追加します。

// vi app/Policies/TaskPolicy.php

namespace App\Policies;

use App\User;
use App\Task;
use Illuminate\Auth\Access\HandlesAuthorization;

class TaskPolicy
{
    use HandlesAuthorization;

    public function destroy(User $user, Task $task)
    {
        return $user->id === $task->user_id;
    }
}

で、モデルとポリシーを関連付けます。

//vi app/Providers/AuthServiceProvider.php

protected $policies = [
    Task::class => TaskPolicy::class,
];

そして、削除すると

004

またか!

えーと、見落としが無いか再度確認しましたが、原因が分かりません。
なので、前回同様フォーラムをみてみましょう。

and add this uses on top of AuthServiceProvider.php:

use App\Policies\TaskPolicy;
use App\Task;

AuthServiceProviderに足せとのことです。

足した後、ついでにルーティングにも名前を付けて、実際の削除処理を追加します。

Route::get('/tasks', 'TaskController@index')->name('task');
public function destroy(Request $request, Task $task)
{
    $this->authorize('destroy', $task);

    $task->delete();

    return redirect(route('task'));
}

これでようやく消せるようになり、一連の動きが出来るようになりました。

まとめ

思いの外長くなりましたが、チュートリアルをやってみました。
幾つか指示が足りなかったり、バージョンの問題で動かなかったりとはありましたが、概ね分かりやすいチュートリアルだったかなと思います。

で、やってみた感想ですが比較的学習コストが低いのではないかと思います。

手軽な感じで、必要な機能はある程度あり、拡張性も高い印象です。
また、関連付けも手間ではありますが手動でつけるものが多く、なぜ動くのか分からないということも少なくドキュメントもそれなりに揃っていて、流行っている理由が何となく分かる気がしました。

実際に仕事でやらないと分からないこともあると思うので、今度何かの案件で使ってみようかと思いました。