Laravel『Undefined Offset』の原因と対処法

Laravel『Undefined Offset』の原因と対処法

Laravelで見かける「Undefined offset」は、PHPのNotice/Warning系(例:Undefined offset: 0)で、配列やコレクションを“存在しない添字(インデックス)”で参照したときに発生する。Laravel固有の例外というより、Controller/Service/Blade/Collection操作のどこかで「配列の0番目がある前提」「explodeした結果が必ず2要素ある前提」「pluck後に特定キーがある前提」などの思い込みが破綻したときに出る。データが空、条件で絞られて0件、形式が崩れている、境界条件(最初/最後)を考慮していない、が根本原因になりやすい。

エラーの出方と発生条件(典型例)

典型メッセージ。

Undefined offset: 0
Undefined offset: 1

発生条件の典型:
・クエリ結果が0件なのに $rows[0] を参照した
・explode(‘,’, $str) の結果が想定より短く $parts[1] が無い
・for ループで配列サイズ以上を回している(<= と < のミス)
・ページングや配列スライスで空配列が返るケースを考慮していない
・Bladeで $items[0] を直接参照しているが、空の時がある
※本番では表示されずログだけに出ることもあるが、原因は同じ。

まず確認:どのファイルの何行目で落ちているか

Laravelのログ(storage/logs/laravel.log)や例外画面に「Undefined offset」が出た行が載る。
その行が、配列参照($a[0] / $a[$i] / $a[‘key’])か、explode結果か、Collection→toArray後の参照かを確認する。

原因1:クエリ結果0件なのに [0] を参照している

ありがちな例。

$users = User::where('email', $email)->get();
$name = $users[0]->name; // 0件なら Undefined offset: 0

対処:
・first() / firstOrFail() を使って「1件前提」を明示する

$user = User::where('email', $email)->first();
if (!$user) {
    // 0件の場合の分岐
    abort(404);
}
$name = $user->name;

・「必ず存在するべき」なら firstOrFail() で404に寄せる

$user = User::where('email', $email)->firstOrFail();

原因2:explode/implode/文字列分割の前提が崩れている

CSVっぽい文字列や「a:b」の形式を想定していたが、実際は区切り文字が無い/空/欠損しているケース。

$parts = explode(':', $input);
$type  = $parts[0];
$value = $parts[1]; // ":" が無いと Undefined offset: 1

対処:分割結果の要素数を確認する。

$parts = explode(':', $input, 2);

if (count($parts) < 2) {
    // 形式不正
    abort(422, 'Invalid format');
}

[$type, $value] = $parts;

「limit付きexplode(第3引数)」を使うと、想定外のコロンが含まれても壊れにくい。

原因3:forループの境界(<= と <)が間違っている

配列の最後の添字は count($a)-1。

for ($i = 0; $i <= count($items); $i++) { // NG: 最後は範囲外
    $x = $items[$i];
}

対処:< にするか、foreachに寄せる。

for ($i = 0; $i < count($items); $i++) {
    $x = $items[$i];
}

foreach ($items as $x) {
    // 安全
}

原因4:Collection操作後に空になっている(filter/whereで0件)

Collectionで絞り込んだ結果が空なのに、先頭要素を参照する。

$hit = $users->filter(fn($u) => $u->is_active)->values();
$id  = $hit[0]['id']; // 空なら Undefined offset: 0

対処:first() や isEmpty() を使う。

$first = $hit->first();
if (!$first) {
    return response()->json(['message' => 'no data'], 404);
}
$id = $first['id'];

または「0件のときのデフォルト」を決める。

$id = optional($hit->first())['id'] ?? null;

原因5:Bladeテンプレートで配列の要素を直参照している

ビューはデータの欠損が起きやすく、空配列で落ちやすい。

<!-- NG -->
{{ $items[0]['name'] }}

対処:@forelse で空表示を用意する、または null 合体で守る。

@forelse ($items as $item)
  {{ $item['name'] }}
@empty
  <p>データがありません</p>
@endforelse


{{ $items[0]['name'] ?? '' }}

ビューで「0番目を参照する」設計自体を避け、Controller側で「先頭要素を取り出して渡す」ほうが事故が減る。

サンプル:安全な取得・分割・先頭要素参照のテンプレ

// 1件前提の取得
$user = User::where('email', $email)->first();
if (!$user) {
    abort(404);
}

// 形式 "type:value" を想定
$parts = explode(':', $request->input('tag', ''), 2);
if (count($parts) < 2) {
    abort(422, 'Invalid tag format');
}
[$type, $value] = $parts;

// コレクションの先頭(空なら分岐)
$list = collect($request->input('items', []))->filter()->values();
$firstItem = $list->first();
if (!$firstItem) {
    return response()->json(['message' => 'items is empty'], 422);
}

return response()->json([
    'user_id' => $user->id,
    'type' => $type,
    'value' => $value,
    'first_item' => $firstItem,
]);

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

1) どの行で落ちているか(storage/logs/laravel.log のファイル/行番号)を特定したか
2) 配列/コレクションが空の可能性を潰したか(isEmpty/first/null分岐)
3) explode結果の要素数を確認しているか(count、limit付きexplode)
4) forループの境界条件が正しいか(<= になっていないか、foreachで代替できないか)
5) Bladeで $items[0] の直参照をしていないか(@forelse、??、Controller側で整形)