「バッチ処理」で経過日数を「毎朝4時」にメールで送る方法【Mailable + バッチ処理を組むやり方】

# やりたいこと
・Laravel 9（PHP 8）で、毎日朝4時に「ポートフォリオの作成日数」をユーザーの登録メール宛に送信したい。

・ユーザーごとに「未完了のポートフォリオ」の経過日数を計算して通知する。

・通知は Laravel の Notification（通知クラス）を使って送信。

・Markdown でメール本文をデザイン。



# 環境
Laravel 9
PHP 8
Mailpit



# 実装手順
# ① Artisanコマンドを作成
```bash
php artisan make:command SendDevDaysNotification
```
生成されるファイル：
```bash
app/Console/Commands/SendDevDaysNotification.php
```



# ② Markdown対応の通知クラスを作成
```
php artisan make:notification DevDaysNotification --markdown=emails.devdays
```
生成されるファイル：
```bash
# 通知（Notification）クラス本体
app/Notifications/DevDaysNotification.php
# メールのMarkdownテンプレート
resources/views/emails/devdays.blade.php
```



# ③ コマンドに処理を書く
```app/Console/Commands/SendDevDaysNotification.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\User;
use App\Notifications\DevDaysNotification;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;

class SendDevDaysNotification extends Command
{
    protected $signature = 'notify:send-devdays';
    protected $description = '毎朝、DevDaysの経過日数を通知';

    public function handle()
    {
        $users = User::with(['counts' => function ($q) {
            $q->where('is_completed', false)->latest('started_at');
        }])->get();

        foreach ($users as $user) {
            $count = $user->counts->first();
            if ($count) {
                $days = Carbon::parse($count->started_at)->diffInDays(now());
                $user->notify(new DevDaysNotification($user->name, $days));
                Log::info("通知を送信: {$user->email}（{$days}日）");
            }
        }

        $this->info('通知の送信が完了しました。');
    }
}
```



# ④ Notificationの中身を書く

```app/Notifications/DevDaysNotification.php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class DevDaysNotification extends Notification
{
    use Queueable;

    protected $name;
    protected $days;

    public function __construct($name, $days)
    {
        $this->name = $name;
        $this->days = $days;
    }

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('【DevDays】現在の経過日数のお知らせ')
            ->markdown('emails.devdays', [
                'name' => $this->name,
                'days' => $this->days,
            ]);
    }
}
```



# ⑤ Markdownメールテンプレートを書く
```resources/views/emails/devdays.blade.php

@component('mail::message')
# DevDays 経過日数のご案内

{{ $name ?? 'お客様' }} さん、こんにちは！

現在の DevDays は開始から **{{ $days }}日** 経過しました。

引き続きがんばっていきましょう！

@endcomponent
```


⭐️
# ⑥ スケジュール登録（毎朝4時）
```app/Console/Kernel.php

protected function schedule(Schedule $schedule)
{
    $schedule->command('notify:send-devdays')->dailyAt('04:00');
}
```



## mailpit 設定方法
https://qiita.com/honaki/items/2906571f60a95c5b4f37#mailpit-%E3%81%AE%E5%B0%8E%E5%85%A5%E3%81%A8%E8%A8%AD%E5%AE%9A%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83


⭐️
# ⑦ テスト実行（手動）
```bash
php artisan notify:send-devdays

```


## ⑦-② 開発環境の Laravel プロジェクトルートへ移動
```bash
# cd /Applications/MAMP/htdocs/Laravel/your_project_name
```


## ⑦-③ コマンドを実行
```bash
php artisan schedule:run
```
設定された時間に該当するコマンド（例：email:devdays）が実行されていれば、
その処理が1回だけ実行されます。
![スクリーンショット 2025-06-08 17.04.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726068/7c1f5f34-736c-4113-b11f-f07b22f16d47.png)

### 実行完了なら「朝4時」に戻す
```Kernel.php
protected function schedule(Schedule $schedule)
{
    $schedule->command('email:send-devdays')->dailyAt('04:00');
    // $schedule->command('email:send-devdays')->everyMinute();
}
```



# ⑧ queue 対応( Laravel キュー対応)
Mail::to(...)->queue(...) を使ったキュー対応の非同期メール送信は、
処理の高速化や安定性向上に役立つ。
→ 件数が多いと、一度に多くのユーザーにメールを送る場合に、ラグが発生する等の問題が起こる。
そのため、キューで大量メールでもコマンドが止まらず、非同期で送信できるようにする。


## ⑧-① .env 設定をキュー用に変更
```.env
# QUEUE_CONNECTION=sync
QUEUE_CONNECTION=database
```
初期は sync（＝同期処理）になってる


## ⑧-② ジョブテーブルを作成・マイグレーション
```bash
pwd
# /Applications/MAMP/htdocs/Laravel/dev_days

php artisan queue:table
php artisan migrate
```
→ jobs テーブルが作られ、そこにキュー待ちの処理が保存される。


## ⑧-③ Mail::to()->send(...) → queue(...) に変更
```dev_days/app/Console/Commands/SendDevDaysEmail.php
// メール送信
// Mail::to($user->email)->send(new DevDaysEmail($user->name, $days));
Mail::to($user->email)->queue(new DevDaysEmail($user->name, $days));
```


## ⑧-④ キューワーカーを起動（開発時）
```bash
php artisan queue:work
```
→ このコマンドを起動しておかないと、メールは送信されず jobs テーブルに溜まるだけになる。


## ⑧-⑤ キューが動いているか確認
おすすめは⑤番

### ①jobs テーブルにレコードが入っているか確認（QUEUE_CONNECTION=database の場合）
```bash
php artisan tinker
>>> DB::table('jobs')->count();
```
・0 → キューは空（処理済みまたは何も入ってない）
・1以上 → キューにまだ残ってる（ワーカーが動いてない可能性あり）


### ③ Mailpit でメールが届いているか
```bash
php artisan queue:work
```
成功すれば、コンソール上に以下のようなログが出る：
```
[2025-06-08 13:00:00] Processing: App\Mail\DevDaysEmail
[2025-06-08 13:00:00] Processed:  App\Mail\DevDaysEmail
```


### ④ ログを確認する（ログ出力を追加）
コマンドの handle() メソッドや Mailable の build() などにログ追加：



### ③ Mailpit でメールが届いているか
・Laravelで Mail::queue() した場合、届いていればキュー処理が成功している証拠。
・逆に jobs テーブルにずっと残ってて届かない → ワーカーが動いてない


### ④ ログを確認する（ログ出力を追加）
コマンドの handle() メソッドや Mailable の build() などにログ追加：
```bash
use Illuminate\Support\Facades\Log;
Log::info('メールをキューに入れました');
```


#### コマンドの handle() メソッドの場合
```/dev_days/app/Console/Commands/SendDevDaysEmail.php
<?php
// ⭐️
use Illuminate\Support\Facades\Log;


// with で読み込んだ counts のコレクションから、最初の1件(=最新の未完了)を取り出す。
foreach($users as $user) {
    $count = $user->counts->first(); // 未完了の最新Count

    if($count) {
        $startedAt = Carbon::parse($count->started_at);
        $days = $startedAt->diffInDays(now());

        // ⭐️ ログ出力
        Log::info('メールをキューに入れました: ' . $user->email);

        // メール送信
        // Mail::to($user->email)->send(new DevDaysEmail($user->name, $days));
        Mail::to($user->email)->queue(new DevDaysEmail($user->name, $days));
    }
}
```

#### Mailable の build() の場合
メールが build() されたタイミングでもログ出せます。
これは実際にキューで処理される段階にて役立ちます。
```/dev_days/app/Mail/DevDaysEmail.php
use Illuminate\Support\Facades\Log;

// ⭐️ メールの内容を組み立てる部分
public function build()
{
    // ⭐️ ログ
    Log::info('DevDaysEmail を build: ' . $this->name . ' さん, 経過日数: ' . $this->days);

    return $this->subject("DevDaysの経過日数のお知らせ") // .subject(...) → メールの件名を設定
                ->markdown('emails.devdays'); // .view(...) → 本文に使う Blade テンプレート(resources/views/emails/devdays.blade.php)を指定
}
```

#### ログの出力先は？
通常は storage/logs/laravel.log に出力される。
```bash
tail -f storage/logs/laravel.log
```
↑これでリアルタイム監視も可能です。


---
### ⑤ テスト用に1通だけ入れて様子を見る方法
```bash
php artisan tinker

use Illuminate\Support\Facades\Mail;
use App\Mail\DevDaysEmail;

Mail::to('test@example.com')->queue(new DevDaysEmail('テスト太郎', 3));
```
そのあと：
```bash
php artisan queue:work
```
Mailpit に届けばOK ✅



# ⑨ 本番環境(エックスサーバー)
方法：cron で定期的にキューワーカーを回す
```bash
* * * * * cd /home/ユーザー名/ドメイン/プロジェクトパス && php artisan queue:work --stop-when-empty
```
--stop-when-empty を付けるのがポイント ✅
→ 無限ループで居座らないようにする（共有サーバーでは必須）

## 実際のcron設定例（Xserver）
```
* * * * * cd /home/akkun1114/akkun1114.com/your_project && /usr/bin/php8.1 artisan queue:work --stop-when-empty >> /dev/null 2>&1
```
php8.1 のパスは、Xserverのサーバーパネルで確認できます。


## どうなるか？
・毎分cronで queue:work を1回だけ実行
・jobs テーブルにキューがあれば1つずつ処理して終わる
・処理がなくなればすぐ終了する
・Supervisor とは違い、常駐しない（共有サーバー向けの運用）


## まとめ：エックスサーバーでのキュー対応はこうする
| 項目              | 対応方法                               |
| --------------- | ---------------------------------- |
| Supervisor      | ❌ 使用不可                             |
| systemd         | ❌ 使用不可                             |
| queue\:work を常駐 | ❌ 不可                               |
| 対応策             | ✅ cron + `--stop-when-empty` を毎分実行 |




# おまけ
## .env の設定サンプル
```.env
MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="DevDays"

QUEUE_CONNECTION=database
```



# 関連
https://qiita.com/honaki/items/11c6ae3f6c075d4050ea