LaravelとぐるなびWebサービスを使って渋谷区の居酒屋をエリアやお酒ごとに分類して一覧表示するアプリを作った

LaravelとVue.jsを使って何かサービスを作りたいなと考えていたところ自社がある渋谷区の居酒屋の一覧が表示出来るサイトがあると便利かも知れないと思い実際に作ってみました。

[渋谷のお酒サイト](https://shibuya.osake.calicocat.tokyo/)
[ソースコードはここから](https://github.com/FumihiroMaejima/shibuosake)

![スクリーンショット 2019-09-17 0.46.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/060bf895-736f-1b74-1d40-d8712b1e74a0.png)


開発からデプロイ、リリースまでの期間は4月下旬から7月中旬までのおよそ3ヶ月間です。
主に休日に作業を行い余裕がある時に平日の終業後に自宅で作業をすると言う形を取っていました。
ここでは主に画面のキャプチャを用いて画面構成やどのように作り込みをしたかをまとめていきます。
また、アプリを開発からリリースするまでに考えた事や実践した事、困った事や解決出来た事を時系列的にまとめて次回また何かを作る時に活かせるようにしておきたいなと思います。



# 開発環境
centOS:7.5
Laravel:5.8
PHP:7.3
Vue.js:2.6.0
MySQL:5.7
Nginx:1.16.0
Docker-composeとLaradockを用いて環境を構築しました。
開発機はMacbook Proです。

## 対象ブラウザ
Safari,GoogleChrome,FireFox

## 外部サービス
[ぐるなびWebService](https://api.gnavi.co.jp/api/)



## 構成

本番環境はVPS上に展開し、githubとCircleCIを通して自動的にデプロイが出来る構成にしました。


![kouseizu2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/5dfe4b73-217b-284d-27a0-5d1221da7053.png)



ブラウザからのリクエストに応じてサーバーサイドからぐるなびのAPIにリクエストを送り、店舗情報を取得する仕組みにしています。
1回の画面出力でより多くの店舗情報を表示出来るようにする為に、DBに一時的にデータを保存してDBからも店舗情報を取得するようにしています。
バッチによる定期的な処理で出来るだけ最新に近い状態のデータを取得して画面に出力しています。
DBの容量の増加に対応する為に古いデータから順番に定期的な削除を行っています。これもバッチ処理です。
上記の対応により新しいコンテンツ(店舗情報)を自動的に取得して画面に出力する仕組みを整えています。


## TOPページ newタブ
TOPページを開いた際は「new」タブの店舗情報が表示されます。

![スクリーンショット 2019-09-17 0.46.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/b80cd85c-4a9e-3645-f897-8a62480defe4.png)


このタブに表示されている店舗は直接APIを実行している為最新の店舗情報となります。

サーバーサイドでは下記のパラメーターを付与してAPIに対してリクエストを送っています。
エリアMコード:渋谷区
大業態:お酒


今回のアプリは渋谷区にある居酒屋やお酒を取り扱う店舗の情報を取得するのが目的の為、上記の通りにパラメーターを設定しています。
取得して来たデータをVue.jsのテンプレートで1店舗に1カードで情報を掲載するようにしています。

![スクリーンショット 2019-09-17 0.46.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/aa79e7bb-fd9a-2850-0a61-f3c8c27298e5.png)


カード内の「詳細」ボタンを押す事で店舗の詳細を閲覧出来るようになっています。
「予約をする」ボタンはぐるなびサイトの店舗情報ページへのリンクとなっています。

![スクリーンショット 2019-09-17 0.46.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/7e348995-63ea-32ca-fadd-a75006e9de02.png)


1ページ10店舗のみ表示させている為、残りの店舗については別のページにて閲覧出来るようにしています。

![スクリーンショット 2019-09-20 2.00.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/aa49faf5-64d9-0b33-2dec-42112524b813.png)



## TOPページ areaタブ

「area」タブは各店舗情報に登録されているエリアコードごとに分類しエリアごとの店舗の一覧を表示しています。
行きたい場所にある店舗を探す為のページとなります。

![スクリーンショット 2019-09-20 2.02.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/2df7aaa7-51b9-75b9-ce28-eb4dcccbe3e7.png)


ページ上部のグラフにカーソルを当てるとエリアごとの店舗数が表示されるようになっています。

![スクリーンショット 2019-09-20 2.03.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/9ad062c8-c8c8-a9dc-ad4d-c4db67a226c3.png)


各カードの店舗名を押下するとすぐにぐるなびサイトの店舗情報ページへアクセスするようになっています。

このページの店舗情報はDBに格納されてあるデータから出力しています。

## TOPページ typeタブ

「type」タブは各店舗情報に登録されている小業態のうち、日本酒やビールと言ったお酒に関するデータが登録されている店舗情報のみを分類して表示をしています。
飲みたいお酒がある店舗を探す為のページとなります。

![スクリーンショット 2019-09-20 2.02.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/9421066e-8102-2302-69aa-f00db354dfdf.png)


ページ上部のグラフにカーソルを当てるとお酒カテゴリーごとの店舗数が表示されるようになっています。

![スクリーンショット 2019-09-20 2.03.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238861/83e01672-3f06-a200-e272-d16474b91653.png)


「小業態」と言う項目はお酒以外にも料理名などもデータとして登録出来る為比較的出力される店舗情報が少なくなっています。

各カードの店舗名を押下するとすぐにぐるなびサイトの店舗情報ページへアクセスするようになっています。

このページの店舗情報もDBに格納されてあるデータから出力しています。


## 開発期間中の流れ

### 4月下旬 開発環境の構築


開発環境は以前経験があり慣れていた事もあり、Laradockを採用しました。
実際に使ってみるとMySQLのバージョンがデフォルトで8系になっており、デフォルトの設定のままではマイグレーションが出来ないなどの現象に見舞われました。
下記の設定をMySQLコンテナの「my.cnf」に記述することにより「php artisan migrate」コマンド等でマイグレーションが出来るようになったのですが、ビジネスロジックを書いている時にControllerからINSERTが出来ないと言う現象が発生しました。


```shell-session
# vim /etc/mysql/my.cnf
[mysqld]
default_authentication_plugin=mysql_native_password
```

原因と解決方法を探したところマイグレーションが実行出来ない時の内容と全く同じ内容でほぼ打つ手無しと言う状態でした。
その為開発の途中でMySQLのバージョンを5.7に戻すことにしました。
しばらくはMySQLのバージョンは5.7にしといた方が良さそうです。


Laradockを使う場合、dockeコンテナを作成する前に「laradock」コンテナ内の「.env」ファイル内でMySQLのバージョンを指定出来ます。
コンテナを作成する前に要チェックと言えます。

```shell-session
MYSQL_VERSION=5.7
```


この辺りに関しては[以前書いた記事](https://qiita.com/maejima_f/items/34cf3b8eb5ffbe256ad9)により詳しくまとめさせて頂きましたがLaravelのバージョン6が出たことでLaradockにも影響が出て来ると思われるので要注意です。


### 5月 プロトタイプの作成とVue.js周りの実装

ぐるなびWebサービスを申し込みする際には利用先のURLを入力する必要があります。
申し込みの段階である程度形になっているサイトが必要なのだと判断し、プロトタイプ作りを行いました。
テスト用のデータをjsonファイルに記述し、それを読み込んで店舗情報を出力するように実装して行きました。

(後から分かったことですが、LaravelのTOP画面が表示されるレベルのものでもとりあえずの申し込みが出来るっぽいです。
最初からAPIからデータを取得してこのしていければこの工数は不要でした。)
サーバーサイドの実装は比較的すぐに出来たのですが、フロントの部分をVue.jsで記述しようと決めていた都合上、ほぼほぼ初めて触るところを調べながら実装するの繰り返しで1ヶ月ほど時間をかけることになりました。

ControllerからViewへ渡したデータを更にVueテンプレートに渡すのに調査と検証と重ねることに時間を費やしました。
一方でVueテンプレートで渡したデータは既に加工済みのデータのを取得する為、ぐるなびAPIの本データを取得後の段階では大きな変更をかけないで済みました。

逆にサーバーサイド側のプログラムは実際のAPIから取得して来たデータを加工する為に、プロトタイプ用のプログラムから更なる修正を重ねる必要がありました。

結果的にはプロトタイプを作る段階でもやはり本番とほぼ同様の形式のデータを利用出来るのが一番望ましかったです。


### 6月 APIデータの利用開始に伴うサーバーサイドの開発

6月に入った段階でぐるなびWebサービスに正式に利用申し込みを行いAPIを実行出来るように修正して行きました。
サーバーサイドからGuzzleHttpClientを用いてAPIを実行してデータを取得し、加工してフロントエンドにデータを渡していくと言う構成で実装して行きました。

また、APIの利用に先立ちプロトタイプを本番環境にデプロイしました。
この段階でCircleCIを使った自動デプロイ環境の構築を試みていました。

具体的には「.circleci/config.yml」に下記の通りデプロイの設定を記載しました。

```yaml
- deploy:
  name: Start master deploy
  command: |
    if [ "${CIRCLE_BRANCH}" == "master" ]; then
        ssh -p ${PORT} ${USER_NAME}@${HOST_NAME} "任意のコマンド1;任意のコマンド2;任意のコマンド3;git pull origin master; 任意のコマンド3; "
    fi
```

${USER_NAME}などの変数はcircleCIの管理画面上で設定出来る環境変数です。
コマンドパラメーターだけでなくコマンド文そのものも文字列として登録出来る為sshアクセス後のコマンドを設定することが出来ます。

またデプロイの前にはブランチに関わらずphpcsとphpmdコマンドによるコーディング規約のチェックを行っています。
これによってソースコードに品質を損なわない仕組みを作りました。
(phpunitなどもこの段階で実行したいです。)

```yaml
  - run: vendor/bin/phpcs --standard=phpcs.xml --extensions=php .
  - run: vendor/bin/phpmd . text ruleset.xml --suffixes php --exclude node_modules,resources,storage,vendor,app/Console
```

セキュリティ絡みの話の為簡単にまとめますが、サーバー上の設定においてはcircleciユーザーの下記の部分が特に詰まりました。
・circleciユーザーの.sshディレクトリとauthorized_keysの権限の変更
・circleciユーザーをnginxグループへ追加


デプロイ環境構築後はテスト用ページの作成、APIデータを利用した店舗情報の出力、店舗情報取得のバッチ処理の作成をメインに行い1番実装が多かった期間でした。
ただ、円グラフの実装ではchart.js(vue-chartjs)を採用する関係でLaravelとVue.jsとの兼ね合いをどうしようかと調査するのに時間をかけ過ぎてしまい、下旬はあまり手を動かさなくなっていました。

### 7月 chart.jsを利用した円グラフの描画

6月下旬から始めていたchart.js(vue-chartjs)の実装方法検討に時間がかかり中旬まで円グラフの実装を行っていました。
他の複数のAPIから取得して来たデータとDBから取得して来たデータを異なるタブ内のページに出力する方法を試行錯誤する期間でした。


最終的に下記の様な具合に実装してグラフを描画させました。
1階層上のdivタグでは「tabCheck」を参照する事で対象のタブを指定しています。

```HTML
<div class="col-md-12" v-else-if="tabCheck == 3">
    <div class="half-chart">
        <doughnut-chart :chart-data="doughnutCategoryCollection"></doughnut-chart>
         <input type="hidden" class="category_count_data" :name="category" :value="count" v-for="(count, category) in categoryCount">
    </div>
・
・
・
```

doughnut-chartタグの「doughnutCategoryCollection」と言うグラフのデータは下記の「getCategoryObjectCount」と言うメソッドで取得しています。
画面ロード時にControllerから渡されて来たお酒ごとのカテゴリーの店舗情報をこのメソッド内でグラフ描画に利用しています。


```Vue.js
mounted () {
    this.getAreaObjectCount()
    this.getCategoryObjectCount()
},
methods: {
    /*****/
    /*省略*/
    /*****/
    getCategoryObjectCount () {
        var labels = []
        var dataset = []
        var loopCnt = 0
        var targetTab = 3
        var selectData = document.querySelectorAll(".category_count_data");
        loopCnt = selectData.length
        for(var i=0;i<selectData.length;i++){
            labels.push(selectData[i].name)
            dataset.push(selectData[i].value)
        }

        this.fillData(labels, dataset, loopCnt, targetTab)
    },
    fillData (labels, dataset, loopCnt, chartKey) {
        var dColors = []

        for (var i = 0; i < loopCnt;i++){
            var code = i * 40
            dColors.push('rgba('+code+',255,'+code+',0.5)')
        }

        if(chartKey == 2){
            this.doughnutAreaCollection = {
                labels: labels,
                datasets: [{
                    data: dataset,
                    backgroundColor: dColors
                }],
                options: {
                    responsive: true,
                    maintainAspectRatio: false
                }
            }
        }
        else{
            this.doughnutCategoryCollection = {
                labels: labels,
                datasets: [{
                    data: dataset,
                    backgroundColor: dColors
                }]
            }
        }

    }
},
```


テスト用のページに実装してから本来のページに実装する形を取っていたのですが、6月頭にプロトタイプをデプロイしてから１度もデプロイをしていなかった為本番公開の準備が膨大になりました。
デプロイの仕組み自体は出来ていた為githubにpushしてからは楽でしたが、basic認証をかけるなどして非公開状態にして少しずつデプロイして行った方が良かったなと今では思っています。


## まとめ

今回の開発を通して特に詰まった箇所としましては下記の2点です。
・デプロイ含めたCI環境の構築
・Vue.js周りの実装

やはり全く未経験の事に関しては調査含めて時間がかかり当初予定していた作業期間より2週間ほど多くかかってしまいました。
特にVue.jsに関しては事前の知識がほぼ無い状態からのスタートで始めた為基本的な事項のキャッチアップを含めた工数を自分が想定するよりも多く見積もりを入れる必要がありました。
作って行く事で身に付くとは行ってもやはりある程度事前のキャッチアップをした上で取り組んだ方がその後の工数が抑えられそうですね。
新しい言語を取り入れる時は難易度の低いかつ小規模のものから作り始めるなどして慣れて行くのが良いと改めて実感しました。