LINE MessageAPIを使って天気予報・ファッションレコメンドアプリを作ってみました。
完成形としては以下の通りです。
どのようなアプリか
皆さんは、今日の気温を聞いて、「快適に過ごすために今日のファッションをこうしよう」ってパッと思いつきますか?
私は、最高気温、最低気温を聞いてもそれがどのくらい暑いのか、寒いのかがピンと来ず、洋服のチョイスを外したことがしばしばあります。
こんな思いを2度としないために今回このアプリを作りました。
LINE MessageAPI初使用+Laravel初心者の自分でも約1週間くらいで作れた、
簡単アプリなので初心者の方はLaravelの知識定着のためにやってみるといいかもしれません。
アプリの流れ
アプリの流れは大まかに以下の4つのステップで成り立っています。
・①クライアントが現在地を送る
・②OpenWeatherから天気予報を取得
・③データの整形
・④クライアントに送る
初期構成
下記のDocker構成をそのまま使っています。
LINE MessageAPI
では、dd
などのデバッグが使えないので、ファサードのログでデバッグを行います。
そのために、Ngrok
でローカル環境を一時的に外部公開しています。
先にデプロイなどをして外部からアクセスできる状況を作れるのであれば、Ngrokはいりません。
Ngrokの初期設定
上記のDockerをクローンしていただければ、$ make install
で環境構築が完了します。
次に、こちらのサイトでAuthToken
を取得してください。
AuthToken
が取得できれば、次にこれを.env
に保存しましょう。
NGROK_AUTH=123456789abcdefghijklmnopqrstuvwxyz
それでは以下のURLにアクセスしましょう!
LINE Developers
にアカウントを作成する
ここはググればいくらでもやり方が載っているので所々端折りつつ説明します。
LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。
その後諸々入力してもらったら以下のように作成できるかと思います。
注意事項としては、今回Messaging API
となるので、チャネルの種類を間違えた方は修正してください。
チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。
ではこの2つを.env
に入力します。
LINE_CHANNEL_SECRET=abcdefg123456
LINE_CHANNEL_ACCESS_TOKEN=HogeHogeHoge123456789HogeHogeHoge
Webhook
の設定
以下にアクセスしてください。
https
のURLをコピーしてください。
これをLINE DevelopersのWebhookに設定します。
これで初期設定は完了です。
コードを書いていきましょう!
linecorp/line-bot-sdk
のインストール
LINE MessageAPIは、公式がPHP向けパッケージを公開しているので、これをインストールします。
$ docker-compose exec app composer require linecorp/line-bot-sdk
Dockerを知らない方もいると思うので一応解説です。
docker-compose.yml
を見てください。
app
コンテナ内にLaravelの環境が作られているので、
こちらのコンテナ内でパッケージをインストールします。
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
って何やねんって方は以下の記事をご覧ください。
なので、自作関数を作りそれをCommon
ディレクトリに配置し、ビジネスロジックをServices
ディレクトリに配置します。
Controller
は、Services
を呼び出すだけという構成を取ります。
ドメイン駆動設計に基づいて設計することが多いNode.jsでは、Controller
とService
に分けることが多いためこのような構成を採用しました。
// 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
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
*/
Route::post("/line/message", "LINEController@sendMessage");
では、このURLをWebhook
に設定します。
.env
の値を取得する関数を作成する
.env
で記述した、LINE_CHANNEL_SECRETとLINE_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
<?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
に追記します。
'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::
でアクセスするのが正しいです!
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;
ドキュメント
<?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
)でリクエストを配列にする。
③解析されたイベントを順次処理し、必要に応じてリアクションを行う。
②が簡単なので②を採用します。
<?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
の作成をします。
使用するクラスは以下の通りです。
ドキュメント
<?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
が作成完了しました!!
それでは次にボタン
を作成しましょう
使用するクラスは以下の3つです。
use LINE\LINEBot\TemplateActionBuilder\UriTemplateActionBuilder;
ドキュメント
use LINE\LINEBot\MessageBuilder\TemplateBuilder\ButtonTemplateBuilder;
ドキュメント
use LINE\LINEBot\MessageBuilder\TemplateMessageBuilder;
ドキュメント
UriTemplateActionBuilder→ButtonTemplateBuilder→TemplateMessageBuilderの順でwrapしていきます。
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
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
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
に追記する
'aliases' => [
## (省略)
"Util" => App\Common\LINE\Util::class,
"ButtonMessages" => App\Common\LINE\Builder\ButtonMessages::class,
"TextMessages" => App\Common\LINE\Event\TextMessages::class,
],
メッセージが送信されるか検証する
では、作ったCommon
ディレクトリを使ってビジネスロジックを記述するService
層の作成をしていきましょう。
Service層をどのように作ればいいのかわからない方は以下の記事をご覧になってから進めてみてください。
<?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
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でメッセージが送られてくるか確認しましょう。
成功ですね!
それでは次に、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キーを発行してください。
発行できたらこのAPIを.env
に保存します。
# Weather
WEATHER_API=a11b22c33d44e55f66g77
あとは関数内で.env
を取得するだけです。
②経度、③緯度
位置情報メッセージをサーバー側に送信しているので、
その位置情報から経度と緯度を取得します。
公式ドキュメントを見ると、getLatitude
とgetLongitude
で取得できることがわかります。
実際にURLを作成する
<?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
に追記します。
'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が非常にわかりやすいのでこちらを見るだけで使えるようになると思います。
<?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
に追記します。
'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
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
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
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
に追記します。
'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
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
内で以下の追記を行います。
(※下記に全コードを貼っているのでそちらからコピーしたほうが正確です)
// 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)
<?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
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;
}
}
}
これで完了です。
正しく作動しましたか?
もし動かなければLog
を使ってデバッグして頑張りましょう。
わからないことがあれば質問してください。
終わりに
これで洋服を間違えずにすみそうです。
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の方が圧倒的に簡単でした。