1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-06-08

やりたいこと

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

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

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

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

環境

Laravel 9
PHP 8
Mailpit

実装手順

① Artisanコマンドを作成

php artisan make:command SendDevDaysNotification

生成されるファイル:

app/Console/Commands/SendDevDaysNotification.php

② Markdown対応の通知クラスを作成

php artisan make:notification DevDaysNotification --markdown=emails.devdays

生成されるファイル:

# 通知(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 設定方法

⭐️

⑦ テスト実行(手動)

php artisan notify:send-devdays

⑦-② 開発環境の Laravel プロジェクトルートへ移動

# cd /Applications/MAMP/htdocs/Laravel/your_project_name

⑦-③ コマンドを実行

php artisan schedule:run

設定された時間に該当するコマンド(例:email:devdays)が実行されていれば、
その処理が1回だけ実行されます。
スクリーンショット 2025-06-08 17.04.39.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(=同期処理)になってる

⑧-② ジョブテーブルを作成・マイグレーション

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));

⑧-④ キューワーカーを起動(開発時)

php artisan queue:work

→ このコマンドを起動しておかないと、メールは送信されず jobs テーブルに溜まるだけになる。

⑧-⑤ キューが動いているか確認

おすすめは⑤番

①jobs テーブルにレコードが入っているか確認(QUEUE_CONNECTION=database の場合)

php artisan tinker
>>> DB::table('jobs')->count();

・0 → キューは空(処理済みまたは何も入ってない)
・1以上 → キューにまだ残ってる(ワーカーが動いてない可能性あり)

③ Mailpit でメールが届いているか

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() などにログ追加:

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 に出力される。

tail -f storage/logs/laravel.log

↑これでリアルタイム監視も可能です。


⑤ テスト用に1通だけ入れて様子を見る方法

php artisan tinker

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

Mail::to('test@example.com')->queue(new DevDaysEmail('テスト太郎', 3));

そのあと:

php artisan queue:work

Mailpit に届けばOK ✅

⑨ 本番環境(エックスサーバー)

方法:cron で定期的にキューワーカーを回す

* * * * * 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

関連

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?