【Laravel】天気予報アプリをLINE MessageAPIで作ってみた！

LINE MessageAPIを使って天気予報・ファッションレコメンドアプリを作ってみました。
完成形としては以下の通りです。
![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/0af0bfce-b695-b341-4e90-aeefde544562.png)

## どのようなアプリか
皆さんは、今日の気温を聞いて、**「快適に過ごすために今日のファッションをこうしよう」**ってパッと思いつきますか？

私は、最高気温、最低気温を聞いてもそれがどのくらい暑いのか、寒いのかがピンと来ず、洋服のチョイスを外したことがしばしばあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/c9583e2c-1e6c-a662-e7cf-b9995ca8e3cb.png)

こんな思いを2度としないために今回このアプリを作りました。

LINE MessageAPI初使用＋Laravel初心者の自分でも約1週間くらいで作れた、
簡単アプリなので初心者の方はLaravelの知識定着のためにやってみるといいかもしれません。

## アプリの流れ
アプリの流れは大まかに以下の4つのステップで成り立っています。

・①クライアントが現在地を送る
・②[OpenWeather](https://openweathermap.org/)から天気予報を取得
・③データの整形
・④クライアントに送る

## 初期構成
下記のDocker構成をそのまま使っています。
`LINE MessageAPI`では、`dd`などのデバッグが使えないので、ファサードのログでデバッグを行います。
そのために、`Ngrok`でローカル環境を一時的に外部公開しています。
先にデプロイなどをして外部からアクセスできる状況を作れるのであれば、Ngrokはいりません。

https://github.com/ssk9597/Docker-php-node-nginx-mysql-ngrok

## Ngrokの初期設定
上記のDockerをクローンしていただければ、`$ make install`で環境構築が完了します。

次に、[こちらのサイト](https://dashboard.ngrok.com/get-started/setup)で`AuthToken`を取得してください。

`AuthToken`が取得できれば、次にこれを`.env`に保存しましょう。

```env:.env
NGROK_AUTH=123456789abcdefghijklmnopqrstuvwxyz
```

それでは以下のURLにアクセスしましょう！

http://localhost:4040

この画面になれば成功です！
![スクリーンショット 2021-05-29 16.07.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/90ae42bd-558e-581d-29ce-88d2fbe97a2a.png)

## `LINE Developers`にアカウントを作成する
ここはググればいくらでもやり方が載っているので所々端折りつつ説明します。

[LINE Developers](https://developers.line.biz/ja/)にアクセスして、「ログイン」ボタンをクリックしてください。

その後諸々入力してもらったら以下のように作成できるかと思います。
注意事項としては、今回`Messaging API`となるので、**チャネルの種類**を間違えた方は修正してください。

![スクリーンショット 2021-05-29 16.13.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/c255d10d-2dcf-15ed-1fea-fdbeed6da750.png)

**チャネルシークレット**と**チャネルアクセストークン**が必要になるのでこの2つを発行します。

![スクリーンショット 2021-05-29 16.16.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/ede74ac9-add0-c180-016d-efcab62e912a.png)

![スクリーンショット 2021-05-29 16.17.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b5c7bf10-1726-28df-0eec-99a1447f7e11.png)

ではこの2つを`.env`に入力します。

```env:src/.env
LINE_CHANNEL_SECRET=abcdefg123456
LINE_CHANNEL_ACCESS_TOKEN=HogeHogeHoge123456789HogeHogeHoge
```

## `Webhook`の設定
以下にアクセスしてください。

http://localhost:4040

![スクリーンショット 2021-05-29 16.22.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/151eb7cc-bfa7-fc06-6033-97310c9f28b4.png)

`https`のURLをコピーしてください。

これをLINE DevelopersのWebhookに設定します。
![スクリーンショット 2021-05-29 16.24.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/085626dd-cc5c-5edc-833c-3a60ea813498.png)



これで初期設定は完了です。
コードを書いていきましょう！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/2a10745f-de7e-c309-3f6e-676e352501e2.png)


## `linecorp/line-bot-sdk`のインストール
LINE MessageAPIは、公式がPHP向けパッケージを公開しているので、これをインストールします。

```terminal:ターミナル
$ docker-compose exec app composer require linecorp/line-bot-sdk
```

Dockerを知らない方もいると思うので一応解説です。
`docker-compose.yml`を見てください。
`app`コンテナ内にLaravelの環境が作られているので、
こちらのコンテナ内でパッケージをインストールします。

```yml:docker-compose.yml
services:
  # php（Laravelのコードなどがある）
  app:
    build: ./docker/php
    volumes:
      - ./src:/laravel

  # nginx（WEBサーバー）
  web:
    build: ./docker/nginx
    ports:
      - 10080:80
    volumes:
      - ./src:/laravel
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    working_dir: /laravel

  # MySQL（DB、今回使用しません）
  db:
    build: ./docker/mysql
    volumes:
      - db-data:/var/lib/mysql
    ports:
      - 3306:3306

  # ngrok（Nginxを外部からアクセスできるようにする）
  ngrok:
    image: wernight/ngrok
    ports:
      - 4040:4040
    environment:
      NGROK_AUTH: ${NGROK_AUTH}
      NGROK_PROTOCOL: http
      NGROK_PORT: web:80
```

## `Controller`, `Services`, `Common`を作成する
今回はすべてのコードをControllerに書くと`Fat Controller`となってしまいます。
**`Fat Controller`って何やねん**って方は以下の記事をご覧ください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/06f1c63a-86c5-6259-3e95-42c2f172dc06.png)


https://qiita.com/Shiruba/items/b0754a5815e0583aa8ce

https://qiita.com/nunulk/items/6ed409345efb6ee4f660

なので、自作関数を作りそれを`Common`ディレクトリに配置し、ビジネスロジックを`Services`ディレクトリに配置します。
`Controller`は、`Services`を呼び出すだけという構成を取ります。

ドメイン駆動設計に基づいて設計することが多いNode.jsでは、`Controller`と`Service`に分けることが多いためこのような構成を採用しました。

https://qiita.com/nunulk/items/6b7a7bbda17192f6b2f5

```terminal:ターミナル
// Controllerの作成
$ docker-compose exec app php artisan make:controller LINEController
// Servicesの作成
$ docker-compose exec app mkdir app/Services
// Commonの作成
$ docker-compose exec app mkdir app/Common
```

## ルーティングを作成する
今回はAPIとして使用するため、`api.php`にルーティング処理を記述します。

```php:routes/api.php
<?php

use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
*/
Route::post("/line/message", "LINEController@sendMessage");
```

では、このURLを`Webhook`に設定します。

![スクリーンショット 2021-05-29 16.35.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/5a987ab0-032e-4309-3919-9756a0597070.png)

## `.env`の値を取得する関数を作成する
`.env`で記述した、**LINE_CHANNEL_SECRET**と**LINE_CHANNEL_ACCESS_TOKEN**を取得する関数を作成します。

**自作関数ってどう作るの？**って方は以下の記事を事前にご覧になっておくと理解が進みやすいかと思います。

https://www.ravness.com/posts/laravelfunction

その前にディレクトリの構成に関して説明します。
最終的には以下のような構成となります。
いろんなところで使い回すものは`Util.php`、メッセージの枠組みを作る、`Builderディレクトリ`、**Services**で呼び出す`Eventディレクトリ`のように分けています。
また、`Guzzle.php`はパッケージのguzzleを使うときに使用します。

```
├── Common/
│   ├── LINE/
│   │   │   └── Builder/
│   │   │   │   └── ButtonMessages.php
│   │   │   │   └── FlexMessages/php
│   │   │   └── Event/
│   │   │   │   └── LocationMessages.php
│   │   │   │   └── TextMessages.php
│   │   │   └── Util.php
│   ├── Guzzle.php
```

```php:app/Common/LINE/Util.php
<?php
namespace App\Common\LINE;

class Util
{
  // channelSecretを取得
  public static function getChannelSecret()
  {
    $channelSecret = env("LINE_CHANNEL_SECRET");
    return $channelSecret;
  }

  // channelAccessTokenを取得
  public static function getChannelAccessToken()
  {
    $channelAccessToken = env("LINE_CHANNEL_ACCESS_TOKEN");
    return $channelAccessToken;
  }
}
```

この自作関数を使えるように、`config/app.php`に追記します。

```php:config/app.php
'aliases' => [
    ## (省略)
    "Util" => App\Common\LINE\Util::class,
  ],
```

## 署名の検証を行う
[公式ドキュメント](https://developers.line.biz/ja/reference/messaging-api/#signature-validation)のこちらの対応を行います。

使用するクラスは以下の2つです。
`use LINE\LINEBot\Constant\HTTPHeader;`
[ドキュメント](https://line.github.io/line-bot-sdk-php/class-LINE.LINEBot.Constant.HTTPHeader.html)

`use LINE\LINEBot\SignatureValidator;`
[ドキュメント](https://line.github.io/line-bot-sdk-php/class-LINE.LINEBot.SignatureValidator.html)

### 自作関数で同一ファイル内の自作関数にアクセスする際は、注意が必要です。
`$this`ではエラーが発生します。

```php:
  public static function isSignature($request)
  {
    // エラー
    $signature = $this->getSignature($request);
  }
```

何故エラーが発生するのかというと、インスタンス化されていないからです。
なので、`self::`でアクセスするのが正しいです！

```php:app/Common/LINE/Util.php
class Util
{
  // channelSecretを取得
  public static function getChannelSecret()
  {
    $channelSecret = env("LINE_CHANNEL_SECRET");
    return $channelSecret;
  }

  // channelAccessTokenを取得
  public static function getChannelAccessToken()
  {
    $channelAccessToken = env("LINE_CHANNEL_ACCESS_TOKEN");
    return $channelAccessToken;
  }

  // 署名を発行
  public static function getSignature($request)
  {
    $signature = $request->headers->get(HTTPHeader::LINE_SIGNATURE);
    return $signature;
  }

  // 署名があるかどうかを判別する
  public static function isSignature($request)
  {
    // 署名を取得
    $signature = self::getSignature($request);
    // channelSecretを取得
    $channelSecret = self::getChannelSecret();

    // 検証
    if (!SignatureValidator::validateSignature($request->getContent(), $channelSecret, $signature)) {
      return;
    }
  }
}
```

## メッセージを送る準備
[公式ドキュメント](https://developers.line.biz/ja/reference/messaging-api/#send-reply-message)のこちらの対応を行います。
ただし、テキストメッセージはまだ作成していないため、`$httpClient`と`$bot`の作成をします。

使用するクラス、ネームスペースは以下の2つです。
`use LINE\LINEBot;`
[ドキュメント](https://line.github.io/line-bot-sdk-php/namespace-LINE.LINEBot.html)

`use LINE\LINEBot\HTTPClient\CurlHTTPClient;`
[ドキュメント](https://line.github.io/line-bot-sdk-php/class-LINE.LINEBot.HTTPClient.CurlHTTPClient.html)

```php:app/Common/LINE/Util.php
<?php

namespace App\Common\LINE;

// LINE
use LINE\LINEBot;
use LINE\LINEBot\Constant\HTTPHeader;
use LINE\LINEBot\SignatureValidator;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;

class Util
{
  // channelSecretを取得
  public static function getChannelSecret()
  {
    $channelSecret = env("LINE_CHANNEL_SECRET");
    return $channelSecret;
  }

  // channelAccessTokenを取得
  public static function getChannelAccessToken()
  {
    $channelAccessToken = env("LINE_CHANNEL_ACCESS_TOKEN");
    return $channelAccessToken;
  }

  // 署名を発行
  public static function getSignature($request)
  {
    $signature = $request->headers->get(HTTPHeader::LINE_SIGNATURE);
    return $signature;
  }

  // 署名があるかどうかを判別する
  public static function isSignature($request)
  {
    // 署名を取得
    $signature = self::getSignature($request);
    // channelSecretを取得
    $channelSecret = self::getChannelSecret();

    // 検証
    if (!SignatureValidator::validateSignature($request->getContent(), $channelSecret, $signature)) {
      return;
    }
  }

  // メッセージを送る準備
  public static function prepareToSendMessage()
  {
    // channelSecretの取得
    $channelSecret = self::getChannelSecret();
    // channelAccessTokenの取得
    $channelAccessToken = self::getChannelAccessToken();

    $httpClient = new CurlHTTPClient($channelAccessToken);
    $bot = new LINEBot($httpClient, ['channelSecret' => $channelSecret]);

    return $bot;
  }
}
```

## `Webhook`でeventsを取得する
[公式ドキュメント](https://github.com/line/line-bot-sdk-php#webhook)のこちらの対応を行います。

ユーザーのアクション（メッセージ、画像、位置情報など）を、クライアントのボットサーバーに送信します。
その際のリクエストにはユーザーのアクションがイベントととして含まれています。

Webhookの処理方法を以下の3つです。

①LINEのサーバーからWebhookを受信する
②parseEventRequest(`$body`, `$signature`)でリクエストを配列にする。
③解析されたイベントを順次処理し、必要に応じてリアクションを行う。

②が簡単なので②を採用します。

```php:app/Common/LINE/Util.php
<?php

namespace App\Common\LINE;

// LINE
use LINE\LINEBot;
use LINE\LINEBot\Constant\HTTPHeader;
use LINE\LINEBot\SignatureValidator;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;

class Util
{
  // channelSecretを取得
  public static function getChannelSecret()
  {
    $channelSecret = env("LINE_CHANNEL_SECRET");
    return $channelSecret;
  }

  // channelAccessTokenを取得
  public static function getChannelAccessToken()
  {
    $channelAccessToken = env("LINE_CHANNEL_ACCESS_TOKEN");
    return $channelAccessToken;
  }

  // 署名を発行
  public static function getSignature($request)
  {
    $signature = $request->headers->get(HTTPHeader::LINE_SIGNATURE);
    return $signature;
  }

  // 署名があるかどうかを判別する
  public static function isSignature($request)
  {
    // 署名を取得
    $signature = self::getSignature($request);
    // channelSecretを取得
    $channelSecret = self::getChannelSecret();

    // 検証
    if (!SignatureValidator::validateSignature($request->getContent(), $channelSecret, $signature)) {
      return;
    }
  }

  // メッセージを送る準備
  public static function prepareToSendMessage()
  {
    // channelSecretの取得
    $channelSecret = self::getChannelSecret();
    // channelAccessTokenの取得
    $channelAccessToken = self::getChannelAccessToken();

    $httpClient = new CurlHTTPClient($channelAccessToken);
    $bot = new LINEBot($httpClient, ['channelSecret' => $channelSecret]);

    return $bot;
  }

  // webhookでeventsを取得
  public static function getEventsByWebhook($request)
  {
    // botを取得
    $bot = self::prepareToSendMessage();
    // signatureを取得
    $signature = self::getSignature($request);

    $events = $bot->parseEventRequest($request->getContent(), $signature);

    return $events;
  }
}
```

## `リプライトークン`を取得する
[公式ドキュメント](https://developers.line.biz/ja/reference/messaging-api/#send-reply-message)のこちらの対応を行います。
ただし、テキストメッセージはまだ作成していないため、`$replyToken`の作成をします。

使用するクラスは以下の通りです。
[ドキュメント](https://line.github.io/line-bot-sdk-php/class-LINE.LINEBot.Event.BaseEvent.html)

```php:app/Common/LINE/Util.php
<?php

namespace App\Common\LINE;

// LINE
use LINE\LINEBot;
use LINE\LINEBot\Constant\HTTPHeader;
use LINE\LINEBot\SignatureValidator;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;

class Util
{
  // channelSecretを取得
  public static function getChannelSecret()
  {
    $channelSecret = env("LINE_CHANNEL_SECRET");
    return $channelSecret;
  }

  // channelAccessTokenを取得
  public static function getChannelAccessToken()
  {
    $channelAccessToken = env("LINE_CHANNEL_ACCESS_TOKEN");
    return $channelAccessToken;
  }

  // 署名を発行
  public static function getSignature($request)
  {
    $signature = $request->headers->get(HTTPHeader::LINE_SIGNATURE);
    return $signature;
  }

  // 署名があるかどうかを判別する
  public static function isSignature($request)
  {
    // 署名を取得
    $signature = self::getSignature($request);
    // channelSecretを取得
    $channelSecret = self::getChannelSecret();

    // 検証
    if (!SignatureValidator::validateSignature($request->getContent(), $channelSecret, $signature)) {
      return;
    }
  }

  // メッセージを送る準備
  public static function prepareToSendMessage()
  {
    // channelSecretの取得
    $channelSecret = self::getChannelSecret();
    // channelAccessTokenの取得
    $channelAccessToken = self::getChannelAccessToken();

    $httpClient = new CurlHTTPClient($channelAccessToken);
    $bot = new LINEBot($httpClient, ['channelSecret' => $channelSecret]);

    return $bot;
  }

  // webhookでeventsを取得
  public static function getEventsByWebhook($request)
  {
    // botを取得
    $bot = self::prepareToSendMessage();
    // signatureを取得
    $signature = self::getSignature($request);

    $events = $bot->parseEventRequest($request->getContent(), $signature);

    return $events;
  }

  // replyTokenを取得
  public static function getReplyToken($event)
  {
    $replyToken = $event->getReplyToken();
    return $replyToken;
  }
}
```

これで`Util.php`が作成完了しました！！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/c8ff7455-7d17-66e4-4613-72cc331c0f4f.png)


## それでは次に`ボタン`を作成しましょう
使用するクラスは以下の3つです。

`use LINE\LINEBot\TemplateActionBuilder\UriTemplateActionBuilder;`
[ドキュメント](https://line.github.io/line-bot-sdk-php/class-LINE.LINEBot.TemplateActionBuilder.UriTemplateActionBuilder.html)

`use LINE\LINEBot\MessageBuilder\TemplateBuilder\ButtonTemplateBuilder;`
[ドキュメント](https://line.github.io/line-bot-sdk-php/class-LINE.LINEBot.MessageBuilder.TemplateBuilder.ButtonTemplateBuilder.html)

`use LINE\LINEBot\MessageBuilder\TemplateMessageBuilder;`
[ドキュメント](https://line.github.io/line-bot-sdk-php/class-LINE.LINEBot.MessageBuilder.TemplateMessageBuilder.html)

UriTemplateActionBuilder→ButtonTemplateBuilder→TemplateMessageBuilderの順でwrapしていきます。

![小見出しを追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/820b0efe-9a5e-a434-3912-81f87e073ef8.png)

### `Event/Textmessages.php`の作成
```php:app/Common/LINE/Event/TextMessages.php
<?php

namespace App\Common\LINE\Event;

// Common
use ButtonMessages;
use Util;

// LINE
use LINE\LINEBot\MessageBuilder\TextMessageBuilder;

class TextMessages
{
  public static function eventTextMessage($event)
  {
    // Utilから必要なものを呼び出す
    // $bot
    $bot = Util::prepareToSendMessage();
    // $replyToken
    $replyToken = Util::getReplyToken($event);

    // テキストメッセージのテキストを取得する
    $message = $event->getText();

    // 入力された文字が「今日の洋服は？」かどうかで応答メッセージを変更する
    if ($message === "今日の洋服は？") {
      // text
      $btn_text = "現在地を送る";
      $btn_url = "https://line.me/R/nv/location/";
      $btn_builder = "現在地を送ってください";
      $btn_message = "今日はどんな洋服にしようかな";
    } else {
      $textMessage = new TextMessageBuilder("ごめんなさい、このメッセージは対応していません。");
      $bot->replyMessage($replyToken, $textMessage);
    }
  }
}
```

### `Builder/Buttonmessages.php`の作成
```php:app/Common/LINE/Builder/Buttonmessages.php
<?php

namespace App\Common\LINE\Builder;

// LINE
use LINE\LINEBot\TemplateActionBuilder\UriTemplateActionBuilder;
use LINE\LINEBot\MessageBuilder\TemplateBuilder\ButtonTemplateBuilder;
use LINE\LINEBot\MessageBuilder\TemplateMessageBuilder;

class ButtonMessages
{
  public static function createButtonMessage($bot, $replyToken, $btn_text, $btn_url, $btn_message, $btn_builder)
  {
    $buttonURL = new UriTemplateActionBuilder($btn_text, $btn_url);
    $buttonMessage = new ButtonTemplateBuilder(null, $btn_message, null, [$buttonURL]);
    $bot->replyMessage($replyToken, new TemplateMessageBuilder($btn_builder, $buttonMessage));
  }
}
```

### `Event/Textmessages.php`で、`createButtonMessage`を呼び出す
```php:app/Common/LINE/Event/TextMessages.php
<?php

namespace App\Common\LINE\Event;

// Common
use ButtonMessages;
use Util;

// LINE
use LINE\LINEBot\MessageBuilder\TextMessageBuilder;

class TextMessages
{
  public static function eventTextMessage($event)
  {
    // Utilから必要なものを呼び出す
    // $bot
    $bot = Util::prepareToSendMessage();
    // $replyToken
    $replyToken = Util::getReplyToken($event);

    // テキストメッセージのテキストを取得する
    $message = $event->getText();

    // 入力された文字が「今日の洋服は？」かどうかで応答メッセージを変更する
    if ($message === "今日の洋服は？") {
      // text
      $btn_text = "現在地を送る";
      $btn_url = "https://line.me/R/nv/location/";
      $btn_builder = "現在地を送ってください";
      $btn_message = "今日はどんな洋服にしようかな";

      // builder->ButtonMessages
      ButtonMessages::createButtonMessage($bot, $replyToken, $btn_text, $btn_url, $btn_message, $btn_builder);
    } else {
      $textMessage = new TextMessageBuilder("ごめんなさい、このメッセージは対応していません。");
      $bot->replyMessage($replyToken, $textMessage);
    }
  }
}
```

### `config/app.php`に追記する

```php:config/app.php
'aliases' => [
    ## (省略)
    "Util" => App\Common\LINE\Util::class,
    "ButtonMessages" => App\Common\LINE\Builder\ButtonMessages::class,
    "TextMessages" => App\Common\LINE\Event\TextMessages::class,
  ],
```

### メッセージが送信されるか検証する
では、作った`Common`ディレクトリを使ってビジネスロジックを記述する`Service`層の作成をしていきましょう。

Service層をどのように作ればいいのかわからない方は以下の記事をご覧になってから進めてみてください。

https://qiita.com/ntm718/items/14751e6d52b4bebde810


```php:app/Services/LINEService.php
<?php

namespace App\Services;

use Illuminate\Http\Request;
// LINE
use LINE\LINEBot\Event\MessageEvent\TextMessage;
// Library
use TextMessages;
use Util;
// logs
use Illuminate\Support\Facades\Log;

class LINEService
{
  public function sendMessage(Request $request)
  {
    //Webhookの処理
    $events = Util::getEventsByWebhook($request);

    // ログの取得
    Log::info($events);

    foreach ($events as $event) {
      // eventがテキストメッセージの時
      if ($event instanceof TextMessage) {
        TextMessages::eventTextMessage($event);
      }

      return;
    }
  }
}
```

これでService層は完成です。
また、ログで正しく値が取れているか検証したいときは、`Log::info()`で取得できます。
このログは、**`storage/logs/laravel.log`**に格納されます。

では、このビジネスロジックを`Controller`で呼び出しましょう。

```php:app/Http/Controllers/LINEController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
// Service
use App\Services\LINEService;

class LINEController extends Controller
{
  public function sendMessage(LINEService $line_service, Request $request)
  {
    $line_service->sendMessage($request);
  }
}
```

では、LINEでメッセージが送られてくるか確認しましょう。

![iOS の画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/836f4aef-2389-248c-f4c3-7cbf954e51d8.png)

成功ですね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/f88d160f-9ab9-5f8f-16c5-b994c45fbcf8.png)

## それでは次に、Flex Messageを作成しましょう
## Flex Messageとは？
Flex Messageは、CSS Flexible box (CSS Flexbox) をもとに自由にカスタマイズできるメッセージです。

メッセージのサイズを調整したり、特定の場所にテキスト、画像、アイコンを割り当てたり、インタラクティブなボタンを追加できます。

作成ツールとしては以下の2つがあります。
[Flex Message Simulator](https://developers.line.biz/flex-simulator/)
[LINE Bot Designer](https://developers.line.biz/ja/bot-designer/download/)

ダウンロード不要で使える、**Flex Message Simulator**でいいかなと思います。

## 作成手順
### ①OpenWeatherで天気予報を取得するためのURLを作る
OpenWeatherで天気予報を取得するために必要な情報が3つあります。
①API
②経度
③緯度
それではこの3つを取得していきましょう。

#### ①API
以下にアクセスしてください。

https://openweathermap.org/

アカウントを作成し、APIキーを発行してください。

![スクリーンショット 2021-05-29 20.28.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/a044d998-32d7-a132-c94b-6e55208b6819.png)

発行できたらこのAPIを`.env`に保存します。

```env:src/.env
# Weather
WEATHER_API=a11b22c33d44e55f66g77
```

あとは関数内で`.env`を取得するだけです。

#### ②経度、③緯度
位置情報メッセージをサーバー側に送信しているので、
その位置情報から経度と緯度を取得します。
[公式ドキュメント](https://line.github.io/line-bot-sdk-php/class-LINE.LINEBot.Event.MessageEvent.LocationMessage.html)を見ると、`getLatitude`と`getLongitude`で取得できることがわかります。

#### 実際にURLを作成する
```php:app/Common/LINE/Event/LocationMessage.php
<?php

namespace App\Common\LINE\Event;

class LocationMessages
{
  // OpenWeatherからguzzleでデータを取得
  public static function getWeatherData($event)
  {
    // 緯度・経度を取得
    $latitude = $event->getLatitude();
    $longitude = $event->getLongitude();

    // API
    $weatherAPI = env("WEATHER_API");

    // OpenWeather
    $openWeather_url = "https://api.openweathermap.org/data/2.5/onecall?lat=" . $latitude . "&lon=" . $longitude . "&units=metric&lang=ja&appid=" . $weatherAPI;
  }
}
```

`config/app.php`に追記します。

```php:config/app.php
'aliases' => [
    ## (省略)
    "Util" => App\Common\LINE\Util::class,
    "ButtonMessages" => App\Common\LINE\Builder\ButtonMessages::class,
    "TextMessages" => App\Common\LINE\Event\TextMessages::class,
    "LocationMessages" => App\Common\LINE\Event\LocationMessages::class,
  ],
```

### ②Guzzleの関数を作る
HTTPリクエストを行います。
`curl`コマンドでも良かったのですが、使いやすそうな[Guzzle](https://github.com/guzzle/guzzle)を使うこととします。
Githubが非常にわかりやすいのでこちらを見るだけで使えるようになると思います。

```php:app/Common/Guzzle.php
<?php

namespace App\Common;

// Guzzle
use GuzzleHttp\Client;

class Guzzle
{
  public static function getGuzzle($url)
  {
    $client = new Client();
    $response = $client->request("GET", $url);
    return $response->getBody();
  }
}
```

`config/app.php`に追記します。

```php:config/app.php
'aliases' => [
    ## (省略)
    "Util" => App\Common\LINE\Util::class,
    "ButtonMessages" => App\Common\LINE\Builder\ButtonMessages::class,
    "TextMessages" => App\Common\LINE\Event\TextMessages::class,
    "LocationMessages" => App\Common\LINE\Event\LocationMessages::class,
    "Guzzle" => App\Common\Guzzle::class,
  ],
```

### ③Guzzleで天気予報を取得する
Guzzleを使って天気予報を取得しましょう。

```php:app/Common/LINE/Event/LocationMessage.php
<?php

namespace App\Common\LINE\Event;

class LocationMessages
{
  // OpenWeatherからguzzleでデータを取得
  public static function getWeatherData($event)
  {
    // 緯度・経度を取得
    $latitude = $event->getLatitude();
    $longitude = $event->getLongitude();

    // API
    $weatherAPI = env("WEATHER_API");

    // OpenWeather
    $openWeather_url = "https://api.openweathermap.org/data/2.5/onecall?lat=" . $latitude . "&lon=" . $longitude . "&units=metric&lang=ja&appid=" . $weatherAPI;

    //common->guzzle
    $weathers = Guzzle::getGuzzle($openWeather_url);

    // JSON->Arrayに変換
    $weathers = json_decode($weathers, true);

    return $weathers;
  }
}
```

### ④その天気予報のデータを整形する
今回必要なデータは以下の3つです。
①今日の日付
②天気予報
③体感温度（朝、日中、夕方、夜）

```php:app/Common/LINE/Event/LocationMessage.php
<?php

namespace App\Common\LINE\Event;

// Common
use Guzzle;
use Util;

class LocationMessages
{
  // OpenWeatherからguzzleでデータを取得
  public static function getWeatherData($event)
  {
    // 緯度・経度を取得
    $latitude = $event->getLatitude();
    $longitude = $event->getLongitude();

    // API
    $weatherAPI = env("WEATHER_API");

    // OpenWeather
    $openWeather_url = "https://api.openweathermap.org/data/2.5/onecall?lat=" . $latitude . "&lon=" . $longitude . "&units=metric&lang=ja&appid=" . $weatherAPI;

    //common->guzzle
    $weathers = Guzzle::getGuzzle($openWeather_url);

    // JSON->Arrayに変換
    $weathers = json_decode($weathers, true);

    return $weathers;
  }

  // getWeatherDataを整形する
  public static function dataFormatting($event)
  {
    // getWeatherDataを取得
    $weathers = self::getWeatherData($event);

    // 時刻
    $time = $weathers["daily"][0]["dt"];
    $time = date("Y/m/d", $time);
    // 天気予報
    $weatherInformation = $weathers["daily"][0]["weather"][0]["description"];
    // 体感温度（ファッション）（朝、日中、夕方、夜）
    $mornTemperature = $weathers["daily"][0]["feels_like"]["morn"];
    $dayTemperature = $weathers["daily"][0]["feels_like"]["day"];
    $eveTemperature = $weathers["daily"][0]["feels_like"]["eve"];
    $nightTemperature = $weathers["daily"][0]["feels_like"]["night"];

    // 最高気温で洋服を分岐する
    $arrayTemperature = array($mornTemperature, $dayTemperature, $eveTemperature, $nightTemperature);
    $highestTemperature = max($arrayTemperature);

    if ($highestTemperature >= 26) {
      $fashionAdvice = "暑い！半袖が活躍する時期です。少し歩くだけで汗ばむ気温なので半袖1枚で大丈夫です。ハットや日焼け止めなどの対策もしましょう";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/60aa3c44153071e6df530eb7_71.png";
    } else if ($highestTemperature >= 21) {
      $fashionAdvice = "半袖と長袖の分かれ目の気温です。日差しのある日は半袖を、曇りや雨で日差しがない日は長袖がおすすめです。この気温では、半袖の上にライトアウターなどを着ていつでも脱げるようにしておくといいですね！";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e58a5923ad81f73ac747_10.png";
    } else if ($highestTemperature >= 16) {
      $fashionAdvice = "レイヤードスタイルが楽しめる気温です。ちょっと肌寒いかな？というくらいの過ごしやすい時期なので目一杯ファッションを楽しみましょう！日中と朝晩で気温差が激しいので羽織ものを持つことを前提としたコーディネートがおすすめです。";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6087da411a3ce013f3ddcd42_66.png";
    } else if ($highestTemperature >= 12) {
      $fashionAdvice = "じわじわと寒さを感じる気温です。ライトアウターやニットやパーカーなどが活躍します。この時期は急に暑さをぶり返すことも多いのでこのLINEで毎日天気を確認してくださいね！";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e498e7d26507413fd853_4.png";
    } else if ($highestTemperature >= 7) {
      $fashionAdvice = "そろそろ冬本番です。冬服の上にアウターを羽織ってちょうどいいくらいです。ただし室内は暖房が効いていることが多いので脱ぎ着しやすいコーディネートがおすすめです！";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e4de7156326ff560b1a1_6.png";
    } else {
      $fashionAdvice = "凍えるほどの寒さです。しっかり厚着して、マフラーや手袋、ニット帽などの冬小物もうまく使って防寒対策をしましょう！";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056ebd3ea0ff76dfc900633_48.png";
    }

    // 上記の必要項目を配列にする
    $weatherArray = array($time, $imageURL, $weatherInformation, $mornTemperature, $dayTemperature, $eveTemperature, $nightTemperature, $fashionAdvice);
  }
}
```

### ⑤整形したデータでFlex Messageのテンプレートを作成する

```php:app/Common/LINE/FlexMessages.php
<?php

namespace App\Common\LINE\Builder;

class FlexMessages
{
  // FlexMessageのテンプレート
  public static function getFlexMessageTemplate($message)
  {
    return [
      "type" => "bubble",
      "header" => [
        "type" => "box",
        "layout" => "vertical",
        "contents" => [
          [
            "type" => "text",
            "text" => $message[0],
            "color" => "#FFFFFF",
            "align" => "center",
            "weight" => "bold"
          ]
        ]
      ],
      "hero" => [
        "type" => "image",
        "url" => $message[1],
        "size" => "full"
      ],
      "body" => [
        "type" => "box",
        "layout" => "vertical",
        "contents" => [
          [
            "type" => "text",
            "text" => "天気は、「" . $message[2] . "」です",
            "weight" => "bold",
            "align" => "center"
          ],
          [
            "type" => "text",
            "text" => "■体感気温",
            "margin" => "lg"
          ],
          [
            "type" => "text",
            "text" => "朝：" . $message[3] . "℃",
            "margin" => "sm",
            "size" => "sm",
            "color" => "#C8BD16"
          ],
          [
            "type" => "text",
            "text" => "日中：" . $message[4] . "℃",
            "margin" => "sm",
            "size" => "sm",
            "color" => "#789BC0"
          ],
          [
            "type" => "text",
            "text" => "夕方：" . $message[5] . "℃",
            "margin" => "sm",
            "size" => "sm",
            "color" => "#091C43"
          ],
          [
            "type" => "text",
            "text" => "夜：" . $message[6] . "℃",
            "margin" => "sm",
            "size" => "sm",
            "color" => "#004032"
          ],
          [
            "type" => "separator",
            "margin" => "xl"
          ],
          [
            "type" => "text",
            "text" => "■洋服アドバイス",
            "margin" => "xl"
          ],
          [
            "type" => "text",
            "text" => $message[7],
            "margin" => "sm",
            "wrap" => true,
            "size" => "xs"
          ]
        ],
      ],
      "styles" => [
        "header" => [
          "backgroundColor" => "#00B900"
        ],
        "hero" => [
          "separator" => false
        ]
      ]
    ];
  }
}
```

`config/app.php`に追記します。

```php:config/app.php
'aliases' => [
    ## (省略)
    "Util" => App\Common\LINE\Util::class,
    "ButtonMessages" => App\Common\LINE\Builder\ButtonMessages::class,
    "TextMessages" => App\Common\LINE\Event\TextMessages::class,
    "LocationMessages" => App\Common\LINE\Event\LocationMessages::class,
    "Guzzle" => App\Common\Guzzle::class,
    "FlexMessages" => App\Common\LINE\Builder\FlexMessages::class,
  ],
```

### ⑥Flex Messageのテンプレートの整形を行う

```php:app/Common/LINE/FlexMessages.php
<?php

namespace App\Common\LINE\Builder;

class FlexMessages
{
  // FlexMessageを作成する
  public static function createFlexMessage($message)
  {
    $contents = self::getFlexMessageTemplate($message);
    return ["type" => "flex", "altText" => "This is a Flex Message", "contents" => $contents];
  }
  // FlexMessageのテンプレート
  public static function getFlexMessageTemplate($message)
  {
    return [
      "type" => "bubble",
      "header" => [
        "type" => "box",
        "layout" => "vertical",
        "contents" => [
          [
            "type" => "text",
            "text" => $message[0],
            "color" => "#FFFFFF",
            "align" => "center",
            "weight" => "bold"
          ]
        ]
      ],
      "hero" => [
        "type" => "image",
        "url" => $message[1],
        "size" => "full"
      ],
      "body" => [
        "type" => "box",
        "layout" => "vertical",
        "contents" => [
          [
            "type" => "text",
            "text" => "天気は、「" . $message[2] . "」です",
            "weight" => "bold",
            "align" => "center"
          ],
          [
            "type" => "text",
            "text" => "■体感気温",
            "margin" => "lg"
          ],
          [
            "type" => "text",
            "text" => "朝：" . $message[3] . "℃",
            "margin" => "sm",
            "size" => "sm",
            "color" => "#C8BD16"
          ],
          [
            "type" => "text",
            "text" => "日中：" . $message[4] . "℃",
            "margin" => "sm",
            "size" => "sm",
            "color" => "#789BC0"
          ],
          [
            "type" => "text",
            "text" => "夕方：" . $message[5] . "℃",
            "margin" => "sm",
            "size" => "sm",
            "color" => "#091C43"
          ],
          [
            "type" => "text",
            "text" => "夜：" . $message[6] . "℃",
            "margin" => "sm",
            "size" => "sm",
            "color" => "#004032"
          ],
          [
            "type" => "separator",
            "margin" => "xl"
          ],
          [
            "type" => "text",
            "text" => "■洋服アドバイス",
            "margin" => "xl"
          ],
          [
            "type" => "text",
            "text" => $message[7],
            "margin" => "sm",
            "wrap" => true,
            "size" => "xs"
          ]
        ],
      ],
      "styles" => [
        "header" => [
          "backgroundColor" => "#00B900"
        ],
        "hero" => [
          "separator" => false
        ]
      ]
    ];
  }
}
```

### ⑦Flex Messageを送信する

④に追記する部分があります。
⑤、⑥で作成した関数を使います。

関数`dataFormatting`内で以下の追記を行います。
（※下記に全コードを貼っているのでそちらからコピーしたほうが正確です）

```php:app/Common/LINE/Event/LocationMessage.php
// common->FlexMessages
$messages = FlexMessages::createFlexMessage($weatherArray);
return $messages;
```

関数`sendReplyMessage`内のjson_encodeに関して解説します。
`JSON_UNESCAPED_UNICODE`で日本語がJSONで使えるようになります。
`JSON_UNESCAPED_SLASHES`でURLにバックスラッシュがつかないようになります。
複数のオプションをつけるときはユニオン型で書きます。

curlコマンドに関して解説します。
curlコマンドの使い方を詳しく知らない方はこちらをご確認ください。

https://qiita.com/tokutoku393/items/3c3ba3ca581bc0381e35

[公式ドキュメント](https://developers.line.biz/ja/docs/messaging-api/using-flex-messages/#sending-hello-world)を参考にしてください。
Headerに、`Content-Type`, `Authorization`、
送信するデータとして、`replyToken`, `messages`を持たせればいいことがわかります。
（※ちなみに私はこれを`Guzzle`でやろうとしましたが、messagesがJSONにならなかったので諦めてcurlを使いました。もしGuzzleを使ってできた方がいましたらコードを教えてくださいw）

```php:app/Common/LINE/Event/LocationMessage.php
<?php

namespace App\Common\LINE\Event;

// Common
use FlexMessages;
use Guzzle;
use Util;

class LocationMessages
{
  // OpenWeatherからguzzleでデータを取得
  public static function getWeatherData($event)
  {
    // 緯度・経度を取得
    $latitude = $event->getLatitude();
    $longitude = $event->getLongitude();

    // API
    $weatherAPI = env("WEATHER_API");

    // OpenWeather
    $openWeather_url = "https://api.openweathermap.org/data/2.5/onecall?lat=" . $latitude . "&lon=" . $longitude . "&units=metric&lang=ja&appid=" . $weatherAPI;

    //common->guzzle
    $weathers = Guzzle::getGuzzle($openWeather_url);

    // JSON->Arrayに変換
    $weathers = json_decode($weathers, true);

    return $weathers;
  }

  // getWeatherDataを整形する
  public static function dataFormatting($event)
  {
    // getWeatherDataを取得
    $weathers = self::getWeatherData($event);

    // 時刻
    $time = $weathers["daily"][0]["dt"];
    $time = date("Y/m/d", $time);
    // 天気予報
    $weatherInformation = $weathers["daily"][0]["weather"][0]["description"];
    // 体感温度（ファッション）（朝、日中、夕方、夜）
    $mornTemperature = $weathers["daily"][0]["feels_like"]["morn"];
    $dayTemperature = $weathers["daily"][0]["feels_like"]["day"];
    $eveTemperature = $weathers["daily"][0]["feels_like"]["eve"];
    $nightTemperature = $weathers["daily"][0]["feels_like"]["night"];

    // 最高気温で洋服を分岐する
    $arrayTemperature = array($mornTemperature, $dayTemperature, $eveTemperature, $nightTemperature);
    $highestTemperature = max($arrayTemperature);

    if ($highestTemperature >= 26) {
      $fashionAdvice = "暑い！半袖が活躍する時期です。少し歩くだけで汗ばむ気温なので半袖1枚で大丈夫です。ハットや日焼け止めなどの対策もしましょう";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/60aa3c44153071e6df530eb7_71.png";
    } else if ($highestTemperature >= 21) {
      $fashionAdvice = "半袖と長袖の分かれ目の気温です。日差しのある日は半袖を、曇りや雨で日差しがない日は長袖がおすすめです。この気温では、半袖の上にライトアウターなどを着ていつでも脱げるようにしておくといいですね！";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e58a5923ad81f73ac747_10.png";
    } else if ($highestTemperature >= 16) {
      $fashionAdvice = "レイヤードスタイルが楽しめる気温です。ちょっと肌寒いかな？というくらいの過ごしやすい時期なので目一杯ファッションを楽しみましょう！日中と朝晩で気温差が激しいので羽織ものを持つことを前提としたコーディネートがおすすめです。";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6087da411a3ce013f3ddcd42_66.png";
    } else if ($highestTemperature >= 12) {
      $fashionAdvice = "じわじわと寒さを感じる気温です。ライトアウターやニットやパーカーなどが活躍します。この時期は急に暑さをぶり返すことも多いのでこのLINEで毎日天気を確認してくださいね！";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e498e7d26507413fd853_4.png";
    } else if ($highestTemperature >= 7) {
      $fashionAdvice = "そろそろ冬本番です。冬服の上にアウターを羽織ってちょうどいいくらいです。ただし室内は暖房が効いていることが多いので脱ぎ着しやすいコーディネートがおすすめです！";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e4de7156326ff560b1a1_6.png";
    } else {
      $fashionAdvice = "凍えるほどの寒さです。しっかり厚着して、マフラーや手袋、ニット帽などの冬小物もうまく使って防寒対策をしましょう！";
      $imageURL = "https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056ebd3ea0ff76dfc900633_48.png";
    }

    // 上記の必要項目を配列にする
    $weatherArray = array($time, $imageURL, $weatherInformation, $mornTemperature, $dayTemperature, $eveTemperature, $nightTemperature, $fashionAdvice);

    // common->FlexMessages
    $messages = FlexMessages::createFlexMessage($weatherArray);
    return $messages;
  }

  // メッセージを送る
  public static function sendReplyMessage($event)
  {
    //Utilから値を取得
    $channelAccessToken = Util::getChannelAccessToken();
    $replyToken = Util::getReplyToken($event);

    // 配列を取得
    $messages = self::dataFormatting($event);

    // JSON化する
    $result = json_encode(['replyToken' => $replyToken, 'messages' => [$messages]], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    $curl = curl_init();
    //POSTリクエスト
    curl_setopt($curl, CURLOPT_POST, true);
    //ヘッダを指定
    curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $channelAccessToken, 'Content-type: application/json'));
    //リクエストURL
    curl_setopt($curl, CURLOPT_URL, 'https://api.line.me/v2/bot/message/reply');
    //送信するデータ
    curl_setopt($curl, CURLOPT_POSTFIELDS, $result);
    // 実行する
    curl_exec($curl);
    // 閉じる
    curl_close($curl);
  }
}
```

### ⑧`Services`を修正する

```php:app/Services/LINEService.php
<?php

namespace App\Services;

use Illuminate\Http\Request;
// LINE
use LINE\LINEBot\Event\MessageEvent\TextMessage;
use LINE\LINEBot\Event\MessageEvent\LocationMessage;
// Library
use TextMessages;
use LocationMessages;
use Util;

class LINEService
{
  public function sendMessage(Request $request)
  {
    //Webhookの処理
    $events = Util::getEventsByWebhook($request);

    foreach ($events as $event) {
      // eventがテキストメッセージの時
      if ($event instanceof TextMessage) {
        TextMessages::eventTextMessage($event);
      }

      // eventが位置情報メッセージの時
      if ($event instanceof LocationMessage) {
        LocationMessages::sendReplyMessage($event);
      }
      return;
    }
  }
}
```

これで完了です。
正しく作動しましたか？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/94172c10-796a-24b4-c350-408dea506c06.png)


もし動かなければ`Log`を使ってデバッグして頑張りましょう。
わからないことがあれば質問してください。

## 終わりに
これで洋服を間違えずにすみそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/52429309-4f8a-b44e-be49-895f8ac67c09.png)

herokuへデプロイしましたが、Flex Messageが届きませんでした。
herokuの無料枠の機能じゃ無理なんでしょうかね。。
AWSにデプロイすることにします。。。

### 追記
原因判明しました。
guzzleがDocker内でインストールされなかったことが原因でした。
`docker-compose exec app composer require guzzlehttp/guzzle`ではなぜか`composer.lock`にしかguzzleが追加されませんでした。

なので諦めて`src`ディレクトリに移動してそこでインストールしました。
そうしたら動きました！！

**※追記　AWSへデプロイしました**
AWS勉強中の方は結構丁寧な解説付きの記事になっているのでぜひ見てもらえると嬉しいです。

https://qiita.com/Ryo9597/items/ce2ce494c720781c5301

**※追記 Node.jsでも作ってみました**
Node.jsの方が圧倒的に簡単でした。

https://qiita.com/Ryo9597/items/bf93618663f28e9d8f7f