Chuyện gì vừa xảy ra?
Bạn gọi một hàm, và PHP đã kill request với thông báo này:
PHP Fatal error: Maximum function nesting level of '100' reached, aborting! in /var/www/html/app.php on line 15
PHP có một giới hạn an toàn tích hợp sẵn về độ sâu của các lời gọi hàm lồng nhau. Khi code của bạn đệ quy quá sâu — hoặc khi Xdebug hạ thấp giới hạn đó — PHP ném ra lỗi fatal này và dừng thực thi hoàn toàn. Không có exception nào được ném ra. Không có try/catch nào có thể cứu bạn.
Hai nguyên nhân gốc rễ, hai cách sửa khác nhau
Trước khi đụng vào bất cứ thứ gì, hãy xác định bạn đang gặp trường hợp nào:
- Trường hợp A: Xdebug đã được cài và giới hạn là 100 — Xdebug đặt giới hạn lồng nhau riêng (mặc định: 100), ghi đè giới hạn native của PHP. Code của bạn có thể hoàn toàn ổn — Xdebug chỉ bắt nó trước.
- Trường hợp B: Đệ quy vô hạn thực sự trong code của bạn — Một hàm cứ gọi chính nó (hoặc một chuỗi hàm gọi lẫn nhau) mà không có điều kiện thoát. Giới hạn 100 chỉ là điểm PHP bỏ cuộc.
Bước 1 — Kiểm tra xem Xdebug có phải nguyên nhân không
Chạy lệnh này trong terminal:
php -m | grep xdebug
Thấy xdebug trong kết quả nghĩa là nó đang được tải. Kiểm tra giới hạn lồng nhau của nó:
php -r "echo ini_get('xdebug.max_nesting_level');"
Kết quả là 100 (hoặc một số thấp nào đó)? Xdebug chính là thủ phạm — không phải code của bạn.
Sửa nhanh cho Xdebug (chỉ dùng trong môi trường development)
Tăng giới hạn lồng nhau của Xdebug trong php.ini hoặc file xdebug.ini riêng:
[xdebug]
xdebug.max_nesting_level = 512
Không chắc nên sửa file ini nào? Chạy:
php --ini
Sau đó khởi động lại PHP-FPM hoặc Apache:
# PHP-FPM
sudo systemctl restart php8.1-fpm
# Apache with mod_php
sudo systemctl restart apache2
512 là đủ cho các đệ quy sâu hợp lệ — ví dụ như duyệt một cây có 200 node lồng nhau. Đừng đặt lên 9999: đệ quy vô hạn thực sự sẽ ngốn hết bộ nhớ của bạn thay vì đưa ra lỗi rõ ràng.
Bước 2 — Tìm đệ quy vô hạn trong code của bạn
Không có Xdebug, hoặc tăng giới hạn vẫn không giúp được? Bạn đang gặp đệ quy mất kiểm soát thực sự. Đây là cách truy tìm nó.
Thêm bộ đếm độ sâu vào các hàm nghi vấn
Tạm thời thêm một guard kiểm tra độ sâu vào bất kỳ hàm đệ quy nào:
<?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);
}
}
Bây giờ bạn sẽ nhận được stack trace thực sự với ID node chính xác gây ra vòng lặp — thay vì chỉ là một lỗi fatal mù quáng không có ngữ cảnh.
Các lỗi đệ quy phổ biến và cách sửa
Lỗi 1: Thiếu trường hợp cơ sở (base case)
// 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
];
}
Lỗi 3: Vòng lặp magic method (__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;
}
}
Bước 3 — Dùng call stack của PHP để truy vết vòng lặp
Không thể tìm ra lỗi chỉ bằng cách đọc code? Dump call stack ngay trước khi xảy ra crash:
<?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
}
Đoạn code này in ra 10 frame cuối cùng trước khi bạn chạm giới hạn, cho thấy chính xác nơi vòng lặp bắt đầu.
Sửa triệt để: viết lại đệ quy sâu thành vòng lặp
Trong môi trường production, cách đúng đắn thường là bỏ đệ quy hoàn toàn. Viết lại hàm thành vòng lặp với một stack tường minh — không giới hạn lồng nhau, không bất ngờ về bộ nhớ với dữ liệu lớn:
<?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;
}
Xác nhận bản sửa đã có hiệu lực
- Tải lại trang hoặc chạy lại script đang bị lỗi.
- Kiểm tra error log của PHP — lỗi fatal phải biến mất:
tail -f /var/log/php/error.log
or for PHP-FPM:
tail -f /var/log/php8.1-fpm.log
- Đã tăng giới hạn của Xdebug? Xác nhận nó đã có hiệu lực:
```
php -r "echo ini_get('xdebug.max_nesting_level');"
Bạn sẽ thấy `512` (hoặc giá trị bạn đã đặt).
- Đã viết lại thành vòng lặp? Chạy kiểm tra nhanh với một đầu vào đã biết:
$tree = ['value' => 1, 'children' => [ ['value' => 2, 'children' => []], ['value' => 3, 'children' => []], ]]; echo sumTreeIterative($tree); // should print 6
## Tóm tắt nhanh
- **Xdebug đã cài + giới hạn là 100** → tăng `xdebug.max_nesting_level` trong php.ini
- **Không có Xdebug, lỗi đệ quy thực sự** → thêm depth guard, dùng `debug_backtrace()` để tìm vòng lặp
- **Code production với cây dữ liệu sâu** → chuyển đệ quy thành vòng lặp stack tường minh
- **Tham chiếu vòng tròn ORM** → serialize ID thay vì toàn bộ object

