はじめに
みなさまLINEは使われていますでしょうか?LINEの国内利用者数は9,000万人以上(2022年8月)と発表されており、国内の方であれば主な連絡手段はLINEになるのかなと思います。僕の祖父(78歳)も登録してるくらい使いこなしてはないですから、登録してない方がめずらしいですよね。
このような状況ですとクライアントから、LINEを使った集客やシステム連携のご相談をいただきます。
そのため今回はLaravel × LINE Messaging API を使ったLINEとシステムの連携、機能実装を進めていく中で気づいたことや注意点をまとめていきます。
参照リンク
実装イメージ
実装していく機能ですが、LINEとWEBアプリでメッセージのやりとりができるものを作っていきます。
- LINEからWEBアプリへメッセージを送信
- WEBアプリからLINEへメッセージを送信
初期設定
LINE Developesの設定
LINE Messaging APIを使うにあたりLINE Developersで設定が必要です。
Webhook
は以下のように設定をお願いします。Laravel側では/line/webhook/message
でエンドポイントを設定します。
https
でないとWebhook
は使えないので注意してください。
ちなみに自分はngrok
を使ってhttps
化、Webhook
の検証をしました。
ngrok
でWebhook
のURLを設定すると以下のようになります。
例) https://ec08-122-249-204-181.jp.ngrok.io/line/webhook/message
Laravelにパッケージのインストール
LINE Developersでチャネルの登録や新規プロバイダーの作成が済んだらLaravelにline-bot-sdk-php
パッケージをインストールします。
composer require linecorp/line-bot-sdk
機能実装1
まずはベースとなる処理を実装していきます。
Botアカウントにメッセージが送られたら、特定のメッセージを返信するようにします。
CSRFの例外設定
Webhook
はPOST
メソッドでリクエストが来るのでline/*
のルートをCSRF
の対象外に設定しておきます。
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
'line/*'
];
}
環境変数の設定
LINE Developersで設定したチャンネルID、チャンネルシークレット、チャンネルアクセストークンを.env
に設定します。
LINE_MESSAGE_CHANNEL_ID=チャンネルID
LINE_MESSAGE_CHANNEL_SECRET=チャンネルシークレット
LINE_MESSAGE_CHANNEL_TOKEN=チャンネルアクセストークン
環境変数を読み込むためconfig/services.php
を設定します。
return [
~ 省略 ~
'line' => [
'message' => [
'channel_id'=>env('LINE_MESSAGE_CHANNEL_ID'),
'channel_secret'=>env('LINE_MESSAGE_CHANNEL_SECRET'),
'channel_token'=>env('LINE_MESSAGE_CHANNEL_TOKEN')
]
],
];
ルーティングの設定
LINE Developersで設定したWebhook
のURLに合わせルーティングを設定します。
Route::post('/line/webhook/message', 'App\Http\Controllers\LineWebhookController@message')->name('line.webhook.message');
LINEBotの継承クラスを準備
このクラスはline-bot-sdk-php
パッケージとLINE Messaging APIのエンドポイントに差異があったり、関数が用意されていない場合に使います。そのためパッケージに元々ある関数だけで実装できるようであれば、このクラスはなくても大丈夫です。
クラス内でやることはシンプルで、LINEBot
クラスを継承してオーバーライド or 関数を作成して、必要な処理を追加していきます。
/app
の配下にServices
フォルダを作成してLineBotService.php
を設置します。
※参考としていくつか関数を用意しておきます。
<?php
namespace App\Services;
use LINE\LINEBot;
use LINE\LINEBot\HTTPClient;
class LineBotService extends LINEBot
{
/** @var string */
private $channelSecret;
/** @var HTTPClient */
private $httpClient;
public function __construct(
HTTPClient $httpClient,
array $args
) {
parent::__construct($httpClient, $args);
$this->httpClient = $httpClient;
$this->channelSecret = $args['channelSecret'];
}
// 例:送信されたメッセージを取得するAPI
public function getMessageContent($messageId)
{
return $this->httpClient->get('https://api-data.line.me/v2/bot/message/' . urlencode($messageId) . '/content');
}
// 例:LINEのグループ情報を取得するためのAPI
public function getGroupSummary($groupId)
{
return $this->httpClient->get('https://api.line.me/v2/bot/group/' . urlencode($groupId) .'/summary');
}
}
Webhook用コントローラの作成
以下のコマンドでコントローラーを作成します。
php artisan make:controller LineWebhookController
LineBotService.php
を使う想定で実装を進めます。
まずはLINEからメッセージが送信された場合に、シンプルにメッセージを返信するコードを書いていきます。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Services\LineBotService as LINEBot;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;
class LineWebhookController extends Controller
{
public function message(Request $request) {
$data = $request->all();
$events = $data['events'];
$httpClient = new CurlHTTPClient(config('services.line.message.channel_token'));
$bot = new LINEBot($httpClient, ['channelSecret' => config('services.line.message.channel_secret')]);
foreach ($events as $event) {
$response = $bot->replyText($event['replyToken'], 'メッセージ送信完了');
}
return;
}
}
これだけでLINEから来たメッセージに対して返信ができるようになりました。
LINE Developersを開きMessaging API
タブを選択すると、QRコードが表示されているので読み取ってBotアカウントを友達に追加しましょう。
Botアカウントにメッセージを送信してメッセージ送信完了
と返信が来たら実装完了です。
通信がうまくいかない場合はWebhook
のURLを確認してみてください。ngrok
を使っている場合は起動しているかなども。
機能実装2
今回はWEBアプリとLINEでメッセージのやり取りをすることが目的なので、先ほどのコードに処理を追加していきます。
messagesテーブルの作成
やり取りしたメッセージを保存するためmessages
テーブルを作成します。以下のコマンドを実行してマイグレーションファイルを作成してください。
php artisan make:migration CreateMessagesTable
テーブルの中身は以下のように設定します。line_message_id
のみnull許容
しておきます。
public function up()
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->string('line_user_id')->comment('LINEユーザーID');
$table->string('line_message_id')->nullable()->comment('LINEメッセージID');
$table->string('text')->comment('テキスト');
$table->timestamps();
});
}
Messageモデルの作成
続いてモデルを作成します。コマンドを実行してファイルを編集してください。
php artisan make:model Message
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
use HasFactory;
protected $fillable = [
'line_user_id',
'line_message_id',
'text',
];
}
$eventsの中身
LineWebhookController.php
にメッセージを保存する処理を追加していきますが、その前にWebhook
で送られてくるリクエストの中身を確認しておきましょう。
$events = $data['events'];
で取得している$events
の中には以下の配列が入っています。
※発生したイベントの数に応じて配列も増えるので注意してください。
array (
'type' => 'message', // イベントタイプ message以外にもjoin・memberJoined・follow等がある
'message' => array (
'type' => 'text', // text以外にimage(画像)・sticker(スタンプ)がある
'id' => '16759029429384', // LINEのメッセージID
'text' => 'テキスト', // LINEから送信したメッセージ
),
'webhookEventId' => '01GCKECY18W8Q8B438GTYCCHYG',
'deliveryContext' =>
array (
'isRedelivery' => false,
),
'timestamp' => 1662804981373,
'source' =>
array (
'type' => 'user',
'userId' => 'U9d85fbafd6e192e2616c783a20666d9c', // LINEのユーザーID
),
'replyToken' => '189df3de294d47f8b33f2d908c04008c', // メッセージ返信のために必要なトークン
'mode' => 'active',
)
イベントタイプごとに送られてくるパラメータが変わるので、各ケースに応じて処理を用意する必要がありますが、この記事ではイベントタイプ
がmessage
かつメッセージタイプ
がtext
のものにのみ処理を設定します。
LineWebhookControllerを編集
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Services\LineBotService as LINEBot;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;
use App\Models\Message; // 追記
class LineWebhookController extends Controller
{
public function message(Request $request) {
$data = $request->all();
$events = $data['events'];
$httpClient = new CurlHTTPClient(config('services.line.message.channel_token'));
$bot = new LINEBot($httpClient, ['channelSecret' => config('services.line.message.channel_secret')]);
foreach ($events as $event) {
// メッセージの保存処理を追記
Message::create([
'line_user_id' => $event['source']['userId'],
'line_message_id' => $event['message']['id'],
'text' => $event['message']['text'],
]);
// 自動返信が不要であれば削除
// $response = $bot->replyText($event['replyToken'], 'メッセージ送信完了');
}
return;
}
}
これでLINEから送られたメッセージがデータベースに保存されるようになりました。
次は保存したメッセージをWEBアプリ側で確認できるよう、メッセージの一覧機能を実装していきます。
ルーティングの追加
以下、二つのルーティングを追加します。
Route::get('/messages', 'App\Http\Controllers\MessageController@index')->name('message.index');
Route::get('/messages/{lineUserId}', 'App\Http\Controllers\MessageController@show')->name('message.show');
メッセージ用コントローラの作成
まずはコマンドを実行してコントローラを作成しましょう。
php artisan make:controller MessageController
MessageController.php
にはindex
とshow
アクションを用意します。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Message;
class MessageController extends Controller
{
public function index(Request $request) {
$lineUsers = Message::groupBy('line_user_id')->get('line_user_id');
return view('message.index', ['lineUsers' => $lineUsers]);
}
public function show(Request $request) {
$messages = Message::where('line_user_id', $request->lineUserId)->get();
return view('message.show', ['lineUserId' => $request->lineUserId, 'messages' => $messages]);
}
}
index
アクションではメッセージを送信したユーザーの一覧を表示し、show
アクションではユーザーに紐づくメッセージを表示します。
ビューファイルの用意
/resources/views
の配下にmessage
フォルダを作成してindex.blade.php
とshow.blade.php
を設置し、それぞれ以下のように設定します。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LINEユーザー</title>
</head>
<body class="antialiased">
LINEユーザー一覧
<ul>
@foreach($lineUsers as $lineUser)
<li>
<a href="{{ route('message.show', ['lineUserId' => $lineUser->line_user_id]) }}">
{{ $lineUser->line_user_id }}
</a>
</li>
@endforeach
</ul>
</body>
</html>
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LINEメッセージ</title>
</head>
<body class="antialiased">
<div>
LINEユーザーID {{ $lineUserId }}
</div>
<ul>
@foreach($messages as $message)
<li>
LINEメッセージ {{ $message->text }}
</li>
@endforeach
</ul>
</body>
</html>
上記の実装でLINEから送信されたメッセージが表示できるようになるので、ブラウザでローカルのURLを叩いて確認してみてください。
機能実装3
ここまでの実装でLINEメッセージの受信と表示ができました。最後にWEBアプリから各LINEアカウントに対してメッセージが送信できるよう実装していきます。
ルーティングの追加
以下のルーティングを追加します。
Route::post('/message/{lineUserId}', 'App\Http\Controllers\MessageController@create')->name('message.create');
メッセージ用コントローラの編集
MessageController.php
に以下のuse宣言とアクションを追加します。
// use宣言追加
use App\Services\LineBotService as LINEBot;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;
use LINE\LINEBot\MessageBuilder\TextMessageBuilder;
// アクション追加
public function create(Request $request) {
Message::create([
'line_user_id' => $request->lineUserId,
'text' => $request->message,
]);
$httpClient = new CurlHTTPClient(config('services.line.message.channel_token'));
$bot = new LINEBot($httpClient, ['channelSecret' => config('services.line.message.channel_secret')]);
$textMessageBuilder = new TextMessageBuilder($request->message);
$response = $bot->pushMessage($request->lineUserId, $textMessageBuilder);
return redirect(route('message.show', ['lineUserId' => $request->lineUserId]));
}
メッセージをDBへ保存し、パッケージで用意されたクラスを使ってLINEアカウントへメッセージを送信します。
show.blade.phpを編集
メッセージがWEBアプリ・LINEどちらから送られてきたか判定するためif
文を設定します。条件は、line_message_id
が空か否かです。
またメッセージを送信するためのフォームを設置します。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LINEメッセージ</title>
</head>
<body class="antialiased">
<div>
LINEユーザーID {{ $lineUserId }}
</div>
<ul>
@foreach($messages as $message)
<li>
@if(empty($message->line_message_id))
WEBアプリメッセージ {{ $message->text }}
@else
LINEメッセージ {{ $message->text }}
@endif
</li>
@endforeach
</ul>
<form method="post" action="{{ route('message.create', ['lineUserId' => $lineUserId]) }}">
@csrf
<input type="text" name="message">
<button type="submit">送信</button>
</form>
</body>
</html>
動作検証
WEBアプリのフォームにメッセージを入力して送信ボタンを押下します。Botアカウントから入力したメッセージが届きます。
続いてLINEからメッセージを送信して、ブラウザをリロードします。WEBアプリ・LINEから送信したメッセージがそれぞれ表示されていれば実装完了です。
linecorp/line-bot-sdkの困りごと
現状、WeアプリとLINEでのメッセージのやり取りの機能しかありません。それだけでは機能不足なので、ここから要望に合わせ機能を追加していくかと思います。
するとLINE Messaging APIにはエンドポイントがあるのにlinecorp/line-bot-sdk
にそれに対応する関数が無いことや、それっぽい関数はあるがエンドポイントが公式と違うことで正しく処理を実行できないことがあり、ハマりました。
ここでは記事の序盤で用意して、放置していたずっと触れずにいた LineBotService.php
を使った対応方法を記載していきます。
どうハマったのか?
まずやりたかったこととしては、LINEから送られてきた画像をサーバーに保存してWEBアプリで表示するということでした。
LINEから画像を送信した際には、Webhook
が叩かれイベント情報は送られてくるのですが、ファイルの情報自体は含まれていないので、LINEのメッセージIDを元にファイル情報を取得する必要があります。
/vendor/linecorp/line-bot-sdk/src/LINEBot.php
にそれっぽい関数があり、LINE Messaging APIのエンドポイントともパスが同じだったので、以下の関数を使って実装してました。
public function getMessageContent($messageId)
{
return $this->httpClient->get($this->endpointBase . '/v2/bot/message/' . urlencode($messageId) . '/content');
}
ただ画像情報はおろかメッセージ情報もとれず返ってくるのはなぜか404
…
いろいろ調べているうちにサブドメインが違う!
と気づきます。
そこでちょっと強引ですがLINEBot
クラスを継承したLineBotService
クラスを用意して、オーバーライドすることでエンドポイントをまるまる指定することにしました。
public function getMessageContent($messageId)
{
return $this->httpClient->get('https://api-data.line.me/v2/bot/message/' . urlencode($messageId) . '/content');
}
画像の保存処理
LineBotService
クラスを使うことで画像情報を取得することができるようになったので、LineWebhookController.php
のmessage
関数に画像の保存処理を追加していきます。
画像のパスや元々のファイル名等は、別途処理を追加してDBに保存してください。
また、メッセージのイベントタイプに合わせswitch
文でそれぞれ処理を分けます。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Services\LineBotService as LINEBot;
use App\Models\Message;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;
use Illuminate\Support\Facades\Storage;
class LineWebhookController extends Controller
{
public function message(Request $request) {
$data = $request->all();
$events = $data['events'];
$httpClient = new CurlHTTPClient(config('services.line.message.channel_token'));
$bot = new LINEBot($httpClient, ['channelSecret' => config('services.line.message.channel_secret')]);
foreach ($events as $event) {
switch ($event['message']['type']) {
case 'text':
Message::create([
'line_user_id' => $event['source']['userId'],
'line_message_id' => $event['message']['id'],
'text' => $event['message']['text'],
]);
// $response = $bot->replyText($event['replyToken'], 'メッセージ送信完了');
break;
case 'image':
$response = $bot->getMessageContent($event['message']['id']);
if ($response->isSucceeded()) {
$contentType = $response->getHeader('content-type');
$arrayContentType = explode('/', $contentType);
$ext = end($arrayContentType);
$path = 'public/line/' .$event['message']['id'] .'.' .$ext;
Storage::put($path, $response->getRawBody());
Storage::url($path);
} else {
error_log($response->getHTTPStatus());
}
break;
case 'sticker':
// スタンプが送信された場合
break;
}
}
return;
}
}
ここまでできたらLINEから画像を送信してみてください。/storage/app/public/line
の配下に画像が保存されいるはずです。
他にも実装したい機能はたくさん出てくるかと思います。まずは公式のエンドポイントを確認してみください。
それに対応した関数がなければLineBotService
クラスに関数を作ってあげれば、要件を満たして実装できるかなと思います。
さいごに
LINEと連携したサービス展開は一般的なものとなっています。そのためLINEと連携するにあたり、独自の設定や機能を追加したいという需要も増えていくかと思いますので、是非参考にしてください。
ではまた次の記事で!
新しいメンバーを募集しています
Ichikawa / Engineer
パン屋から転身してエンジニア3年目。主にPHP/Laravelを使っています。最近ではVue.js/Nuxt.jsと人間に興味あり。