【Laravel11】LINEでの勤怠管理を実装する

# はじめに
社内のプロジェクトで、実施未定のものがあり、その下調べをしようとしたらドツボにはまったので、これで命を削る人がひとりでも減るように、ここに記録しようと思いました。

なんせ、情報が少ない。

Laravelが11になってガラッと変わってしまって、以前参考にしていた先人の記事では追い付かなくなってしまいました。そこで、調べまくった結果をここに残しておこうと思います。
初めての投稿なので、変なところがあったらご指摘ください。

環境は、2025.1.5現在
Windows 11
PHP 8.3
Laravel 11.5.0
docker desktop 4.37.1
Visual Studio Code 1.96.2
また、Laravel公式だと「php～」ではなく「sail～」で書かれているので、この記事ではsailで押し通します。
## LINE側の準備
すみません。
あまりに項目が多いので、テキストだけで説明させてください。
不親切ですみません。
<ul>
<li>まず、LINE Developersで、LINE Buisiness IDでアカウントを作成、ログインする。</li>
<li>プロバイダーを作成。</li>
<li>新規チャネル作成で、Messaging APIを選択する。</li>
<li>LINE公式アカウントを作成する。</li>
<li>LINE Official Accountに飛ぶ。</li>
<li>ここで、LINE公式アカウントの作成をする。項目を埋めていく。</li>
<li>認証リクエストするか、後で認証するか、選べる（はず）</li>
<li>同意画面が2つ出るので、同意する。
この時点で、LINE Offical Account Managerにいるはず。
<li>チャットタブを選択すると、「チャットがオフに設定されています」と出るので、「応答設定に移動」を選択する。</li>
<li>左の設定のうち、Messaging APIを選択する。</li>
<li>Messaging APIを利用するボタンを押す。</li>
<li>プロバイダーを作成、または選択する。同意する。</li>
<li>プライバシーポリシー、利用規約は任意。</li>
<li>Channel ID、Channel secretが表示される。Channel secretは、後で使用するので控えておく。あとで.envに書く。</li>
<li>Webhook URLを入力する。</li>
ここは、httpsでなければならない。例えば、https://xxxxx.xxx/webhook とか、実URLにwebhookなどをくっつけて入力しておく。（後で、rouitingで使う）</li>

<li>ローカル環境を公開するには、ngrokを使うといい。（一位番下に、おまけとして書いておきました）</li>

<li>左の設定の応答設定を選択して、応答機能で、WebhookをONにする。
（Messaging APIで設定をした後でないと、ここはONにできない）</li>
<li>「応答メッセージ」はOFFにする。（ここはお好みで）</li>
<li>LINE Developersに移動して、作成したチャネルを選択する。
<li>Messaging API設定タブを選択する。
<li>ここで、ボットのベーシックIDと、QRコードを得られる。</li>
<li>一番下の、チャネルアクセストークン（長期）を発行する。
これも控えておく。あとで.envに書く。</li>
</ul>

## Laravelでの実装の前に

### aliasの設定
~/.bash_aliasesファイルを作り、下記の行を入れておくといいです。（もちろん、.bashrcでもいいです）

```
alias sail='./vendor/bin/sail'
```
編集したら、.bashrcの実行か、めんどくさかったら新しいウインドウを開くのがいいです。

### php-xml、php-domのインストール
どうも、php関連で標準ではインストールされないものがあるようです。
念のためインストールされているか確認してみます。

```
php -m | grep xml
```
これでxmlの項目が表示されていれば有効なようです。
logを見ると、～.xml、～.domがない、と言われることがあります。
この場合、以下のコマンドを実行してください。

```
sudo apt update
sudo apt install php-xml
sudo apt install php-dom
```

### VSCodeの用意
以下の拡張機能をインストールしておきます。
・Japanese Language Pack
・Laravel Snippets
（それ以外にも、お好みで。良いものがあったら教えてください。）
以下、ファイル・フォルダの新規作成・編集などは、VSCode上で行います。
（というか、今後使えって言われちゃった笑）

## Laravelプロジェクトの作成

プロジェクトを作りたいディレクトリで実行します。
```bash
curl -s https://laravel.build/Attendance | bash
```
```bash
cd Attendance
sail up -d
code .
```
VSCodeが起動するので、まず「.env」ファイルを編集します。</li>
```
APP_TIMEZONE=Asia/Tokyo
...
APP_LOCALE=jp
APP_FALLBACK_LOCALE=jp
APP_FAKER_LOCALE=ja_JP
```
以下を追加します。
```
LINE_CHANNEL_SECRET=xxxxxxxxxxxxxxx（上記、Channel secret）
LINE_CHANNEL_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxx（上記チャネルアクセストークン（長期））
```
とりあえず、ここだけ。
MySQLでのユーザー名は、sailになります。

### tailwindcssのインストール
Laravelでは、tailwindcssを使用しているので、下記を実行してインストールしておきます。

```bash
sudo npm install -D tailwindcss
sudo npx tailwindcss init
```
npmがないと言われたら、先にインストールします。

```
sudo apt update
sudo apt install npm
```

LINE Messaging APIを利用するために、下記を実行します。
```
composer require linecorp/line-bot-sdk:^7.6
```
もしcomposerがないなんて言われたら、先に

```
sudo apt install composer
```
を実行します。
ここでなぜline-bot-sdkのバージョンを指定しているかというと、最新版（2025.1.5現在）は10っぽいのですが、それでは使い方がわからず、あえてバージョンを落として使ってます。（バージョン8とか9でもダメでした…）

#### Laravelでの実装（webhook部分の実装）
<ol>
<li>ルート設定（１）</li>
routes/web.phpに以下を追加します。

```
use App\Http\Controllers\LineWebhookController;

Route::post('/webhook', [LineWebhookController::class, 'handle']);
```
<li>コントローラ作成（１）</li>

```bash
sail artisan make:controller LineWebhookController
```
LineWebhookControllerのコードを以下のように変更します。
```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use LINE\LINEBot;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;
use LINE\LINEBot\MessageBuilder\TextMessageBuilder;
use App\Models\Attendance;

class LineWebhookController extends Controller
{
    private $bot;

    public function __construct()
    {
        $httpClient = new CurlHTTPClient(env('LINE_CHANNEL_ACCESS_TOKEN'));
        $this->bot = new LINEBot($httpClient, ['channelSecret' => env('LINE_CHANNEL_SECRET')]);
    }

    public function handle(Request $request)
    {
        $events = $request->input('events', []);

        foreach ($events as $event) {
            if ($event['type'] === 'message' && $event['message']['type'] === 'text') {
                $replyToken = $event['replyToken'];
                $userId = $event['source']['userId'];
                $messageText = $event['message']['text'];

                $this->processAttendance($userId, $messageText, $replyToken);
            }
        }

        return response()->json(['status' => 'ok'], 200);
    }

    private function processAttendance($userId, $messageText, $replyToken)
    {
        if (strtolower($messageText) === '出勤') {
            Attendance::create([
                'user_id' => $userId,
                'type' => 'clock_in',
                'timestamp' => now(),
            ]);
            $message = new TextMessageBuilder('出勤を記録しました。');
        } elseif (strtolower($messageText) === '退勤') {
            Attendance::create([
                'user_id' => $userId,
                'type' => 'clock_out',
                'timestamp' => now(),
            ]);
            $message = new TextMessageBuilder('退勤を記録しました。');
        } else {
            $message = new TextMessageBuilder('「出勤」または「退勤」と入力してください。');
        }

        $this->bot->replyMessage($replyToken, $message);
    }
}
```

<li>モデルとマイグレーション作成</li>

```bash
sail artisan make:model Attendance -m
```
マイグレーションファイル
database/migrations/xxxx_xx_xx_xxxxxx_create_attendances_table.phpを編集します。
（upのほうだけ）
```php
public function up()
{
    Schema::create('attendances', function (Blueprint $table) {
        $table->id();
        $table->string('user_id');
        $table->enum('type', ['clock_in', 'clock_out']);
        $table->timestamp('timestamp');
        $table->timestamps();
    });
}
```
このあたりで、セッションテーブルも作っておきます。

```
sail artisan make:session-table
```
マイグレーションを実行してみます。
```bash
sail artisan migrate
```
ただ単に「すでにある」といわれて終わるならいいのですが、「テーブルがすでにある」といわれてエラーするときがあります。（実際は英語ですが）
その時は、

```
sail mysql
mysql>show tables;
```
して、エラーで「すでにある」といわれているテーブルを、例えば、

```
mysql>drop table attendances;
```
とかして削除します。
mysqlから抜けるには、

```
mysql>exit;
```
などとしてください。
まだmysqlサービスが起動していないとかの場合は、この手は使えないので、思い切って、
```
sail down（または、sail stop）して、
docker desktopでコンテナを削除、
sail up -d
```
という手もあります。

<li>モデルの編集</li>
app/Models/Attendance.phpを以下のように編集します。

```php
<?php

namespace App\Models;

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

class Attendance extends Model
{
    use HasFactory;

    protected $fillable = ['user_id', 'type', 'timestamp'];
}
```
<li>CSRF除外設定</li>
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',
        // api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        // channels: __DIR__.'/../routes/channels.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // CSRF除外
        $middleware->validateCsrfTokens(except: [
            '/webhook',
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();
```
この変更をしたら、configをクリアしておきます。（この段階では、まだ必要ないかもしれませんが、一応）

```bash
sail artisan config:clear
```
#### Laravelでの実装（勤怠データ表示部分の実装）
<li>ルート設定（２）</li>
routes/web.phpに以下を追加します。

```php
use App\Http\Controllers\AttendanceController;

Route::get('/attendance', [AttendanceController::class, 'index']);
```
<li>コントローラ作成（２）</li>
下記を実行

```bash
sail artisan make:controller AttendanceController
```
app/Http/Controller/AttendanceControllerを以下のように編集します。

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Attendance;

class AttendanceController extends Controller
{
    public function index()
    {
        $attendances = Attendance::all();

        return view('attendance.index', compact('attendances'));
    }
}
```
<li>ビューの作成</li>
resources/views/attendance/index.blade.phpを作成します。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>勤怠履歴</title>
</head>
<body>
    <h1>勤怠履歴</h1>
    <table border="1">
        <thead>
            <tr>
                <th>ID</th>
                <th>ユーザーID</th>
                <th>タイプ</th>
                <th>タイムスタンプ</th>
            </tr>
        </thead>
        <tbody>
            @foreach ($attendances as $attendance)
            <tr>
                <td>{{ $attendance->id }}</td>
                <td>{{ $attendance->user_id }}</td>
                <td>{{ $attendance->type }}</td>
                <td>{{ $attendance->timestamp }}</td>
            </tr>
            @endforeach
        </tbody>
    </table>
</body>
</html>
```
<li>動作確認</li>
友達登録をして、「出勤」とLINEすると「出勤を記録しました。」と答え、「退勤」とLINEすると「退勤を記録しました。」と答え、どちらでもなければ『「出勤」または「退勤」と入力してください。』と返事をします。
また、ブラウザのほうで、勤怠記録が表示されることを確認します。
</ol>

ローカル環境を外部から参照するためにngrokをお使いの方は、

```
ngrok http 80
```
（ポート番号はお好みで）して出てくるURLをコピーし、LINE Developersで開発中のチャネルを選択し Messaging API タブを選ぶと一番下に「Webhook URL」を指定するところがあるので、ここを編集してURLをペーストして、後ろに「/webhook」をつけてから実行します。
おそらく、ngrokを起動するたびにURLが変わるので、ご注意ください。
友達登録するには、同ページにあるQRコードを使うといいです。

固定IPの場合や、デプロイした後の検証はしていないので、そのあたりの情報もあったら追記したいです。

以上です。


（おまけ）
ngrokのインストール。
https://download.ngrok.com
で log in します。
ここから、「linux用の」ngrokをインストールします。
左の「Serup and installastion」が選ばれていると思うので、右側のコマンドをコピペして実行します。
次に、その下にある

```
ngrok config add-authtoken xxxxxxxxxx
```
をコピペして実行します。


【更新履歴】
2025.01.10 エラーする部分などの検証・追記。ngrokのインストールも追記。
2025.01.09 一部文章の追記。
2025.01.05 バグ修正（CSRF除外設定のところ。ごめんなさい）ngrok起動とwebhookのURLについての記述追加。微調整。
2025.01.03 微調整
2025.01.01 初版