LoginSignup
10
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-05-29

LINE MessageAPIを使って天気予報・ファッションレコメンドアプリを作ってみました。
完成形としては以下の通りです。
名称未設定.png

どのようなアプリか

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

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

image.png

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

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

アプリの流れ

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

・①クライアントが現在地を送る
・②OpenWeatherから天気予報を取得
・③データの整形
・④クライアントに送る

初期構成

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

Ngrokの初期設定

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

次に、こちらのサイトAuthTokenを取得してください。

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

.env
NGROK_AUTH=123456789abcdefghijklmnopqrstuvwxyz

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

この画面になれば成功です!
スクリーンショット 2021-05-29 16.07.31.png

LINE Developersにアカウントを作成する

ここはググればいくらでもやり方が載っているので所々端折りつつ説明します。

LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。

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

スクリーンショット 2021-05-29 16.13.47.png

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

スクリーンショット 2021-05-29 16.16.20.png

スクリーンショット 2021-05-29 16.17.51.png

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

src/.env
LINE_CHANNEL_SECRET=abcdefg123456
LINE_CHANNEL_ACCESS_TOKEN=HogeHogeHoge123456789HogeHogeHoge

Webhookの設定

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

スクリーンショット 2021-05-29 16.22.54.png

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

これをLINE DevelopersのWebhookに設定します。
スクリーンショット 2021-05-29 16.24.42.png

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

image.png

linecorp/line-bot-sdkのインストール

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

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

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

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

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

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

ターミナル
// 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にルーティング処理を記述します。

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

.envの値を取得する関数を作成する

.envで記述した、LINE_CHANNEL_SECRETLINE_CHANNEL_ACCESS_TOKENを取得する関数を作成します。

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

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

├── Common/
│   ├── LINE/
│   │   │   └── Builder/
│   │   │   │   └── ButtonMessages.php
│   │   │   │   └── FlexMessages/php
│   │   │   └── Event/
│   │   │   │   └── LocationMessages.php
│   │   │   │   └── TextMessages.php
│   │   │   └── Util.php
│   ├── Guzzle.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に追記します。

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

署名の検証を行う

公式ドキュメントのこちらの対応を行います。

使用するクラスは以下の2つです。
use LINE\LINEBot\Constant\HTTPHeader;
ドキュメント

use LINE\LINEBot\SignatureValidator;
ドキュメント

自作関数で同一ファイル内の自作関数にアクセスする際は、注意が必要です。

$thisではエラーが発生します。

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

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

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;
    }
  }
}

メッセージを送る準備

公式ドキュメントのこちらの対応を行います。
ただし、テキストメッセージはまだ作成していないため、$httpClient$botの作成をします。

使用するクラス、ネームスペースは以下の2つです。
use LINE\LINEBot;
ドキュメント

use LINE\LINEBot\HTTPClient\CurlHTTPClient;
ドキュメント

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を取得する

公式ドキュメントのこちらの対応を行います。

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

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

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

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

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の作成をします。

使用するクラスは以下の通りです。
ドキュメント

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

それでは次にボタンを作成しましょう

使用するクラスは以下の3つです。

use LINE\LINEBot\TemplateActionBuilder\UriTemplateActionBuilder;
ドキュメント

use LINE\LINEBot\MessageBuilder\TemplateBuilder\ButtonTemplateBuilder;
ドキュメント

use LINE\LINEBot\MessageBuilder\TemplateMessageBuilder;
ドキュメント

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

小見出しを追加.png

Event/Textmessages.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の作成

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を呼び出す

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に追記する

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層をどのように作ればいいのかわからない方は以下の記事をご覧になってから進めてみてください。

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で呼び出しましょう。

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

成功ですね!

image.png

それでは次に、Flex Messageを作成しましょう

Flex Messageとは?

Flex Messageは、CSS Flexible box (CSS Flexbox) をもとに自由にカスタマイズできるメッセージです。

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

作成ツールとしては以下の2つがあります。
Flex Message Simulator
LINE Bot Designer

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

作成手順

①OpenWeatherで天気予報を取得するためのURLを作る

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

①API

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

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

スクリーンショット 2021-05-29 20.28.58.png

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

src/.env
# Weather
WEATHER_API=a11b22c33d44e55f66g77

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

②経度、③緯度

位置情報メッセージをサーバー側に送信しているので、
その位置情報から経度と緯度を取得します。
公式ドキュメントを見ると、getLatitudegetLongitudeで取得できることがわかります。

実際にURLを作成する

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に追記します。

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を使うこととします。
Githubが非常にわかりやすいのでこちらを見るだけで使えるようになると思います。

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に追記します。

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を使って天気予報を取得しましょう。

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つです。
①今日の日付
②天気予報
③体感温度(朝、日中、夕方、夜)

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のテンプレートを作成する

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に追記します。

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のテンプレートの整形を行う

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内で以下の追記を行います。
(※下記に全コードを貼っているのでそちらからコピーしたほうが正確です)

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コマンドの使い方を詳しく知らない方はこちらをご確認ください。

公式ドキュメントを参考にしてください。
Headerに、Content-Type, Authorization
送信するデータとして、replyToken, messagesを持たせればいいことがわかります。
(※ちなみに私はこれをGuzzleでやろうとしましたが、messagesがJSONにならなかったので諦めてcurlを使いました。もしGuzzleを使ってできた方がいましたらコードを教えてくださいw)

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を修正する

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

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

終わりに

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

image.png

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

追記

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

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

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

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

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7