Laravel5 チュートリアル ブログもどきを作る(10) フロントカテゴリー別表示

[前回](https://qiita.com/yumgoo17/items/9376bf1c851c4c003253)は、管理画面からカテゴリーを追加・編集、削除する機能を実装したので、今回はフロント側でブログ記事をカテゴリー別に表示する機能を実装します。このチュートリアルは、今回で最終回となります。



## パラメータ
ルーティングは特に追加せず、パラメータとして、cateogry_id パラメータを追加することにします。これまで登場したパラメータと合わせて、フロント側の表示で利用するパラメータは下記の通りとなります。

| パラメータ名 	| 意味   	| 備考                     	|
|--------------	|--------	|--------------------------	|
| year        	| 対象年 	| 対象年月を指定するときは必須 |
| month       	| 対象月 	| 任意のパラメータとする   	|
| category_id   | 対象カテゴリー | カテゴリーによる絞り込みを行うときは必須   	|

例えば、
`http://{ホスト名}/?year=2018&month=7&category_id=3` なら2018年7月の記事で、`category_id = 3` に属する記事を表示する、という意味になります。



## モデルの編集
### リレーションの追加
リレーションとは、各モデル（テーブル）間の関係を定義するものです。
今回は、記事から見ると設定できるカテゴリーは1つのみ、カテゴリーから見ると、あるカテゴリーに属する記事は多数あるので、記事とカテゴリーの関係は一対多の関係になります。この関係を定義するのがリレーションです。

`app/Models/Article.php` を開いて下記のメソッドを追加します。

```app/Models/Article.php
    /**
     * Category モデルのリレーション
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function category()
    {
        // 記事は1つのカテゴリーと関係しているので、hasOne メソッドを利用する
        // 第一引数は関係するモデルの名前で、第二・第三引数は外部キーです
        return $this->hasOne('App\Models\Category', 'category_id', 'category_id');
    }
```

Eloquent は、自分自身のモデル名のスネークケース（上記の場合は`article`）に、`_id` サフィックスを付けた名前（上記の場合は`article_id`）が、第一引数に指定したモデル（上記の場合は Category モデル）に対する外部キーであると、デフォルトで判断します。
しかしながら今回、Category モデルに `article_id` は持っていないので、設定をする必要があります。この場合、hasOne() メソッドの第二引数に `category_id` と指定してやると、Category モデルに対する外部キーは `category_id` であると設定することができます。

また、Eloquent は、モデルの id カラム（もしくは `$primaryKey` に設定したカラム）と一致する外部キーの値を持っていると仮定します。つまり、今回の場合だと、Article モデルの `$primaryKey` に設定した `article_id` の値を、Category レコードの `article_id` カラムに存在しないかどうかを探します。しかしながら、上述した通り Category レコードには `article_id` を持っていません。そこで、hasOne() メソッドの第三引数に `category_id` を設定してやると、`category_id` の値で、Category レコードの `category_id` カラムを探してくれるようになります。

リレーションを定義しておくと、下記のようにプロパティにアクセスするだけで、関係したレコードを取得することができるようになり非常に便利です。

```
$category = Article::find(1)->category;
```

続いて `app/Models/Category.php` を開いて、先ほどと逆のリレーションを定義します。

```app/Models/Category.php
    /**
     * Article モデルのリレーション
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function articles()
    {
        // 1つのカテゴリーは多くの記事と関係しているので hasMany メソッドを利用する
        return $this->hasMany('App\Models\Article', 'category_id', 'category_id');
    }
```

今度は、一つのカテゴリーには多くの記事が属しているので、hasMany() メソッドを使って、リレーションを定義しています。引数は hasOne() メソッドと同じです。
詳細や、他のリレーションについては、日本語ドキュメントの[リレーション](https://readouble.com/laravel/5.5/ja/eloquent-relationships.html)を参照してください。



### getArticleList() の編集
`app/Models/Article.php` の getArticleList() メソッドを下記のように編集します。

```app/Models/Article.php
    /**
     * 記事リストを取得する
     *
     * @param  int   $num_per_page 1ページ当たりの表示件数
     * @param  array $condition    検索条件
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
     */
    public function getArticleList(int $num_per_page = 10, array $condition = [])
    {
        // パラメータの取得
        $category_id = array_get($condition, 'category_id');
        $year        = array_get($condition, 'year');
        $month       = array_get($condition, 'month');

		// Eager ロードの設定を追加
        $query = $this->with('category')->orderBy('article_id', 'desc');

        // カテゴリーIDの指定
        if ($category_id) {
            $query->where('category_id', $category_id);
        }

        // 期間の指定
        if ($year) {
            if ($month) {
                // 月の指定がある場合はその月を設定し、Carbonインスタンスを生成
                $start_date = Carbon::createFromDate($year, $month, 1);
                $end_date   = Carbon::createFromDate($year, $month, 1)->addMonth();     // 1ヶ月後
            } else {
                // 月の指定が無い場合は1月に設定し、Carbonインスタンスを生成
                $start_date = Carbon::createFromDate($year, 1, 1);
                $end_date   = Carbon::createFromDate($year, 1, 1)->addYear();           // 1年後
            }
            // Where句を追加
            $query->where('post_date', '>=', $start_date->format('Y-m-d'))
                  ->where('post_date', '<',  $end_date->format('Y-m-d'));
        }

        // paginate メソッドを使うと、ページネーションに必要な全件数やオフセットの指定などは全部やってくれる
        return $query->paginate($num_per_page);
    }
```

今回追加したのは、引数 `$condition` から `$category_id` を取り出して、値があれば where メソッドで設定する部分と、Eager ロードの設定です。

リレーションのデータは、プロパティにアクセスされる時に初めてロードされ、それまでロードされることはありません。
ここで例えば、下記のような View テンプレートの処理があったとすると、カテゴリー情報を取得するために都度クエリーが実行され、計50回（`$articles` を取得するクエリーも含めると51回）のクエリーが実行されます。

```
{{-- $articles には 50 の記事データが入っているものとする --}}
@foreach ($articles as $article) 
	記事のカテゴリー：{{ $article->category->name }}
@endforeach
```

そこで、あらかじめ with() メソッドを使って、関連モデルを指定しておくと、クエリ数を減らすことができます。今回のように `with('category')` を設定しておけば、下記2本のクエリだけしか実行されません。これが Eager ロードになります。詳しくは日本語ドキュメントの[リレーション](https://readouble.com/laravel/5.5/ja/eloquent-relationships.html)を参照してください。

```
select * from articles;
select * from categories where category_id in (1, 2, 3, 4, 5, .....)
```



## リクエストクラスの編集
パラメータに `$category_id` が追加されたので、そのバリデートを追加します。
`app/Http/Requests/FrontBlogRequest.php` を開いて、下記のように編集します。

```app/Http/Requests/FrontBlogRequest.php
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'year'        => 'integer',
            'month'       => 'integer',
            'category_id' => 'integer|min:1',
        ];
    }

    public function messages()
    {
        return [
            'year.integer'        => '年は整数にしてください',
            'month.integer'       => '月は整数にしてください',
            'category_id.integer' => 'カテゴリーIDは整数にしてください',
            'category_id.min'     => 'カテゴリーIDは1以上にしてください',
        ];
    }
```

category_id は整数で、1以上でなければならない、というバリデーションルールを追加しました。



## コントローラーの編集
`app/Http/Controllers/FrontBlogController.php` を開いて、下記のように編集します。

```app/Http/Controllers/FrontBlogController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\FrontBlogRequest;
use App\Models\Article;
use App\Models\Category;

class FrontBlogController extends Controller
{
    /** @var Article */
    protected $article;
    
    /** @var Category */
    protected $category;

    // 1ページ当たりの表示件数
    const NUM_PER_PAGE = 10;

    function __construct(Article $article, Category $category)
    {
        $this->article = $article;
        $this->category = $category;
    }

    /**
     * ブログトップページ
     *
     * @param FrontBlogRequest $request
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    function index(FrontBlogRequest $request)
    {
        // パラメータを取得
        $input = $request->input();

        // ブログ記事一覧を取得
        $list = $this->article->getArticleList(self::NUM_PER_PAGE, $input);

        // ページネーションリンクにクエリストリングを付け加える
        $list->appends($input);

        // カテゴリー一覧を取得
        $category_list = $this->category->getCategoryList();

        // 月別アーカイブの対象月リストを取得
        $month_list = $this->article->getMonthList();

        return view('front_blog.index', compact('list', 'month_list', 'category_list'));
    }
}
```

まず、コンストラクターインジェクションで、Category モデルのインスタンスを生成します。そして、それを利用してカテゴリー一覧を取得して、 View に渡す処理を追加しています。



## View テンプレートの編集
右カラムにカテゴリーの一覧を表示させます。
`resources/views/front_blog/right_column.blade.php` を開いて下記のように編集します

```html:resources/views/front_blog/right_column.blade.php
{{--右カラム--}}
<div class="col-md-2">
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">カテゴリー</h3>
        </div>
        <div class="panel-body">
            <ul class="monthly_archive">
                @forelse($category_list as $category)
                    <li>
                        <a href="{{ route('front_index', ['category_id' => $category->category_id]) }}">
                            {{ $category->name }}
                        </a>
                    </li>
                @empty
                    <p>カテゴリーがありません</p>
                @endforelse
            </ul>
        </div>
    </div>

    ...（省略）...
</div>
```

月別アーカイブの表示とほぼ同じですね。

では、引き続いてブログ記事にカテゴリ名を表示させます。
`resources/views/front_blog/index.blade.php` を開いて下記のように編集します。

```html:resources/views/front_blog/index.blade.php
@extends('front_blog.app')
@section('title', '私のブログ')

@section('main')
    <div class="col-md-8 col-md-offset-1">
        {{--forelse ディレクティブを使うと、データがあるときはループし、無いときは @empty 以下を実行する--}}
        @forelse($list as $article)
            <div class="panel panel-default">
            
                ...（中略）...
                
                <div class="panel-footer text-right">
                    <a href="{{ route('front_index', ['category_id' => $article->category->category_id]) }}">
                        {{ $article->category->name }}
                    </a>
                    &nbsp;&nbsp;
                    {{--updated_at も同様に自動的に Carbon インスタンスにキャストされる--}}
                    {{ $article->updated_at->format('Y/m/d H:i:s') }}
                </div>
            </div>
        @empty
            <p>記事がありません</p>
        @endforelse

        {{ $list->links() }}
    </div>
@endsection
```

更新日時の左側にカテゴリ名が表示されるようにしました。同じカテゴリの記事を閲覧するためのリンクも設定しておきます。

ここまでできたら、トップページにアクセスして、記事や登録したカテゴリーが想定通りに正しく表示されることを確認してください。各記事が、登録したカテゴリーの category_id を持っていないと正しく表示されないので、注意してください。上手くいけば、下記のような表示になると思います。
![like_blog_front.png](https://qiita-image-store.s3.amazonaws.com/0/252417/5aa807c0-ed09-8d74-5f72-d1a1d9446607.png)


これで、長々と続けてきたチュートリアルは終わりです。これから Laravel を始める人のお役に立てれば嬉しいです。
間違いなどありましたら、コメントにてお知らせください。



## 参考資料
### Laravel 日本語ドキュメント
* [リレーション](https://readouble.com/laravel/5.5/ja/eloquent-relationships.html)
* [Eloquentの準備](https://readouble.com/laravel/5.5/ja/eloquent.html)
* [バリデーション](https://readouble.com/laravel/5.5/ja/validation.html)
* [コントローラ](https://readouble.com/laravel/5.5/ja/controllers.html)
* [Bladeテンプレート](https://readouble.com/laravel/5.5/ja/blade.html)

### プログラムソース（github）
* [github yumgoo17/like_blog](https://github.com/yumgoo17/like_blog)