یادم می‌آید اولین بار که خطای Trying to access array offset on value of type null را در لاگ یک سایت مشتری دیدم، تصور کردم با یک Warning بی‌خطر طرفم؛ اما وقتی دیدم همین Warning در هر ریکوئست صدبار در error_log نوشته می‌شود و I/O دیسک سرور را می‌خورد، فهمیدم این خطا نه‌تنها بی‌خطر نیست، بلکه خودش یک مشکل عملکردی است. اگر با خطای Parse error در PHP آشنایی دارید، می‌دانید که بخش بزرگی از باگ‌های پروژه‌های وردپرسی از همین دست هشدارها آغاز می‌شود؛ هشدارهایی که در نگاه اول کوچک به نظر می‌رسند ولی می‌توانند منطق برنامه را بی‌صدا بشکنند. در این راهنما، از ریشه‌ی زبانی این خطا شروع می‌کنم، سناریوهای واقعی وردپرس و PHP را کالبدشکافی می‌کنم، و چند راه‌حل با درجات متفاوت تمیزی و امنیت ارائه می‌دهم.

خطای array offset on null دقیقاً چیست؟

خطای Trying to access array offset on value of type null یک هشدار (Warning) در زبان PHP است که وقتی رخ می‌دهد که شما با استفاده از براکت (square brackets)، روی متغیری که مقدارش null است، به یک کلید (key) دسترسی پیدا کنید. مثلاً کدی مثل $user["name"] وقتی $user مقدار null داشته باشد، همین پیام را تولید می‌کند. نکته مهم اینجاست که پیام با دقت از عبارت array offset استفاده می‌کند نه undefined index؛ چون در موتور Zend Engine، موتور اجرای PHP، عملیات دسترسی به offset آرایه روی یک مقدار غیرآرایه، در یک کد جداگانه بررسی می‌شود و از سال ۲۰۱۹ به بعد به‌جای Notice، به‌صورت Warning گزارش می‌شود.

تفاوت این خطا با خطای Undefined index را باید درست فهمید. Undefined index وقتی رخ می‌دهد که متغیر شما آرایه معتبری است ولی کلید موردنظر در آن وجود ندارد؛ در حالی که array offset on null وقتی رخ می‌دهد که خودِ متغیر null است. همین تفاوت، مسیر دیباگ را کاملاً عوض می‌کند. در حالت اول، شما باید وجود کلید در آرایه را چک کنید؛ در حالت دوم، ابتدا باید بفهمید چرا متغیر در این نقطه از اجرای کد null شده است.

هر هشدار از جنس array offset، در حقیقت پیام‌رسانی PHP است که یک فرض ضمنی در کد شما شکسته شده: جایی از برنامه فرض کرده‌اید یک مقدار آرایه است، در حالی که در واقعیت null بوده.

چرا PHP 7.4 این خطا را معرفی کرد؟

پیش از PHP 7.4، کدی مثل $value = null; echo $value["key"]; یک E_NOTICE تولید می‌کرد که در بیشتر محیط‌های تولید (production) خاموش بود و به همین دلیل ساعت‌ها می‌توانست بی‌صدا اجرا شود و مقدار null را به عنوان نتیجه برگرداند. این رفتار باعث می‌شد باگ‌های منطقی به‌سختی پیدا شوند؛ چرا که به‌جای اینکه برنامه با صدای بلند بگوید یک فرض غلط داشته، در سکوت داده اشتباه تولید می‌کرد و این داده به لایه‌های بالاتر می‌رسید.

تیم توسعه PHP در نسخه ۷.۴ تصمیم گرفت این نوع دسترسی را از E_NOTICE به E_WARNING ارتقا دهد تا توسعه‌دهندگان جدی‌تر بگیرندش. این تصمیم بخشی از روند کلی سخت‌گیرانه‌تر شدن PHP در نسخه‌های ۷ و ۸ بود که در تفاوت PHP 7 و PHP 8 به آن پرداخته‌ام. نتیجه این تغییر: کدهایی که سال‌ها بی‌صدا کار می‌کردند، ناگهان با انبوه هشدارها مواجه شدند و همین باعث شد یک موج بزرگ از درخواست‌های پشتیبانی در انجمن‌های وردپرس و Stack Overflow شکل بگیرد.

در PHP 8.x این هشدار همچنان باقی مانده و حتی در برخی موارد مثل تلاش برای دسترسی به offset روی bool یا int هم گزارش می‌شود. نکته مهم برای توسعه‌دهنده وردپرسی این است که حتی اگر سایت شما روی PHP 7.3 باشد و این هشدار را نبیند، هنگام ارتقا به نسخه جدیدتر (که در اکثر هاست‌های امروزی اجتناب‌ناپذیر است) این باگ‌ها یک‌شبه ظاهر می‌شوند و می‌توانند عملکرد سایت را تحت تأثیر قرار دهند. به همین دلیل، بهتر است حتی قبل از ارتقا، کد را با ابزارهایی مثل PHPStan یا Psalm بررسی کنید.

چرا این خطا در وردپرس زیاد دیده می‌شود؟

وردپرس در هسته‌ی خود کدی دارد که با استانداردهای سخت‌گیرانه‌ی امروز PHP نوشته نشده و بیشتر روی نسخه‌های قدیمی PHP تست شده است. بسیاری از توابع وردپرس در شرایط خاص، به‌جای آرایه، مقدار false یا null برمی‌گردانند. مثلاً get_post_meta() وقتی کلید وجود ندارد، رشته خالی برمی‌گرداند؛ get_the_terms() وقتی ترمی وجود ندارد، false برمی‌گرداند؛ و get_option() اگر آپشن ثبت نشده باشد و مقدار پیش‌فرضی نداشته باشید، false می‌دهد. اگر توسعه‌دهنده این مقادیر برگشتی را چک نکند و مستقیماً رویشان offset آرایه اعمال کند، همین خطا رخ می‌دهد.

در توسعه PHP در وردپرس، این مشکل با اضافه شدن افزونه‌ها دوچندان می‌شود؛ چون هر افزونه ممکن است انتظار داشته باشد یک آپشن یا متای خاص همیشه وجود داشته باشد و اگر کاربر یا افزونه‌ی دیگری آن را حذف کرده باشد، خطا در لایه‌های پایین‌تر پدیدار می‌شود. در پروژه‌های واقعی من، بیشترین موارد بروز این خطا از سه منبع بوده: افزونه‌های قدیمی که روی PHP 7.0 تست شده‌اند، کدهای سفارشی مشتری که بدون بررسی مقدار برگشتی نوشته شده، و قالب‌هایی که در بخش‌هایی از خود مثل سربرگ یا فوتر از متغیرهای سراسری بدون چک کردن استفاده می‌کنند.

هر افزونه‌ای که در تابع فعال‌سازی‌اش هیچ بررسی نسخه PHP نداشته باشد، کاندیدای اصلی بروز این هشدار در آینده است؛ حتی اگر امروز بی‌صدا کار کند.

سناریوهای واقعی بروز این خطا

سناریوهای بروز این خطا بسیار متنوع‌اند، ولی همه در یک الگوی مشترک خلاصه می‌شوند: جایی از کد شما انتظار دارد متغیری آرایه باشد، اما آن متغیر null است. در ادامه پنج سناریوی رایجی که در پروژه‌های واقعی با آن‌ها مواجه شده‌ام را با هم مرور می‌کنیم.

سناریو ۱: مقدار برگشتی تابع null است

رایج‌ترین سناریو. تابعی مثل get_user_by() در وردپرس وقتی کاربری با شناسه مشخص وجود نداشته باشد، false برمی‌گرداند؛ و اگر توسعه‌دهنده بدون چک کردن، مستقیماً $user["name"] بنویسد، هشدار صادر می‌شود. همین الگو در توابع سفارشی هم بسیار دیده می‌شود. یک تابع مشتری که باید آرایه‌ای از دیتابیس برگرداند، در صورت خطای کوئری ممکن است null بدهد و بقیه‌ی زنجیره‌ی کد روی همین null ادامه یابد.

سناریو ۲: متغیر قبل از حلقه مقداردهی نشده

الگوی کلاسیک: یک متغیر به‌عنوان accumulator تعریف می‌شود، ولی به‌دلیل شرطی در حلقه، قبل از رسیدن به بلوک استفاده مقداردهی نمی‌شود. مثلاً در کدی که در پیاده‌سازی خطای Undefined variable در PHP هم مشابهش را بررسی کرده‌ام، گاهی فقط یک شاخه از if متغیر را مقدار می‌دهد و شاخه دیگر نه. در این حالت، خروجی حلقه null است و هر دسترسی offset روی آن هشدار می‌دهد.

سناریو ۳: خروجی json_decode ناموفق

تابع json_decode() وقتی رشته ورودی JSON معتبر نباشد، null برمی‌گرداند. اگر پاسخ یک API خارجی خراب یا ناقص باشد و کد شما بدون چک کردن نتیجه، مستقیماً $data["items"] بنویسد، همین خطا رخ می‌دهد. در پروژه‌هایی که با APIهای خارجی کار می‌کنند، این سناریو بسیار شایع است؛ چرا که API خارجی می‌تواند هر لحظه تغییر کند و ساختار پاسخ را عوض کند.

سناریو ۴: اشتباه گرفتن object و array

در وردپرس توابع زیادی داریم که object برمی‌گردانند (مثل get_post() یا get_user_by())، و در مقابل توابعی داریم که array برمی‌گردانند. اگر توسعه‌دهنده اشتباهاً از براکت روی object استفاده کند، خطای متفاوتی صادر می‌شود که در خطای Cannot use object as array به آن پرداخته‌ام؛ ولی اگر تابع در شرایط خاص null برگرداند، این خطا جایگزین می‌شود. علت شیوعش این است که در کد legacy، بسیاری از متغیرها بدون type hint نوشته شده‌اند و PHP نمی‌تواند از قبل به شما هشدار بدهد.

سناریو ۵: wp_parse_args و آرایه‌های خالی

الگوی رایج در افزونه‌ها: تابعی با پارامتر پیش‌فرض array تعریف می‌شود، ولی در فراخوانی مقدار null به آن پاس می‌شود. تابع wp_parse_args() این را مدیریت می‌کند و آرایه خالی برمی‌گرداند، ولی اگر مستقیم روی پارامتر offset بخواهید، هشدار می‌گیرید. همین الگو در توابع سفارشی هم تکرار می‌شود و در آمار، یکی از شایع‌ترین منشأهای این هشدار است.

ریشه‌یابی: چرا متغیر شما null است؟

پس از تشخیص مکان بروز خطا، گام بعدی پاسخ به این سؤال است: چرا این متغیر در این نقطه از اجرا null است؟ این سؤال را باید جدی گرفت، چون بی‌توجهی به آن باعث می‌شود شما صرفاً هشدار را با یک شرط ساده خفه کنید و باگ واقعی سر جای خود باقی بماند. در تجربه‌ی خودم، ریشه‌های null شدن متغیر تقریباً همیشه در یکی از این پنج دسته قرار می‌گیرند:

  1. مقدار برگشتی تابع صریحاً null یا false بوده و چک نشده است.
  2. یک رکورد دیتابیس به‌دلیل شرط نامناسب یا نبود داده، وجود ندارد.
  3. یک API خارجی پاسخ نامعتبر داده و کد به‌جای مدیریت خطا، ادامه داده است.
  4. در کد سفارشی، یک شرط منطقی اشتباه باعث شده متغیر مقدار نگیرد.
  5. یک افزونه یا قالب، متغیر سراسری را با مقدار null بازنویسی کرده است.

در هر یک از این پنج دسته، درمان متفاوتی لازم است. اگر مشکل از مقدار برگشتی تابع است، کافی است یک چک اضافه کنید؛ ولی اگر از نوع چهارم باشد، باید منطق شرط را بازنویسی کنید. این تفکیک، مرز بین توسعه‌دهنده‌ای است که باگ را می‌فهمد و توسعه‌دهنده‌ای که باگ را فقط از چشم کاربر پنهان می‌کند. برای تشخیص دقیق دسته، باید لاگ کامل خطا، لاگ سرور و مسیر اجرای کد را با ابزارهایی مثل Xdebug یا Query Monitor بررسی کنید. اگر تازه با این ابزارها آشنا می‌شوید، پیشنهاد می‌کنم ابتدا روش‌های رفع Fatal error در PHP را بخوانید؛ چرا که همان ابزارهای دیباگ در اینجا هم به کار می‌آیند.

روش تشخیص گام‌به‌گام

در پروژه‌های بزرگ، پیدا کردن خط دقیق بروز هشدار بدون یک روش منظم تقریباً غیرممکن است؛ چون در یک سایت وردپرسی معمولی، ده‌ها افزونه و قالب می‌توانند باعث این هشدار شوند و هر یک از آن‌ها هزاران خط کد دارد. روشی که در سال‌های اخیر به آن رسیده‌ام و در پروژه‌های مشتریان هم اجرا می‌کنم، ترکیبی از این پنج گام است:

  1. فعال‌سازی لاگ خطا در محیط staging: به‌جای دیباگ روی production، سایت را در یک نسخه staging کپی کنید و در فایل wp-config.php این خطوط را اضافه کنید: define("WP_DEBUG", true); define("WP_DEBUG_LOG", true); define("WP_DEBUG_DISPLAY", false);. نتیجه در فایل wp-content/debug.log ذخیره می‌شود.
  2. فیلتر کردن خطا در لاگ: با یک دستور grep مثل grep "array offset" wp-content/debug.log | awk -F: "{print $1}" | sort | uniq -c | sort -rn می‌توانید بفهمید کدام فایل‌ها بیشترین سهم را در تولید این هشدار دارند.
  3. شناسایی افزونه یا قالب مقصر: با شمارش تعداد خطا در هر مسیر، می‌توانید حدس بزنید کدام افزونه مسئول است. اگر شک دارید، افزونه‌ها را یکی‌یکی غیرفعال کنید تا خطا متوقف شود.
  4. بازتولید خطا در محیط لوکال: قطعه‌کد مقصر را جدا کنید و در یک فایل مستقل PHP اجرا کنید؛ این کار باعث می‌شود بدون سربار وردپرس، منطق را بهتر ببینید.
  5. استفاده از stack trace: اگر از Xdebug استفاده می‌کنید، با تنظیم xdebug.show_error_trace = 1 می‌توانید مسیر دقیق فراخوانی تابع را ببینید و بفهمید داده از کجا آمده است.

روش گام‌به‌گام دیباگ را در دیباگ کد سفارشی وردپرس با جزئیات بیشتری توضیح داده‌ام؛ در همین‌جا فقط این نکته را اضافه کنم که حذف موقت و ذهنی خطا، اولین جایی است که اکثر توسعه‌دهندگان تازه‌کار به آن پناه می‌برند و همین کار، بعدها فاجعه می‌آفریند. تا زمانی که ریشه‌ی null شدن متغیر را نفهمید، رفع هشدار فقط یک تأخیر در بروز مشکل بعدی است.

راه‌حل‌های عملی از ساده تا حرفه‌ای

برای رفع این خطا چند راه‌حل با درجات متفاوت تمیزی و امنیت وجود دارد. انتخاب درست به موقعیت شما بستگی دارد؛ ولی قاعده‌ای که در همه‌ی پروژه‌های حرفه‌ای رعایت می‌کنم این است: کمترین تغییری که هم هشدار را برطرف کند و هم منطق برنامه را حفظ کند. در ادامه از ساده‌ترین راه‌حل تا حرفه‌ای‌ترین را می‌بینیم.

راه‌حل اول: چک کردن با isset

قدیمی‌ترین و در دسترس‌ترین ابزار، تابع isset() است. اگر کدی دارید مثل:

$user = get_user_by("id", $id);
echo $user["name"];

با افزودن یک شرط می‌توانید هشدار را حذف کنید:

$user = get_user_by("id", $id);
if (isset($user["name"])) {
    echo $user["name"];
}

نکته مهم این است که isset() وقتی متغیر null باشد، false برمی‌گرداند و همین باعث می‌شود دسترسی offset رخ ندهد. این روش، هم سریع است و هم سازگاری بالایی با نسخه‌های قدیمی PHP دارد.

راه‌حل دوم: چک کردن نوع با is_array

اگر به‌جای null ممکن است مقدار دیگری هم برگردد (مثلاً رشته یا عدد)، بهتر است نوع متغیر را قبل از دسترسی چک کنید:

$items = json_decode($response, true);
if (is_array($items) && isset($items["data"])) {
    foreach ($items["data"] as $item) {
        process($item);
    }
}

این روش در برابر پاسخ‌های نامعتبر از APIهای خارجی مقاوم‌تر است، چون هم null و هم رشته و هم عدد را رد می‌کند.

راه‌حل سوم: Null Coalescing Operator

از PHP 7.0 به بعد، اپراتور ?? در دسترس است که هم چک isset و هم مقدار پیش‌فرض را در یک خط ترکیب می‌کند:

$name = $user["name"] ?? "مهمان";

این اپراتور در برابر null و undefined index هر دو مقاوم است و برای مقدار پیش‌فرض ایده‌آل است. استفاده از آن باعث می‌شود کد کوتاه‌تر و خواناتر شود، ولی توجه داشته باشید که اگر مقدار پیش‌فرض نداشته باشید، مقدار نهایی ممکن است null بماند و در ادامه دوباره خطا بدهد. بنابراین در جاهایی که مقدار نهایی به هر حال باید معتبر باشد، از راه‌حل دوم استفاده کنید.

راه‌حل چهارم: Null-Safe Operator در PHP 8

اگر پروژه‌تان روی PHP 8 اجرا می‌شود، اپراتور ?-> در دسترس است که قبل از دسترسی، null بودن را بررسی می‌کند:

$user = get_user_by("id", $id);
echo $user?->name ?? "ناشناس";

این اپراتور برای object کار می‌کند نه array، ولی در کدهایی که با object کار می‌کنند، خوانایی را به‌شدت بالا می‌برد. تفاوت این اپراتور با ?? این است که ?-> فقط null را مدیریت می‌کند نه undefined index. این دو مکمل یکدیگرند و در پروژه‌های واقعی معمولاً با هم استفاده می‌شوند. در خطای Call to a member function on null نمونه‌های بیشتری از این الگو را دیده‌ام.

راه‌حل پنجم: توابع defensive و type declaration

حرفه‌ای‌ترین راه‌حل، تغییر معماری خود تابع است. اگر یک تابع نوشته‌اید که همیشه باید آرایه برگرداند، در سر تابع یک راهکار دفاعی اضافه کنید:

function get_safe_items(): array {
    $raw = fetch_from_api();
    if (!is_array($raw)) {
        return [];
    }
    return $raw;
}

با این کار، مصرف‌کننده‌ی تابع همیشه آرایه می‌گیرد و مجبور نیست در ده جای مختلف کد defensive بنویسد. این رویکرد در پروژه‌های بزرگ، هم‌زمان تعداد خطاها و تعداد خطوط کد را کاهش می‌دهد. اگر به‌طور جدی با PHP مدرن کار می‌کنید، مطالعه روش‌های PHP مدرن را در اولویت قرار دهید؛ بخش بزرگی از این الگوها همان‌جا توضیح داده شده است.

راه‌حل ششم: مدیریت متمرکز خطا

در سطح بالاتر، می‌توانید یک error handler سفارشی نصب کنید که هشدارهای از نوع array offset را در سطح برنامه بگیرد و به‌جای چاپ در front-end، به یک لاگ ساختاریافته بفرستد:

set_error_handler(function ($severity, $message, $file, $line) {
    if (strpos($message, "array offset") !== false) {
        error_log(sprintf("Null offset in %s:%d - %s", $file, $line, $message));
        return true;
    }
    return false;
});

این روش باعث می‌شود خطاهای مشابه در آینده به‌جای چاپ برای کاربر، در یک فایل متمرکز جمع شوند و شما بتوانید الگوهای تکرارشونده را پیدا کنید. توجه کنید که این روش را فقط در محیط staging یا بعد از تست کامل روی production فعال کنید، چرا که خفه کردن همه هشدارها می‌تواند باگ‌های مهم‌تر را پنهان کند.

مقایسه روش‌های رفع خطا

هر کدام از راه‌حل‌های بالا، نقاط قوت و ضعف مشخصی دارند. جدول زیر خلاصه‌ای از مقایسه‌ی آن‌هاست تا بر اساس نیاز پروژه، انتخاب آگاهانه‌ای داشته باشید:

روشسرعت اجراخوانایی کدسازگاری نسخه PHPمناسب برای
issetبالامتوسطهمه نسخه‌هاچک ساده پیش از دسترسی
is_arrayبالاخوبهمه نسخه‌هامقاوم‌سازی برابر پاسخ API
Null coalescingبالاعالیPHP 7.0+مقدار پیش‌فرض سریع
Null-safe operatorبالاعالیPHP 8.0+زنجیره‌های object
تابع defensiveبالاعالیهمه نسخه‌هاپروژه‌های بزرگ و تیمی
Error handler متمرکزمتوسطمتوسطهمه نسخه‌هاپایش و شناسایی الگو

در انتخاب بین این روش‌ها، همیشه این سؤال را از خودتان بپرسید: آیا در این نقطه از کد، null شدن متغیر یک حالت عادی است یا نشانه‌ی یک باگ؟ اگر حالت عادی است، Null coalescing انتخاب خوبی است. اگر نشانه باگ است، به‌جای خفه کردن با مقدار پیش‌فرض، بهتر است تابع را اصلاح کنید تا دیگر null برنگرداند.

ملاحظات امنیتی و کارایی

نکته‌ای که در بحث‌های معمول درباره این خطا کمتر به آن پرداخته می‌شود، بُعد امنیتی آن است. اگر شما با یک چک ساده هشدار را خفه کنید و مقدار پیش‌فرض نامناسب بگذارید، ممکن است داده‌ی اشتباه به لایه‌های بالاتر برود و در نهایت به یک آسیب‌پذیری تبدیل شود. مثلاً فرض کنید تابعی که سطح دسترسی کاربر را تعیین می‌کند، در شرایط خطا null برگرداند و شما با ?? مقدار پیش‌فرض ادمین بگذارید. این دقیقاً همان چیزی است که در دنیای امنیت به آن fail-open می‌گویند: سیستم در شرایط خطا، به‌جای بستن دسترسی، آن را باز می‌کند.

برای جلوگیری از این دام، همیشه مقدار پیش‌فرض را به‌سمت امن انتخاب کنید: دسترسی پیش‌فرض کاربر عادی باشد نه ادمین؛ مقدار پیش‌فرض قیمت صفر نباشد بلکه خطا بدهد؛ مقدار پیش‌فرض وضعیت پرداخت، ناموفق باشد نه موفق. این اصل ساده، در نوشتن کد PHP امن برای وردپرس با مثال‌های بیشتر توضیح داده شده است.

از بُعد کارایی، هر هشدار PHP یک هزینه‌ی نه‌چندان کوچک دارد. هر بار که PHP یک E_WARNING تولید می‌کند، باید رشته پیام را بسازد، فایل و شماره خط را بگیرد، به error handler ارسال کند و در نهایت در لاگ بنویسد. اگر این اتفاق در هر ریکوئست ده‌ها بار بیفتد، روی سایت‌های پربازدید می‌تواند فشار قابل توجهی روی I/O دیسک و CPU وارد کند. در پروژه‌ای که بررسی کردم، همین هشدارها سالانه حدود سه گیگابایت لاگ تولید می‌کردند و فضای دیسک سرور را به‌طور جدی مصرف می‌کردند.

هر هشدار تکرارشونده، یک مالیات پنهان روی کارایی است؛ حتی اگر کاربر نهایی چیزی نبیند، سرور هزینه‌اش را می‌دهد.

در بُعد دیگر، اگر روی داده‌ی ورودی کاربر offset اعمال می‌کنید، حتماً قبل از چک کردن نوع، داده را پاک‌سازی کنید. چرا که مهاجم می‌تواند ساختار داده را طوری دستکاری کند که به‌جای آرایه، یک رشته با محتوای مخرب بفرستد و اگر شما فقط array بودن را چک نکنید، ممکن است offset روی رشته اجرا شود و نتایج غیرمنتظره بگیرد. روش‌های پاک‌سازی داده‌ها در وردپرس برای این منظور ضروری است.

پرسش‌های پرتکرار درباره این خطا

در ادامه به پرتکرارترین پرسش‌هایی که در ایمیل‌ها و دیدگاه‌ها درباره این خطا مطرح می‌شود، پاسخ می‌دهم. اگر پاسخ سؤالتان اینجا نبود، در بخش دیدگاه همین صفحه مطرح کنید.

آیا این خطا سایت را از کار می‌اندازد؟

خیر، این خطا از نوع Warning است نه Fatal Error؛ بنابراین صفحه به‌طور کامل از کار نمی‌افتد. ولی این بدان معنا نیست که می‌توانید نادیده‌اش بگیرید. اگر این هشدار در تابعی رخ دهد که مقدار مهمی را برمی‌گرداند، ممکن است مقدار null یا مقدار پیش‌فرض به لایه‌های بالاتر برود و باعث رفتار اشتباه یا حتی از کار افتادن بخشی از عملکرد سایت شود. علاوه بر این، روی سایت‌های پربازدید، حجم لاگ تولیدشده خودش یک مشکل جدی است.

چرا در هاست من این خطا نمایش داده نمی‌شود ولی در هاست دیگر نمایش داده می‌شود؟

نمایش یا عدم نمایش این خطا به تنظیمات error_reporting و display_errors در فایل php.ini بستگی دارد. بسیاری از هاست‌ها به‌طور پیش‌فرض نمایش خطا را خاموش می‌کنند تا اطلاعات حساس برای کاربر نمایش داده نشود. ولی حتی اگر خطا نمایش داده نشود، در لاگ سرور ثبت می‌شود. برای دیدن لاگ، از طریق cPanel یا SSH به فایل error_log دسترسی پیدا کنید. اگر می‌خواهید فقط برای خودتان خطاها را ببینید، در محیط staging تنظیمات را فعال کنید ولی در production خاموش نگه دارید.

تفاوت این خطا با Undefined index چیست؟

Undefined index وقتی رخ می‌دهد که متغیر شما آرایه است ولی کلید موردنظر در آن وجود ندارد؛ در حالی که array offset on null وقتی رخ می‌دهد که خود متغیر null است. در حالت اول، راه‌حل این است که وجود کلید را با isset() یا array_key_exists() چک کنید. در حالت دوم، ابتدا باید بفهمید چرا متغیر null شده است و سپس تصمیم بگیرید که آیا این رفتار طبیعی است یا نشانه‌ی یک باگ.

آیا استفاده از @ قبل از متغیر مشکل را حل می‌کند؟

کاراکتر @ در PHP یک error suppressor است که هشدار را موقتاً خفه می‌کند، ولی هیچ‌کدام از مشکلات ریشه‌ای را حل نمی‌کند. استفاده از آن در کد حرفه‌ای یک ضدالگو (anti-pattern) شناخته می‌شود، چون هم کارایی را کم می‌کند (چون PHP باید خطا را بسازد و بعد سرکوب کند) و هم باگ‌های پنهان را عمیق‌تر می‌کند. توصیه من این است که به هیچ وجه از آن استفاده نکنید و به‌جایش با یکی از راه‌حل‌های پیشنهادی بالا مشکل را از ریشه حل کنید.

اگر خطا از یک افزونه معروف باشد چه کنیم؟

اول مطمئن شوید آخرین نسخه افزونه را دارید؛ بسیاری از افزونه‌ها در به‌روزرسانی‌ها این نوع خطاها را رفع کرده‌اند. اگر خطا همچنان باقی بود، یک تیکت پشتیبانی بزنید و مسیر فایل و خط دقیق را با آن‌ها به اشتراک بگذارید. اگر افزونه دیگر پشتیبانی نمی‌شود، یا جایگزین بهتری پیدا کنید یا از هوک‌های خود وردپرس برای override کردن بخش مسئول استفاده کنید. در نهایت اگر هیچ‌کدام از این راه‌حل‌ها جواب نداد، به‌عنوان آخرین راهکار می‌توانید از یک mu-plugin برای پیاده‌سازی راهکار defensive روی همان نقطه خاص استفاده کنید.

آیا این خطا در PHP 8 هم دیده می‌شود؟

بله، این هشدار در PHP 8.x همچنان وجود دارد و حتی در برخی موارد شدیدتر می‌شود. چرا که PHP 8 رفتار سخت‌گیرانه‌تری نسبت به نسخه ۷ دارد و در شرایطی که قبلاً Notice تولید می‌کرد، الان Warning یا حتی TypeError می‌دهد. به همین دلیل، اگر روی PHP 7.4 این خطا را رفع کرده‌اید ولی هنوز روی PHP 8 خطا می‌گیرید، احتمالاً باید به سراغ نوع‌های داده دقیق‌تر و declarationها بروید. مطالعه خطای Deprecated در PHP در این مسیر به شما کمک می‌کند.

نگاه معماری: از خطا تا طراحی مقاوم

اگر از زاویه مهندسی نرم‌افزار به این خطا نگاه کنیم، می‌بینیم که این هشدار یک نشانه است از یک مشکل عمیق‌تر: نبود قرارداد روشن بین توابع و مصرف‌کنندگانشان. وقتی تابعی می‌تواند هم آرایه برگرداند و هم null و هم false، مصرف‌کننده نمی‌داند در هر لحظه چه انتظاری داشته باشد. در زبان‌های مدرن، این مشکل با استفاده از type systems قوی یا Result Types حل می‌شود؛ در PHP هم با افزودن type declarationها، استفاده از union types و رویکرد اشتباه نکردن حالت خطا با حالت عادی، می‌توان به همان سطح از شفافیت رسید.

الگوی دیگری که در پروژه‌های بزرگ پیشنهاد می‌کنم، استفاده از Value Objects و DTO (Data Transfer Objects) است. اگر تابعی که با API خارجی کار می‌کند، به‌جای آرایه خام، یک object با فیلدهای نوع‌دار برگرداند، دیگر کسی در کد نگران null بودن نخواهد بود؛ چون constructor آن object، در هنگام ساخت اگر ورودی نامعتبر باشد، استثنا پرتاب می‌کند. این رویکرد باعث می‌شود خطا در مرز سیستم رخ دهد و در عمق کد منتشر نشود؛ چیزی که در ادبیات معماری به آن fail-fast boundary گفته می‌شود.

در سطح بالاتر، اگر پروژه شما از نظر تعداد فایل و تیم توسعه بزرگ است، پیشنهاد می‌کنم از ابزارهای تحلیل ایستا مثل PHPStan در سطح بالا (level 8 یا 9) استفاده کنید. این ابزارها با تحلیل نوع‌ها و ترِیس مسیر اجرا، قبل از اجرای کد هشدار می‌دهند که در یک نقطه خاص ممکن است متغیر null باشد. هزینه‌ی راه‌اندازی این ابزار در ابتدای پروژه ناچیز است، ولی در بلندمدت صدها ساعت دیباگ را صرفه‌جویی می‌کند. حتی برای پروژه‌های کوچک هم توصیه‌ام این است که حداقل روی فایل‌های حساس مثل controllerها و پردازش‌های اصلی، PHPStan را با تنظیمات پایه اجرا کنید.

نکته‌ای که ارزش به‌خاطر سپردن دارد

خطای Trying to access array offset on value of type null یک هشدار ساده نیست؛ یک فرصت برای بازبینی معماری کد شماست. اگر بارها این خطا را می‌بینید، احتمالاً مشکلی ساختاری در نحوه بازگرداندن داده از توابع دارید و بهتر است به‌جای خفه کردن هشدار، روی قرارداد تابع‌ها وقت بگذارید. اگر فقط یک بار دیده‌اید، کافی است آن یک نقطه را با یکی از راه‌حل‌های بالا اصلاح کنید و به کارتان ادامه دهید. نکته‌ای که در طول سال‌ها کار با PHP برای من همیشه یادآوری مفیدی بوده این است: هر هشدار را به‌عنوان یک سؤال ببینید، نه یک مزاحم؛ پاسخ آن سؤال، شما را از نسخه ضعیف‌ترِ خودتان به نسخه قوی‌تر می‌برد.

اگر این خطا را در یک پروژه واقعی تجربه کرده‌اید و مسیر رفعش برایتان نکته‌ای داشته، خوشحال می‌شوم آن را در دیدگاه همین صفحه بخوانم؛ به‌ویژه اگر منشأ null شدن متغیر در پروژه‌ی شما چیز غیرمعمولی بوده. تجربه‌های واقعی خوانندگان این صفحه، همان چیزی است که این نوشته را در طول زمان کامل‌تر می‌کند. 🙂