やりたいこと
・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
③ コマンドに処理を書く
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の中身を書く
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メールテンプレートを書く
@component('mail::message')
# DevDays 経過日数のご案内
{{ $name ?? 'お客様' }} さん、こんにちは!
現在の DevDays は開始から **{{ $days }}日** 経過しました。
引き続きがんばっていきましょう!
@endcomponent
⭐️
⑥ スケジュール登録(毎朝4時)
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回だけ実行されます。

実行完了なら「朝4時」に戻す
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 設定をキュー用に変更
# 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(...) に変更
// メール送信
// 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() メソッドの場合
<?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() されたタイミングでもログ出せます。
これは実際にキューで処理される段階にて役立ちます。
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 の設定サンプル
MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="DevDays"
QUEUE_CONNECTION=database
関連