laravel学んで2か月で自サービスを開発した話 Part8

こんにちは、ブログの更新で転職活動がなかなか進まないワイです。

今までの開発記録はこちらへ
<a href="https://qiita.com/k-hayack884/items/e571138e8bf7f35f7502">胡蝶蘭を捨てるくらいならワイが欲しいので、サービス開発する編</a>
<a href="https://qiita.com/k-hayack884/items/720c7a6300139317deac">公式ドキュメントの言う通り、パッケージをインストールされたら、Inertia.jsが導入されて？？？になった編</a>
<a href="https://qiita.com/k-hayack884/items/dccb9d479bd162144559">マルチログインを作ってみた編</a>
<a href="https://qiita.com/k-hayack884/items/0354c0ddecee58272e46">デザインをtailwindcssに丸投げする編</a>
<a href="https://qiita.com/k-hayack884/items/e4c691041373a97afd97">デザイナーに怒られないために、画像をリサイズする編</a>
<a href="https://qiita.com/k-hayack884/items/ab8ddbb97b035c55fd1b">AWS×リボ払いで破産へGO編</a>
<a href="https://qiita.com/k-hayack884/items/cefae022df84a57df53f">Google MAPに無人島に飛ばされる編</a>

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f323238313934362f30396630376266302d323930352d643666652d366366632d6138373864653736386438342e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/6f7ad561-e6fd-c3eb-e56b-f3519da50a54.png)

# 今回やること
- 検索機能の作成（ゲスト画面とユーザーのホーム画面に実装）

検索機能の前に、先に定数の設定をしていきます。
なぜなら、検索機能の条件は、アップデートするたびに条件が変わってしまっては困る。
また、条件を数字で管理するよりも、定数で管理したほうが、メンテナンスもしやすいからね。

さきにapp/Constants/common.phpを作成
```common.php
<?php

namespace App\Constants;

class Common
{  
    const ORDER_LATER = '0';　//新しい順
    const ORDER_OLDER = '1';　//古い順
    const ORDER_LIKE = '2';   //いいね順
    const ORDER_SELL = '3';   //取引中（取引中を除くソートを作るために使用）

    const ORDER_LIST = [

        'later' => self::ORDER_LATER,
        'older' => self::ORDER_OLDER,
        'like' => self::ORDER_LIKE,
        'sell' => self::ORDER_SELL
    ];

```
config.phpにcommon.phpを使えるように登録します
```config.php
    'aliases' => [
//略
        'Constant'=>App\Constants\common::class,

    ],
```
準備はできたので、ProductControllerに新しくviewメゾットを作成して書いていきます

```ProductController.php
    public function view(Request $request)
    {
        $categories = PrimaryCategory::with('secondary')->get();
        $productInfo =  Product::with('category')
            ->where('status', 1)
            ->orWhere('status', 2)
            ->orderBy('created_at', 'desc')
            ->orwhere('products.secondary_category_id', $request->category);
            if (!is_null($request->keyword)) {
                 $spaceConvert = mb_convert_kana($keyword, 's'); //全角スペースを半角にする
                 $keywords = preg_split('/[\s]+/', $spaceConvert, -1, PREG_SPLIT_NO_EMPTY);
                 foreach ($keywords as $word) {
                    ->orwhere('products.name', 'like', '%' . $word . '%');
            }
            if ($request->sort === null || $request->sort === \Constant::ORDER_LIST['later']) {
            ->where('status', 1)->orWhere('status', 2)->orderBy('created_at', 'desc');
            }
            if ($request->sort === \Constant::ORDER_LIST['older']) {
            ->where('status', 1)->orWhere('status', 2)->orderBy('created_at', 'asc');
            }
            if ($request->sort === \Constant::ORDER_LIST['sell']) {
            ->where('status', 1)->orderBy('created_at', 'desc');
            }
            ->with('category')->paginate(20); 
        return view('user.dashboard', compact('productInfo', 'categories'));
    }
```
![necchusyou_face_boy4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/ef96b79d-3349-6a65-ac67-02e183e2c8a7.png)

### えっ、一つのメゾットですよね？長くないですか？
この長いコードをもう一回書くんですよ？容量どれだけ食うんですか？

私みたいな初心者は、コードが足りないことを恐れてしまうため、余分にコードを書きがちですが、このようにコードが長いコントローラーファイルを**ファットコントローラー**といいます。
![himan05_man.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/0922b57e-5fd0-75c1-4f85-18e9db24a5a7.png)
### しかし、プロのエンジニアは太ったコードが嫌いで、できる限り少ないコードで書くようにしています
### ~~ただし、恋愛ではぽっちゃりのほうが好きな傾向があります~~

**ということで、分離します**上のコードは忘れてください！
```ProductController.php
    public function view(Request $request)
    {
        $categories = PrimaryCategory::with('secondary')->get();
        $productInfo = Product::availableItems()
            ->selectCategory($request->category ?? '0')
            ->searchKeyword($request->keyword ?? '')
            ->sortOrder($request->sort)->with('category')->paginate(20);
        return view('user.dashboard', compact('productInfo', 'categories'));
    }
```
#### このように短くできます
まあ、何が起きたかというと、検索機能の部分を**availableItems**にして、モデルファイルに分離させました
laravelの公式にも
> ローカルスコープを使用すると、アプリケーション全体で簡単に再利用できる、共通のクエリ制約を定義できます。たとえば、「人気がある（popular）」と思われるすべてのユーザーを頻繁に取得する必要があるとしましょう。スコープを定義するには、Eloquentモデルメソッドの前にscopeを付けます。
<a href="https://readouble.com/laravel/8.x/ja/eloquent.html">
Laravel 8.x Eloquentの準備
</a>
と書いていますので、何度も使う機能は使いまわしにしましょう！

## ローカルスコープを書く
ということでモデルファイルに行きます

```Product.php 

 public function scopeAvailableItems($query)
    {
        Product::with('category')
            ->where('status', 1)
            ->orWhere('status', 2)
            ->orderBy('created_at', 'desc');
        return $query;
    }

    public function scopeSortOrder($query, $sortOrder)
    {
        if ($sortOrder === null || $sortOrder === \Constant::ORDER_LIST['later']) {
            return $query->where('status', 1)
                ->orWhere('status', 2)->orderBy('created_at', 'desc');
        }
        if ($sortOrder === \Constant::ORDER_LIST['older']) {
            return $query->where('status', 1)
                ->orWhere('status', 2)->orderBy('created_at', 'asc');
        }
        if ($sortOrder === \Constant::ORDER_LIST['sell']) {
            return $query->where('status', 1)->orderBy('created_at', 'desc');
        }
        return $query;
    }

    public function scopeSelectCategory($query, $categoryId)
    {
        if ($categoryId !== '0') {
            return $query->where('products.secondary_category_id', $categoryId);
        } else {
            return;
        }
    }

    public function scopeSearchKeyword($query, $keyword)
    {
        if (!is_null($keyword)) {
            $spaceConvert = mb_convert_kana($keyword, 's'); //全角スペースを半角にする
            $keywords = preg_split('/[\s]+/', $spaceConvert, -1, PREG_SPLIT_NO_EMPTY);
            foreach ($keywords as $word) {
                $query->where('products.name', 'like', '%' . $word . '%');
            }
            return $query;
        } else {
            return;
        }
    }
```
引数として、クエリ、入力されたキーワードなどの情報を保持しています。
そして、戻り値にクエリを返して、コントローラーがクエリを読み込むというシステムです。
関数名の前に```scope```も忘れずつけておきましょう

## view側の処理
さっそく、検索フォームを作りましょう
```dashboard.blade.php
<form action="{{ route('user.dashboard') }}" method="get">
        <div class="md:flex md:justify-around mt-20">
            <div class="md:flex items-center">
                <select name="category" class="my-2 md:mr-4 md:my-4">
                    <option value="0" @if (\Request::get('category') === '0') selected @endif>すべての商品</opition>
                        @foreach ($categories as $category)
                            <optgroup label={{ $category->name }}>
                                @foreach ($category->secondary as $secondary)
                                    <option value="{{ $secondary->id }}"
                                        @if (\Request::get('category') == $secondary->id) selected @endif>
                                        {{ $secondary->name }}
                                    </option>
                                @endforeach
                        @endforeach
                </select>
                <div class="flex space-x-2 items-center">
                    <div><input name="keyword"
                            class="w-full my-2 md:my-4 bg-gray-100 bg-opacity-50 rounded border border-black focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out sm:my-4"
                            placeholder="キーワードを入力" value="{{ request()->keyword }}"></div>
                    <div><button type="submit"
                            class="flex mx-auto  text-white bg-indigo-500 border-0 py-2 px-4 md:px-4 focus:outline-none hover:bg-indigo-600 rounded text-lg mx-4">検索する</button>
                    </div>
                </div>
            </div>
            <div class="text-sm my-2 sm:my-4">表示順
                <select name="sort" id="sort" class="ml-4">
                    <option value="{{ \Constant::ORDER_LIST['later'] }}"
                        @if (\Request::get('sort') === \Constant::ORDER_LIST['later']) selected @endif>新しい順
                    </option>
                    <option value="{{ \Constant::ORDER_LIST['older'] }}"
                        @if (\Request::get('sort') === \Constant::ORDER_LIST['older']) selected @endif>古い順
                    </option>
                    <option value="{{ \Constant::ORDER_LIST['sell'] }}"
                        @if (\Request::get('sort') === \Constant::ORDER_LIST['sell']) selected @endif>取引中を除く
                    </option>
                </select>
            </div>
    </form>
//略
 {{ $productInfo->appends(request()->query())->links() }}
```
注意するのが最後のページネーションのところです。
今までの```$productInfo->links() ```では、２ページ目などに移動した際に、検索結果がリセットされてしまいます。
そのため、ページ移動しても、検索結果を保持するために```appends```という関数を使います

### 結果
![FireShot Capture 008 - flower-gift - flower-gift.herokuapp.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/d645b1cf-b7c4-fc3e-de78-c345aaddbac8.png)
ちゃんと、左の検索ボックスと右の並び変えのボックスがついていますね
![FireShot Capture 009 - flower-gift - flower-gift.herokuapp.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/ef5a3d4f-8430-263c-b60a-df516551f90c.png)
並び替えも問題がなさそうです！
![FireShot Capture 011 - flower-gift - flower-gift.herokuapp.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/b75286a8-2292-d463-8bca-95c5e87a1f68.png)
検索ボックスもちゃんと機能しています。
あとはゲストページにも同じ機能を追加します！

## 終わりに
このサイトの場合、そこまでクエリ部分を切り分ける必要はないかなと思いますけれど、
大規模なサイトだと複数のクエリを一つにまとめるのはしんどいですね。
ということで、切り分けに挑戦してみてください！