エンジニアを目指す私が実装したタグをつける機能とタグの検索機能(n番煎じ)

この記事は[TechCommit Advent Calendar 2022](https://adventar.org/calendars/7673)の22日目の記事です。
なんとなく参加しました｡

# 注意
ただの個人用メモ
解説用に現物の一部を改変,省略,簡易化,簡略化などしています｡
やり方が間違っている可能性が高いです｡
参考にする場合は**自己責任**でお願いします｡

# バージョン,環境など
バック:php,laravel
フロント:html,css,javascript,Vue,axios
自分ではわからないもの:Inertia

|言語 フレームワーク|バージョン|
|--|--|
|PHP|8.0.24|
|Laravel|9.43.0|
|Vue|3.2.31|
|axios|0.25|

# 事の始まり
個人開発でブックマークにタグを付けて保存するアプリを開発しているから

# ディレクトリ構造
今回の解説に必要な分だけ取り出しています｡
repositoryディレクトリは自分でつくりました｡[^2]
![ディレクトリ構造.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421721/7e430e84-24f9-867c-3ae6-208e2511a5a7.png)



# データベース
#### 仕様
* `deleted_at`に`null`以外の値が入っていたら削除扱いとする
* `tag_id`が`null`の場合タグが1つもついていないものとする[^1]

### book_marks
|名前|タイプ|その他|
|--|--|--|
|id|bigint|主キー|
|title|varchar||
|url|longtext||
|deleted_at|timestamp|nullable,デフォルト:null|
|created_at|timestamp|nullable,デフォルト:null|
|updated_at|timestamp|nullable,デフォルト:null|

### tags
|名前|タイプ|その他|
|--|--|--|
|id|bigint|主キー|
|name|varchar||
|deleted_at|timestamp|nullable,デフォルト:null|
|created_at|timestamp|nullable,デフォルト:null|
|updated_at|timestamp|nullable,デフォルト:null|

### book_mark_tags
book_marksテーブルとtagsテーブルの中間テーブル

|名前|タイプ|その他|
|--|--|--|
|book_mark_id|bigint||
|tag_id|bigint|nullable|
|deleted_at|timestamp|nullable,デフォルト:null|
|created_at|timestamp|nullable,デフォルト:null|
|updated_at|timestamp|nullable,デフォルト:null|

# 3つのテーブルの関係
book_mark_tagsテーブルはbook_marksテーブルとtagsテーブルの中間テーブル

※図は一部カラムを省略
![bookmarktags.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421721/fa699c5b-9e5e-124f-fb60-10835d71e220.png)


|子カラム|親テーブル|親カラム|
|--|--|--|
|book_mark_id|book_marks|id|
|tag_id|tags|id|

# 前提など
各ファイルには以下のようなファイルをuseして､変数を宣言してある
```php:BookMarkController.php
use DB;
use App\Repository\BookMarkRepository;
use App\Repository\BookMarkTagRepository;
use App\Tools\NullAvoidanceToolKit; 

private $bookMarkRepository;
private $bookMarkTagRepository;
private $nullAvoidanceToolKit;

class BookMarkController extends Controller {
    public function __construct()
    {
        $this->bookMarkRepository    = new BookMarkRepository();
        $this->bookMarkTagRepository = new BookMarkTagRepository();
        $this->nullAvoidanceToolKit  = new NullAvoidanceToolKit();
    }
}
```
```php:BookMarkRepository.php
use DB;
use App\Tools\searchToolKit;

use App\Models\BookMark;
use App\Models\BookMarkTag;

use App\Tools\NullAvoidanceToolKit;

class BookMarkRepository
{
    private $nullAvoidanceToolKit;

    public function __construct()
    {
        $this->nullAvoidanceToolKit = new NullAvoidanceToolKit();
    }
}
```
```php:BookMarkTagRepository.php
use DB;
use App\Models\BookMarkTag;
```

### 自作したツールについて
#### searchToolKit
一部サイトを参考に複数のキーワードで検索できるに変換する処理をまとめたファイル
```php:searchToolKit.php
class searchToolKit
{
    //sqlでlike検索する前にするエスケープ処理
    public function sqlEscape($arg)
    {
        // %をエスケープ
        $escaped = preg_replace(
        '/%/',
        '\%',
        $arg);

        // _をエスケープ
        $escaped = preg_replace(
        '/_/',
        '\_',
        $escaped);

        return $escaped;
    }

    //and検索できるように空白で区切って､配列にする
    public function preparationToAndSearch($arg)
    {
        // 全角スペースを半角に変換
        $spaceConversion = mb_convert_kana($arg, 's');

        // 単語を半角スペースで区切り、配列にする
        return preg_split('/[\s,]+/', $spaceConversion, -1, PREG_SPLIT_NO_EMPTY);

    }
}
```
#### NullAvoidanceToolKit
nullを入れてしまうとエラーになるような処理で使う
ファイルにif文を書いて分岐するよりこのツールを使ったほうがコード短くきれいにかけると私は思う
```php:NullAvoidanceToolKit.php
class NullAvoidanceToolKit
{
    // 引数1がnullだったら引数2を返す
    // 引数1がnullでなかったら引数1を返す
    public function ifnull($arg1,$arg2)
    {
        if (is_null($arg1)) { return $arg2; }
        else {return $arg1;}
    }
}
```


# ものすごく雑な全体の流れ
大体こんな感じだと思います｡
![ものすごく雑な全体のながれ.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421721/91e78c6c-84e3-e580-0fd7-76bbb99d5c3e.png)
※ requestだとかミドルウェアだとかポリシーだとかは今回全部省略します｡
※ BookMarkControllerの一部の処理が今回の解説に関係ないので一部処理します｡



# 新規作成でのタグの付け外し
## フロント
#### 前提
私が作ったアプリでは以下のようなチェックボックスリストを採用しています｡
![スクリーンショット (4265).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421721/10f744bf-62e8-13c0-6952-3131cbe18482.png)
タグはサーバーから以下のような形で渡される
```javascript
[{id:1,name:アーキテクチャ},{id:2,name:アイデア}]
```

大まかな流れとしては
1. データベースから登録してあるタグをとってくる
1. 取ってきたタグを`tagList`配列に保存
1. `tagList`配列をもとに､ユーザーがブックマークに紐づけるタグにチェックを入れる
1. チェックをつけたタグを`checkedTagList`配列に保存
1. `checkedTagList`配列をサーバーに送信(私の場合はaxiosを使いました)

一部のみ解説する
### 3 `tagList`配列をもとに､ユーザーがブックマークに紐づけるタグにチェックを入れる
[コードはここ](#チェックボックスリスト)

### 4 チェックをつけたタグを`checkedTagList`配列に保存
チェックボックスリストのコードに`<input type="checkbox" :id="tag.id" v-model="checkedTagList" :value="{tag.id}">`と書いてあるので
チェックボックスにチェックを入れると`checkedTagList`は以下ようにタグのidが入る
```javascript:checkedTagList
[1,2,3]
```

## サーバーサイド
### BookMarkController
ここではタグが一つでも紐づいているかどうかで処理を分岐させる
#### 前提
フロントからは､以下のようなデータが送られてくる
```php:フロントから渡される例
[
    "bookMarkTitle"  => "タイトル", // ブックマークのタイトル
    "bookMarkUrl"    => "url", //ブックマークのurl
    "checkedTagList" => [1,2,3] // チェックを付けたタグのid
]
```


##### 雑な全体の流れ
![BookMarkController全体図.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421721/824c00fc-952c-3a0c-8118-90fcc4d4990a.png)


##### コード
```php:BookMarkController.php
public function store($request)
    {
        DB::transaction(function () use($request){
            // ブックマークを保存してブックマークのidを取得
            $bookMarkId = $this->bookMarkRepository->store(
                title    : $request->bookMarkTitle,
                url      : $request->bookMarkUrl,
            );

            // なんのタグも設定されていない時(checkedTagListが空の時)
            if (empty($request->checkedTagList) == true) {
                $this->bookMarkTagRepository->store(
                    tagId      : null,
                    bookMarkId : $bookMarkId,
                );
            }
            //タグが設定されている時
            else {
                foreach($request->checkedTagList as $tagId){
                    $this->bookMarkTagRepository->store(
                        tagId      : $tagId,
                        bookMarkId : $bookMarkId,
                    );
                }
            }
        });
    }
```
[bookMarkRepository->store](#bookmarkrepository-store)については解説の重要度が低いので下の方に書いてあります

#### 例1
**受け取ったデータ**
```php
[
    "bookMarkTitle"  => "タイトル", 
    "bookMarkUrl"    => "url", 
    "checkedTagList" => [] 
]
```
**$bookMarkId=1**

**book_mark_tagsテーブルには以下のように保存される**(一部省略)
|book_mark_id|tag_id|
|--|--|
|1|null|


#### 例2
**受け取ったデータ**
```php
[
    "bookMarkTitle"  => "タイトル", 
    "bookMarkUrl"    => "url", 
    "checkedTagList" => [1,2,3] 
]
```
**$bookMarkId=1**

**book_mark_tagsテーブルには以下のように保存される**(一部省略)
|book_mark_id|tag_id|
|--|--|
|1|1|
|1|2|
|1|3|

### BookMarkTagRepository
データベースにデータを保存する
```php:BookMarkRepository.php
public  function store($tagId,$bookMarkId)
    {
        BookMarkTag::create([
            'book_mark_id' => $bookMarkId,
            'tag_id'     => $tagId,
        ]);
    }
```

# 更新での付け外し
## フロント
#### 大まかな流れ
1. データベースから登録してあるタグをとって来て`tagList`配列に保存
1. データベースからブックマークに紐づいているタグを取ってくるきて`checkedTagList`配列に保存
1. ユーザーがブックマークに紐づけるタグを追加したり､紐づけたタグを削除したりする(`checkedTagList`配列を変化させる)
1. `checkedTagList`配列をサーバーに送信

## バック
### BookMarkController
フロントから受け取ったデータをBookMarkTagRepositoryに渡す
BookMarkTagRepositoryから受け取ったデータをフロントに渡す

### BookMarkTagRepository
#### ブックマークに紐づいているタグのIDを取ってくる処理
```php:BookMarkTagRepository.php
public  function serveTagsRelatedToBookMark($bookMarkId){
        $relatingTagList = DB::table('book_mark_tags')
        ->select('tag_id')
        ->whereNull('deleted_at')
        ->where('book_mark_id','=',$bookMarkId)
        ->get();

        // [[tag_id => 1],[tag_id => 2],...]みたいな形を
        // [1,2,...]みたいな形にする
        $convertedRelatingTagList = $this->convertAssociativeArrayToSimpleArray($relatingTagList);

        // 何もタグがついてなかったら空配列を返す
        if (is_null($convertedRelatingTagList[0])) {return [];}
        else {return $convertedRelatingTagList }
    }
```

```php:BookMarkTagRepository.php
    //[[tag_id => 1],[tag_id => 2],...]みたいな配列が渡されるので
    //[1,2...]みたいな形に変換する
    public function convertAssociativeArrayToSimpleArray($array)
    {
        $temp = [];
        foreach($array as $element){array_push($temp,$element->tag_id);}
        return $temp;
    }
```
※2 なぜ空配列を返すのか
フロントの`checkedTagList`配列にわた時,空配列ならどのチェックボックスにもチェックがついていない状態になるから

##### 例
**book_mark_tagテーブル**が以下のような状態とする
|book_mark_id|tag_id|delete_at|
|--|--|--|
|1|1|null|
|1|2|null|
|1|3|null|
|1|4|2022-12-19 16:26:56|
|2|1|null|
|2|4|null|
|3|6|null|

* `book_mark_id`が1 
* `deleted_at`が`null`

のデータを探す


|book_mark_id|tag_id|delete_at|
|--|--|--|
|1|1|null|
|1|2|null|
|1|3|null|

ここから加工して`tag_id`だけを配列に入れる -> `[1,2,3]`

#### 更新作業 
##### 前提
フロントから以下のようなデータが渡される
```php:フロントから渡される例
[
    "bookMarkId"     => 1, //更新するブックマークのId
    "bookMarkTitle"  => "タイトル", // ブックマークのタイトル
    "bookMarkUrl"    => "url", //ブックマークのurl
    "checkedTagList" => [1,2,3] // チェックを付けたタグのid
]
```

##### 雑な全体の流れ
![更新.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421721/4156deda-8735-1dd9-5a97-8fed8bb01dc0.png)
##### コード
```php:BookMarkTag.php
public  function update($bookMarkId,$checkedTagList)
    {
        // 更新前のブックマークに紐付けられていたタグを取得
        $originalTagList = $this->serveTagsRelatedToBookMark($bookMarkId);

        // 更新前のブックマークにタグが1つでもついていたかいなかったかで処理を分ける
        if (empty($originalTagList)) {$this->ProcessingifOriginalHasNoTags($bookMarkId,$checkedTagList,$originalTagList);}
        else {$this->ProcessingifOriginalHasAnyTags($bookMarkId,$originalTagList,$checkedTagList);}
    }

// タグが1つもついてなかった
public function ProcessingifOriginalHasNoTags($bookMarkId,$checkedTagList,$originalTagList)
    {
        // 追加されたタグ
        $addedTagList = array_diff($checkedTagList, $originalTagList);

        // なにか新しくタグが紐づけられていた場合
        if (!empty($addedTagList)) {
            // 追加
            foreach($addedTagList as $tag) {
                $this->store(
                    tagId:$tag,
                    bookMarkId:$bookMarkId,
                );
            }

            // nullの削除
            $this->delete(
                tagId:null,
                bookMarkId:$bookMarkId,
            );
        }
    }

// タグがついていた
public function ProcessingifOriginalHasAnyTags($bookMarkId,$originalTagList,$checkedTagList)
    {
        // 追加されたタグ
        $addedTagList = array_diff($checkedTagList, $originalTagList);

        // 削除されたタグ
        $deletedTagList = array_diff($originalTagList, $checkedTagList);

        //削除
        if (!empty($deletedTagList)) {
            foreach($deletedTagList as $tag) {
                $this->delete(
                    tagId:$tag,
                    bookMarkId:$bookMarkId,
                );
            }
        }

        // ブックマークのタグをすべて消した時の処理
        // 新しく追加されたタグがない場合
        if (empty($addedTagList)) {
            //もともとついていたタグがすべてはずされたか確認
            $isAllDeleted = array_diff($originalTagList,$deletedTagList);

            if (empty($isAllDeleted)) {
                // 紐付けられていたタグすべて削除されたのならtag_id = nullのデータをついか
                $this->store(
                    tagId:null,
                    bookMarkId:$bookMarkId,
                );
            }
        } else {
            // 新しく追加されたタグがある場合
            foreach($addedTagList as $tag) {
                $this->store(
                    tagId:$tag,
                    bookMarkId:$bookMarkId,
                );
            }
        }
    }
```
#### 例1
フロントからのデータ(一部省略)
```php
[
    "bookMarkId"     => 1,
    "checkedTagList" => [1,2,3]
]
```
更新前データベースの状態
|book_mark_id|tag_id|deleted_at|
|--|--|--|
|1|null|null|
|2|1|null|
|2|2|null|

↓

更新後データベースの状態
|book_mark_id|tag_id|deleted_at|
|--|--|--|
|1|null|2022-12-19 16:26:56|
|1|1|null|
|1|2|null|
|1|3|null|
|2|1|null|
|2|2|null|

#### 例2
フロントからのデータ(一部省略)
```php
[
    "bookMarkId"     => 1,
    "checkedTagList" => [3,4]
]
```
更新前データベースの状態
|book_mark_id|tag_id|deleted_at|
|--|--|--|
|1|1|null|
|1|2|null|
|1|3|null|
|2|1|null|
|2|2|null|

↓

更新後データベースの状態
|book_mark_id|tag_id|deleted_at|
|--|--|--|
|1|1|2022-12-19 16:26:56|
|1|2|2022-12-19 16:26:56|
|1|3|null|
|1|4|null|
|2|1|null|
|2|2|null|

#### 例3
フロントからのデータ(一部省略)
```php
[
    "bookMarkId"     => 1,
    "checkedTagList" => []
]
```
更新前データベースの状態
|book_mark_id|tag_id|deleted_at|
|--|--|--|
|1|1|null|
|1|2|null|
|1|3|null|
|2|1|null|
|2|2|null|

↓

更新後データベースの状態
|book_mark_id|tag_id|deleted_at|
|--|--|--|
|1|1|2022-12-19 16:26:56|
|1|2|2022-12-19 16:26:56|
|1|3|2022-12-19 16:26:56|
|1|null|null|
|2|1|null|
|2|2|null|


#### array_diff()とは
上記のコードで出てきたarray_diff()について自分なりの意訳で説明
簡単に説明すると
引数1の配列から引数2の配列の要素を消して残った要素を返す｡
※人によっては引き算と表したほうがわかるかもしれない

##### 例1
配列1:[1,2,3]
配列2:[2,4,6]
array_diff(配列1,配列2) →[1,3]
```php
array_diff(配列1,配列2) = [1,2,3] - [2,4,6] = [1,3]
```

##### 例2
配列1:[1,2,3]
配列2:[4,5,6]
array_diff(配列1,配列2) →[1,2,3]
```php
array_diff(配列1,配列2) = [1,2,3] - [4,5,6] = [1,2,3]
```

##### 例3
配列1:[1,2,3]
配列2:[1,2,3]
array_diff(配列1,配列2) →[]
```php
array_diff(配列1,配列2) = [1,2,3] - [1,2,3] = []
```

# 検索
#### 前提
vueとInertiaとlaravelを組み合わせてしまっているのでlaravelのpagenationがそのまま使うことができない｡
なので色々自分でコードを用意しないといけない｡

## フロント
1. データベースから登録してあるタグをとってくる
1. 取ってきたタグを`tagList`配列に保存
1. `tagList`配列をもとに､ユーザーがブックマークに紐づけるタグにチェックを入れる
1. チェックをつけたタグを`checkedTagList`配列に保存
1. フォームからユーザーからキーワードを受け取る
1. 受け取った`keyword`変数に代入
1. `keyword`変数,`checkedTagList`配列をサーバーに送信(私の場合はinertiaを使いました)

## バック
### BookMarkController

#### 前提
フロントからは以下のようなデータが渡される 
```php:渡される例
[
    "keyword"        => "検索するキーワード"
    "checkedTagList" => [1,2,3]
]
```
キーワードもタグもすべて`And`検索で実行する


##### コード
```php:BookMarkController.php
public function search(Request $request)
    {        
        // 検索
        $result = $this->bookMarkRepository->search(
            keyword:$request->keyword,
            checkedTagList:$request->checkedTagList
        );

        // 私の場合はInertia::renderを使ってフロントにデータを返すことにした
        return Inertia::render('BookMark/SearchBookMark',[
            'result' => $result,
        ]);
    }
```

### BookMarkController

##### コード
```php:BookMarkRepository.php
public  function search($keyword,$checkedTagList)
    {
        // ツールを実体化
        $searchToolKit = new searchToolKit();

        // %と_をエスケープ
        $escaped = $searchToolKit->sqlEscape($keyword);
        //and検索のために空白区切りでつくった配列を用意
        $wordListToSearch = $searchToolKit->preparationToAndSearch($escaped);

        //タグも検索する場合
        if (!empty($checkedTagList)) {
            //副問合せのテーブルから検索
            $subTable = $this->createSubTableForSearch($userId,$tagList);
            $query = DB::table($subTable,"book_marks");
        } else {
            //タグ検索が不要な場合
            $query = DB::table("book_marks")
            ->select('*')
            ->whereNull('deleted_at');
        }

        // titleをlike検索
        foreach($wordListToSearch as $word){ $query->where('title','like',"%$word%");

        //検索
        return $query->get();
    }

//検索時のサブテーブル作成
    public  function createSubTableForSearch($checkedTagList)
    {
        //book_markテーブルとbook_mark_tagsテーブルを結合
        $subTable = BookMark::select('book_marks.*')
        ->leftjoin('book_mark_tags','book_mark_tags.book_mark_id','=','book_marks.id') 
        ->where(function($subTable) { // 削除されたブックマーク､外されたブックマークのデータをとってこないようにする
            $subTable->WhereNull('book_marks.deleted_at')
                     ->WhereNull('book_mark_tags.deleted_at')
        })
        ->where(function($subTable) use($checkedTagList) { // 指定したタグがついたデータをとってくる
            foreach($checkedTagList as $tag){
                $subTable->orWhere('book_mark_tags.tag_id','=',$tag);
            }
        });
        
        // 指定したタグがすべて付いているデータだけに絞る
        $subTable->groupBy('book_marks.id')
        ->having(DB::raw('count(*)'), '=', count($checkedTagList));

        return $subTable;
    }
```

#### `createSubTableForSearch`で実行されるsqlがどんな感じに動くかのイメージ
##### 例 
以下のようなデータベースがあるとする
book_mark_tagsテーブル
|book_mark_id|tag_id|deleted_at|
|--|--|--|
|1|1|null|
|1|2|null|
|1|3|null|
|1|4|2022-12-19 16:26:56|
|2|1|null|
|2|2|null|
|2|3|2022-12-19 16:26:56|
|3|3|null|
|4|null|null|
|5|5|null|

book_marksテーブル
|id|title|url|deleted_at|
|--|--|--|--|
|1|aaa|aaa|null|
|2|bbb|bbb|null|
|3|ccc|ccc|null|
|4|ddd|ddd|null|
|5|eee|eee|2022-12-19 16:26:56|

以下のようなデータが渡されたとする
```
[
    "checkedTagList" => [1,2,3]
]
```
バックでは大体以下のように動く
1. book_markテーブルとbook_mark_tagsテーブルを結合
1. 削除されたブックマーク､外されたブックマークのデータを除外する
1. 指定したタグがついたデータをとってくる
1. `book_mark_id`ごとにデータを分ける
1. 配列の要素数と､データの数が同じものだけ取ってくる

**1. book_markテーブルとbook_mark_tagsテーブルを結合**
|book_mark_id|tag_id|deleted_at|title|url|deleted_at|
|--|--|--|--|--|--|
|1|1|null|aaa|aaa|null|
|1|2|null|aaa|aaa|null|
|1|3|null|aaa|aaa|null|
|1|4|2022-12-19 16:26:56|aaa|aaa|null|
|2|1|null|bbb|bbb|null|
|2|2|null|bbb|bbb|null|
|2|3|2022-12-19 16:26:56|bbb|bbb|null|
|3|3|null|ccc|ccc|null|
|4|null|null|ddd|ddd|null|
|5|5|null|eee|eee|2022-12-19 16:26:56|

**2. 削除されたブックマーク､外されたブックマークのデータをとってこないようにする**
|book_mark_id|tag_id|deleted_at|title|url|deleted_at|
|--|--|--|--|--|--|
|1|1|null|aaa|aaa|null|
|1|2|null|aaa|aaa|null|
|1|3|null|aaa|aaa|null|
|2|1|null|bbb|bbb|null|
|2|2|null|bbb|bbb|null|
|3|3|null|ccc|ccc|null|
|4|null|null|ddd|ddd|null|

**3. 指定したタグがついたデータをとってくる**
今回は`"checkedTagList" => [1,2,3]`なので`tag_id`が`1`か`2`か`3`のデータを取ってくる
|book_mark_id|tag_id|deleted_at|title|url|deleted_at|
|--|--|--|--|--|--|
|1|1|null|aaa|aaa|null|
|1|2|null|aaa|aaa|null|
|1|3|null|aaa|aaa|null|
|2|1|null|bbb|bbb|null|
|2|2|null|bbb|bbb|null|
|3|3|null|ccc|ccc|null|

**4. `book_mark_id`ごとにデータを分ける**
`book_mark_id = 1` 
|book_mark_id|tag_id|deleted_at|title|url|deleted_at|
|--|--|--|--|--|--|
|1|1|null|aaa|aaa|null|
|1|2|null|aaa|aaa|null|
|1|3|null|aaa|aaa|null|

`book_mark_id = 2` 
|book_mark_id|tag_id|deleted_at|title|url|deleted_at|
|--|--|--|--|--|--|
|2|1|null|bbb|bbb|null|
|2|2|null|bbb|bbb|null|

`book_mark_id = 3` 
|book_mark_id|tag_id|deleted_at|title|url|deleted_at|
|--|--|--|--|--|--|
|3|3|null|ccc|ccc|null|

**5. 配列の要素数と､データの数が同じものだけ取ってくる**
`"checkedTagList" => [1,2,3]`なので要素数は3つ
なので､データが3つのデータを取ってくる
|book_mark_id|tag_id|deleted_at|title|url|deleted_at|
|--|--|--|--|--|--|
|1|1|null|aaa|aaa|null|
|1|2|null|aaa|aaa|null|
|1|3|null|aaa|aaa|null|

**実際のデータベースでは以下のように返される**
`select book_marks.* `とかいたので
|id|title|url|deleted_at|create_at|updated_at|
|--|--|--|--|--|--|
|1|aaa|aaa|null|2022-12-19 16:26:56|2022-12-19 16:26:56|

これを`search`関数に返して､そこから更にタイトルを`like`検索したりする


# 解説重要度が低いが,気になる人のためにここコードをまとめておこうのコーナー:clap:

##### チェックボックスリスト
```javascript:チェックボックスリスト
<v-list>
    <v-list-item v-for="tag of tagList" :key="tag.id">
        <input type="checkbox" :id="tag.id" v-model="checkedTagList" :value="{tag.id}">
        <label :for="tag.id">{{tag.name}}</label>
    </v-list-item>
</v-list>
```
##### bookMarkRepository->store
```php:BookMarkRepository.php
//新規ブックマーク作成 登録したブックマークのIDを返す
public  function store($title,$url)
    {
        $bookMark = BookMark::create([
            // タイトルが産められてなかったら日時で埋める
            'title'    => $this->nullAvoidanceToolKit->ifnull($title,Carbon::now()),
            'url'      => $url,
        ]);
        return $bookMark->id;
    }
```
##### bookMarkTagRepository->store
```php:BookMarkRepository.php
//ブックマークに紐付けらたタグを登録
    public  function store($tagId,$bookMarkId)
    {
        BookMarkTag::create([
            'book_mark_id' => $bookMarkId,
            'tag_id'     => $tagId,
        ]);
    }
```
##### bookMarkTagRepository->delete
```php:BookMarkRepository.php
//ブックマークからはずされたタグを削除
    public  function delete($tagId,$bookMarkId)
    {
        BookMarkTag::where('book_mark_id','=',$bookMarkId)
            ->where('tag_id','=',$tagId)
            ->delete();
    }
```

# 参考にしたもの
https://takuma-it.com/laravel-keyword-search/

[^1]:なぜわざわざrepositoryディレクトリを作ったのか
もともとデータベースに関するコードはモデルの方に書いていたが､数人のエンジニアの方々にみてもらった時に､ファットモデルになってしまうから別のファイルに書いたほうが良いというアドバイスをうけたため｡

[^2]:なぜそのような仕様にしたのか
現段階ではまだ実装できてないが､タグが1つもついていないブックマークを検索するのに使えそうだと判断したため｡