[LINE bot] 食べログ3.5以上を検索できるbotに「お気に入り登録」を実装した

#作ったもの
Qiita トレンド入りしたLINE bot「コスパごはん」の機能追加です。

参考▼
[[LINE Bot] 位置情報から食べログ3.5以上の優良店を検索するbot作った](https://qiita.com/NARI_Creator/items/f29112e6f604c86b3c0d)

気になったお店を保存していつでも見れる「お気に入り登録」機能を実装しました。

①「お気に入り登録する」をタップします。
<img width="375" alt="IMG_3006.png" src="https://qiita-image-store.s3.amazonaws.com/0/246680/17be2a7b-0c88-3482-52ce-a4d02b76b894.png">

②"お気に入り"と打つと、①で登録したお店を表示してくれます。
<img width="375" alt="IMG_3007.png" src="https://qiita-image-store.s3.amazonaws.com/0/246680/7d0dbbd2-420f-12f0-6654-ec72c84bc516.png">


#実装内容

## (1)DB用意
Airtableを今回使いました。
(ブラウザ上でサッと簡単にDBを作れるのでめっちゃ楽)

参考記事▼
[Airtable API の使い方](https://qiita.com/rex0220/items/f5fc1e167c24da6ccf49)

MySQLでもpostgresでも使い慣れてるものがあればそちらで構いません。

## (2)Daoクラス作成
DAOってなに？って方は[こちら](https://ja.wikipedia.org/wiki/Data_Access_Object)

＊背景：もうさすがに１ファイル(ajax.php)にまとめていくとコード長くなって可読性悪くなるのと、ちゃんと役割ごとにクラスを作って階層化していく方が全体の構成もキレイになるので、この機会に実装方法を見直しました。（最初はスピード重視だったので１ファイルにドンドン関数書いていくスタイルだった）

### (2-1)Baseクラス作成
よくCurl使うので基底クラスを作りました。
これを**extends**(継承)することで、Daoの実装が楽になり、コードもシンプルになります。
( 継承なにそれ？って方は[こちら](http://www.objective-php.net/basic/extend)を一度読んで理解しましょう)

get, postそれぞれ動的な部分のみ引数としています。
curlする→returnするとシンプルにそれ以上のことはしません。
phpdocもちゃんと書きましょう。

```base-curl-dao.php
<?php

class BaseCurlDao {

  public function __construct() {
     //
  }

  /**
   * Execute GET Curl.
   *
   * @param {Array} $header (HTTPHEADER)
   * @param {String} $url
   * @return {String} curl result (json)
   */
  public function get($header, $url) {
    $conn = curl_init();

    curl_setopt($conn, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($conn, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($conn, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($conn, CURLOPT_POST, false);
    curl_setopt($conn, CURLOPT_HTTPHEADER, $header);
    curl_setopt($conn, CURLOPT_URL, $url);

    $result = curl_exec($conn);

    curl_close($conn);

    return $result;
  }

  /**
   * Execute POST curl.
   *
   * @param {Array} $header (HTTPHEADER)
   * @param {String} $url
   * @param {String} $json_param
   */
  public function post($header, $url, $json_param) {
    $conn = curl_init();

    curl_setopt($conn, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($conn, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($conn, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($conn, CURLOPT_POST, true);
    curl_setopt($conn, CURLOPT_HTTPHEADER, $header);
    curl_setopt($conn, CURLOPT_URL, $url);
    curl_setopt($conn, CURLOPT_POSTFIELDS, $json_param);

    $result = curl_exec($conn);

    curl_close($conn);
  }

}

```
### (2-2)FavoriteDaoクラス作成
先ほどの**BaseCurlDao**をextendsします。
そうするとFavoriteDao内でpost, getが呼び出せるようになります。
→`$this->get()`、`$this->post()`

```favorite-dao.php
<?php
define('FAVORITE_GET_URL', 'https://api.airtable.com/v0/{key}/favorite?maxRecords=10&view=Grid%20view');
define('FAVORITE_POST_URL', 'https://api.airtable.com/v0/{key}/favorite');
define('FAVORITE_HTTPHEADER_AUTH', 'Authorization: Bearer {auth key}');

require('base-curl-dao.php');

class FavoriteDao extends BaseCurlDao {

  public function __construct() {
     //
  }

  /**
   * Get favorite shop records.
   *
   * @param {String} $user_id (LINE userId)
   * @return {Array} favorite records
   */
  public function getFavoriteList($user_id) {
    $filter = "&filterByFormula=AND({user_id}='".$user_id."')";

    $result = $this->get(array(FAVORITE_HTTPHEADER_AUTH), FAVORITE_GET_URL.$filter);

    $records = json_decode($result)->{'records'};
    $list = [];
    $time = [];
    foreach ($records as $record) {
      $list[] = $record->{'fields'};
      $time[] = $record->{'fields'}->{'timestamp'};
    }
    array_multisort($time, SORT_DESC, $list);
    return $list;
  }

  /**
   * Insert favorite shop record.
   *
   * @param {String} $user_id (LINE userId)
   * @param {String} $name (taberogu shop name)
   * @param {String} $service (taberogu service)
   * @param {String} $street (taberogu address street)
   * @param {String} $image_url (taberogu image url)
   * @param {String} $url (taberogu url)
   * @param {Integer} $distance (taberogu distance)
   * @param {String} $rate (taberogu rating)
   */
  public function insertFavorite($user_id, $name, $service, $street, $image_url, $url, $distance, $rate) {
    $data = ['user_id'=>$user_id, 'timestamp' => time(),
    'name'=>$name,'service'=>$service,'street'=>$street,'image_url'=>$image_url,'url'=>$url,
    'distance'=>$distance,'rating'=>$rate];

    $params = ['fields' => $data];

    $this->post(array(FAVORITE_HTTPHEADER_AUTH,'Content-type: application/json'), FAVORITE_POST_URL, json_encode($params));
  }

}
```

// getFavoriteList補足
AirtableはSQLのWhere句の代りに**filterByFormula**を使ってフィルターをかけます。
`&filterByFormula=AND({user_id}="hogehoge");`
となるようにするとuser_idに一致するレコードのみ出力できるようになります。

最近追加したお店を最初に出したいのでtimestampでソースします。
→`array_multisort($time, SORT_DESC, $list);`

こちらを参考に▼
[PHPで連想配列のソート（キーと値基準で昇順と降順）](https://www.flatflag.nir87.com/sort-385)

// insertFavorite補足
jsonでpostするので必ず`'Content-type: application/json'`をHTTPHEADERに入れて、送るデータは`json_encode()`しておきましょう。


## (3)本体への追加実装
### (3-1)お気に入り登録ボタンの追加
`"type"=> "postback"`にして、flex messageに追加します。

また、postbackのデータ(裏側の値)をお気に入りに保存するため、`"data"`に「|」を区切りとして文字列で詰め込みます。


```line-message-service.php
       "contents"=> [
         [
           "type"=> "button",
           "style"=> "link",
           "height"=> "sm",
           "action"=> [
             "type"=> "uri",
             "label"=> "食べログをみる",
             "uri"=> $taberogu->{'url'}
           ]
         ],
         [
           "type"=> "button",
           "style"=> "link",
           "height"=> "sm",
           "action"=> [
             "type"=> "postback",
             "label"=> "お気に入りに登録する",
             "data"=> $taberogu->{'name'}.'|'.$taberogu->{'service'}.'|'.$taberogu->{'street'}.'|'.$taberogu->{'image_url'}.'|'.$taberogu->{'url'}.'|'.$distance.'|'.$taberogu->{'rating'},
             "text"=> "お気に入りに登録する"
           ]
         ],
```

### (3-2)お気に入り登録タップ時の挙動
1. Postbackイベントを受け取る
2. dataを区切り文字で変数に分解し、favoriteにInsertする (**saveToFavorite**)
3. 登録しましたとreplyする

```ajax.php
    foreach ($events as $event) {
       if ($event instanceof FollowEvent) {
          continue;
       } else if ($event instanceof UnfollowEvent) {
          continue;
       } else if ($event instanceof PostbackEvent) { //★ここ★
          $targetName = saveToFavorite($event);
          $bot->replyText($event->getReplyToken(), '「'.$targetName.'」をお気に入りに登録しました');
          continue;
       } else if ($event instanceof TextMessage) {
          processTextMessageEvent($bot, $event);
          continue;
       } else if ($event instanceof LocationMessage) {
          replyTaberguList($bot, $event, $event->getLatitude(), $event->getLongitude());
          continue;
       } else {

       }

    }

--------------

function saveToFavorite($event) {
   list($name, $service, $street, $image_url, $url, $distance, $rate) = explode("|", $event->getPostbackData());
   $favoriteDao = new FavoriteDao();
   $favoriteDao->insertFavorite($event->getUserId(), $name, $service, $street, $image_url, $url, intval($distance), $rate);
   return $name;
}
```

### (3-3)お気に入り一覧の表示 
1.「お気に入り」のテキストで応答
2. お気に入りをDBから取得
3. 一覧(カルーセル)表示

`$text === 'お気に入り'`の時にFavoriteDaoを呼び出してgetFavoriteListを実行→Flex messageをreplyする

```ajax.php
function processTextMessageEvent($bot, $event) {
  $text = $event->getText();
  if (isCategoryText($text)) {
    putCategory($event->getUserId(), $text);
    replayLocationActionMessage($bot, $event->getReplyToken());
  } else if ($text === 'お気に入り'){
    $favoriteDao = new FavoriteDao();
    $favoriteList = $favoriteDao->getFavoriteList($event->getUserId());
    $lineService = new LineMessageService(LINE_MESSAGING_API_CHANNEL_TOKEN);
    $res = $lineService->postFlexMessage($event->getReplyToken(), $favoriteList, true);
  } else if ($text === '更新情報'){
    replyInfomation($bot, $event);
  } else if ($text !== 'お気に入りに登録する'){
    searchFromLocationWord($bot, $event);
    $res = $bot->replyText($event->getReplyToken(),'ジャンル(1〜4)を入力してください。(和=1,洋=2,中=3,その他=4)');
  }
}
```

listの型/中身が**LineMessageService#postFlexMessage**と合うようになっているので既存のメソッドを使えます。
(booleanの引数を追加しているのは、distanceを変換するか否かの意味なのでソースは割愛)


#あとがき
ありがたいことにこのLINE botは友だち**700**を超え、多くの方に利用していただいています。
LINE bot開発者が増えて、便利なbotがたくさん出てくるといいなと思っています。
何かあればDM／コメントください。([作成者Twitter](https://twitter.com/NARI_Creator))

**LINE bot開発参考記事**
[LineBot PHPで簡単シンプルに使えるライブラリを使ってLineBotを作る FlexMessaging対応](https://qiita.com/jyuki/items/035cfeb72d7d359c738b)