LaravelでLINEのリッチメニュー(切り替え可)をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、ご存知の「LINE」がメッセージアプリだけじゃなく、ビジネスでも重要なのをご存知でしょうか。なぜなら、

9,500万人という圧倒的なユーザーがいるから

ですね。

しかも、1日に1回以上利用するユーザーは86%


引用:LINEのユーザーはどんな人? – LINEキャンパス

実際に私も1日に数回使っている印象です。
そして、今回LINEでつくりたくなったのは・・・・・・

リッチメニュー

です。

リッチメニューとは、LINEのタイムライン下部に表示されるメニューのこと。

リッチメニューは、以下のように使われたりします。

  • 動画を配信する
  • 診断機能を開始する
  • 販売ページへ移動する

つまり、マーケティングとして活用されているわけですね。

そこで!

Laravelを使ってリッチメニューを切り替える機能をつくってみます。

今回は以下のような方を想定して記事を書きました。

  • リッチメニューがどんなものか知りたい
  • リッチメニューとLaravelがどう連携するのか知っておきたい
  • リッチメニューをつかってマーケティングに役立てたい

ぜひ最後まで読んでくださいね!

「18きっぷを使って、
片道2時間の町にある
ビール醸造所いってきました🍻」

リッチメニューを操作するLINE Messaging APIが使えるようにする

前提としてLINE Messaging APIが使えないと何もできません。

このブログでは以下のページで手順をまとめてますので、先に準備をしておいてください。

📝参考ページ:Laravel: LINE の顧客リストを失わないリスクヘッジ(前提として)

※今回はテストなのでngrokをつかってローカルで運用します。

リッチメニューなど各種データが保持できるようにする

必要になるのは友だち登録してくれたLINEユーザーの情報です。
以下のコマンドでサクッとつくっていきましょう!

php artisan make:model LineUser -m

ファイルが2つ作成されます。
それぞれ中身を変更しましょう!

database/migrations/****_**_**_******_create_line_users_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('line_users', function (Blueprint $table) {
            $table->id();
            $table->string('line_id')->comment('LINEのID');
            $table->string('mode')->comment('チャネルの状態');
            $table->string('display_name')->comment('LINEの名前');
            $table->string('rich_menu_type')->comment('リッチメニューの種類');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('line_users');
    }
};

app/Models/LineUser.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class LineUser extends Model
{
    use HasFactory;

    protected $fillable = [
        'line_id',
        'mode',
        'display_name',
        'rich_menu_type',
    ];
}

ではこの状態でデータベースを初期化してみましょう。
以下のコマンドを実行してください。

php artisan migrate:fresh --seed

実際のテーブルはこうなりました。

リッチメニューの背景につかう画像を用意する

リッチメニューで表示する画像を用意します。

というのも、リッチメニューで表示されるボタンの実体は単なる画像なんです。
仕様としてはこんなカンジです。

  1. 画像をタップする
  2. タップされた画像の場所によってアクションが変わる

今回はそこまできれいな画像は不要なので、LINEがサンプルで用意してくれてるものをつかって実装しましょう。

画像は「storage/app/rich_menu_images/background.png」として設置しておいてください。

※なお、サンプル画像のダウンロードは、以下のとおりです。

  1. LINE Official Account Managerにアクセス
  2. リッチメニューを追加したいチャンネルをクリック
  3. 画面左側の「リッチメニュー」をクリック
  4. 「デザインガイド」というボタンをクリック
  5. 「テンプレートをダウンロード」ボタンをクリック

リッチメニューを登録できるArtisanコマンドをつくる

結論から言うと、ウェブ上(LINE Official Account Manager)でリッチメニューをつくると切り替えはできません。

ウェブ版は、表示期間が重複していると登録できないからです。

ではどうするかと言うと、プログラムからリッチメニューをつくります。(※postmanでもいいですね)

そのため、LaravelArtisanコマンドをつくります!
以下のコマンドを実行してください。

php artisan make:command MakeLineRichMenuCommand

ファイルが作成されるので中身を以下のように変更します。

app/Console/Commands/MakeLineRichMenuCommand.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

class MakeLineRichMenuCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'make:line-rich-menu {name} {action_text}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'LINEのリッチメニューを作成します';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $access_token = env('LINE_ACCESS_TOKEN'); // 本来はconfigから取得すべき
        $this->client = Http::withToken($access_token);

        $name = $this->argument('name');
        $action_text = $this->argument('action_text');

        $rich_menu_id = $this->createRichMenu($name, $action_text);
        $image_path = storage_path('app/rich_menu_images/background.png');
        $result = $this->uploadRichMenuImage($rich_menu_id, $image_path);

        if($result === true) {

            $this->info('リッチメニューの作成に成功しました');

        } else {

            $this->error('リッチメニューの作成に失敗しました');

        }
    }

    private function createRichMenu(string $name, string $action_text): string
    {
        $rich_menu_data = [
            'size' => [
                'width' => 1200,
                'height' => 405,
            ],
            'selected' => false,
            'name' => $name,
            'chatBarText' => 'メニュー',
            'areas' => [
                [
                    'bounds' => [
                        'x' => 0,
                        'y' => 0,
                        'width' => 800,
                        'height' => 405,
                    ],
                    'action' => [
                        'type' => 'message',
                        'label' => '左側ボタン',
                        'text' => $action_text,
                    ],
                ],
                [
                    'bounds' => [
                        'x' => 800,
                        'y' => 0,
                        'width' => 400,
                        'height' => 405,
                    ],
                    'action' => [
                        'type' => 'message',
                        'label' => '右側ボタン',
                        'text' => 'リッチメニュー切り替え',
                    ],
                ],
            ],
        ];

        try {

            $response = $this->client
                ->withHeader('Content-Type', 'application/json')
                ->post('https://api.line.me/v2/bot/richmenu', $rich_menu_data);
            $rich_menu_id = $response->json('richMenuId');

            $this->info('RichMenuId: ' . $rich_menu_id);

            return $rich_menu_id;

        } catch (\Exception $e) {

            throw $e;

        }
    }

    private function uploadRichMenuImage(string $rich_menu_id, string $image_path): bool
    {
        try {

            $response = $this->client
                ->withHeaders([
                    'Content-Type' => 'image/png',
                ])
                ->withBody(fopen($image_path, 'r'), 'image/png')
                ->post('https://api-data.line.me/v2/bot/richmenu/'. $rich_menu_id .'/content');

            $this->info($response->body());

            return $response->ok();

        } catch (\Exception $e) {

            throw $e;

        }
    }
}

ここでやっている手順は以下のとおりです。

  1. リッチメニュー本体を作成
  2. 結果からリッチメニューIDを取得
  3. IDを使って画像をアップロード

つまり2段階でAPIにアクセスしています。

これで以下のようにコマンドを実行すると、リッチメニューが登録され画像もアップロードされることになります。

php artisan make:line-rich-menu "リッチメニュー1" "リッチメニュー1がタップされました"

では、上記のコマンドと以下のコマンドで2つリッチメニューをつくり、IDを取得しておきましょう。

php artisan make:line-rich-menu "リッチメニュー2" "リッチメニュー2がタップされました"

うまくいくと以下のようになります。{}は成功した証です。

では、取得した2つのIDを.envにセットしておきましょう。

.env

RICH_MENU_ID_1="richmenu-********************************"
RICH_MENU_ID_2="richmenu-********************************"

Enum(列挙型)でリッチメニューの種類を定義する

今回は2種類のリッチメニューを切り替えますので、データを取得しやすいようにEnumをつくっておきましょう。

Enumの詳しい説明は以下ページをご覧ください。

📝 【Laravel】PHP 8.1 から使える Enumで、選択肢〜バリデーションまで実装してみる

app/Enums/RichMenuType.php

<?php

namespace App\Enums;

enum RichMenuType: string
{
    case RichMenu1 = 'rich-menu-1';
    case RichMenu2 = 'rich-menu-2';

    public function id(): string
    {
        return match ($this) {
            self::RichMenu1 => env('RICH_MENU_ID_1'),
            self::RichMenu2 => env('RICH_MENU_ID_2'),
        };
    }
}

コントローラーでウェブフックをつくり、リッチメニューを操作する

ウェブフック有効にすると、以下のような操作があったときにLaravel側へ通知をしてくれます。

  • 友だち登録したとき
  • メッセージを受信したとき
  • 画像が送信されたとき

このウェブフックをLaravelで作っていきます。
以下のコマンドを実行してください。

php artisan make:controller LineRichMenuWebhookController

ファイルが作成されるので、中身を以下のように変更します。

app/Http/Controllers/LineRichMenuWebhookController.php

<?php

namespace App\Http\Controllers;

use App\Enums\RichMenuType;
use App\Models\LineUser;
use GuzzleHttp\Client as GuzzleClient;
use Illuminate\Http\Request;
use LINE\Clients\MessagingApi\Configuration;
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Model\TextMessage;
use LINE\Clients\MessagingApi\Model\Message;
use LINE\Clients\MessagingApi\Model\ReplyMessageRequest;
use LINE\Constants\MessageType;
use LINE\Parser\EventRequestParser;
use LINE\Webhook\Model\FollowEvent;
use LINE\Webhook\Model\PostbackEvent;

class LineRichMenuWebhookController extends Controller
{
    public function __construct()
    {
        // 本来は config から取得するべきです
        $this->channel_secret = env('LINE_CHANNEL_SECRET');
        $this->access_token = env('LINE_ACCESS_TOKEN');
    }

    public function receive(Request $request): void
    {
        $request_body = $request->getContent();
        $hash = hash_hmac('sha256', $request_body, $this->channel_secret, true);
        $signature = base64_encode($hash);

        if($signature === $request->header('X-Line-Signature')) {

            $client = new GuzzleClient;
            $config = new Configuration();
            $config->setAccessToken($this->access_token);

            $this->api = new MessagingApiApi(
                client: $client,
                config: $config,
            );

            try {

                $event_parser = EventRequestParser::parseEventRequest($request_body, $this->channel_secret, $signature);

                foreach($event_parser->getEvents() as $event) {

                    if($event instanceof FollowEvent) { // お友だち登録したとき

                        $this->onFollow($event);

                    } else if($event instanceof PostbackEvent) { // ポストバック:リッチメニューがタップされたとき

                        $this->onPostback($event);

                    }

                }

            } catch (\Exception $e) {

                logger()->error($e->getMessage());

            }

        } else {

            abort(404);

        }
    }

    private function onFollow(FollowEvent $event): void
    {
        $reply_token = $event->getReplyToken();
        $line_id = $event->getSource()->getUserId();
        $mode = $event->getMode();
        $profile = $this->api->getProfile($line_id);
        $display_name = $profile->getDisplayName();

        $line_user = LineUser::firstOrNew(['line_id' => $line_id]);
        $line_user->display_name = $display_name;
        $line_user->mode = $mode;
        $line_user->rich_menu_type = RichMenuType::RichMenu1;
        $line_user->save();

        $message = new TextMessage([
            'type' => MessageType::TEXT,
            'text' => '友だち登録ありがとうございます!',
        ]);

        $this->replyMessage($reply_token, $message);

        $this->setRichMenu($line_id, RichMenuType::RichMenu1);
    }

    private function onPostback(PostbackEvent $event): void
    {
        $line_id = $event->getSource()->getUserId();
        $data = $event->getPostback()->getData();
        parse_str($data, $parsed_data);

        $action = data_get($parsed_data, 'action');

        if($action === 'switch_rich_menu') {

            $line_user = LineUser::where('line_id', $line_id)->first();

            if(! is_null($line_user)) {

                $rich_menu_type = ($line_user->rich_menu_type === RichMenuType::RichMenu1->value)
                    ? RichMenuType::RichMenu2
                    : RichMenuType::RichMenu1;
                $line_user->rich_menu_type = $rich_menu_type->value;
                $line_user->save();

                $this->setRichMenu($line_id, $rich_menu_type);

            }

        }
    }

    private function replyMessage(string $reply_token, Message $message): bool
    {
        try {

            $request = new ReplyMessageRequest([
                'replyToken' => $reply_token,
                'messages' => [$message],
            ]);
            $this->api->replyMessage($request);
            return true;

        } catch (\Exception $e) {

            logger()->error($e->getMessage());

        }

        return false;
    }

    private function setRichMenu(string $line_id, RichMenuType $rich_menu_type): void
    {
        $rich_menu_id = $rich_menu_type->id();

        try {

            $this->api->linkRichMenuIdToUser($line_id, $rich_menu_id);

        } catch (\Exception $e) {

            logger()->error($e->getMessage());

        }
    }
}

リッチメニューを操作するためのウェブフックのルートをつくる

最後にルートです。

routes/web.php

// 省略

use App\Http\Controllers\LineRichMenuWebhookController;

// 省略

// LINE rich menu
Route::prefix('line_rich_menu')->group(function(){

    Route::prefix('webhook')->controller(LineRichMenuWebhookController::class)->group(function(){

        Route::post('receive', 'receive')->name('line_marketing.receive');

    });

});

// 省略

これで作業は完了です!
お疲れ様でした😊

実際にLINEのリッチメニューが切り替わるかテストしてみる

テストの手順は次のとおりです。

  1. LINEのQRコードを使って友だち登録する
  2. メニューが表示されているか確認
  3. 左側エリアをタップして「今がどっちのリッチメニューか」確認
  4. 右側エリアをタップしてリッチメニュー切り替え
  5. 再度、左側エリアをタップして「どっちのリッチメニューか」確認

では、友だち登録してメニューを見てみましょう。

登録が完了して・・・・・・

はい!
画面下部にリッチメニューが表示されました。

では、左側エリア(A)をタップしてみましょう。

どうなるでしょうか・・・・・・

はい!
まずは現在が「リッチメニュー1」であることがわかりました。

では、右側部分(B)をタップして、リッチメニューを切り替え、同じくリッチメニュー2の(A)部分をタップしてみましょう。

うまくいくでしょうか・・・・・・

はい!
現在がリッチメニュー2であることがわかります。

すべて成功です😊✨

※今回は同じ画像を使ったので見た目に変化はありませんが、もちろん別の画像を使うこともできるので、全く違ったリッチメニューに切り替えることもできますよ!

リッチメニューを活用したい企業様へのご提案

LINEがメッセージアプリだけでなく、マーケティング・ツールとして利用されることは珍しくなくなりました。

今回のようにリッチメニューを切り替えることで、より見込み客への「価値観教育」をすることができるようになります。

また、LINEのメッセージ開封率やリンククリック率は、メルマガと比べても格段に多いことがわかっています。

画像:メルマガ・LINE公式アカウント(*)・Lステップの比較

もしLINEを使った施策をご希望でしたら、ぜひお問い合わせからご連絡ください。お待ちしております。😊✨

おわりに

ということで、今回はLaravelLINEのリッチメニュー切り替えを実装してみました。

ちなみに、LINEcomposerパッケージが新しくなってから毎回なのですが、情報が少なすぎてメソッドはどれを使えばいいかわからないんですよね…。

なので、いっそのことパッケージなんか使わずにHTTPクライアントを使ったところスンナリいきました(笑)

今後はLINE Messaging API使うときはもうパッケージは使わないかもしれません(わりと本気!)

ぜひ皆さんも試してみてくださいね。

ではでは〜!

「いや、夜中3時で
気温28度って
罰ゲームですか…😫」

このエントリーをはてなブックマークに追加       follow us in feedly  
開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ