خطای Call to a member function on null یکی از آن خطاهای PHP است که در نگاه اول ساده به نظر می‌رسد ولی در پروژه‌های وردپرسی، به‌طور مکرر در قالب‌ها و افزونه‌های سفارشی ظاهر می‌شود. اولین بار که این خطا را در یک پروژه واقعی دیدم، در یک افزونه اختصاصی بود که در آن، تابعی که باید یک آبجکت برگرداند، در شرایط خاصی null برمی‌گرداند و کد فراخوانی، بدون بررسی، متد را روی آن صدا می‌زد. نتیجه این خطا، یک صفحه سفید در پیشخوان بود که هیچ سرنخی از علت اصلی نمی‌داد. درس آن روز این بود که در PHP، هر متد صدا زده شده روی متغیری که ممکن است null باشد، یک بمب ساعتی است.

اگر با مفاهیم پایه PHP در وردپرس آشنایی کمتری دارید، پیش از ادامه PHP در وردپرس: از مبتدی تا حرفه‌ای را بخوانید. این نوشته، لایه عیب‌یابی همان بحث است: از آناتومی دقیق خطا و تفاوت آن با خطاهای مشابه خطای Object could not be converted to string و خطای Cannot use object as array شروع می‌کنم و تا الگوهای رفع و پیشگیری پیش می‌روم. برای درک عمومی روش‌های دیباگ در وردپرس، دیباگ کردن کدهای سفارشی وردپرس مرجع مکمل این نوشته است.

خطای Call to a member function on null دقیقاً چه می‌گوید؟

این خطا در PHP یعنی: کد شما روی متغیری که مقدارش null است، یک متد (method) یا خاصیت (property) صدا زده. به‌عنوان مثال، در کدی که به‌شکل $object->method() یا $object->property نوشته شده، اگر $object مقدار null داشته باشد، این خطا رخ می‌دهد. دلیل مرگبار بودن این خطا این است که در PHP، صدا زدن متد روی null یک خطای قطعی است، نه یک هشدار قابل چشم‌پوشی.

پیام دقیق این خطا معمولاً به شکل زیر است:

PHP Fatal error:  Uncaught Error: Call to a member function get_id() on null
in /path/to/plugin/file.php:85
Stack trace:
#0 /path/to/wordpress/wp-includes/class-wp-hook.php(324): myplugin_render_box()
#1 /path/to/wordpress/wp-includes/plugin.php(205): WP_Hook->apply_filters()
...

سه چیز در این پیام مهم است. اول، نام متد فراخوانی‌شده: مثلاً get_id()، render() یا save(). دوم، نام فایل و شماره خط که در آن فراخوانی شکست خورده. سوم، Stack Trace که مسیر اجرای کد را نشان می‌دهد. هر سه این‌ها ابزار تشخیص هستند و در ادامه به آن‌ها برمی‌گردم.

نکته مهم: این خطا در دو سناریو ظاهر می‌شود. سناریوی اول، متغیری که در انتظار آبجکت است، در واقع null است — مثلاً تابعی که در حالت موفق آبجکت و در حالت خطا null برمی‌گرداند. سناریوی دوم، متغیری که در ابتدا مقدار گرفته ولی در طول اجرا، در یک شرط، به null تبدیل شده است. هر دو سناریو یک ریشه دارند: مدیریت نادرست حالت null در کد.

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

null در PHP و چرا این نوع خطا مرگبار است؟

برای درک دقیق این خطا، باید مفهوم null در PHP و نقش آن در سیستم نوع را بشناسید. در PHP، null یک مقدار ویژه است که نشان‌دهنده عدم وجود مقدار است. این مفهوم در مرجع فنی وب با عنوان Null pointer شناخته می‌شود و در زبان‌های مختلف، روش‌های متفاوتی برای مدیریتش وجود دارد.

تفاوت null با undefined و false

در PHP، سه مقدار متفاوت وجود دارد که در نگاه اول مشابه به نظر می‌رسند ولی رفتارشان کاملاً متفاوت است. null به‌معنای عدم وجود مقدار است. undefined زمانی رخ می‌دهد که متغیری قبل از استفاده مقداردهی نشده باشد. false یک مقدار بولی است که به‌معنای نادرست بودن است. این سه، در مقایسه‌ها رفتارهای متفاوتی دارند و اشتباه گرفتنشان منبع باگ‌های پنهان است.

چرا صدا زدن متد روی null خطا می‌دهد

وقتی PHP به کدی مثل $object->method() می‌رسد، اول نیاز دارد که $object را پیدا کند. اگر مقدار null باشد، PHP نمی‌داند که کدام متد را صدا بزند چون null هیچ متدی ندارد. این همان دلیلی است که این خطا در PHP رخ می‌دهد. در زبان‌های با نوع‌دهی قوی‌تر مثل Java، این خطا در سطح کامپایلر تشخیص داده می‌شود، ولی در PHP به‌دلیل ماهیت داینامیک، این خطا در زمان اجرا بروز می‌کند.

مفهوم Null Safety در زبان‌های مدرن

در چند سال اخیر، زبان‌های مدرن مثل Swift، Kotlin و TypeScript مفهوم Null Safety را معرفی کرده‌اند که در آن، نوع‌ها به‌طور صریح مشخص می‌کنند که آیا ممکن است مقدار null باشند یا نه. این رویکرد، در PHP به‌شکل کامل وجود ندارد ولی از PHP 7.0، عملگر null coalescing (??) و از PHP 8.0 عملگر nullsafe (?->) به زبان اضافه شده‌اند که مدیریت null را ساده‌تر می‌کنند.

عملگرنسخه PHPکاربرد
??PHP 7.0 به بالامقدار پیش‌فرض در صورت null
?->PHP 8.0 به بالافراخوانی متد فقط در صورت غیر null
??=PHP 7.4 به بالاتخصیص در صورت null بودن

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

رفتار خطا در نسخه‌های مختلف PHP

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

PHP 5.x و PHP 7.0

در نسخه‌های قدیمی‌تر PHP، صدا زدن متد روی null معمولاً یک Fatal Error تولید می‌کرد که اجرای اسکریپت را کاملاً متوقف می‌کرد. در بعضی سناریوهای خاص، این خطا به‌شکل Notice یا Warning ظاهر می‌شد ولی نتیجه معمولاً همان توقف اجرا بود.

PHP 7.4 و PHP 8.0 به بعد

از PHP 7.4، پیام‌های این خطا با جزئیات بیشتری همراه شدند و شامل نام دقیق متد و کلاس می‌شدند. از PHP 8.0، این خطا در همه سناریوها به Fatal Error تبدیل شده که اجرای اسکریپت را کاملاً متوقف می‌کند. علاوه بر این، PHP 8.0 عملگر nullsafe را معرفی کرد که به‌طور قابل توجهی مدیریت این خطا را ساده‌تر می‌کند.

جدول تفاوت رفتار در نسخه‌های مختلف:

نسخه PHPرفتار خطاپیامد روی سایت
PHP 5.xFatal Error در بیشتر سناریوهاتوقف اجرا و صفحه سفید
PHP 7.0 تا 7.3Fatal Error با جزئیات بیشترتوقف اجرا با پیام دقیق‌تر
PHP 7.4Fatal Error با Trace کاملتوقف اجرا با ابزار دیباگ بهتر
PHP 8.0 به بعدFatal Error + nullsafe operatorتوقف اجرا با امکان جلوگیری

نتیجه عملی: اگر افزونه یا قالب شما در PHP 7.4 کار می‌کرد ولی در PHP 8.0 از کار افتاد، این خطا یکی از مظنون‌های اصلی است. این تفاوت رفتاری، به این دلیل به وجود آمده که PHP 8 تصمیم گرفته در پیام‌های خطا، اطلاعات بیشتری ارائه کند. اصول دقیق مهاجرت امن PHP در تست و دیباگ پروژه‌های توسعه وردپرس آمده است.

هفت علت ریشه‌ای در پروژه‌های وردپرسی

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

علت اول: خروجی توابع وردپرس که null برمی‌گردانند

شایع‌ترین علت. توابع وردپرس در صورت عدم یافتن داده، معمولاً null یا false برمی‌گردانند. اگر کد بدون بررسی، متد روی خروجی صدا بزند، خطا رخ می‌دهد:

// اشتباه
$post = get_post( $post_id );
$author = $post->post_author; // خطا اگر post وجود نداشته باشد

// درست
$post = get_post( $post_id );

if ( ! $post instanceof WP_Post ) {
    return;
}

$author = $post->post_author;

سه تابع وردپرس که در پروژه‌های واقعی بیشتر از بقیه این خطا را ایجاد می‌کنند: get_post()، get_user_by() و get_term(). در همه این سه، بررسی نوع قبل از دسترسی ضروری است. اصول دقیق در اعتبارسنجی داده‌ها در کدنویسی وردپرس آمده است.

علت دوم: بازگشت WP_Error و نبود بررسی نوع

توابع وردپرس در صورت خطا، به‌جای داده مورد انتظار، آبجکت WP_Error برمی‌گردانند. بعضی توابع در سناریوهای دیگر null برمی‌گردانند. اگر کد بدون بررسی نوع، متد صدا بزند، خطا رخ می‌دهد:

// اشتباه
$user = wp_authenticate( $username, $password );
$id = $user->get_id(); // خطا اگر احراز هویت شکست خورده باشد

// درست
$user = wp_authenticate( $username, $password );

if ( is_wp_error( $user ) || ! $user instanceof WP_User ) {
    return;
}

$id = $user->get_id();

تابع is_wp_error استاندارد وردپرس برای تشخیص این سناریو است و در همه پروژه‌ها استفاده می‌کنم. اصول دقیق در PHP در وردپرس: از مبتدی تا حرفه‌ای آمده است.

علت سوم: بازگشت null از query سفارشی

وقتی از WP_Query یا $wpdb->get_row استفاده می‌کنید، در صورت عدم یافتن داده، مقدار null برگردانده می‌شود. اگر کد بعدی روی این مقدار متد صدا بزند، خطا رخ می‌دهد:

// اشتباه
$row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM my_table WHERE id = %d', $id ) );
echo $row->column_name; // خطا اگر ردیفی یافت نشود

// درست
$row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM my_table WHERE id = %d', $id ) );

if ( null === $row ) {
    return;
}

echo esc_html( $row->column_name );

این علت در پروژه‌هایی که با کوئری‌های سفارشی کار می‌کنند، رایج است. اصول دقیق کار با دیتابیس در بهینه‌سازی کوئری‌های وردپرس با کدنویسی آمده است.

علت چهارم: متغیر تنظیم‌نشده در شرط

گاهی متغیری در یک شرط مقدار می‌گیرد ولی در مسیر دیگری از کد، بدون مقدار می‌ماند. الگوی رایج:

// اشتباه
if ( $some_condition ) {
    $user = get_user_by( 'id', $user_id );
}

$profile = $user->get_profile(); // خطا اگر شرط false باشد

// درست
$user = null;

if ( $some_condition ) {
    $user = get_user_by( 'id', $user_id );
}

if ( ! $user instanceof WP_User ) {
    return;
}

$profile = $user->get_profile();

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

علت پنجم: بازگشت null از متد یک کلاس سفارشی

در کلاس‌های سفارشی، متدی که در شرایطی نمی‌تواند مقدار برگرداند، ممکن است null برگرداند. اگر کد فراخوانی، این حالت را پیش‌بینی نکند، خطا رخ می‌دهد:

class MyPlugin_Order {
    public function get_customer(): ?MyPlugin_Customer {
        if ( ! $this->has_customer() ) {
            return null;
        }
        return $this->customer;
    }
}

$order = myplugin_get_order( 5 );
$customer = $order->get_customer();
$name = $customer->get_name(); // خطا اگر customer null باشد

راه‌حل، بررسی خروجی متد قبل از فراخوانی متد بعدی است:

$customer = $order->get_customer();

if ( null === $customer ) {
    return;
}

$name = $customer->get_name();

این علت در پروژه‌هایی که با الگوهای Domain-Driven Design کار می‌کنند، زیاد دیده می‌شود.

علت ششم: ذخیره null در Options یا Meta

وقتی داده‌ای به‌عنوان null در دیتابیس ذخیره می‌شود و بعداً بدون بررسی خوانده می‌شود، خطا رخ می‌دهد:

// ذخیره اشتباه
update_option( 'my_plugin_data', null );

// بازیابی اشتباه
$data = get_option( 'my_plugin_data' );
$value = $data->get_value(); // خطا

// بازیابی درست
$data = get_option( 'my_plugin_data' );

if ( ! is_object( $data ) ) {
    return;
}

$value = $data->get_value();

راه‌حل استاندارد، بررسی نوع داده بعد از بازیابی است. اصول دقیق کار با Options در کار با Options API در کدنویسی وردپرس آمده است.

علت هفتم: نبود مقدار در برگشتی از API

وقتی داده‌ای از یک API بیرونی می‌آید و ساختار آن تغییر کرده یا در بعضی شرایط، فیلدی وجود ندارد، متد روی null صدا زده می‌شود:

// اشتباه
$response = wp_remote_get( $url );
$data = json_decode( wp_remote_retrieve_body( $response ) );
$result = $data->result->get_value(); // خطا اگر فیلد result وجود نداشته باشد

// درست
$response = wp_remote_get( $url );

if ( is_wp_error( $response ) ) {
    return;
}

$data = json_decode( wp_remote_retrieve_body( $response ) );

if ( ! isset( $data->result ) ) {
    return;
}

$result = $data->result->get_value();

در PHP 8.0، می‌توان از عملگر nullsafe برای کاهش این نوع کد استفاده کرد: $result = $data?->result?->get_value();. اصول دقیق کار با داده‌های خارجی در پاک‌سازی داده‌ها در کدنویسی وردپرس آمده است.

در میان این هفت علت، سه علت اول (خروجی توابع وردپرس، WP_Error و query سفارشی) بیشترین سهم را در پروژه‌های واقعی دارند. اگر فقط این سه را در پروژه خود بررسی کنید، احتمالاً در ۸۰ درصد موارد به علت اصلی می‌رسید.

هفت علت، یک ریشه مشترک دارند: نبود بررسی null قبل از فراخوانی متد. راه‌حل ریشه‌ای، بررسی دقیق است نه پوشاندن خطا.

مرحله تشخیص: از لاگ تا Xdebug

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

ابزار اول: WP_DEBUG و debug.log

اولین قدم، فعال‌سازی حالت دیباگ در wp-config.php است:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

با این تنظیمات، پیام‌های خطا در فایل wp-content/debug.log ثبت می‌شوند. Stack Trace موجود در لاگ، مسیر دقیق خطا را نشان می‌دهد. در این Stack Trace، سه چیز را جستجو می‌کنم: نام متد صدا زده شده، نام فایل و شماره خط، و پارامترها یا آرگومان‌های تابع. اصول دقیق در دیباگ کردن کدهای سفارشی وردپرس آمده است.

ابزار دوم: var_dump و print_r

گاهی Stack Trace به‌تنهایی کافی نیست چون مقدار متغیر در لحظه خطا را نشان نمی‌دهد. در این سناریو، از var_dump یا error_log استفاده می‌کنم:

var_dump( $user );
// یا در لاگ
error_log( 'User value: ' . print_r( $user, true ) );

خروجی var_dump هم نوع و هم مقدار متغیر را نشان می‌دهد و در تشخیص سریع کمک می‌کند. یک نکته عملی: در محیط production، استفاده از var_dump مستقیم توصیه نمی‌شود چون می‌تواند ساختار داده را برای کاربران نمایش دهد. به‌جای آن، از error_log استفاده کنید.

ابزار سوم: Xdebug برای تحلیل عمیق

در پروژه‌های پیچیده که Stack Trace طولانی است، Xdebug ابزار قابل اتکایی است. با Xdebug می‌توانید در IDE با یک breakpoint، مقدار دقیق همه متغیرها در لحظه خطا را ببینید و مسیر اجرای کد را گام‌به‌گام طی کنید. این رویکرد، در پروژه‌های بزرگ که چندین لایه کد بین تابع اصلی و خطا وجود دارد، تفاوت محسوسی در زمان تشخیص می‌سازد.

ابزار چهارم: توابع بررسی null

در بعضی سناریوها، قبل از خطا، می‌توانید با توابع بررسی نوع، ساختار واقعی داده را تشخیص دهید:

if ( null === $user ) {
    error_log( 'User is null' );
} elseif ( ! $user instanceof WP_User ) {
    error_log( 'User has wrong type: ' . gettype( $user ) );
} else {
    error_log( 'User ID: ' . $user->ID );
}

سه تابع اصلی که در این تشخیص استفاده می‌کنم: is_null، isset و instanceof. علاوه بر این‌ها، gettype نوع دقیق متغیر را برمی‌گرداند که در تشخیص منبع خطا مفید است.

نکته امنیتی در تشخیص

در سایت production، هرگز WP_DEBUG را برای مدت طولانی فعال نگه ندارید چون سه مشکل ایجاد می‌کند: اول، فایل debug.log می‌تواند به‌سرعت به چند گیگابایت برسد. دوم، اطلاعات حساس ممکن است در لاگ‌ها ثبت شوند. سوم، نمایش خطاها به کاربران، سطح امنیتی سایت را پایین می‌آورد. اصول کامل را در نوشتن کد PHP امن برای وردپرس آورده‌ام.

الگوهای رفع برای هر علت

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

رفع با بررسی instanceof

الگوی استاندارد، استفاده از instanceof برای بررسی نوع آبجکت قبل از فراخوانی متد است:

$user = get_user_by( 'id', $user_id );

if ( ! $user instanceof WP_User ) {
    return;
}

$name = $user->display_name;

این الگو، در همه پروژه‌ها استفاده می‌کنم چون هم امن است و هم خوانایی کد را بالا می‌برد. توابع instanceof در PHP برای بررسی نوع آبجکت استفاده می‌شود.

رفع با is_wp_error

برای خروجی توابع وردپرس، همیشه is_wp_error را بررسی کنید:

$user = wp_authenticate( $username, $password );

if ( is_wp_error( $user ) ) {
    error_log( $user->get_error_message() );
    return;
}

$id = $user->get_id();

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

رفع با null coalescing operator

در PHP 7.0 به بالا، عملگر ?? می‌تواند در بعضی سناریوها کار را ساده کند:

$post = get_post( $post_id );
$title = $post->post_title ?? '(No title)';

توجه: این الگو فقط زمانی کار می‌کند که مقدار سمت راست به‌طور مستقیم از متغیر قابل null استفاده کند. اگر متد فراخوانی در سمت چپ باشد (مثل $post->get_title() ?? '')، در بعضی نسخه‌های PHP این کد خطا می‌دهد چون $post قبل از ارزیابی ?? خطا تولید می‌کند.

رفع با nullsafe operator در PHP 8

در PHP 8.0 به بالا، عملگر ?-> روش تمیزی برای فراخوانی متد روی متغیری که ممکن است null باشد فراهم می‌کند:

$post = get_post( $post_id );
$title = $post?->post_title ?? '(No title)';

// یا برای زنجیره بلندتر
$value = $data?->result?->get_value() ?? null;

این عملگر، در پروژه‌های مدرن که از PHP 8 استفاده می‌کنند، خوانایی کد را محسوس بالا می‌برد. نکته مهم: اگر $post مقدار null داشته باشد، نتیجه null می‌شود بدون خطا.

رفع با تابع کمکی برای بررسی‌های مکرر

در پروژه‌های بزرگ که چند بخش از کد با ساختارهای داده مختلف کار می‌کنند، یک تابع کمکی می‌سازم:

function myplugin_get_post_safe( int $post_id ): ?WP_Post {
    $post = get_post( $post_id );

    if ( ! $post instanceof WP_Post ) {
        return null;
    }

    return $post;
}

// استفاده
$post = myplugin_get_post_safe( 5 );

if ( null === $post ) {
    return;
}

$title = $post->post_title;

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

زمینه‌های خاص وردپرس: query، post، user

در پروژه‌های وردپرسی، سه زمینه خاص وجود دارد که این خطا در آن‌ها بیشتر دیده می‌شود. در این بخش، هر زمینه را جداگانه بررسی می‌کنم.

زمینه اول: حلقه اصلی وردپرس

در حلقه اصلی وردپرس، استفاده از the_post() و get_post() باید با دقت انجام شود. اگر حلقه بدون بررسی، روی نوشته‌ای که وجود ندارد اجرا شود، خطا رخ می‌دهد:

// اشتباه در بعضی سناریوها
if ( have_posts() ) {
    the_post();
    $post_id = $post->ID;
}

// درست
if ( have_posts() ) {
    while ( have_posts() ) {
        the_post();
        $post_id = get_the_ID();
    }
}

راه‌حل استاندارد در حلقه‌ها، استفاده از توابعی مثل get_the_ID() و get_the_title() است که خودشان مدیریت null را انجام می‌دهند.

زمینه دوم: WP_Query سفارشی

در WP_Query سفارشی، بعد از پرس‌وجو، باید have_posts() را بررسی کنید:

$query = new WP_Query( [
    'post_type' => 'post',
    'posts_per_page' => 10,
] );

if ( ! $query->have_posts() ) {
    return;
}

while ( $query->have_posts() ) {
    $query->the_post();
    $post_id = get_the_ID();
}

wp_reset_postdata();

فراموش کردن wp_reset_postdata() در پایان، منبع خطاهای پنهان در بخش‌های دیگر سایت می‌شود.

زمینه سوم: کاربر جاری

در کار با کاربر جاری وردپرس، تابع wp_get_current_user() همیشه یک آبجکت برمی‌گرداند (حتی برای کاربران مهمان، یک آبجکت خالی)، ولی تابع get_user_by() در صورت عدم یافتن کاربر، false برمی‌گرداند:

// درست
$user = get_user_by( 'id', $user_id );

if ( false === $user ) {
    return;
}

$email = $user->user_email;

توجه کنید که get_user_by مقدار false برمی‌گرداند نه null. این تفاوت، در سناریوهای خاص مهم است. اصول دقیق کار با کاربران در کار با User Meta در کدنویسی وردپرس آمده است.

زمینه چهارم: متاباکس و برگه تنظیمات

در متاباکس‌ها و برگه‌های تنظیمات، گاهی متغیرهایی که در فرم ذخیره می‌شوند، در حالت خاصی مقدار نمی‌گیرند. الگوی امن:

function myplugin_save_metabox( int $post_id ): void {
    if ( ! isset( $_POST['my_nonce'] ) ) {
        return;
    }

    $nonce = sanitize_text_field( wp_unslash( $_POST['my_nonce'] ) );

    if ( ! wp_verify_nonce( $nonce, 'my_plugin_metabox' ) ) {
        return;
    }

    $value = isset( $_POST['my_field'] )
        ? sanitize_text_field( wp_unslash( $_POST['my_field'] ) )
        : '';

    update_post_meta( $post_id, 'my_field', $value );
}

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

پیشگیری با null-safe و بازبینی کد

بهترین راه‌حل برای خطای Call to a member function on null، پیشگیری است. در پروژه‌های واقعی، تجربه‌ام این است که اگر در پنج لایه پیشگیری انجام شود، این خطا تقریباً هرگز ظاهر نمی‌شود.

لایه اول: type-hint در توابع

در PHP 7.4 و بالاتر، استفاده از type-hint الزامی را توصیه می‌کنم. علاوه بر این، مشخص کردن صریح nullable type با ?Type می‌تواند به خواننده کد بگوید که این متغیر ممکن است null باشد:

function myplugin_process_user( ?WP_User $user ): void {
    if ( null === $user ) {
        return;
    }

    $id = $user->ID;
}

این رویکرد، کد را صریح‌تر می‌کند و در بازبینی، نقطه تمرکز برای بررسی null مشخص است.

لایه دوم: بررسی null قبل از فراخوانی

هرگاه متغیری که ممکن است null باشد، قبل از فراخوانی متد، بررسی کنید:

if ( null === $user ) {
    return;
}

$name = $user->display_name;

این الگو در پروژه‌های بزرگ که داده‌ها از منابع مختلف می‌آیند، بسیار مؤثر است. اصول دقیق در اعتبارسنجی داده‌ها در کدنویسی وردپرس آمده است.

لایه سوم: استفاده از nullsafe operator

در PHP 8.0 به بالا، از عملگر ?-> استفاده کنید که هم کد را تمیزتر می‌کند و هم خطا را جلوگیری می‌کند:

$title = $post?->post_title ?? '';
$value = $data?->result?->get_value();

این رویکرد، در پروژه‌های جدید که روی PHP 8 اجرا می‌شوند، توصیه می‌شود. اصول دقیق در PHP در وردپرس: از مبتدی تا حرفه‌ای آمده است.

لایه چهارم: بازبینی کد با static analysis

ابزارهای تحلیل استاتیک کد مثل PHPStan و Psalm، بسیاری از خطاهای دسترسی به null را قبل از اجرای کد تشخیص می‌دهند. تجربه‌ام این است که در پروژه‌های بزرگ، این ابزارها در بازبینی کد، نقش محسوسی در کاهش خطاهای زمان اجرا دارند. اگر با استانداردهای کد وردپرس آشنا نیستید، استانداردهای کدنویسی وردپرس چیست پیش‌نیاز خوبی است.

لایه پنجم: تست‌های واحد برای سناریوهای null

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

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

در PHPDoc هر تابع، صریح مشخص کنید که آیا ممکن است null برگرداند:

/**
 * Get user by ID.
 *
 * @param int $user_id User ID.
 * @return WP_User|null User object or null if not found.
 */
function myplugin_get_user( int $user_id ): ?WP_User {
    $user = get_user_by( 'id', $user_id );

    if ( ! $user instanceof WP_User ) {
        return null;
    }

    return $user;
}

این رویکرد، در پروژه‌های تیمی، به توسعه‌دهنده بعدی می‌گوید که این تابع ممکن است null برگرداند و باید بررسی کند.

نگاه مهندسی به مدیریت null

برای مهندسان ارشد، این خطا در بستر عمیق‌تری معنا پیدا می‌کند: مدیریت null یکی از کلاسیک‌ترین مشکلات در طراحی زبان‌های برنامه‌نویسی است. تونی هور، مخترع مفهوم null reference، خودش در سال ۲۰۰۹ این انتخاب را «خطای میلیارد دلاری» نامید. این تعبیر، نشان می‌دهد که مشکل null نه یک مسئله سلیقه‌ای بلکه یک چالش بنیادی در طراحی زبان‌ها است.

در سال‌های اخیر، سه رویکرد اصلی برای مدیریت null در زبان‌های برنامه‌نویسی شکل گرفته است. رویکرد اول: Option/Maybe Type که در زبان‌هایی مثل Haskell و Rust استفاده می‌شود و در آن، وجود یا عدم وجود مقدار در سطح نوع مشخص است. رویکرد دوم: Null Safety که در زبان‌هایی مثل Kotlin و Swift استفاده می‌شود و در آن، نوع‌ها صریحاً nullable یا non-nullable هستند. رویکرد سوم: عملگرهای خاص مثل ?. و ?? که در PHP 7 و 8 اضافه شدند و در زبان‌های دیگر مثل JavaScript و C# هم وجود دارند.

PHP در این زمینه، رویکرد سوم را انتخاب کرده و به‌تدریج در نسخه‌های جدید، ابزارهای بیشتری فراهم کرده است. در پروژه‌های خودم، سه اصل را در مدیریت null رعایت می‌کنم. اصل اول: هر تابعی که ممکن است null برگرداند، باید در PHPDoc صریح مشخص کند. اصل دوم: در مصرف‌کننده‌های خروجی، همیشه بررسی null انجام شود. اصل سوم: در پروژه‌های جدید، استفاده از nullable type-hint و nullsafe operator جدی گرفته شود.

یک نکته عملی در این حوزه: زمانی که از عملگر nullsafe استفاده می‌کنید، توجه داشته باشید که اگر $data به‌طور کامل وجود نداشته باشد (نه اینکه null باشد)، خطای Undefined variable رخ می‌دهد. یعنی عملگر nullsafe فقط برای متغیرهایی مفید است که مطمئن هستید تعریف شده‌اند ولی ممکن است null باشند. این نکته، در پروژه‌های بزرگ که متغیرهای عمومی و شرطی زیاد هستند، اهمیت دارد.

خطای Call to a member function on null، یک پیام واضح است: در جایی از کد شما، مدیریت null ناقص است. رفع کوتاه‌مدت، رفع خطاست؛ رفع بلندمدت، تقویت قراردادهای صریح در مورد null در پروژه است.

پرسش‌های پرتکرار درباره Call to member function on null

خطای Call to a member function on null چه معنایی دارد؟ این خطا در PHP یعنی کد شما روی متغیری که مقدارش null است، یک متد یا خاصیت صدا زده. این اتفاق وقتی رخ می‌دهد که متغیر در انتظار آبجکت است ولی مقدار null گرفته است. راه‌حل ریشه‌ای، بررسی null قبل از فراخوانی است.

تفاوت این خطا با خطای Object could not be converted to string چیست؟ این دو خطا ریشه‌های متفاوتی دارند. خطای Call to a member function on null وقتی رخ می‌دهد که روی null متد صدا زده می‌شود، درحالی‌که خطای Object to string وقتی رخ می‌دهد که آبجکت در جایی که رشته انتظار می‌رود استفاده شود. راه‌حل هر دو، تقویت نوع‌دهی است ولی الگوهای رفع متفاوتند. اگر با خطای دوم مواجه هستید، خطای Object could not be converted to string راهنمای مکملی است.

تفاوت this خطا با خطای Cannot use object as array چیست؟ خطای Call to a member function on null وقتی رخ می‌دهد که روی null متد صدا زده می‌شود. خطای Cannot use object as array وقتی رخ می‌دهد که آبجکت با سینتکس آرایه دسترسی می‌شود. ریشه هر دو مشکل مشابه است — ناهماهنگی نوع — ولی راه‌حل دقیق، متفاوت است. اگر با خطای دوم مواجه هستید، خطای Cannot use object as array راهنمای مکملی است.

چرا این خطا در PHP 8 بیشتر از PHP 7.4 دیده می‌شود؟ چون PHP 8 در پیام‌های خطا، اطلاعات بیشتری ارائه می‌کند و نام دقیق متد را نشان می‌دهد. علاوه بر این، PHP 8 در بعضی سناریوهای قبلاً مجاز، حالا سختگیرانه‌تر عمل می‌کند. این تغییر، باعث شده خطاهای پنهان قبلی، بیشتر خود را نشان دهند.

آیا راه‌حل سریع وجود دارد؟ اگر بخواهید فقط خطا را از بین ببرید، بررسی null قبل از فراخوانی متد یا استفاده از عملگر nullsafe (در PHP 8 به بالا) کار می‌کند. ولی این راه‌حل موقت است اگر ریشه منطقی مشکل حل نشود. راه‌حل ریشه‌ای، بررسی دقیق قرارداد توابع و بررسی خروجی‌ها است.

چطور بفهمم کجا خطا رخ می‌دهد؟ فعال‌سازی WP_DEBUG و بررسی فایل debug.log، اولین قدم است. Stack Trace موجود در لاگ، مسیر دقیق خطا و نام متد را نشان می‌دهد. اگر Stack Trace پیچیده بود، استفاده از Xdebug تشخیص را ساده‌تر می‌کند. اصول دقیق در دیباگ کردن کدهای سفارشی وردپرس آمده است.

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

چرا این خطا در افزونه‌های REST API زیاد دیده می‌شود؟ چون در REST API، داده‌ها به‌صورت JSON دریافت می‌شوند و ساختارشان ممکن است در سناریوهای مختلف متفاوت باشد. اگر فیلدی در پاسخ وجود نداشته باشد و کد بدون بررسی، متد روی آن صدا بزند، خطا رخ می‌دهد. راه‌حل، بررسی وجود فیلد قبل از دسترسی است. اصول دقیق در REST API در وردپرس آمده است.

چطور از این خطا در آینده جلوگیری کنم؟ شش لایه پیشگیری: استفاده از type-hint و nullable type، بررسی null قبل از فراخوانی، استفاده از nullsafe operator در PHP 8، بازبینی کد با static analysis، نوشتن تست‌های واحد برای سناریوهای null، و مستندسازی قرارداد توابع. این شش لایه، در پروژه‌های واقعی، تقریباً این خطا را حذف می‌کنند.

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

آیا خطای Call to a member function on null همیشه ناشی از کد است؟ در بعضی سناریوها، این خطا از داده‌های ناسازگار در دیتابیس می‌آید. مثلاً اگر یک ردیف در دیتابیس به‌طور ناقص ذخیره شده باشد و در بازخوانی، مقداری برگرداند که انتظار نمی‌رفت، خطا رخ می‌دهد. در این سناریو، علاوه بر رفع کد، باید داده‌های دیتابیس هم تمیز شوند. اصول دقیق در کار با Options API در کدنویسی وردپرس آمده است.

تفاوت null با false در بررسی نوع چیست؟ در PHP، null و false دو مقدار متفاوت هستند. برای بررسی دقیق، از === به‌جای == استفاده کنید. مثلاً if ( null === $value ) و if ( false === $value ) رفتارهای متفاوتی دارند. توابع وردپرس ممکن است در سناریوهای مختلف هر کدام را برگردانند، پس بررسی دقیق ضروری است.

آیا می‌توانم از عملگر @ برای پنهان کردن این خطا استفاده کنم؟ توصیه نمی‌کنم چون این رویکرد خطا را پنهان می‌کند ولی مشکل را حل نمی‌کند. علاوه بر این، عملگر @ روی خطاهای Fatal Error اثر ندارد چون این نوع خطا در PHP قابل پنهان‌سازی نیست. راه‌حل ریشه‌ای، بررسی null است نه پنهان کردن خطا.

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

آیا این خطا با خطای Trying to get property of non-object یکی است؟ این دو خطا نزدیک ولی متفاوتند. خطای Call to a member function on null وقتی رخ می‌دهد که متد روی null صدا زده می‌شود. خطای Trying to get property of non-object وقتی رخ می‌دهد که خاصیت از یک متغیر غیرآبجکت خوانده می‌شود. ریشه هر دو مشکل مشابه است — ناهماهنگی نوع — ولی راه‌حل دقیق، متفاوت است.

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

آیا می‌توانم از try-catch برای مدیریت این خطا استفاده کنم؟ در PHP 7 به بالا، این خطا به‌شکل Error پرتاب می‌شود که می‌توان آن را با try-catch گرفت. ولی استفاده از try-catch برای این خطا توصیه نمی‌شود چون این خطا نشانه‌ای از باگ منطقی در کد است نه یک خطای مورد انتظار. راه‌حل ریشه‌ای، بررسی null است نه گرفتن خطا.

الگویی برای فردا

در پایان این مسیر، یک حقیقت را باید پذیرفت: خطای Call to a member function on null، یک خطای ساده نیست؛ نشانه‌ای از یک الگوی ناهماهنگ در مدیریت null است. اگر این خطا را فقط با بررسی موضعی رفع کنید، ریشه مشکل همچنان باقی می‌ماند و در آینده، در سناریوهای پیچیده‌تر، دوباره ظاهر می‌شود. راه‌حل بلندمدت، تقویت قراردادهای صریح در مورد null در پروژه است.

سه اصل که در همه پروژه‌های خودم رعایت می‌کنم. اصل اول: هر تابعی که ممکن است null برگرداند، در PHPDoc صریح مشخص کند. اصل دوم: در مصرف‌کننده‌های خروجی، همیشه بررسی null انجام شود. اصل سوم: در پروژه‌های جدید، از nullable type-hint و nullsafe operator استفاده شود. این سه اصل، در بلندمدت، این خطا را تقریباً حذف می‌کنند.

اگر در ابتدای مسیر یادگیری هستید، سه تمرین را پیشنهاد می‌کنم. اول، در یک نصب تستی وردپرس، یک صفحه ساده بسازید که در آن، به‌طور عمدی این خطا رخ دهد و آن را با روش‌های این نوشته رفع کنید. دوم، در یک پروژه واقعی، فایل debug.log را برای این خطا جستجو کنید و ببینید چند نقطه از کد شما آسیب‌پذیر است. سوم، در کد افزونه خود، از عملگر nullsafe در PHP 8 استفاده کنید و ببینید چطور خوانایی کد را بالا می‌برد — این تجربه، درک عمیقی از اهمیت مدیریت null به شما می‌دهد که هیچ مقاله‌ای جایگزینش نمی‌شود. 🛠️