何が起きたのか?
関数を呼び出したところ、PHPがこのエラーでリクエストを強制終了しました:
PHP Fatal error: Maximum function nesting level of '100' reached, aborting! in /var/www/html/app.php on line 15
PHPには関数呼び出しのネスト深度に対する組み込みの安全制限があります。コードが深すぎる再帰を行ったとき、またはXdebugがその制限を引き下げたとき、PHPはこのfatalエラーをスローして実行を完全に停止します。例外はスローされません。try/catchでも防ぐことはできません。
2つの根本原因、それぞれ異なる対処法
何かを変更する前に、どちらのケースに該当するかを確認しましょう:
- ケースA:Xdebugがインストールされており、制限が100の場合 — XdebugはPHPのネイティブ制限を上書きして独自のネスト制限(デフォルト:100)を設定します。コード自体は正常かもしれません — Xdebugが先に検出しているだけです。
- ケースB:コード内に実際の無限再帰がある場合 — 関数が終了条件なしに自分自身(または関数の循環)を呼び続けています。100という制限はPHPが諦めた地点に過ぎません。
ステップ1 — Xdebugがトリガーかどうか確認する
ターミナルで以下を実行してください:
php -m | grep xdebug
出力にxdebugが表示された場合、ロードされています。ネスト制限を確認しましょう:
php -r "echo ini_get('xdebug.max_nesting_level');"
100(または小さな数値)が出力された場合、犯人はXdebugであり、コードではありません。
Xdebugのクイックフィックス(開発環境のみ)
php.iniまたは別のxdebug.iniでXdebugのネスト制限を引き上げます:
[xdebug]
xdebug.max_nesting_level = 512
どのiniファイルを編集すればよいかわからない場合は、以下を実行してください:
php --ini
その後、PHP-FPMまたはApacheを再起動します:
# PHP-FPM
sudo systemctl restart php8.1-fpm
# Apache with mod_php
sudo systemctl restart apache2
512は正当な深い再帰(例えば200段ネストしたノードのツリーを走査する場合)には十分な値です。9999に設定しないでください:本当の無限再帰が発生した場合、明確なエラーの代わりにメモリをすべて消費してしまいます。
ステップ2 — コード内の無限再帰を見つける
Xdebugがない場合、または制限を引き上げても解決しない場合は、実際の暴走再帰が存在します。以下の方法で追跡してください。
疑わしい関数に深度カウンターを追加する
再帰関数に一時的な深度ガードを追加します:
<?php
function processNode(array $node, int $depth = 0): void {
if ($depth > 50) {
throw new RuntimeException('Recursion too deep at node: ' . $node['id']);
}
// ... your logic ...
foreach ($node['children'] as $child) {
processNode($child, $depth + 1);
}
}
これにより、文脈のない盲目的なfatalエラーではなく、循環を引き起こしている正確なノードIDを含む本物のスタックトレースが得られます。
よくある再帰バグとその修正方法
バグ1:ベースケースの欠如
// BROKEN — no base case
function factorial(int $n): int {
return $n * factorial($n - 1); // never stops
}
// FIXED
function factorial(int $n): int {
if ($n $this->id,
'parent' => $this->parent->toArray(), // recurses into parent, then its parent...
'children' => array_map(fn($c) => $c->toArray(), $this->children),
];
}
}
// FIXED — break the cycle by serializing IDs, not full objects
public function toArray(): array {
return [
'id' => $this->id,
'parent_id' => $this->parent->id, // ID only
'children' => array_map(fn($c) => $c->id, $this->children), // IDs only
];
}
バグ3:マジックメソッドのループ(__get / __call)
// __get calling itself indirectly
class Config {
public function __get(string $key): mixed {
return $this->$key; // triggers __get again → infinite loop
}
}
// FIXED — access the backing store directly
class Config {
private array $data = [];
public function __get(string $key): mixed {
return $this->data[$key] ?? null;
}
}
ステップ3 — PHPのコールスタックを使ってループを追跡する
コードを読むだけではバグを特定できない場合は、クラッシュ直前にコールスタックをダンプします:
<?php
function myRecursiveFunction(array $data): void {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
if (count($trace) > 8) {
echo "<pre>";
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
echo "</pre>";
exit;
}
// ... rest of function
}
これにより制限に達する直前の最後の10フレームが出力され、循環がどこで始まったかを正確に把握できます。
恒久的な修正:深い再帰をイテレーションに書き換える
本番環境では、再帰を完全に廃止することが正しい選択であることが多いです。関数を明示的なスタックを使ったループとして書き直すことで、ネスト制限もなく、大きなデータセットでのメモリの予期しない消費もありません:
<?php
// Recursive version — hits nesting limit on large trees
function sumTree(array $node): int {
$total = $node['value'];
foreach ($node['children'] as $child) {
$total += sumTree($child);
}
return $total;
}
// Iterative version — no recursion limit
function sumTreeIterative(array $root): int {
$stack = [$root];
$total = 0;
while (!empty($stack)) {
$node = array_pop($stack);
$total += $node['value'];
foreach ($node['children'] as $child) {
$stack[] = $child;
}
}
return $total;
}
修正が機能したかを確認する
- ページをリロードするか、失敗したスクリプトを再実行します。
- PHPエラーログを確認します — fatalエラーが消えているはずです:
tail -f /var/log/php/error.log
or for PHP-FPM:
tail -f /var/log/php8.1-fpm.log
- Xdebugの制限を引き上げた場合は、設定が反映されているか確認します:
```
php -r "echo ini_get('xdebug.max_nesting_level');"
`512`(または設定した値)が表示されるはずです。
- イテレーションに書き換えた場合は、既知の入力でクイックサニティチェックを実行します:
$tree = ['value' => 1, 'children' => [ ['value' => 2, 'children' => []], ['value' => 3, 'children' => []], ]]; echo sumTreeIterative($tree); // should print 6
## クイックリファレンス
- **Xdebugがインストールされていて制限が100の場合** → php.iniで`xdebug.max_nesting_level`を引き上げる
- **Xdebugなし、実際の再帰バグの場合** → 深度ガードを追加し、`debug_backtrace()`を使って循環を特定する
- **深いツリーを扱う本番コードの場合** → 再帰を明示的なスタックループに変換する
- **ORMの循環参照の場合** → オブジェクト全体ではなくIDをシリアライズする

