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


今までの開発記録はこちらへ
<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>
<a href="https://qiita.com/k-hayack884/items/ea97f393140c764e41d8">エンジニアはやせ型が好き編</a>
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f323238313934362f30396630376266302d323930352d643666652d366366632d6138373864653736386438342e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/516320d9-e88d-4fb1-6bcf-d9b814687896.png)
# 今回やること
- お気に入り機能作成
- vueコンポーネントの導入
- 非同期通信
- larabel sanctumによるAPI認証

今回はバックエンドより、フロントエンドが大変そうですわ・・・

## 準備
お気に入りボタンはフォームで作るんだけれど、お気に入りを押すたびにページが更新されるのは面倒くさいですよね
この通信を**同期通信**というんですけれど、相手が受信するまで待たないといけないとで、利便性が落ちます。
一方で、相手からの受信を待たずに処理を進めてしまう通信のことを**非同期通信**といいます
**えっ、わかるように説明しろって？**
それでは宅配便の例を出しましょう。
通常、相手に宅配しようと玄関のチャイムを押したら、相手が出てくるのを待ちますよね？
これが同期通信です
一方で、最近は置き配というサービスがありますよね？
配達員は荷物を置けば仕事が終わり、家主を待つ必要がありません。
これが非同期通信です。
![雑な同期通信.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/1b2d4e67-091c-0cc7-5705-2cf68003395e.jpeg)

というわけで、Ajaxを利用したいといいたいところですが、**axios**というJavaScriptのライブラリを使用していきます。
#### インストール
```npm install axios```
resources/js/componentsにFavorite.vueファイルを作成し、書いていきます

```Favorite.vue
<template>
  <div v-if="favo">
    <button @click="postFavo" class="pt-2">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        class="h-8 w-8"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="2"
          d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
        />
      </svg>
    </button>
  </div>
  <div v-else>
    <button @click="deleteFavo" class="pt-2">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        class="h-8 w-8"
        fill="yellow"
        viewBox="0 0 24 24"
        stroke="gold"
      >
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="2"
          d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
        />
      </svg>
    </button>
  </div>
</template>

<script>
export default {
  el: "#favo",
  data() {
    return {
      favo: this.canfavorite, 
      error: "",
    };
  },
  props: ["productid", "canfavorite"],
  methods: {
    postFavo() {
        .then((response) => {
          axios
            .post(`/api/favorites/add`, {
              productId: this.productid,
              withCredentials: true,
            })
            .then((response) => (this.favo = false))
            .catch((response) => console.error(response.message));
    },
    deleteFavo() {
          axios
            .post(`/api/favorites/delete`, {
              productId: this.productid,
              withCredentials: true,
            })
            .then((response) => (this.favo = true))
            .catch((response) => console.error(response.message));
    },
  },
};
</script>
```
Bladeファイルからは"productid", "canfavorite"の値を受け取っています。（後で書きます）
お気に入り登録ボタンと削除ボタンをクリックしたときに、それぞれに対応したメゾットが発火します。
**ただし、このままでは機能しません**
なぜなら、お気に入りはログインしていないと利用できないため、ユーザーを認証しなければならないからです。
なので、それぞれのユーザーがAPI認証機能を使えるために導入するのが**Laravel Sanctum**です  

> Laravel Sanctum（サンクタム、聖所）は、SPA(シングルページアプリケーション)、モバイルアプリケーション、およびシンプルなトークンベースのAPIに軽い認証システムを提供します。Sanctumを使用すればアプリケーションの各ユーザーは、自分のアカウントに対して複数のAPIトークンを生成できます。これらのトークンには、そのトークンが実行できるアクションを指定するアビリティ／スコープが付与されることもあります
<a href="https://readouble.com/laravel/8.x/ja/sanctum.html#:~:text=Laravel%20Sanctum%EF%BC%88%E3%82%B5%E3%83%B3%E3%82%AF%E3%82%BF%E3%83%A0%E3%80%81%E8%81%96%E6%89%80,%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E7%94%9F%E6%88%90%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82">
Laravel 8.x Laravel Sanctum</a>
ということで、こちらもインストールしていきましょう。  

```composer require laravel/sanctum```

```php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"```

```php artisan migrate```

```Kernel.php
'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],
```
これでapiのミドルウェアを使えるようになるはずです。
## ルート
ユーザー機能はweb.phpに書いていましたが、今回はAPIを使うので、api.phpに書いていきます。
どっちに書けばいいのかは調べたところ結構意見が分かれていたので、とりあえず書きます。

```api.php
Route::prefix('favorites')->middleware(['api', 'auth:users'])->group(function () {
    Route::post('/add', [FavoriteController::class, 'add'])->name('favorites.add');
    Route::post('/delete', [FavoriteController::class, 'delete'])->name('favorites.delete');
});
```
## お気に入り登録機能実装
まずはお気に入りテーブルとリレーションを行います
画像一枚で説明すると構成はこうなります
![お気に入り.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/021a1a99-f0e0-eaea-0ad3-61d05a3c5f51.jpeg)

コントローラーも書いていきます
```FavoriteController.php
class FavoriteController extends Controller
{
    public function index() //登録したお気に入りを表示する機能
    {
        $favoriteItem = Favorite::with('product') 
            ->where('user_id', Auth::id())->get();
        return view('user.favorites.index', compact('favoriteItem'));
    }

    public function add(Request $request)　//登録機能
    {
        $product_id = $request->input('productId');

        $itemInFavorite = Favorite::where('user_id', Auth::id())
            ->where('product_id', $product_id)->first();
        if (empty($itemInFavorite)) {
            Favorite::create([
                'user_id' => Auth::id(),
                'product_id' => $product_id,

            ]);
            return response()->json([
                'message' => 'お気に入り登録に成功しました'
            ]); 
        } else {
            return response()->json([
                'message' => 'お気に入り登録済みです'
            ], 403);
        }
    }

    public function delete(Request $request)　//削除機能
    {
        $product_id = $request->input('productId');
        Favorite::where('product_id', $product_id)
            ->where('user_id', Auth::id())->delete();
        return response()->json([
            'message' => 'お気に入り登録を削除しました'
        ]); 
    }
}
```
解説としては、まずはリクエストからproduct_idを取得し、お気に入りテーブルからuser_idとproduct_idを検索しています。
成功すれば、jsonとして、メッセージを返します。

## viewファイル
先に書いていきます。商品詳細のviewファイルとして、resources/views/user/trades/show.blade.phpを作成します
```show.blade.php 
<span id="api"> //お気に入りボタン
                        @if (empty($favorite)) //お気に入りしていない場合
                            <favorite-component :canFavorite="true" :productId="{{ $productInfo->id }}" />
                            </favorite-component>
                        @else //お気に入りしている場合
                            <favorite-component :canFavorite="false" :productId="{{ $productInfo->id }}" />
                            </favorite-component>
                        @endif
                    </span>
```
さっきつくったFavorite.vueコンポーネントを挿入しています。
```v-bind```をして、vueファイルにcanFavoriteとproductIdの値を送ります。

## vueを読み込む
意外に忘れてしまいがちですが、app.jsにコンポーネントをマウントしましょう！
```app.js
import { createApp } from 'vue'
import FavoriteComponent from './components/Favorite.vue'
createApp({
    components:{
     FavoriteComponent,     
    }
}).mount('#api')
```
それでは再びvueファイルに戻ります。ボタン部分以外は省略
```Favorite.vue
    postFavo() {
      axios
        .get("/sanctum/csrf-cookie", {
          withCredentials: true,
        })
        .then((response) => {
          axios
            .post(`/api/favorites/add`, {
              productId: this.productid,
              withCredentials: true,
            })
            .then((response) => (this.favo = false))
            .catch((response) => console.error(response.message));
        });
    },
```
```.get("/sanctum/csrf-cookie", {withCredentials: true, })```が追加されましたね
お気に入り処理をする前にユーザーが持つAPIトークンを照合させているんです。
![スクリーンショット (1663).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/95d1f96f-bb0c-4cdc-664a-88a669ff07d4.png)
### やったぜ

## 終わりに
実はサイト作成でここが一番苦戦しました。
苦戦したところは
- API自体をそもそもよくわかっていなかった（~~API？外部からとってくるやつでしょ？~~）
- 半日つかったにもかかわらず、なんか解決してしまった401エラーさん
- 素では機能するけど、なぜかlaravelでは機能しないvueさん

![necchusyou_face_boy4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/87a8f865-a9b9-950b-e929-2389bef99f0a.png)
### しばらくはvueアレルギーになりそう・・・
![necchusyou_face_boy1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2281946/b716bb76-c377-33b4-81fb-3545bbdff07a.png)
### でも、フロントエンドは遊び心を直接表現できるから楽しいよね