0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テストユーザー作成で500→非表示になった話:Bladeを先に読むべき理由

0
Posted at

環境

  • Laravel 10.43.0
  • PHP 8.2.17
  • MySQL 8.0.35
  • Blade
  • Laravel Tinker
  • Docker Compose(php-fpm + nginx + mysql)

結論から

テストユーザーを設計する前に、対象のBladeを上から読みましょう。特に次の2つを先に洗い出します。

  1. nullチェックなしで参照されているhasOneリレーション
  2. 目的のセクションを包んでいる外側の@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は表示されません。

デバッグ手順

  1. ログを確認する
tail -f storage/logs/laravel.log
  1. Bladeの該当行を特定する
grep -RIn '\$user->profile->' resources/views
  1. 外側の条件分岐を洗い出す
grep -n '@if' resources/views/members/show.blade.php
  1. tinkerでリレーションの実データを確認する
>>> App\Models\User::with(['profile', 'certifications'])
...     ->where('email', 'test@example.com')
...     ->first();
  1. 表示条件をデータ要件に変換する
Bladeの条件 必要なテストデータ
$user->profile->display_name profiles行が必須
certifications->where('type','paid')->isNotEmpty() type=paidのcertifications行が必須
status=approved status=approvedの行が必須
  1. 上から順にデータを作成する
$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) — 日々の知見を発信中

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?