LaravelでLINE botを作る最低限の実装方法

はじめに

LaravelでLINEのボットを作成する際の最低限の実装手順です。

line-bot-sdkが昨年のアップデートで大幅に仕様が変わったらしく、ネットの記事だとうまくいかない部分があったので、一応現時点のバージョンでの方法を残しておきます。

前提

  • php 8.1
  • Laravel 10
  • line-bot-sdk-php 9.5

ゴール

ユーザーが送ったメッセージをそのまま返信するボットを作る。

実装手順

全体の流れとしては、

  • LINE Developersへ登録して必要なキーを取得
  • Laravelにline-bot-sdkをインストール
  • Webhookでメッセージを受け取って、そのまま返す

となります。

LINE Developersへ登録

LINE Developersではいくつかのサービスを提供していますが、今回はMessaging APIというサービスを使って実装します。

プロバイダーとチャネルの作成

アカウントを登録したらプロバイダーとチャネルを作成します。

プロバイダー
事業やプロジェクト単位で作成します。1つのアカウントで複数作る事ができます。

チャネル
プロバイダーの中にチャネルを作成します。用途ごとにチャネルを分けて複数作る事ができます。

チャネルを作ると、チャネルシークレットチャネルアクセストークンを取得できます。これは後ほどLaravelの.envで使用します。

Webhook URLの設定

Messaging APIは、ユーザーが友達追加したりメッセージを送った場合、イベントを送信します。その送信先URLを設定します。

「Webhookの利用」を有効にして、例えば以下のようなURLを設定します。

https://[ドメイン]/webhook/linebot

後ほど、LaravelでこのWebhookを受け取るようルーティング設定をします。

参考

開発環境でWebhookを利用する場合、http://localhostでは受け取れないので、ngrokなどのサービスを使って外部からのPOSTを受け取れるようにする必要があります。

Docker環境の場合ですが、ngrokについての記事を書いたので良かったら参考にしてください。

ngrokをdocker composeで使う

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

LaravelでMessaging APIを簡単に使うために用意されたline-bot-sdkという公式のツールをインストールします。

(Laravel環境は各自事前に構築してください)

$ composer require linecorp/line-bot-sdk

チャネルシークレットとチャネルアクセストークンを設定

先程取得したチャネルシークレットチャネルアクセストークンをLaravelの.envにセットします。

.env

LINE_CHANNEL_ID=""
LINE_CHANNEL_SECRET="チャネルシークレット"
LINE_CHANNEL_ACCESS_TOKEN="チャネルアクセストークン"

LINE_CHANNEL_IDは今回は使用しません。

.envの内容を直接コントローラーなどで取得するのは望ましくないので、configで一旦読み込んでおきます。configならどこでも良いですが、今回はservices.phpに追加しておきます。

config/services.php

return [

    // 前後省略

    'line' => [
        'id' => env('LINE_CHANNEL_ID'),
        'secret' => env('LINE_CHANNEL_SECRET'),
        'token' => env('LINE_CHANNEL_ACCESS_TOKEN'),
    ],
];

コントローラー作成

Webhookを処理するためのコントローラーを作成します。名前はなんでもいいです。

$ php artisan make:controller LineBotController

例えば、以下の内容で実装します。(コピペでOK)

各所にコメントを付けてあるので、参考にしてください。

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use GuzzleHttp\Client;
use LINE\Clients\MessagingApi\Configuration;
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Model\TextMessage;
use LINE\Clients\MessagingApi\Model\ReplyMessageRequest;
use LINE\Clients\MessagingApi\ApiException;
use LINE\Webhook\Model\MessageEvent;
use LINE\Webhook\Model\TextMessageContent;
use LINE\Parser\EventRequestParser;
use Illuminate\Support\Facades\Log;

class LineBotController extends Controller
{
    public function reply(Request $request)
    {
        // チャネルシークレットとチャネルアクセストークンを読み込む
        $channelSecret = config('services.line.secret');
        $channelToken = config('services.line.token');

        // Webhookイベントを取得する
        $httpRequestBody = $request->getContent();

        // 署名を検証する(Messaging APIから送られたものであるかチェック)
        $hash = hash_hmac('sha256', $httpRequestBody, $channelSecret, true);
        $signature = base64_encode($hash);
        if ($signature !== $request->header('X-Line-Signature')) return;

        // LINEBOTクライアントを作成
        $client = new Client();
        $config = new Configuration();
        $config->setAccessToken($channelToken);

        // LINE Messaging APIを作成
        $messagingApi = new MessagingApiApi(
            client: $client,
            config: $config,
        );

        try {
            // イベントリクエストをパース
            $parsedEvents = EventRequestParser::parseEventRequest($httpRequestBody, $channelSecret, $signature);

            // イベントは配列(必ずしも1つとは限らない)で来るのでforeachで回す
            foreach ($parsedEvents->getEvents() as $event) {

                // メッセージイベント以外は無視
                if (!($event instanceof MessageEvent)) continue;

                // メッセージを取得
                $eventMessage = $event->getMessage();

                // テキストメッセージ以外は無視
                if (!($eventMessage instanceof TextMessageContent)) continue;

                // テキストメッセージを取得
                $eventMessageText = $eventMessage->getText();

                // 応答メッセージを作成
                $message = new TextMessage([
                    'type' => 'text',
                    'text' => $eventMessageText,
                ]);

                // 応答リクエストを作成
                $request = new ReplyMessageRequest([
                    'replyToken' => $event->getReplyToken(),
                    'messages' => [$message], // 配列である必要があります
                ]);

                // 応答リクエストを送信する
                $response = $messagingApi->replyMessageWithHttpInfo($request);

                // レスポンスをチェックする(エラーの場合の処理)
                $responseBody = $response[0];
                $responseStatusCode = $response[1];
                if ($responseStatusCode != 200) {
                    throw new \Exception($responseBody);
                }
            }

            return;

        } catch (ApiException $e) {
            // エラー内容をログに出力
            Log::error($e->getCode() . ':' . $e->getResponseBody());
        }
    }
}

ルーティング作成

WebhookのPOSTリクエストをコントローラーへ送ります。

routes/web.php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\LineBotController;

Route::post('webhook/linebot', [LineBotController::class, 'reply']);

CSRFトークンのチェックを解除

デフォルトではCSRF対策でcsrfトークンがないとPOSTを受け付けないようになっています。

Messaging APIから送られてくるWebhookは当然csrfトークンがないので、なくても受け取れるようにWebhookのルートを除外します。

app/Http/Middleware/VerifyCsrfToken.php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array<int, string>
     */
    protected $except = [
        'webhook/*' // これを追加
    ];
}

確認

実装は以上です。実際に動くか確認してみましょう。

LINE Developersのチャネルページの「Messaging API設定」を開きます。

Webhookの検証

Webhook設定の「検証」ボタンを押して、Webhookが正しく届いているかチェックします。

「成功」と表示されたらOKです。

実際に動作テスト

実際に動くかチェックしてみます。

「友達追加」用のQRコードをスマホで読み取って友達追加します。

「あいさつメッセージ」が届くはずです。

「あいさつメッセージ」の内容は変更できます。無効にもできます。

何かメッセージ(テキスト)を送ります。すぐに同じメッセージが返ってきたら成功です。

自動の「応答メッセージ」も一緒に返ってきますが、設定で無効にできます。

返ってこない場合は、どこかでエラーが出ています。Laravelのログを確認してみてください。

まとめ

今回は最低限の実装なので、実際に稼働させる上で必要な処理が出てくると思いますが、これをベースに肉付けしてもらえたらと思います。

LINE Developersのドキュメントは充実していて見やすいのですが、line-bot-sdkは簡単なマニュアルしかない?のでリポジトリ内のサンプルコードなどを参考にするしかないのですかね。。。