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 とシステムの連携、機能実装を進めていく中で気づいたことや注意点をまとめていきます。

参照リンク

https://developers.line.biz/ja/reference/messaging-api/ https://developers.line.biz/ja/

実装イメージ

実装していく機能で すが、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 で設定が必要です。

公式のガイドこちらの記事 を参考に設定を進めてください。

https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console https://biz.addisteria.com/laravel_line_integration/

Webhook は以下のように設定をお願いします。 Laravel 側では /line/webhook/message でエンドポイントを設定します。

https でないと Webhook は使えないので注意してください。

ちなみに自分は ngrok を使って https 化、 Webhook の検証をしました。

https://biz.addisteria.com/ngrok-windows/

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=チャンネルアクセストークン
.env

環境変数を読み込むため 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 slash forward icon Engineer

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

関連記事