Tech blog Produced by FOURIER

Laravelで使う Line Messaging API(line-bot-sdk-php)パート1

Ichikawa Ichikawa 2022.09.12

はじめに

みなさまLINEは使われていますでしょうか?LINEの国内利用者数は9,000万人以上(2022年8月)と発表されており、国内の方であれば主な連絡手段はLINEになるのかなと思います。僕の祖父(78歳)も登録してるくらい使いこなしてはないですから、登録してない方がめずらしいですよね。

このような状況ですとクライアントから、LINEを使った集客やシステム連携のご相談をいただきます。

そのため今回はLaravel × LINE Messaging API を使ったLINEとシステムの連携、機能実装を進めていく中で気づいたことや注意点をまとめていきます。

参照リンク

実装イメージ

実装していく機能ですが、LINEとWEBアプリでメッセージのやりとりができるものを作っていきます。

1. LINEからWEBアプリへメッセージを送信

flowchart LR
		subgraph "LINE"
	    A[LINEアカウント/メッセージ送信] -->|"API"| B[LINE Messaging Api]
		end
		subgraph "WEBサーバー"
		  B -->|"webhhok"| C[Laravel / DB]
		end
		subgraph "WEBアプリ"
	    C -->|"同期通信"| D[ブラウザ / メッセージ表時]
		end

2. WEBアプリからLINEへメッセージを送信

flowchart RL
		subgraph "WEBアプリ"
	    A[ブラウザ/メッセージ送信]
		end
		subgraph "WEBサーバー"
		  A -->|"httpリクエスト"| B[Laravel / DB]
		end
		subgraph "LINE"
	    B -->|"API"| D[LINE Messaging Api] -->|"API"| E[LINEアカウント/メッセージ受信]
		end

初期設定

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でチャネルの登録や新規プロバイダーの作成が済んだらLaravelline-bot-sdk-phpパッケージをインストールします。

composer require linecorp/line-bot-sdk

機能実装1

まずはベースとなる処理を実装していきます。

Botアカウントにメッセージが送られたら、特定のメッセージを返信するようにします。

CSRFの例外設定

WebhookPOSTメソッドでリクエストが来るので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アプリ側で確認できるよう一覧機能を実装していきます。

ルーティングの追加

以下、2つのルーティングを追加します。

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 にはindexshow アクションを用意します。

<?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.phpshow.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.phpmessage関数に画像の保存処理を追加していきます。

画像のパスや元々のファイル名等は、別途処理を追加して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

Ichikawa / Engineer

パン屋から転身してエンジニア3年目。主にPHP/Laravelを使っています。最近ではVue.js/Nuxt.jsと人間に興味あり。