روی دکمه «ذخیره تنظیمات» کلیک می‌کنید، پیام موفقیت سبز ظاهر می‌شود، صفحه رفرش می‌شود و همان مقادیر قبلی برگشته‌اند؛ یا با صفحه سفید، خطای ۴۰۳ یا پیام «شما مجاز به انجام این کار نیستید» روبرو می‌شوید. اگر با خطای عدم ذخیره تنظیمات افزونه درگیر هستید، این مقاله همان مسیری را پیش می‌رود که سال‌ها در توسعه و دیباگ افزونه‌های وردپرس با آن زندگی کرده‌ام. این خطا تقریباً همیشه یک علت مشخص دارد که در یکی از چهار لایه پشته اجرا پنهان شده: نبود nonce و مجوز کاربری، ثبت نادرست register_setting، تابع پاک‌سازی معیوب در sanitize_callback، و تعارض با افزونه‌های امنیتی یا کش. اگر این چهار لایه را به ترتیب بررسی کنید، در نود درصد موارد به علت دقیق می‌رسید بدون اینکه نیمی از کد را زیر و رو کنید.

دکمه ذخیره کار می‌کند اما مقدار ذخیره نمی‌شود — تفکیک علامت و علت

اولین قدم در عیب‌یابی، تفکیک سه سناریوی متفاوت است که همگی زیر چتر یک نام دیده می‌شوند. در سناریوی اول، فرم ارسال می‌شود، پیام Settings saved نمایش داده می‌شود اما مقدار فیلد پس از رفرش به حالت قبلی برمی‌گردد. این حالت تقریباً همیشه یعنی sanitize_callback شما ورودی را رد کرده و مقدار پیش‌فرض یا مقدار قبلی ذخیره‌شده را برگردانده است. اگر با مفهوم افزونه وردپرس چیست آشنا نیستید، بهتر است قبل از ادامه نگاهی به آن بیندازید تا معماری کلی افزونه‌ها را در ذهن داشته باشید.

سناریوی دوم: صفحه بعد از ارسال، به options.php می‌رود اما با پیام خطای You do not have sufficient permissions یا خطای ۴۰۳ روبرو می‌شود. این حالت معمولاً از نبود nonce، نادرست بودن settings_fields()، یا تداخل با افزونه‌های امنیتی می‌آید. سناریوی سوم: صفحه سفید یا خطای ۵۰۰ بدون هیچ پیام واضحی — که در بیشتر موارد ناشی از یک خطای fatal در تابع sanitize یا خطای syntax در فایل است. تفکیک این سه حالت، نیمی از کار عیب‌یابی است. من در پروژه‌های واقعی دیده‌ام که دو تیم توسعه‌دهنده، ماه‌ها وقت خود را صرف بررسی کش کرده‌اند درحالی‌که مشکل واقعی یک تابع sanitize_text_field معیوب در لایه سوم بود.

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

چهار لایه‌ای که باید به ترتیب عیب‌یابی شوند

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

  1. لایه امنیتی: nonce، مجوز کاربری، referer check. اگر مشکل اینجا باشد، دیگر لایه‌ها بی‌فایده‌اند چون درخواست هرگز به لایه ذخیره‌سازی نمی‌رسد.
  2. لایه ثبت‌نام گزینه: register_setting، add_settings_section، add_settings_field و اینکه در کدام هوک ثبت شده‌اند.
  3. لایه پاک‌سازی داده: sanitize_callback، نوع فیلد، ساختار آرایه‌ای، و بازگشت مقدار.
  4. لایه محیطی: تعارض با افزونه‌های امنیتی، کش، قوانین mod_security روی سرور، و کد سفارشی در functions.php قالب.

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

لایه اول — nonce، مجوز کاربری و امنیت فرم

وردپرس همه فرم‌های صفحه تنظیمات را از طریق options.php پردازش می‌کند. قبل از اینکه هسته وردپرس هر گزینه‌ای را ذخیره کند، سه بررسی انجام می‌دهد: ۱) آیا کاربر جاری مجوز manage_options دارد؟ ۲) آیا nonce ارسالی با wp_verify_nonce معتبر است؟ ۳) آیا صفحه ثبت‌شده با گزینه هدف هم‌خوانی دارد؟ اگر هر یک از این سه شکست بخورد، به جای ذخیره، پیام خطا برمی‌گردد.

چرا nonce نبودن باعث ذخیره نشدن می‌شود؟

تابع settings_fields() که معمولاً در ابتدای فرم قرار می‌گیرد، به‌طور خودکار فیلد option_page، action=update، و _wpnonce را تولید می‌کند. اگر این تابع را فراموش کنید — یا اگر خودتان به‌جای آن یک فیلد دستی بنویسید — هسته وردپرس درخواست را رد می‌کند و پیام Are you sure you want to do this? نمایش داده می‌شود. الگوی درست:

<form method="post" action="options.php">
  <?php settings_fields( 'my_plugin_settings_group' ); ?>
  <?php do_settings_sections( 'my_plugin_settings_page' ); ?>
  <?php submit_button(); ?>
</form>

نکته‌ای که خیلی از توسعه‌دهندگان جوان نمی‌دانند: تابع settings_fields با wp_nonce_field کار می‌کند، و nonce پیش‌فرض ۲۴ ساعت اعتبار دارد. اگر کاربر صفحه تنظیمات را یک روز باز بگذارد و بعد دکمه ذخیره را بزند، درخواست رد می‌شود. برای صفحه‌های تنظیمات طولانی مثل فرم‌های چندمرحله‌ای، این مسئله را جدی بگیرید. برای درک عمیق‌تر اینکه هوک‌ها چطور در چرخه اجرای وردپرس قرار می‌گیرند، نگاهی به نحوه استفاده صحیح از هوک‌های وردپرس بیندازید.

مجوز کاربری و current_user_can

وردپرس پیش از هر چیز بررسی می‌کند که کاربر جاری مجوز manage_options دارد. اگر صفحه تنظیمات را برای نقش‌های پایین‌تر از مدیر باز کرده‌اید و از add_menu_page با capability متفاوت استفاده کرده‌اید، این بررسی می‌تواند شکست بخورد. الگوی درست افزودن صفحه به منو:

add_action( 'admin_menu', 'my_plugin_add_settings_page' );
function my_plugin_add_settings_page() {
    add_options_page(
        'تنظیمات افزونه من',
        'افزونه من',
        'manage_options',
        'my-plugin-settings',
        'my_plugin_render_settings_page'
    );
}

اگر capability را عمداً تغییر داده‌اید (مثلاً edit_posts برای اجازه دسترسی به نویسندگان)، باید داخل تابع رندر هم همین بررسی را تکرار کنید. ساخت صفحه تنظیمات اختصاصی در وردپرس الگوی کاملی دارد که در ساخت صفحه تنظیمات اختصاصی در وردپرس مرحله‌به‌مرحله توضیح داده‌ام.

بررسی امنیتی WP_Filesystem و htaccess

در موارد کمتر شایع، خطا نه از nonce می‌آید و نه از capability، بلکه از یک لایه بالاتر: تغییرات .htaccess که روی wp-admin/admin-post.php یا options.php محدودیت اعمال کرده، یا فیلترهای mod_security روی سرور که بدنه POST را مسدود می‌کنند. تشخیص این حالت با ابزار شبکه مرورگر ساده است: اگر درخواست POST با کد ۴۰۳ برگردد ولی در لاگ‌های وردپرس هیچ پیامی نباشد، احتمالاً لایه سرور درگیر است.

خطای عدم ذخیره تنظیمات تقریباً همیشه در یکی از سه‌گانه «nonce، capability، sanitize» پنهان است. تا این سه‌گانه را با ابزار شبکه مرورگر نبینید، سراغ تعارض افزونه و کش نروید.

لایه دوم — ثبت نادرست گزینه‌ها با register_setting

لایه دوم، جایی است که بیشترین باگ‌های واقعی رخ می‌دهد و در عین حال کمترین توجه را می‌گیرد. تابع register_setting سه آرگومان می‌گیرد: نام گروه تنظیمات (option_group)، نام گزینه (option_name)، و یک آرایه از پارامترها شامل type، sanitize_callback، default، و show_in_rest. اگر این آرگومان‌ها با settings_fields و do_settings_sections هم‌خوانی نداشته باشند، ذخیره شکست می‌خورد.

الگوی درست ثبت تنظیمات

الگویی که در افزونه‌های شخصی همیشه استفاده می‌کنم، این است که هر سه بخش (register، section، field) را در یک هوک واحد admin_init قرار می‌دهم:

add_action( 'admin_init', 'my_plugin_register_settings' );
function my_plugin_register_settings() {
    register_setting(
        'my_plugin_settings_group',
        'my_plugin_options',
        array(
            'type'              => 'array',
            'sanitize_callback' => 'my_plugin_sanitize_options',
            'default'           => array(),
            'show_in_rest'      => false,
        )
    );

    add_settings_section(
        'my_plugin_main_section',
        'تنظیمات اصلی',
        'my_plugin_section_callback',
        'my_plugin_settings_page'
    );

    add_settings_field(
        'my_plugin_api_key',
        'API Key',
        'my_plugin_field_api_key_render',
        'my_plugin_settings_page',
        'my_plugin_main_section'
    );
}

سه نکته حساس در این کد: اول اینکه option_group در register_setting باید عیناً با آرگومان اول settings_fields در فرم یکی باشد. دوم اینکه نام صفحه (my_plugin_settings_page) باید بین add_settings_section، do_settings_sections، و add_settings_field یکسان باشد. سوم اینکه اگر آرگومان show_in_rest روی true باشد اما تابع sanitize_callback وجود نداشته باشد، ذخیره از طریق REST API کار می‌کند ولی از طریق فرم سنتی ممکن است رد شود. برای درک کامل اینکه این گزینه‌ها چطور در دیتابیس ذخیره می‌شوند، کار با Options API در کدنویسی وردپرس را بخوانید.

اشتباه رایج: register_setting بعد از admin_init

اگر register_setting را در هوک اشتباهی مثل init یا admin_menu ثبت کنید، رفتار ممکن است ظاهراً کار کند ولی در واقع با ترتیب اجرا می‌جنگد. وردپرس هوک admin_init را قبل از پردازش options.php اجرا می‌کند و اگر ثبت گزینه در این نقطه نباشد، هسته وردپرس نمی‌داند این گزینه معتبر است و درخواست را رد می‌کند. همچنین، اگر ثبت را درون یک تابع کلاس انجام می‌دهید، مطمئن شوید که متد با array( $this, 'method_name' ) به‌درستی به هوک متصل شده باشد.

form action و options.php

یکی از خطاهای کلاسیک: فرم را به یک action سفارشی ارسال می‌کنید (مثلاً یک روت AJAX یا یک صفحه ادمین دیگر) و انتظار دارید register_setting خودکار تنظیمات را ذخیره کند. این غلط است. register_setting و settings_fields فقط با options.php کار می‌کنند. اگر می‌خواهید منطق پردازش سفارشی داشته باشید، دو راه دارید: یا از admin_post_{action} استفاده کنید و خودتان update_option را صدا بزنید، یا از REST API استفاده کنید که در بخش‌های بعدی به آن می‌رسیم.

لایه سوم — sanitize_callback و پاک‌سازی داده‌ها

یک بار در پروژه‌ای، سه روز برای پیدا کردن علت یک «ذخیره نمی‌شود» وقت گذاشتم. کد افزونه در نگاه اول بی‌نقص بود. مشکل در واقع این بود که sanitize_callback من هنگام دریافت ورودی، مقدار را به‌جای برگرداندن، echo می‌کرد — و نتیجه آن بازگشت null بود. وردپرس در چنین حالتی، null را به‌عنوان «درخواست نامعتبر» تفسیر می‌کند و ذخیره انجام نمی‌شود بدون اینکه هیچ پیام خطایی نمایش دهد.

چرا sanitize اشتباه باعث بازگشت مقدار قبلی می‌شود؟

وقتی sanitize_callback شما مقدار ورودی را برمی‌گرداند، وردپرس همان مقدار را ذخیره می‌کند. اگر تابع شما مقدار را اصلاح کند و همان اصلاح‌شده را برگرداند، ذخیره درست انجام می‌شود. اما اگر تابع شما به‌جای return از echo، print یا یک side effect دیگر استفاده کند، مقدار خروجی null می‌شود و وردپرس رفتار متفاوتی در پیش می‌گیرد. در برخی نسخه‌ها مقدار قبلی حفظ می‌شود و در برخی دیگر گزینه با مقدار پیش‌فرض بازنویسی می‌شود. علت این رفتار متناقض این است که وردپرس در تابع update_option بررسی می‌کند آیا مقدار جدید با مقدار قبلی یکسان است یا نه، و اگر یکسان باشد، عملاً به‌روزرسانی انجام نمی‌شود.

انواع فیلدها و sanitizer مناسب

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

نوع فیلدتابع sanitize پیشنهادیاشتباه رایج
textsanitize_text_fieldاستفاده از sanitize_email برای فیلد عمومی
textareasanitize_textarea_fieldاستفاده از wp_kses_post زمانی که HTML مجاز نیست
emailsanitize_emailبازگشت false در صورت خطا بدون مدیریت
urlesc_url_rawاستفاده از esc_url که برای نمایش است نه ذخیره
checkboxتبدیل به boolean با بررسی issetذخیره مقدار خالی به‌جای false
selectبررسی مقادیر مجاز با in_arrayذخیره هر مقدار دریافتی بدون بررسی
arraysanitize تودرتو با حلقه روی کلیدهاsanitize کردن کل آرایه با یک تابع تک

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

array settings و sanitization تودرتو

وقتی گزینه شما یک آرایه است — که در افزونه‌های پیچیده تقریباً همیشه همین‌طور است — باید کل آرایه را با یک تابع سفارشی پاک‌سازی کنید که هر مقدار را به‌تناسب نوعش بررسی کند. الگوی درستی که در پروژه‌ها به کار می‌برم:

function my_plugin_sanitize_options( $input ) {
    $output = array();

    if ( isset( $input['api_key'] ) ) {
        $output['api_key'] = sanitize_text_field( $input['api_key'] );
    }
    if ( isset( $input['notify_email'] ) ) {
        $email = sanitize_email( $input['notify_email'] );
        $output['notify_email'] = is_email( $email ) ? $email : '';
    }
    if ( isset( $input['enable_sync'] ) ) {
        $output['enable_sync'] = (bool) $input['enable_sync'];
    } else {
        $output['enable_sync'] = false;
    }
    if ( isset( $input['sync_frequency'] ) &&
         in_array( $input['sync_frequency'], array( 'hourly', 'daily', 'weekly' ), true ) ) {
        $output['sync_frequency'] = $input['sync_frequency'];
    }

    return $output;
}

سه نکته مهم: اول، همیشه در انتهای تابع مقدار را با return برگردانید. دوم، برای فیلدهای checkbox که در نبود، مقدارشان در POST غایب است، با isset بررسی کنید و مقدار پیش‌فرض را صریحاً بگذارید. سوم، برای select، همیشه مقادیر مجاز را با in_array با آرگومان سوم true (چک strict) بررسی کنید.

لایه چهارم — تعارض افزونه، کش و قالب

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

تعارض با افزونه‌های امنیتی

افزونه‌های امنیتی مثل Wordfence یا Sucuri که در بهترین افزونه‌های امنیتی وردپرس معرفی کرده‌ام، در مواردی مسیر options.php را محدود می‌کنند. اگر تعداد پارامترهای POST زیاد باشد یا مقدار طولانی داشته باشد، ممکن است به‌عنوان حمله تلقی شود. راه تشخیص این حالت ساده است: افزونه امنیتی را موقتاً روی محیط استجینگ غیرفعال کنید و دوباره تست کنید. اگر مشکل حل شد، باید در تنظیمات همان افزونه یک استثنا برای مسیر /wp-admin/options.php تعریف کنید. برای روش شناسایی قدم‌به‌قدم مشکل‌ساز واقعی، چگونه افزونه مشکل‌ساز وردپرس را پیدا کنیم را بخوانید.

تعارض با افزونه‌های کش

افزونه‌های کش صفحه، گاهی خروجی /wp-admin/options.php را به اشتباه در حافظه نگه می‌دارند. اگر افزونه کش شما تنظیمات admin را هم کش می‌کند، ممکن است مقادیر قدیمی برگردند و به‌نظر برسد که ذخیره انجام نشده. تفاوت را با پاک کردن کامل کش و رفرش سخت صفحه تشخیص دهید. اگر پس از پاک کردن کش مقدار درست ظاهر شد، مشکل در تنظیمات افزونه کش است نه در افزونه شما. فهرست افزونه‌های کش و رفتار هرکدام در بهترین افزونه‌های کش وردپرس بررسی شده است.

hook‌های سفارشی و فیلترها

در نهایت، ممکن است یک افزونه دیگر از طریق فیلتر pre_update_option_{option_name} یا sanitize_option_{option_name} مقدار شما را دستکاری کند. برای تشخیص این حالت باید همه افزونه‌های فعال را غیرفعال کنید، سپس یکی‌یکی فعال کنید تا مقصر پیدا شود. این دقیقاً همان پروتکل تشخیص تعارض است که در بررسی سازگاری قالب با افزونه‌ها توضیح داده‌ام. در همین مسیر، اگر تعداد افزونه‌های نصب‌شده زیاد است، احتمال این نوع تعارض بالا می‌رود؛ برای درک منطق «چند افزونه کافی است» به چند افزونه وردپرس روی یک سایت نصب کنیم مراجعه کنید.

تشخیص سیستماتیک — چک‌لیست گام‌به‌گام

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

  1. در مرورگر، تب Network را باز کنید و روی دکمه ذخیره کلیک کنید. کد وضعیت درخواست POST به options.php را یادداشت کنید. کد ۲۰۰ یعنی درخواست پردازش شده، ولی ممکن است رد شده باشد؛ کد ۴۰۳ یعنی لایه امنیتی درگیر است؛ کد ۵۰۰ یعنی خطای PHP دارید.
  2. پاسخ HTML options.php را در تب Response بررسی کنید. اگر پیام Settings saved بود ولی مقدار قبلی برگشت، مشکل در sanitize_callback است.
  3. در wp-config.php موقتاً WP_DEBUG را روی true و WP_DEBUG_LOG را روی true تنظیم کنید و پس از تلاش برای ذخیره، فایل wp-content/debug.log را بررسی کنید.
  4. گزینه را مستقیماً در دیتابیس بررسی کنید: SELECT option_value FROM wp_options WHERE option_name = 'my_plugin_options';. اگر مقدار ذخیره‌شده با آنچه در فرم هست یکی است ولی در صفحه نمایش داده نمی‌شود، مشکل در خواندن است نه نوشتن.
  5. اگر در حالت محلی کار می‌کند ولی روی سرور خطا می‌دهد، به تنظیمات mod_security و محدودیت‌های WAF روی سرور شک کنید.
  6. برای قطعی کردن تشخیص، تمام افزونه‌های دیگر را غیرفعال کنید و تست را دوباره اجرا کنید. اگر مشکل حل شد، مقصر را با فعال‌سازی یکی‌یکی پیدا کنید.

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

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

لاگ‌گیری و debug حرفه‌ای در وردپرس

بسیاری از خطاهای ذخیره تنظیمات، هیچ پیام قرمزی روی صفحه تولید نمی‌کنند؛ در سکوت شکست می‌خورند. برای دیدن دلیل، در wp-config.php پیش از خط That is all, stop editing این سه خط را اضافه کنید:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

با این تنظیم، خطاها به فایل wp-content/debug.log نوشته می‌شوند و روی صفحه نمایش داده نمی‌شوند. اگر خطایی مثل Warning: call_user_func_array() expects parameter 1 to be a valid callback در لاگ ببینید، یعنی یکی از توابع callback شما اشتباه ثبت شده — که خودش می‌تواند دلیل ذخیره نشدن باشد. اگر Fatal error: Uncaught TypeError ببینید، یعنی sanitize شما به نوع داده اشتباه برخورده است. لاگ‌گیری حرفه‌ای، به‌ویژه با error_log() سفارشی و ثبت داده‌های ورودی، در همان مقاله تست و دیباگ توضیح داده شده است.

اشتباهات رایج در کدنویسی صفحه تنظیمات

فهرست اشتباهاتی که در بازبینی کد افزونه‌های دیگران بارها دیده‌ام و هر کدام به‌تنهایی می‌تواند باعث خطای عدم ذخیره تنظیمات افزونه شود:

  • عدم استفاده از settings_fields: فرم بدون فیلد nonce ساخته می‌شود و درخواست رد می‌شود. این شایع‌ترین اشتباه است.
  • عدم تطابق option_group: نام گروه در register_setting با آرگومان اول settings_fields یکسان نیست.
  • تکرار option_name: دو گزینه با یک نام در دو هوک مختلف ثبت می‌شوند و آخری جای اولی را می‌گیرد.
  • sanitize_callback که echo می‌کند: همان خطایی که در بخش لایه سوم توضیح دادم.
  • استفاده از submit_button بدون آرگومان type: در بعضی تنظیمات قالب، دکمه به‌عنوان button رندر می‌شود نه submit و فرم ارسال نمی‌شود.
  • update_option مستقیم به‌جای استفاده از فرم: گاهی توسعه‌دهنده همزمان از فرم و یک تابع سفارشی update_option استفاده می‌کند و مقادیر یکدیگر را بازنویسی می‌کنند.
  • عدم مدیریت checkbox خالی: وقتی checkbox تیک نخورده، اصلاً در POST نمی‌آید و کد شما مقدار قبلی را حفظ می‌کند بدون اینکه متوجه شود.
  • استفاده از $_POST مستقیم در تابع رندر: این کار باعث می‌شود مقادیر پس از ذخیره قدیمی نمایش داده شوند، چون تابع رندر از آرگومان‌های ثبت‌شده استفاده نمی‌کند.
  • نادیده گرفتن صفحه شبکه‌ای: اگر گزینه در context شبکه‌ای (multisite) ذخیره می‌شود، باید از update_site_option استفاده کنید نه update_option.
  • cache کردن مقدار در متغیر استاتیک: اگر مقدار گزینه را در متغیر استاتیک کش می‌کنید و پس از ذخیره، متغیر را invalidate نمی‌کنید، رفتار متناقض می‌بینید.

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

پرسش‌های پرتکرار درباره خطای ذخیره تنظیمات افزونه

این بخش را به پرسش‌هایی اختصاص می‌دهم که در انجمن‌ها و تیکت‌ها بیشترین تکرار را دارند و در نتایج جستجو به‌عنوان پاسخ کوتاه ارزشمندند.

چرا تنظیمات افزونه ذخیره نمی‌شود ولی پیام موفقیت می‌آید؟

پیام موفقیت در واقع از خود هسته وردپرس می‌آید، نه از افزونه شما. وقتی options.php درخواست را می‌پذیرد و sanitize_callback شما مقدار را دستکاری می‌کند، وردپرس پیام موفقیت را نشان می‌دهد حتی اگر مقدار جدید با مقدار قبلی یکسان باشد. در این حالت، به‌جای «ذخیره ناموفق» با «ذخیره بی‌اثر» روبرو هستید. کد sanitize_callback را چاپ کنید و مقدار ورودی و خروجی را مقایسه کنید.

آیا افزونه‌های امنیتی می‌توانند باعث این خطا شوند؟

بله. به‌خصوص افزونه‌هایی که فایروال سطح اپلیکیشن دارند و قوانین سختگیرانه روی مسیر wp-admin اعمال می‌کنند. در صورت شک، ابتدا روی محیط استجینگ غیرفعالشان کنید. اگر خطا از بین رفت، یک استثنا برای IP یا مسیر در همان افزونه تنظیم کنید.

چرا فقط روی بعضی مرورگرها ذخیره نمی‌شود؟

احتمالاً به کوکی‌های SameSite مربوط است. اگر افزونه شما ریدایرکت بین ساب‌دامین‌ها انجام می‌دهد و کوکی nonce با SameSite=Strict تنظیم شده، درخواست بعدی بدون کوکی می‌رود و nonce شکست می‌خورد. راه‌حل: یکسان‌سازی دامنه، یا تنظیم SameSite=Lax در فیلتر auth_cookie_samesite.

چطور بفهمم مشکل از افزونه من است یا افزونه دیگری؟

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

آیا خطای ذخیره می‌تواند به دلیل مجوز فایل‌ها باشد؟

خیر، به‌طور مستقیم. ذخیره تنظیمات در دیتابیس انجام می‌شود، نه در فایل. با این حال، اگر افزونه شما برای ذخیره، فایل‌های کش یا فایل‌های تنظیمات اختصاصی می‌نویسد، مجوز پوشه می‌تواند مشکل‌ساز شود. در این حالت، error_log معمولاً پیام Permission denied نشان می‌دهد. اگر افزونه‌تان ذخیره‌سازی فایل دارد، حداقل مجوز توصیه‌شده برای پوشه‌ها 755 و برای فایل‌ها 644 است.

آیا کش می‌تواند باعث ذخیره نشدن شود؟

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

مسیر مدرن — ذخیره تنظیمات با REST API

از وردپرس ۴.۷ به بعد، REST API به بخش ثابت معماری وردپرس تبدیل شده و برای بعضی سناریوها بهتر از مسیر سنتی options.php کار می‌کند. اگر افزونه شما رابط کاربری مبتنی بر جاوااسکریپت دارد یا از ابزارهای فرانت‌اند مثل React استفاده می‌کند، REST API راه تمیزتری است. الگوی ثبت گزینه برای دسترسی از REST:

add_action( 'rest_api_init', 'my_plugin_register_rest_settings' );
function my_plugin_register_rest_settings() {
    register_setting(
        'my_plugin_options_group',
        'my_plugin_options',
        array(
            'type'              => 'object',
            'sanitize_callback' => 'my_plugin_sanitize_options',
            'show_in_rest'      => array(
                'schema' => array(
                    'type'       => 'object',
                    'properties' => array(
                        'api_key' => array( 'type' => 'string' ),
                    ),
                ),
            ),
        )
    );
}

با این ثبت، وردپرس خودکار نقاط پایانی GET و POST روی /wp-json/wp/v2/settings تولید می‌کند و افزونه شما می‌تواند از طریق fetch یا axios تنظیمات را بخواند و ذخیره کند. توجه کنید که REST API برای ذخیره، به کوکی احراز هویت و هدر X-WP-Nonce نیاز دارد که از wp_create_nonce( 'wp_rest' ) گرفته می‌شود. این مسیر مزیت مهمی دارد: وردپرس خودش مدیریت nonce، capability، و schema validation را انجام می‌دهد و شما کمتر در معرض خطای دستی هستید. برای مطالعه بیشتر درباره ساخت endpoint‌های سفارشی، به راهنمای REST API در وردپرس مراجعه کنید.

جلوگیری از تکرار — رویکرد پایدار به تنظیمات افزونه

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

  1. همیشه از register_setting استفاده کنید، نه update_option دستی. چراکه به‌طور خودکار nonce، capability و sanitize را مدیریت می‌کند.
  2. sanitize_callback را کوتاه و قابل تست نگه دارید. اگر تابع بیش از پنجاه خط شد، آن را به چند تابع کوچک‌تر تقسیم کنید.
  3. مقادیر پیش‌فرض را همیشه در register_setting تعریف کنید. این کار از رفتار نامشخص در نصب‌های تازه جلوگیری می‌کند.
  4. همیشه روی محیط استجینگ تست کنید. روی سرور محلی، همه‌چیز کار می‌کند چون محدودیت‌های سرور و WAF وجود ندارد.
  5. لاگ خطاهای sanitize را در حافظه نگه دارید. اگر کاربر مقدار نامعتبر وارد کرد، به‌جای اینکه بی‌صدا رد شود، یک add_settings_error نمایش دهید.
  6. یک تست واحد ساده برای هر گزینه بنویسید. با PHPUnit می‌توانید بدون بالا آوردن وردپرس کامل، sanitize را تست کنید.

در پایان، به‌عنوان یک رویکرد معماری، پیشنهاد می‌کنم که کلاس مدیریت تنظیمات افزونه را در یک فایل جدا نگه دارید و از الگوی Singleton یا Dependency Injection استفاده کنید. این کار باعث می‌شود تست کردن، بازنویسی، و انتقال کد به پروژه‌های دیگر بسیار ساده‌تر شود. تجربه شخصی من این است که پروژه‌هایی که در ابتدا با ساختار کلاسی منظم شروع می‌شوند، در بلندمدت باگ‌های این‌چنینی کمتری تولید می‌کنند.

سخن پایانی

خطای عدم ذخیره تنظیمات افزونه در ظاهر ساده است اما می‌تواند ساعت‌ها وقت بگیرد اگر بدون نقشه سراغش بروید. همان چهار لایه‌ای که در این مقاله توضیح دادم — امنیت فرم، ثبت گزینه، پاک‌سازی داده، و لایه محیطی — تقریباً همیشه شما را به علت واقعی می‌رساند. مهم‌تر از پیدا کردن راه‌حل، این است که پس از حل، دلیل ریشه‌ای آن را در معماری افزونه اصلاح کنید تا این خطا بازنگردد. افزونه‌هایی که سخت‌ترین باگ‌هایشان در لایه sanitize و nonce است، معمولاً همان‌هایی هستند که در بازبینی کد، نظم ساختاری کمتری دارند — و این خودش درس بزرگ‌تری است که از عیب‌یابی هر باگ بیرون می‌آید.

اگر این مشکل را در یک پروژه واقعی تجربه کرده‌اید و به علت غیرمنتظره‌ای برخورده‌اید — مثلاً یک فیلتر سفارشی در functions.php قالب که مقدار گزینه را دستکاری می‌کرد، یا یک افزونه امنیتی که فقط روی IP شما رفتار خاصی داشت — خوشحال می‌شوم تجربه‌تان را در دیدگاه‌ها بنویسید. به‌ویژه اگر راه‌حل دیگری پیدا کرده‌اید که در این مقاله نیامده، آن تجربه برای نفر بعدی که با همان خطا روبرو می‌شود، ارزشمندتر از هر مستند رسمی است. 🛠️