1
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?

Bladeの条件分岐が本番で効かない → 原因は「データの経路」だった。3時間デバッグした記録😇

1
Posted at

Bladeの条件分岐が本番で効かない → 原因は「データの経路」だった。3時間デバッグした記録😇

はじめに

バイクポータルサイト「MotoHub」を個人開発しています。

ニュースページのサムネイルに、メーカーロゴが object-cover でトリミングされて「kaw」しか見えない問題を直しました。

修正は Str::contains($url, 'manufacturers/') の1行。

でもその1行に辿り着くまで3時間かかりました。ローカルでは動くのに本番では動かない。OPcache? Cloudflare? Nginxキャッシュ? 全部疑って全部ハズレ。

原因は自分の条件分岐の前提が間違っていた

これはキャッシュの話じゃなくて、フォールバック画像の「データの経路」を把握していなかった話です。


環境

さくらVPS 4GB
├── Docker Compose
│   ├── Nginx(webコンテナ)
│   ├── PHP-FPM 8.3 + OPcache(appコンテナ)
│   └── MySQL 8
├── Cloudflare CDN(HTML含むフルキャッシュ)
└── デプロイ: git pull → artisan view:clear → config:cache → Cloudflare Purge Everything

問題:ロゴが見切れて何のメーカーか分からない

ニュース一覧ページで、サムネイルにメーカーロゴが使われるケースがあります。

例えばカワサキのニュースで、ニュース記事自体にサムネイル画像がない場合、フォールバックでカワサキのロゴが表示される。

でも object-cover でトリミングされるので、ロゴの中央部分だけが切り取られて「kaw」しか見えない。

image.png

これを object-contain にすればロゴ全体が表示される。簡単。


最初の修正:条件分岐で出し分け

サムネイルが「ニュース記事の画像」なら object-cover、「メーカーロゴにフォールバック」なら object-contain

ニュース一覧のBladeテンプレートには、サムネイルのフォールバックチェーンが書いてある。

@php
    $featThumb = $item->thumbnail_url
        ?: ($item->bikeModel ? $item->bikeModel->image_url : null)
        ?: ($item->manufacturer && $item->manufacturer->local_logo_path
            ? asset('storage/' . ltrim($item->manufacturer->local_logo_path, '/'))
            : null)
        ?: ($item->manufacturer ? $item->manufacturer->logo_url : null);

    // ロゴにフォールバックしたかどうかを判定
    $isFeatLogo = !$item->thumbnail_url
        && !($item->bikeModel && $item->bikeModel->image_url)
        && $featThumb;
@endphp

<img src="{{ $featThumb }}"
     class="w-full h-full {{ $isFeatLogo ? 'object-contain p-2' : 'object-cover' }}"
>

ロジック的には完璧。ローカルでもちゃんと動いた。

image.png

push → pull → view:clear → config:cache → Cloudflare Purge Everything。

本番:変わらない。


デバッグ地獄の始まり

ここから3時間の旅が始まります。

試したこと①:Cloudflareのキャッシュ?

# Cloudflare → Purge Everything
# → シークレットウィンドウで確認
# → 変わらない

試したこと②:ブラウザキャッシュ?

# Ctrl + Shift + R(スーパーリロード)
# → 変わらない

試したこと③:OPcache?

docker compose exec app php -r "opcache_reset(); echo 'OK';"
# OK
# → 変わらない

docker compose restart app
# → 変わらない

試したこと④:Viewキャッシュが残ってる?

docker compose exec app rm -rf storage/framework/views/*.php
docker compose exec app php artisan view:clear
# → 変わらない

試したこと⑤:コンテナ内のファイルは更新されてる?

docker compose exec app grep "object-contain p-2" resources/views/news/index.blade.php
<img src="{{ $featThumb }}" class="w-full h-full {{ $isFeatLogo ? 'object-contain p-2' : 'object-cover' }}"

ファイルは更新されている。 コードは正しい。デプロイも成功している。

じゃあなぜ object-cover のままなのか?


転機:実際のHTMLを見る

Cloudflareを通さず、サーバーから直接HTMLを取得。

curl -skL -H "Host: motohub.jp" https://localhost/news | grep "object-" | head -10
<img src="https://motohub.jp/storage/manufacturers/2/0.png"
     class="w-full h-full object-cover group-hover:scale-105 ..."

object-cover のまま。 $isFeatLogofalse になっている。

Cloudflareのせいじゃない。OPcacheのせいでもない。条件分岐自体が false を返している。


原因究明:tinkerで実データを確認

docker compose exec app php artisan tinker --execute="
\$news = \App\Models\BikeNews::with(['bikeModel','manufacturer'])->latest()->take(5)->get();
foreach(\$news as \$n) {
    echo \$n->id . ': thumb=' . (\$n->thumbnail_url ?: 'NULL') . PHP_EOL;
}
"

結果:

1051: thumb=NULL
1049: thumb=https://motohub.jp/storage/listings/goobike/32/830032/0.jpg
1050: thumb=https://motohub.jp/storage/listings/goobike/32/830032/0.jpg
1048: thumb=https://motohub.jp/storage/manufacturers/4/0.png  ← !!!
1047: thumb=NULL

ID 1048の thumbnail_url に、メーカーロゴのURL manufacturers/4/0.png が直接入っている。


真の原因

条件分岐を振り返る。

$isFeatLogo = !$item->thumbnail_url    // ← thumbnail_urlが空ならtrue
    && !($item->bikeModel && $item->bikeModel->image_url)
    && $featThumb;

ID 1048は $item->thumbnail_urlhttps://motohub.jp/storage/manufacturers/4/0.png が入っている。

つまり !$item->thumbnail_urlfalse

$isFeatLogo常にfalse

条件分岐が一生通らない。

なぜ thumbnail_url にロゴのURLが入っていたのか

ニュースのクローリング時に、RSSフィードの画像がなかった場合に、MotoHub側でメーカーロゴのURLを thumbnail_url に保存していた。つまりフォールバック処理がDB保存の段階で既に行われていて、Bladeテンプレートのフォールバックチェーンは通らなかった。

期待していた流れ:
thumbnail_url = NULL → bikeModel.image_url = NULL → manufacturer.logo_url ← ここでフォールバック

実際の流れ:
thumbnail_url = "https://...manufacturers/4/0.png" ← 既にロゴのURLが入っている
→ フォールバック不要 → $isFeatLogo = false

修正:URLの「中身」で判定する

「thumbnail_urlが空かどうか」ではなく、「URLに manufacturers/ が含まれているかどうか」で判定する。

@php
    $isFeatLogo = $featThumb && Str::contains($featThumb, 'manufacturers/');
@endphp

これだけ。 thumbnail_urlにロゴが直接入っていても、URLの文字列でロゴかどうかを判定できる。

image.png


教訓

教訓①:「本番で動かない」= まず自分のロジックを疑う

キャッシュを疑うのは間違いじゃない。でも3時間のうち2時間をキャッシュ系の調査に使ってしまった。

正しいデバッグ順序:

1. curl で実際のHTMLを確認(何が出力されているか)
2. tinker で実データを確認(条件分岐に渡っているデータは何か)
3. 条件の前提が正しいか確認 ← ここが本丸
4. キャッシュを疑うのはその後

教訓②:フォールバックの「データの経路」を全部把握する

今回の問題は、フォールバックが2段階で起きていたこと。

段階1(DB保存時): RSSに画像なし → メーカーロゴURLをthumbnail_urlに保存
段階2(Blade表示時): thumbnail_url → bikeModel.image_url → manufacturer.logo_url

段階1で既にロゴURLが入っているので、段階2のフォールバックは通らない。自分のコードの中に2つの経路があることに気づいていなかった。

教訓③:「nullかどうか」より「中身が何か」で判定する

// ❌ Bad: データの経路によっては thumbnail_url にロゴが入る
$isLogo = !$item->thumbnail_url;

// ✅ Good: 中身を見て判定する
$isLogo = Str::contains($thumb, 'manufacturers/');

nullチェックは「データがどこから来たか」に依存する。URLの文字列チェックは「データの中身」を見ている。後者の方が堅牢。


デバッグ3時間の時系列

時刻 やったこと 結果
21:00 コード修正、push/pull ローカルOK、本番変わらず
21:10 Cloudflare Purge 変わらず
21:20 シークレットウィンドウ 変わらず
21:30 OPcacheリセット 変わらず
21:45 docker compose restart app 変わらず
22:00 Viewキャッシュ強制削除 変わらず
22:15 Nginxキャッシュ削除試行 Nginxはsystemctlで管理されてなかった
22:30 curl で直接HTML取得 object-coverのまま
22:45 コンテナ内ファイル確認 コードは正しい
23:00 tinkerで実データ確認 thumbnail_urlにロゴURL発見
23:10 Str::containsで判定に変更 解決

22:30のcurlの時点で「キャッシュの問題じゃない」と気づくべきだった。 そこからtinkerまで30分かかったのは反省。


まとめ

問題 原因 解決
ロゴが見切れる object-cover でトリミング ロゴの場合はobject-contain
条件分岐が効かない thumbnail_urlにロゴURL直入り URLの文字列で判定に変更
本番に反映されない(と思った) 実はキャッシュの問題じゃなかった tinkerでデータを見て原因判明

「本番で動かない」の9割はキャッシュじゃない。自分のロジックの前提が間違っている。

…とはいえ、残りの1割は本当にキャッシュなので、docker compose restart appCloudflare Purge Everything は覚えておいて損はないです。


前回の記事:「売り切れデータ」を捨てずに全部残したら、GooBikeに真似できない武器になった💎

🏍 MotoHub: https://motohub.jp
X: https://x.com/motohub_jp
GitHub: https://github.com/ausssxi/MotoHub

1
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
1
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?