九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、今回も「エンジニア x マーケティング」で、「スキルを掛け算、倍増」できるようにやっていきましょう❗
今回は・・・・・・
LINE 友達登録で特典動画
を配信する機能です。
そして、今回はさらに、「動画の視聴履歴」を保存するようにします。
なぜかと言うと・・・・・・
成約率を上げるため
です。
つまり「資産運用の動画を見た人」とか「株式投資の動画を見た人」という履歴が残るので、そのデータから「興味を持っている人だけ」に濃いセールスできるわけですね。
すると、興味がない人のコミュニケーションを省略できるので、無駄な時間&コストも減らすことができます。
ということで、今回は「Laravel + LINE Messaging API」で複数の動画を配信し、さらに、見た動画の視聴履歴を取って成約率を高める機能を作ってみましょう。
ぜひ何かの参考になりましたら嬉しいです。😄✨
「イングリッシュおさる さんも
エグい凄腕の
マーケターですね(尊敬!)」
開発環境: Laravel 11.x(PHP 8.3)
目次
前提として
前回記事で紹介した以下2つの作業がすでに完了していることが前提です。
- LINE Messaging API が使えるようにする
- パッケージ「linecorp/line-bot-sdk」をインストール
詳しくは、前回記事にまとめています。
📝 参考ページ: Laravel + LINE マーケティングで「パーソナライズされたリスト取り」機能をつくる
まだの方は先にそちらを準備しておいてください。
パッケージをインストールする
実際にプログラムする前に、LINEがPHP用のパッケージを用意してくれているのでこれをインストールおきます。
以下のコマンドを実行してください。
composer require linecorp/line-bot-sdk
【追記:2024.4.15】
訪問ユーザーさんが混乱されているようでしたので追記しました。貴重なご意見ありがとうございました😊
Laravel 11.x を投入
今回からは、ついに公開されたLaravel 11.xを使って開発を進めていきます。
ファイル数が劇的に減っているようでより管理しやすくなってるみたいですね。
開発者さんたちには感謝です😄✨
動画を用意する
LINE Messaging APIを使って動画を配信するためには、ウェブ上に動画とサムネイル画像が存在している必要があります。
そのため、今回は以下の動画をウェブ上にアップロードしておき、サムネイルはダミー画像サービスの placehold.jp さんを使わせていただくことにします。

今回も楽しんでやっていきましょう❗
DB周りをつくる
では、前置きが長くなりましたがここから作業にはいりますね。
以下のコマンドでモデル&マイグレーションをつくってください。
php artisan make:model LineUser -m php artisan make:model LineUserVideoHistory -m
そして、作成されたファイルをそれぞれ変更します。(今回モデルは変更しなくてもOKです)
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->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('line_users');
}
};
database/migrations/****_**_**_******_create_line_user_video_histories_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_user_video_histories', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('line_user_id')->comment('LINE のユーザー ID');
$table->string('video_key')->comment('動画のキー');
$table->string('tracking_id')->comment('トラッキング ID');
$table->boolean('is_completed')->default(false)->comment('動画を最後まで視聴したかどうか');
$table->timestamps();
$table->foreign('line_user_id')->references('id')->on('line_users');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('line_user_video_histories');
}
};
では、この状態でデータベースを初期化しておきます。
以下のコマンドを実行してください。
php artisan migrate:fresh --seed
すると、実際のテーブルはこうなりました。


ウェブフックのコントローラーをつくる
次に今回のメインになる「ウェブフック(LINEからデータを受け取る部分)」です。
以下のコマンドでコントローラーをつくってください。
php artisan make:controller LineMarketingWebhookController
ちょっと複雑ですが、中身はこうします。
app/Http/Controllers/LineMarketingWebhookController.php
<?php
namespace App\Http\Controllers;
use App\Models\LineUser;
use App\Models\LineUserVideoHistory;
use GuzzleHttp\Client as GuzzleClient;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use LINE\Clients\MessagingApi\Configuration;
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Model\CarouselColumn;
use LINE\Clients\MessagingApi\Model\CarouselTemplate;
use LINE\Clients\MessagingApi\Model\Message;
use LINE\Clients\MessagingApi\Model\PostbackAction;
use LINE\Clients\MessagingApi\Model\ReplyMessageRequest;
use LINE\Clients\MessagingApi\Model\TemplateMessage;
use LINE\Clients\MessagingApi\Model\TextMessage;
use LINE\Clients\MessagingApi\Model\VideoMessage;
use LINE\Constants\ActionType;
use LINE\Constants\MessageType;
use LINE\Constants\TemplateType;
use LINE\Parser\EventRequestParser;
use LINE\Webhook\Model\FollowEvent;
use LINE\Webhook\Model\PostbackEvent;
use LINE\Webhook\Model\VideoPlayCompleteEvent;
class LineMarketingWebhookController extends Controller
{
const VIDEO_ITEMS = [
'test_1' => [
'video_url' => 'https://(あなたが設置した動画のあるドメイン)/videos/test1.mp4',
'thumbnail_url' => 'https://placehold.jp/3d4070/ffffff/640x480.jpg?text=video-1',
'consultation_url' => 'https://example.com/consultation-1',
'title' => '初心者OK!ウェブマーケティングの基本',
'text' => '特典動画 1',
],
'test_2' => [
'video_url' => 'https://(あなたが設置した動画のあるドメイン)/videos/test2.mp4',
'thumbnail_url' => 'https://placehold.jp/3d4070/ffffff/640x480.jpg?text=video-2',
'consultation_url' => 'https://example.com/consultation-2',
'title' => '小学生でもわかる!らくらくプログラミング講座',
'text' => '特典動画 2',
],
'test_3' => [
'video_url' => 'https://(あなたが設置した動画のあるドメイン)/videos/test3.mp4',
'thumbnail_url' => 'https://placehold.jp/3d4070/ffffff/640x480.jpg?text=video-3',
'consultation_url' => 'https://example.com/consultation-3',
'title' => '稼ぐスキルを身につける!動画編集入門',
'text' => '特典動画 3',
],
];
private $channel_secret, $access_token;
public function __construct()
{
// 本来は config から取得するべきです
$this->channel_secret = env('LINE_CHANNEL_SECRET');
$this->access_token = env('LINE_ACCESS_TOKEN');
}
public function receive(Request $request)
{
$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) {
logger(get_class($event));
if($event instanceof FollowEvent) { // お友達の登録したとき
$this->replyFollowEvent($event);
} else if($event instanceof PostbackEvent) { // ポストバック(動画がクリック)されたとき
$this->replyPostbackEvent($event);
} else if($event instanceof VideoPlayCompleteEvent) {
$this->replyVideoCompleteEvent($event);
}
}
} catch (\Exception $e) {}
} else {
abort(404);
}
}
private function replyFollowEvent(FollowEvent $event)
{
$reply_token = $event->getReplyToken();
$line_id = $event->getSource()->getUserId();
$mode = $event->getMode();
$profile = $this->api->getProfile($line_id);
$display_name = $profile->getDisplayName();
if(is_null($line_id) || is_null($mode)) {
return;
}
$line_user = LineUser::where('line_id', $line_id)->first();
if(is_null($line_user)) {
$line_user = new LineUser;
$line_user->line_id = $line_id;
}
$line_user->mode = $mode; // チャネルの状態
$line_user->display_name = $display_name; // LINEの表示名
$line_user->save();
$this->replySpecialVideo($reply_token);
}
private function replyPostbackEvent(PostbackEvent $event)
{
$reply_token = $event->getReplyToken();
$line_id = $event->getSource()->getUserId();
$line_user = LineUser::where('line_id', $line_id)->first();
$tracking_id = $line_user->id . '-' . time() . '-' . Str::random(5);
// 本来は try ~ catch でエラーハンドリングを行うべきです
$query = $event->getPostback();
parse_str($query['data'], $output);
$video_key = $output['video_key'];
$video_item = self::VIDEO_ITEMS[$video_key];
$video_history = new LineUserVideoHistory();
$video_history->line_user_id = $line_user->id;
$video_history->video_key = $video_key;
$video_history->tracking_id = $tracking_id;
$video_history->save();
// 動画を返信する
$message = new VideoMessage([
'type' => MessageType::VIDEO,
'originalContentUrl' => $video_item['video_url'],
'previewImageUrl' => $video_item['thumbnail_url'],
'trackingId' => $tracking_id,
]);
$this->replyMessage($reply_token, $message);
}
private function replyVideoCompleteEvent(VideoPlayCompleteEvent $event)
{
$reply_token = $event->getReplyToken();
$tracking_id = $event->getVideoPlayComplete()->getTrackingId();
$video_history = LineUserVideoHistory::where('tracking_id', $tracking_id)->first();
$video_history->is_completed = true;
$video_history->save();
$video_item = self::VIDEO_ITEMS[$video_history->video_key];
$consultation_url = $video_item['consultation_url'];
$message = new TextMessage([
'type' => 'text',
'text' => "「". $video_item['title'] ."」を最後までご視聴いただいた方限定で、無料の個別相談の案内をさせていただいております。\n↓↓↓\n\n{$consultation_url}",
]);
$this->replyMessage($reply_token, $message);
}
private function replySpecialVideo(string $reply_token)
{
$template_columns = [];
foreach (self::VIDEO_ITEMS as $key => $video_item) {
$template_columns[] = new CarouselColumn([
'title' => $video_item['title'],
'text' => $video_item['text'],
'thumbnailImageUrl' => $video_item['thumbnail_url'],
'actions' => [
new PostbackAction([
'type' => ActionType::POSTBACK,
'label' => '再生する',
'data' => 'video_key='. $key,
]),
],
]);
}
$template_message = new TemplateMessage([
'type' => MessageType::TEMPLATE,
'altText' => 'Button alt text',
'template' => new CarouselTemplate([
'type' => TemplateType::CAROUSEL,
'columns' => $template_columns,
]),
]);
$this->replyMessage($reply_token, $template_message);
}
private function replyMessage(string $reply_token, Message $message): bool
{
try {
$request = new ReplyMessageRequest([
'replyToken' => $reply_token,
'messages' => [$message],
]);
$this->api->replyMessage($request);
} catch (\Exception $e) {
logger($e->getMessage());
}
return true;
}
}
実行時の流れはこうなります。
- お友達登録する
- 3つの特典動画を配信
- 動画を見る
- 見た動画に関する個別相談URLを自動で送信
こうすることによって、冒頭部分で紹介した「興味を持っている人だけに濃いセールス」ができる仕組みになってます。
しかもこれ、「プッシュ送信」は一切使っていないので、すべて無料で運用することができます❗(サーバー代などは除く)
LINE Messaging API、すごいですね👍
.env にトークンをセットする
なお、コントローラー内では「シークレットキー」と「アクセストークン」は.envから取得するようになっているので気をつけてください。
.env
LINE_CHANNEL_SECRET="(ここにシークレットキー)" LINE_ACCESS_TOKEN="(ここにアクセストークン)"
CSRFトークンのチェックを解除する
今回もウェブフックを使うので、そのURLだけはCSRFトークンのチェックを解除しておきます(しないとウェブフックがブロックされます)
なお、Laravel 11.xからは解除の方法が変わっていますので、注意してください。
bootstrap/app.php
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [ // 👈 ここを追加しました
'line_marketing/webhook/receive',
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
Laravel 11.xからはbootstrap/app.phpをよく使うことになりそうですね。
ルートをつくる
では、最後にルートをつくります。
routes/web.php
use App\Http\Controllers\LineMarketingWebhookController;
// 省略
Route::prefix('line_marketing')->group(function(){
Route::prefix('webhook')->controller(LineMarketingWebhookController::class)->group(function(){
Route::post('receive', 'receive')->name('line_marketing.receive');
});
});
これで作業は完了です❗
お疲れ様でした。😄
テストしてみる
では、実際にテストしてみましょう❗
まず、お友達登録をします。(LINE DevelopersのコンソールにQRコードがあるので、それを読み取ってください)
すると・・・・・・

はい❗
(見えていない部分もありますが)3つ動画が送信されてきました。
では、1つ目の動画下にある「再生する」をクリックしてみましょう。
どうなるでしょうか・・・・・・

はい❗
「video-1」が配信されました。
では、この状態でDBテーブルも確認しておきましょう。

トラッキングID(動画の追跡ID)など保存され、さらに「動画を最後まで見た」フラグはfalseになっています。
いいですね。
では、これを再生して最後まで見てみましょう。
(少し時間がかかりますが・・・)

はい❗
自動で個別相談のメッセージが送信されてきました。
しかも、「動画1」を見たのでURLは1番目のものになっています。
DBテーブルはどうでしょうか。

はい❗
動画視聴が完了したことが分かるis_completedが1になっています。
すべて成功です😄✨
企業様へのご提案
今回のシステムを利用することで、より確実性の高い集客をすることができます。
また、今回は動画を見てすぐ個別相談のURLを送付していますが、少し「教育」が必要な場合、LINEでメルマガを発行し、その中で「買わない理由」をできるだけなくして、最終的にセールスをするという作成を取ることもできます。
もしそういったご相談がありましたら、いつでもお気軽にお問い合わせください。
お待ちしております。😄✨
おわりに
ということで、今回は動画を使ったLINEマーケティングのシステムを構築してみました。
こういったシステムは有料サービスだけでしか実現できないと思われているかもしれませんが、工夫によっては無料で運用することができます。
それにしてもマーケティング手法を考える人ってホント賢いですね。
言われれば、「あ、たしかに😄」ってなりますけど、こういうのができる人は強いでしょうね。
私はTTP(徹底的にパクる)でやっていきます(尊敬を込めて!)
皆さんもぜひマーケティングを使ったシステムを作ってみてくださいね。
ではでは〜❗
「友人が東京丸の内
のビルで次長になってました
上り詰めたなぁ😂」





