環境
- Laravel 10.43.0
- PHP 8.2.17
- MySQL 8.0.35
- Blade
- Laravel Tinker
- Docker Compose(php-fpm + nginx + mysql)
結論から
テストユーザーを設計する前に、対象のBladeを上から読みましょう。特に次の2つを先に洗い出します。
- nullチェックなしで参照されているhasOneリレーション
- 目的のセクションを包んでいる外側の
@if条件
コントローラだけを読んでも、画面の表示条件までは分かりません。テストデータは「DBを埋めるもの」ではなく「Bladeの分岐を満たすもの」と考えると、無駄な500や非表示を減らせます。
起きたこと
某会員システムで、ロジック検証用のテストユーザーをtinkerでDBに作成しました。ブラウザで確認するとまず500。原因は、User hasOne Profileの関連行が存在しないのに、Bladeが$user->profile->display_nameを直参照していたことでした。
関連行を追加すると500は解消。しかし今度は、目的の有料会員セクション自体が非表示になりました。原因は、そのセクションを包む外側の@ifが、別の認定データの有無で分岐していたことです。
つまり、
- 1段落ち:
profileがnullなのに直接カラム参照して500 - 2段落ち:
certificationsが空で外側@ifがfalseになり非表示
という構造でした。
再現コード
テーブル
// database/migrations/..._create_users_table.php
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->timestamps();
});
// ..._create_profiles_table.php
Schema::create('profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
$table->string('display_name');
$table->text('bio')->nullable();
$table->timestamps();
});
// ..._create_certifications_table.php
Schema::create('certifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('type'); // paid, free, trial...
$table->string('status'); // approved, pending...
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
モデル
// app/Models/User.php
class User extends Authenticatable
{
use HasFactory;
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
public function certifications(): HasMany
{
return $this->hasMany(Certification::class);
}
}
問題のBlade
{{-- resources/views/members/show.blade.php --}}
<header>
<h1>{{ $user->profile->display_name }}</h1>
</header>
@if ($user->certifications->where('type', 'paid')->isNotEmpty())
<section id="paid-section">
<h2>有料会員向け情報</h2>
<p>{{ $user->profile->bio }}</p>
</section>
@endif
tinkerでユーザーだけ作成
php artisan tinker
>>> $user = App\Models\User::create([
... 'name' => 'テスト太郎',
... 'email' => 'test@example.com',
... 'password' => bcrypt('password'),
... ]);
>>> $user->profile; // null
>>> $user->certifications; // 空のCollection
この状態で画面を開くと、$user->profile->display_nameで500になります。
profileを追加
>>> $user->profile()->create([
... 'display_name' => 'テスト太郎',
... 'bio' => '検証用プロフィール',
... ]);
500は消えますが、certificationsにpaidがないため、@ifがfalseになりpaid-sectionは表示されません。
デバッグ手順
- ログを確認する
tail -f storage/logs/laravel.log
- Bladeの該当行を特定する
grep -RIn '\$user->profile->' resources/views
- 外側の条件分岐を洗い出す
grep -n '@if' resources/views/members/show.blade.php
- tinkerでリレーションの実データを確認する
>>> App\Models\User::with(['profile', 'certifications'])
... ->where('email', 'test@example.com')
... ->first();
- 表示条件をデータ要件に変換する
| Bladeの条件 | 必要なテストデータ |
|---|---|
$user->profile->display_name |
profiles行が必須 |
certifications->where('type','paid')->isNotEmpty() |
type=paidのcertifications行が必須 |
status=approved |
status=approvedの行が必須 |
- 上から順にデータを作成する
$user = User::factory()->create();
$user->profile()->create([...]);
$user->certifications()->create([
'type' => 'paid',
'status' => 'approved',
'expires_at' => now()->addYear(),
]);
修正パターン
null安全にする
<header>
<h1>{{ $user->profile?->display_name ?? '未設定' }}</h1>
</header>
PHP 8.0未満ならoptional()を使います。
<h1>{{ optional($user->profile)->display_name ?? '未設定' }}</h1>
外側の条件を明確にする
@if ($user->certifications()->where('type', 'paid')->where('status', 'approved')->exists())
<section id="paid-section">
<h2>有料会員向け情報</h2>
<p>{{ $user->profile?->bio ?? '' }}</p>
</section>
@endif
eager loadを忘れない
$user = User::with(['profile', 'certifications'])->findOrFail($id);
ハマりポイント
-
@if ($user->certifications)は直感に反して常にtrueになりやすいです。hasManyは空でもCollectionオブジェクトを返すためです。isNotEmpty()やexists()を使いましょう。 -
$user->profile ?? nullでは、$user->profile->display_nameのnull参照は防げません。?->かoptional()が必要です。 -
tinkerでUserだけ作ると、Bladeの外側@ifを満たせず「データはあるのに画面が出ない」状態になります。 - コントローラで
with()していても、Blade内の直接参照や外側@ifの条件までは保証できません。
比較表
| 調査起点 | 分かること | 見落としやすいこと | 向くケース |
|---|---|---|---|
| コントローラ | 取得条件、eager load、認可 | Blade内の直接参照、外側@if
|
APIやロジック修正 |
| Blade | 表示条件、null参照、分岐 | DB制約、業務ロジック | テストデータ設計、画面検証 |
| 両方 | 画面とデータの全体像 | ほぼなし | 障害調査、リリース前確認 |
FAQ
Q. テストデータを作るとき、Bladeから先に読みますか?コントローラから追いますか?
A. 画面検証やテストデータ設計ならBladeから先に読むのがおすすめです。表示条件をそのままデータ要件に変換できます。APIやバッチの検証ならコントローラやServiceから読む方が早いです。
Q. @if ($user->certifications)ではダメですか?
A. ダメなケースが多いです。hasManyのリレーションは空でもCollectionオブジェクトを返すため、PHPのtruthy判定ではtrueになります。isNotEmpty()またはexists()を使いましょう。
Q. $user->profile ?? nullで防げますか?
A. 防げません。$user->profileがnullの場合、$user->profile->display_nameはエラーになります。$user->profile?->display_nameやoptional($user->profile)->display_nameを使います。
Q. すべてのBladeを読む必要がありますか?
A. 対象画面と、その画面が読み込む部分テンプレート、レイアウトだけで十分なことが多いです。grep -RIn '\$user->' resources/viewsで直接参照を洗い出すと効率的です。
テストデータ作成チェックリスト
- 対象Bladeを上から読む
-
$user->relation->column形式の直接参照をgrepする - セクションを包む
@ifをすべて書き出す - hasOneは行が存在しない前提で確認する
- hasManyは空Collectionのtruthy判定に注意する
- statusやtype、expires_atなど分岐に使うカラムを埋める
- FactoryやSeederに状態メソッドを追加する
- 最後にブラウザでセクションの表示/非表示まで確認する
まとめ
テストユーザー作成で500や非表示にハマったら、コントローラだけでなくBladeを先に読みましょう。特に「nullチェックなしのhasOne参照」と「外側の@if条件」を先に洗い出すだけで、テストデータの抜けを大きく減らせます。
テストデータはDBの行を増やす作業ではなく、Bladeの分岐を満たす作業です。画面の条件をデータ要件に変換する習慣をつけると、検証の再現性が上がります。
この記事を書いた人
BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。
GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。
👉 BENTEN Web Works — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中