Laravel『Mailable Class Error』の原因と対処法

Laravel『Mailable Class Error』の原因と対処法

Laravelの「Mailable Class Error」は、メール送信時にMailableクラス(App\Mail…)の読み込み・生成・ビルド・ビュー描画・添付処理・キュー実行のどこかで例外が起きて送信できない状態を指すことが多い。特に多いのは「クラスが見つからない(Class not found)」「コンストラクタ引数が違う」「build()/envelope()/content() の実装ミス」「メール用ビューが無い」「キュー化したらシリアライズできない」「添付ファイルのパスが不正」の6系統。まずは例外メッセージを見て、“クラス解決の問題か / ビュー描画か / キューか / 添付か” を切り分ける。

症状と発生条件(典型例)

よくあるメッセージ例。

Class "App\Mail\OrderShipped" not found
Too few arguments to function App\Mail\NoticeMail::__construct()
Undefined variable: user (View: emails.notice)
View [emails.notice] not found.
Serialization of 'Closure' is not allowed
Typed property App\Mail\NoticeMail::$user must not be accessed before initialization
Call to undefined method App\Mail\NoticeMail::build()

発生条件の典型:
・Mail::to(…)->send(new XxxMail(…)) 実行時に即エラー
・キュー送信(queue/ShouldQueue)にした途端に失敗ジョブになる
・メール本文ビュー(Blade/Markdown)が存在しない、変数が足りない
・添付ファイルのパスが存在しない/権限がない
・LaravelのMailable仕様(古い build() / 新しい envelope/content)を混在させている

まず確認:失敗している場所(同期送信かキュー送信か)

キュー送信にしていると、失敗は queue:failed 側に出て「送ったつもり」になりやすい。切り分けとして、まずは同期送信で同じMailableを試す。

// 同期で切り分け
Mail::to($to)->send(new NoticeMail($user));

キューの場合は failed_jobs を確認する。

php artisan queue:failed
php artisan queue:retry all

原因1:Mailableクラスが見つからない(namespace/ファイル配置/autoload)

発生条件:
・use App\Mail\NoticeMail; を書いていない
・クラス名/ファイル名の大小文字がズレている(Linux本番で顕在化しやすい)
・移動/リネームしたがautoloadが更新されていない
対処:
・正しい名前空間で import する
・クラスのパスを確認して作り直す

php artisan make:mail NoticeMail
composer dump-autoload

Mailクラスが App\Mail にある前提で、use を必ず揃える。

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

原因2:コンストラクタ引数の不一致(Too few arguments / 型違い)

発生条件:
・Mailableの __construct($user, $order) に必要数があるのに、new NoticeMail($user) しか渡していない
・型宣言付きで null を渡している
対処:呼び出し側とMailable側の契約を一致させる。

// Mailable
class NoticeMail extends Mailable
{
    public function __construct(
        public readonly User $user,
        public readonly Order $order,
    ) {}
}

// 呼び出し側
Mail::to($user->email)->send(new NoticeMail($user, $order));

「途中で引数を追加した」場合は、全呼び出し箇所が更新されているか検索する。

原因3:build()/envelope()/content() の実装ミス(Laravelバージョン差)

Laravelの世代で書き方が異なる。プロジェクトがどちらの流儀かで統一しないと混乱する。
・従来:build() で subject/view/attach を組む
・新しめ:envelope()/content()/attachments() を使う
同一Mailable内で中途半端に混ぜると、意図通りに呼ばれず“値が入らない”状態になりやすい。
build方式の例。

class NoticeMail extends Mailable
{
    public function __construct(public readonly User $user) {}

    public function build()
    {
        return $this->subject('お知らせ')
            ->view('emails.notice')
            ->with(['user' => $this->user]);
    }
}

原因4:メール用ビューが無い/変数が不足(View not found / Undefined variable)

発生条件:
・view(‘emails.notice’) なのに resources/views/emails/notice.blade.php がない
・Blade内で $user を使っているのに with していない
・Markdownメールで resources/views/vendor/mail が未整備
対処:
・ビューのパスを実ファイルと一致させる
・必要な変数を必ず渡す

// Mailable側
return $this->view('emails.notice')
    ->with([
        'user' => $this->user,
        'url'  => route('home'),
    ]);

ビュー確認の目安。

resources/views/emails/notice.blade.php

原因5:キュー送信時のシリアライズ失敗(Serialization of ‘Closure’ is not allowed 等)

発生条件:
・ShouldQueue を実装している(または queue() で送っている)
・Mailableのプロパティに Closure、Request、UploadedFile、DB接続、未シリアライズオブジェクトが入っている
・Eloquentモデル自体は大抵OKだが、リレーションを詰め込みすぎて肥大化する
対処:
・キューに渡すのは “IDやプリミティブ” を基本にする
・必要ならジョブ側で再取得する

// NG例:Requestを持たせる
public function __construct(public Request $request) {}

// OK例:必要な値だけ持つ
public function __construct(public int $userId) {}

// build時に再取得
$user = User::findOrFail($this->userId);

failed_jobs を確認し、どの例外が出たかで原因を確定させる。

原因6:添付(attach/attachFromStorage)でパスが不正・権限がない

発生条件:
・attach(‘/path/to/file’) のファイルが存在しない
・storageの権限がない
・S3等の外部ストレージで署名URL前提なのにローカルパス扱いしている
対処:
・存在確認とディスク指定を明確にする

class ReportMail extends Mailable
{
    public function __construct(public string $path) {}

    public function build()
    {
        return $this->subject('レポート')
            ->view('emails.report')
            ->attachFromStorageDisk('public', $this->path, 'report.pdf');
    }
}

publicディスクなら storage:link の有無も影響する。

原因7:送信先やヘッダの組み立てミス(from/replyTo/subjectが不正)

発生条件:
・fromアドレスが空、形式不正
・件名に不正文字列(稀)
・to/cc/bcc に配列構造のミス
対処:Mailableでの指定を最小構成にしてから段階的に追加する。

return $this->subject('test')
    ->view('emails.test');

サンプル:動く最小のMailable(同期送信で切り分け)

まずこの形で送信できることを確認し、ビュー/変数/添付/キューを足していく。

// app/Mail/SimpleNotice.php
namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class SimpleNotice extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public string $name) {}

    public function build()
    {
        return $this->subject('通知')
            ->view('emails.simple-notice')
            ->with(['name' => $this->name]);
    }
}

<p>こんにちは、{{ $name }}さん</p>

// 送信側
use App\Mail\SimpleNotice;
use Illuminate\Support\Facades\Mail;

Mail::to('you@example.com')->send(new SimpleNotice('Taro'));

チェックリスト(上から順に確認する)

1) 例外メッセージは “Class not found / 引数不足 / View not found / Serialization / 添付パス” のどれか
2) php artisan route:list や処理経路を確認し、実際にそのMailableが呼ばれているか
3) use App\Mail\XxxMail; の import、namespace、大小文字、ファイル名が一致しているか
4) __construct の引数と new XxxMail(…) の呼び出しが一致しているか
5) view名と resources/views の実ファイルが一致しているか、with変数が足りているか
6) キュー送信なら failed_jobs を確認し、Closure/Request/巨大オブジェクトを持たせていないか
7) 添付は存在するパスか、ディスク指定が正しいか、権限があるか
8) .env変更後なら config:clear / cache:clear を実施し反映漏れを排除したか