در سال‌های اول کارم با PHP، خطاها را مثل مزاحم می‌دیدم. کافی بود یک Warning ظاهر شود تا سریع با error_reporting(0) پنهانش کنم و بروم سراغ کار بعدی. آن روزها فکر می‌کردم خطا یعنی «چیزی خراب است» و پاک کردنش از صفحه، یعنی «حل شد». اما در پروژه‌ی سومم، وقتی یک باگ مرموز یک هفته تمام وقت تیم را گرفت و در نهایت معلوم شد که ریشه‌اش یک Warning بوده که ماه‌ها پیش ساکت شده بود، فهمیدم که خطاها نه مزاحم، بلکه پیام‌رسان هستند. آن روز اولین روزی بود که مدیریت خطا در PHP را جدی گرفتم. این مقاله، همان مسیری است که در سال‌های بعد طی کردم تا از یک توسعه‌دهنده‌ی «خفه‌کننده‌ی خطا» به یک توسعه‌دهنده‌ی «شنونده‌ی خطا» تبدیل شوم.

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

چرا مدیریت خطا، مهارت پایه است؟

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

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

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

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

انواع خطا در PHP: از Notice تا Fatal

PHP چند سطح مختلف خطا دارد و هر کدام معنی و کاربرد خاصی دارند. تفکیک این سطوح، اولین گام در مدیریت درست است:

سطحمقدار عددیتوضیحمثال
E_ERROR1خطای Fatal، اجرای اسکریپت متوقف می‌شودفراخوانی تابعی که وجود ندارد
E_WARNING2هشدار، اجرای اسکریپت ادامه می‌یابدباز کردن فایلی که وجود ندارد
E_NOTICE8اطلاعیه، معمولاً نکته‌ی کیفی کددسترسی به آرایه با کلید ناموجود
E_DEPRECATED8192استفاده از قابلیت منسوخ‌شدهاستفاده از تابعی که در نسخه‌ی بعدی حذف می‌شود
E_STRICT2048پیشنهاد برای سازگاری بهتر (منسوخ شده)استفاده از متد استاتیک به‌صورت غیراستاتیک
E_PARSE4خطای سینتکسی، اجرای اسکریپت متوقف می‌شودفراموش کردن یک ;

در تجربه‌ی من، سه سطح اول بیشترین سهم را در پروژه‌های واقعی دارند:

E_ERROR: این خطا جدی‌ترین است چون اجرای اسکریپت را متوقف می‌کند. باید در همه‌ی کوئری‌ها و فراخوانی‌های بحرانی مدیریت شود. اگر یک E_ERROR مدیریت‌نشده در کد شما ظاهر شد، کاربر صفحه‌ی سفید یا پیام خطای خام می‌بیند.

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

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

یک نکته‌ی مهم: خطاهای E_NOTICE و E_WARNING را نادیده نگیرید. در محیط توسعه، همیشه این خطاها را فعال و مشاهده کنید. تجربه‌ی من این است که ۹۰ درصد باگ‌های Fatal، ریشه‌شان در یک E_NOTICE یا E_WARNING است که ماه‌ها پیش نادیده گرفته شده.

error_reporting و display_errors

در PHP، دو تنظیمات اصلی تعیین می‌کنند که چه خطاهایی گزارش شوند و چه خطاهایی به کاربر نمایش داده شوند:

  • error_reporting: تعیین می‌کند چه سطوح خطایی گزارش شوند. می‌توانید همه را فعال کنید یا بعضی سطوح را فیلتر کنید.
  • display_errors: تعیین می‌کند که خطاها به کاربر نمایش داده شوند یا فقط در لاگ ثبت شوند.

تنظیمات درست در دو محیط مختلف تفاوت اساسی دارند:

// در محیط توسعه
error_reporting( E_ALL );
ini_set( 'display_errors', '1' );

// در محیط تولید
error_reporting( E_ALL );
ini_set( 'display_errors', '0' );
ini_set( 'log_errors', '1' );
ini_set( 'error_log', '/path/to/php-error.log' );

سه نکته‌ی مهم در این تنظیمات:

  1. در محیط توسعه، همه‌چیز را نمایش دهید: حتی E_NOTICE و E_DEPRECATED. این عادت، باگ‌های پنهان را قبل از تولید کشف می‌کند.
  2. در محیط تولید، هیچ‌چیز را نمایش ندهید: نمایش خطاها به کاربر، اولین در ورودی برای مهاجم است؛ چون خطاها می‌توانند شامل مسیر فایل، نام دیتابیس، و حتی رمز عبور باشند. خطاها را در لاگ ثبت کنید، اما به کاربر نشان ندهید.
  3. خطاها را حتما لاگ کنید: log_errors باید همیشه فعال باشد، حتی در محیط توسعه. لاگ‌ها، تاریخچه‌ی رفتار سیستم شما را می‌سازند.

یک الگوی حرفه‌ای که در پروژه‌های خودم پیاده کرده‌ام: تفاوت بین محیط‌ها را از یک متغیر محیطی بگیرید، نه از کد:

if ( getenv( 'APP_ENV' ) === 'development' ) {
    error_reporting( E_ALL );
    ini_set( 'display_errors', '1' );
} else {
    error_reporting( E_ALL );
    ini_set( 'display_errors', '0' );
    ini_set( 'log_errors', '1' );
}

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

try, catch, finally: پایه‌ی مدیریت استثنا

ساختار try/catch ابزار اصلی مدیریت خطا در PHP است. ایده‌ی ساده: کدی که ممکن است خطا بدهد، در بلوک try قرار می‌گیرد؛ اگر خطا رخ دهد، در بلوک catch گرفته می‌شود؛ و بلوک finally همیشه اجرا می‌شود:

try {
    $result = risky_operation();
    echo 'عملیات موفق بود: ' . $result;
} catch ( RuntimeException $e ) {
    error_log( 'Runtime error: ' . $e->getMessage() );
    echo 'خطا در اجرای عملیات.';
} catch ( Exception $e ) {
    error_log( 'Generic error: ' . $e->getMessage() );
    echo 'خطای غیرمنتظره.';
} finally {
    echo 'عملیات، با موفقیت یا شکست، پایان یافت.';
}

سه نکته‌ی کلیدی در این ساختار:

ترتیب catch ها مهم است: catch های خاص‌تر را اول بنویسید. در مثال بالا، RuntimeException قبل از Exception آمده. اگر ترتیب برعکس بود، RuntimeException همیشه در اولین catch گرفته می‌شد و بلوک دوم هرگز اجرا نمی‌شد.

finally همیشه اجرا می‌شود: حتی اگر در بلوک try یک return یا throw باشد، یا حتی اگر بلوک catch خودش خطا بدهد. این ویژگی، برای پاک‌سازی منابع (بستن فایل، بستن اتصال دیتابیس) بسیار مفید است:

function process_file( string $path ): void {
    $handle = fopen( $path, 'r' );

    if ( ! $handle ) {
        throw new RuntimeException( 'فایل باز نشد.' );
    }

    try {
        while ( ( $line = fgets( $handle ) ) !== false ) {
            process_line( $line );
        }
    } finally {
        fclose( $handle );
    }
}

در این کد، حتی اگر پردازش خطا بدهد، فایل بسته می‌شود. بدون finally، احتمال نشت منابع (resource leak) وجود داشت.

گیر افتادن استثنا را هرگز نادیده نگیرید: یک catch خالی (بدون هیچ کد) بدترین الگو است:

// این کد را هرگز ننویسید:
try {
    risky_operation();
} catch ( Exception $e ) {
    // هیچ کاری نکن
}

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

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

Exception در برابر Error

در PHP ۷ به بعد، دو کلاس پایه برای مدیریت خطا وجود دارد: Exception و Error. تفاوت این دو، یکی از پرتکرارترین سوالات در پروژه‌های واقعی است:

معیارExceptionError
کلاس پایهExceptionError (هر دو از Throwable)
چه چیزی را نشان می‌دهد؟خطاهای منطقی برنامهخطاهای سطح پایین PHP
مثالداده‌ی نامعتبر، ورودی نامناسبفراخوانی تابع ناموجود، تقسیم بر صفر
قابل مدیریت در try/catchبلهبله (از PHP ۷ به بعد)
معمولاً کجا رخ می‌دهد؟کد شماهسته‌ی PHP یا کد خارجی

در تجربه‌ی من، سه نکته‌ی مهم در این تفکیک:

  1. هر دو از Throwable ارث می‌برند: در PHP ۷ به بعد، هر دو Exception و Error از یک interface مشترک به نام Throwable ارث می‌برند. یعنی می‌توانید هر دو را در یک catch ( Throwable $e ) بگیرید. این الگو، در پروژه‌های واقعی برای گرفتن همه‌ی خطاها بسیار مفید است.
  2. خطاهای Error نشانه‌ی باگ هستند: وقتی یک TypeError یا DivisionByZeroError می‌گیرید، این معمولاً نشانه‌ی یک باگ در کد شماست. مدیریت این خطاها، به معنای پنهان کردن باگ نیست؛ به معنای ثبت و پاسخ‌دهی درست است.
  3. Exception نشانه‌ی جریان کنترلی است: وقتی یک InvalidArgumentException پرتاب می‌کنید، در واقع دارید به کد بالادستی می‌گویید «داده‌ی ورودی مشکل دارد، خودت تصمیم بگیر». این استفاده از Exception، بخشی از طراحی طبیعی است.

ساخت Exception اختصاصی

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

class ValidationException extends Exception {}
class DatabaseException extends Exception {}
class PaymentException extends Exception {}

// استفاده
try {
    validate_user_input( $data );
    save_to_database( $data );
} catch ( ValidationException $e ) {
    // نمایش خطای کاربر
    show_error( 'داده‌ی ورودی نامعتبر: ' . $e->getMessage() );
} catch ( DatabaseException $e ) {
    // نمایش خطای عمومی
    error_log( 'DB Error: ' . $e->getMessage() );
    show_error( 'سرویس موقتا در دسترس نیست.' );
} catch ( PaymentException $e ) {
    // رفتار ویژه برای خطای پرداخت
    log_payment_failure( $e );
}

مزایای Exception اختصاصی:

  • خوانایی بالاتر: هر catch دقیقا می‌گوید با چه نوع خطایی سر و کار دارد.
  • رفتار خاص برای هر دامنه: خطای پرداخت با خطای اعتبارسنجی، رفتار متفاوتی می‌طلبد.
  • امکان ساخت سلسله‌مراتب: می‌توانید یک AppException پایه بسازید و بقیه را از آن ارث ببرید. این الگو، پایه‌ی مدیریت خطا در فریم‌ورک‌هایی مثل لاراول است.

یک نکته‌ی ظریف: Exception اختصاصی می‌تواند داده‌های بیشتری هم نگه دارد. مثلاً برای ValidationException، می‌توانید فهرست خطاهای هر فیلد را همراه بیاورید:

class ValidationException extends Exception {
    public function __construct(
        private array $errors,
        string $message = 'خطای اعتبارسنجی',
    ) {
        parent::__construct( $message );
    }

    public function getErrors(): array {
        return $this->errors;
    }
}

این الگو، در فرم‌های پیچیده بسیار مفید است. اگر با مفاهیم شی‌گرایی PHP آشنایی ندارید، آموزش شی‌گرایی در PHP پیش‌نیاز این بخش است.

سه هندلر: error، exception، shutdown

PHP سه هندلر اصلی دارد که برای مدیریت خطاهای خارج از try/catch استفاده می‌شوند. این سه، ابزار حیاتی برای پوشش همه‌ی سناریوهای خطا هستند:

یک — set_error_handler: خطاهای PHP را (مثل E_WARNING، E_NOTICE) می‌گیرد. این هندلر، وقتی خطایی رخ دهد که در try/catch نیست، فراخوانی می‌شود:

set_error_handler( function ( $severity, $message, $file, $line ) {
    throw new ErrorException( $message, 0, $severity, $file, $line );
} );

این الگو، همه‌ی خطاهای PHP را به Exception تبدیل می‌کند. یعنی حتی یک E_NOTICE هم می‌تواند در try/catch گرفته شود. تجربه‌ی من: این الگو، تفاوت بین یک پروژه‌ی قابل دیباگ و یک پروژه‌ی سردرگم را می‌سازد. اما در محیط تولید، این کار می‌تواند رفتار کد را عوض کند؛ پس با احتیاط استفاده کنید.

دو — set_exception_handler: استثناهایی را می‌گیرد که در try/catch گرفته نشده‌اند. آخرین خط دفاعی در برابر صفحه‌ی سفید:

set_exception_handler( function ( Throwable $e ) {
    error_log( 'Uncaught: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );

    if ( getenv( 'APP_ENV' ) === 'development' ) {
        echo 'خطا: ' . $e->getMessage();
    } else {
        http_response_code( 500 );
        echo 'خطای غیرمنتظره. تیم فنی ما در جریان است.';
    }
} );

سه — register_shutdown_function: آخرین تابعی است که قبل از پایان اسکریپت اجرا می‌شود. این تابع، می‌تواند برای گرفتن خطاهای Fatal استفاده شود؛ چون این خطاها در try/catch گرفته نمی‌شوند:

register_shutdown_function( function () {
    $error = error_get_last();

    if ( $error !== null && in_array( $error['type'], [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ], true ) ) {
        error_log( 'Fatal: ' . $error['message'] . ' in ' . $error['file'] . ':' . $error['line'] );

        // نمایش یک صفحه‌ی خطای دوستانه
        if ( ! headers_sent() ) {
            http_response_code( 500 );
        }
        echo '

خطای غیرمنتظره

تیم فنی در جریان است.

'; } } );

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

لاگ‌گیری حرفه‌ای

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

سطح اول — error_log(): تابع ساده‌ی PHP برای ثبت خطا. در پروژه‌های کوچک کافی است، اما در پروژه‌های بزرگ کم می‌آورد؛ چون فقط متن می‌گیرد و ساختار ندارد:

error_log( 'User login failed for: ' . $email );
error_log( json_encode( [ 'event' => 'login_failed', 'email' => $email, 'ip' => $_SERVER['REMOTE_ADDR'] ] ) );

الگوی دوم — لاگ به‌صورت JSON — بسیار مفیدتر است؛ چون بعدا می‌توانید آن را به‌سادگی پارس و جستجو کنید.

سطح دوم — Monolog: کتابخانه‌ی استاندارد لاگ‌گیری در PHP. نصب با Composer (مسیر در آموزش Composer در PHP) و استفاده از چند خط کد:

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger( 'app' );
$log->pushHandler( new StreamHandler( '/path/to/app.log', Logger::WARNING ) );

$log->warning( 'کاربری تلاش ناموفق کرد', [
    'email' => $email,
    'ip'    => $_SERVER['REMOTE_ADDR'],
] );

$log->error( 'پرداخت ناموفق', [
    'order_id' => 1234,
    'reason'   => 'gateway_timeout',
] );

سطح سوم — سرویس لاگ متمرکز: در پروژه‌های بزرگ، لاگ‌ها به یک سرویس متمرکز مثل Sentry، Loggly، یا Elasticsearch فرستاده می‌شوند. مزیت: امکان جستجو و تحلیل در حجم بالا، هشدارهای خودکار، و امکان دیدن correlation بین خطاها.

الگوهای لاگ برای پروژه‌های بزرگ

در پروژه‌های واقعی، سه الگو را در لاگ‌گیری رعایت می‌کنم:

الگوی اول — سطح‌بندی درست: همیشه سطح لاگ را درست انتخاب کنید:

  • DEBUG: اطلاعات تشخیصی برای توسعه‌دهنده.
  • INFO: رویدادهای عادی (ثبت سفارش، ورود کاربر).
  • WARNING: مشکلی که هنوز بحرانی نیست.
  • ERROR: خطای جدی که نیاز به بررسی دارد.
  • CRITICAL: خطای بحرانی که سیستم را از کار می‌اندازد.

الگوی دوم — context در لاگ: همیشه لاگ را با داده‌های context همراه کنید. یعنی به‌جای error_log( 'login failed' )، از error_log( json_encode( [ 'event' => 'login_failed', 'email' => $email, 'ip' => $ip ] ) ) استفاده کنید. این کار، در دیباگ چند برابر کمک می‌کند.

الگوی سوم — correlation id: در پروژه‌های پیچیده، هر درخواست یک شناسه‌ی یگانه می‌گیرد و این شناسه در همه‌ی لاگ‌های آن درخواست حضور دارد. با این روش، می‌توانید کل مسیر یک درخواست را بازسازی کنید. این الگو، در معماری‌های میکروسرویس استاندارد است، اما در پروژه‌های خالص PHP هم بسیار مفید است.

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

تفکیک محیط توسعه از تولید

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

معیارمحیط توسعهمحیط تولید
display_errorsروشنخاموش
پیام خطا به کاربردقیق و کاملعمومی و مبهم
سطح جزئیات لاگDEBUG و بالاترWARNING و بالاتر
ابزار لاگفایل محلیسرویس متمرکز
پاسخ HTTP در خطا200 با پیام500 با پیام عمومی

الگوی پیاده‌سازی این تفکیک، از یک متغیر محیطی شروع می‌شود:

if ( getenv( 'APP_ENV' ) === 'development' ) {
    error_reporting( E_ALL );
    ini_set( 'display_errors', '1' );
} else {
    error_reporting( E_ALL );
    ini_set( 'display_errors', '0' );
    ini_set( 'log_errors', '1' );
}

نکته‌ی حیاتی: در محیط تولید، حتی پیام خطای سفارشی هم باید مبهم باشد. مثلاً به‌جای «خطا در اتصال به دیتابیس shop_db با کاربر admin»، بگویید «سرویس موقتا در دسترس نیست». تجربه‌ی من: پیام‌های دقیق خطا، در پروژه‌های واقعی چندین بار منجر به افشای اطلاعات حساس شده است.

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

اعتبارسنجی در برابر Exception

یکی از تصمیم‌های طراحی که در پروژه‌های واقعی زیاد پیش می‌آید: چه زمانی از اعتبارسنجی استفاده کنیم و چه زمانی از Exception؟ پاسخ در تجربه‌ی من ساده است:

  • اعتبارسنجی برای ورودی کاربر: اگر داده‌ی ورودی از کاربر می‌آید و احتمال خطا بالاست، بهتر است آن را با اعتبارسنجی برگردانید، نه با Exception.
  • Exception برای شرایط غیرمنتظره: اگر شرطی است که «نباید اتفاق بیفتد» (مثل نبود یک تنظیم ضروری)، از Exception استفاده کنید.
  • اعتبارسنجی برای منطق برنامه: اگر شرطی است که بخشی از منطق طبیعی است (مثل موجودی ناکافی در فروشگاه)، از بازگشت مقدار (نه Exception) استفاده کنید.

نمونه‌ی عملی: در یک فرم ثبت‌نام، «ایمیل تکراری است» یک وضعیت منطقی است، نه یک خطای فنی. بنابراین بهتر است تابع یک پاسخ ساختاریافته برگرداند، نه اینکه Exception پرتاب کند:

function register_user( string $email, string $password ): array {
    if ( user_exists( $email ) ) {
        return [ 'success' => false, 'error' => 'email_taken' ];
    }

    if ( strlen( $password ) < 8 ) {
        return [ 'success' => false, 'error' => 'weak_password' ];
    }

    // ذخیره‌ی کاربر
    $user_id = save_user( $email, $password );

    if ( ! $user_id ) {
        throw new RuntimeException( 'خطا در ذخیره‌ی کاربر' );
    }

    return [ 'success' => true, 'user_id' => $user_id ];
}

در این کد، خطاهای منطقی (ایمیل تکراری، رمز ضعیف) به‌عنوان پاسخ برگردانده می‌شوند؛ فقط خطای فنی (ذخیره نشدن در دیتابیس) به‌عنوان Exception پرتاب می‌شود. این تفکیک، کد را خواناتر و قابل مدیریت‌تر می‌کند.

الگوی Retry و مدارا کردن با شکست

در سیستم‌های واقعی، بعضی خطاها موقتی هستند: قطعی شبکه، خوابیدن موقت دیتابیس، Timeout. برای این نوع خطاها، الگوی Retry بسیار مفید است:

function retry( callable $operation, int $max_attempts = 3, int $delay_ms = 500 ): mixed {
    $attempt = 0;

    while ( true ) {
        try {
            return $operation();
        } catch ( RuntimeException $e ) {
            $attempt++;

            if ( $attempt >= $max_attempts ) {
                throw $e;
            }

            usleep( $delay_ms * 1000 * $attempt );
        }
    }
}

// استفاده
$result = retry( function () {
    return fetch_remote_data();
} );

در این کد، تابع retry یک عملیات را تا سه بار تلاش می‌کند؛ با تأخیر فزاینده بین تلاش‌ها. مزیت: در برابر خطاهای موقتی مقاوم می‌شود. تجربه‌ی من: در پروژه‌هایی که با API های بیرونی کار می‌کنند، این الگو تفاوت بین یک سیستم شکست‌پذیر و یک سیستم مقاوم را می‌سازد.

یک هشدار مهم: از الگوی Retry برای همه‌ی خطاها استفاده نکنید. خطاهایی که نشانه‌ی باگ هستند (مثل TypeError) با Retry حل نمی‌شوند و فقط وقت تلف می‌کنند. Retry برای خطاهای موقتی و بیرونی مناسب است.

مدیریت خطا در PDO

در کار با دیتابیس، مدیریت خطا اهمیت ویژه‌ای دارد. PDO چند حالت مدیریت خطا دارد:

$pdo = new PDO( $dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
] );

تنظیم ERRMODE_EXCEPTION مهم‌ترین تنظیمات PDO است. بدون این تنظیم، خطاها به‌صورت سکوت نادیده گرفته می‌شوند و شما ممکن است ساعاتی روی باگی کار کنید که ریشه‌اش یک کوئری شکست‌خورده است. مسیر کامل کار با PDO را در آموزش PDO در PHP و اتصال PHP به MySQL به تفصیل باز کرده‌ام.

یک الگوی حرفه‌ای در مدیریت خطای دیتابیس:

try {
    $pdo->beginTransaction();

    $stmt = $pdo->prepare( 'UPDATE products SET stock = stock - :qty WHERE id = :id AND stock >= :qty' );
    $stmt->execute( [ ':qty' => 2, ':id' => 5 ] );

    if ( $stmt->rowCount() === 0 ) {
        throw new RuntimeException( 'موجودی کافی نیست' );
    }

    $pdo->commit();
} catch ( PDOException $e ) {
    $pdo->rollBack();
    error_log( 'DB Error: ' . $e->getMessage() );
    throw new DatabaseException( 'خطا در ثبت سفارش' );
} catch ( RuntimeException $e ) {
    $pdo->rollBack();
    throw $e;
}

در این کد، خطای PDOException گرفته و به یک DatabaseException اختصاصی تبدیل می‌شود. مزیت: کد بالادستی، به جزئیات PDO وابسته نمی‌شود. اصول کامل کار با دیتابیس در آموزش PDO و مسیر بهینه‌سازی در بهینه‌سازی کوئری‌های MySQL آمده است.

مدیریت خطا در بستر وردپرس

اگر در بستر وردپرس کار می‌کنید، مدیریت خطا با رویکرد متفاوتی روبرو می‌شود. وردپرس از همان سیستم خطای PHP استفاده می‌کند، اما چند لایه‌ی مخصوص خود را هم اضافه می‌کند. مهم‌ترین ابزار، ثابت‌های WP_DEBUG در فایل wp-config.php هستند:

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

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

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

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

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

اشتباهات رایج در مدیریت خطا

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

اشتباهپیامدروش درست
غیرفعال کردن display_errors بدون لاگخطاهای پنهان، دیباگ دشوارخطاها را در لاگ ثبت کنید
catch خالی بدون هیچ کدیخفه کردن مشکل، باگ‌های پنهانحداقل در لاگ ثبت کنید
نمایش خطای دقیق به کاربرافشای اطلاعات حساسپیام عمومی، لاگ دقیق
استفاده از Exception برای اعتبارسنجیپیچیدگی، کاهش خواناییاعتبارسنجی برای ورودی کاربر
نادیده گرفتن E_NOTICE و E_WARNINGتبدیل به E_ERROR در آیندهرفع از همان ابتدا
نبود هندلر برای خطاهای Fatalصفحه‌ی سفید، تجربه‌ی بد کاربرregister_shutdown_function
استفاده از die و exit به‌جای مدیریت خطااجرای ناقص، نشت منابعException و finally
لاگ‌های متنی غیرساختاریافتهغیرقابل جستجو، غیرقابل تحلیللاگ‌های JSON ساختاریافته
یک تنظیمات برای همه‌ی محیط‌هاافشای اطلاعات در تولیدتفکیک محیط توسعه و تولید

یک اشتباه ظریف که در دیدگاه‌ها زیاد می‌بینم: کاربران، مدیریت خطا را فقط برای کدهایی به کار می‌برند که «احتمال خطا در آن‌ها زیاد است» — مثل کار با API یا دیتابیس. اما تجربه‌ی من این است که مدیریت خطا باید در تمام لایه‌های کد وجود داشته باشد، حتی در کدهایی که در ظاهر بی‌خطر به نظر می‌رسند. مثلاً توابع ساده‌ای که با آرایه‌ها کار می‌کنند، اگر ورودی نامعتبر بگیرند، می‌توانند خطاهای پنهان بسازند. مسیر کامل کار با آرایه‌ها و مدیریت خطا در آن‌ها را در کار با آرایه‌ها در PHP باز کرده‌ام.

عادت‌های حرفه‌ای

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

  • در محیط توسعه، خطاها را ببینید: همه‌ی سطوح را فعال کنید؛ حتی E_NOTICE و E_DEPRECATED.
  • در محیط تولید، خطاها را لاگ کنید اما نمایش ندهید: کاربر نباید جزئیات فنی را ببیند.
  • هر catch را با دلیل بنویسید: یک catch خالی، نشانه‌ی سهل‌انگاری است، نه حرفه‌ای‌گری.
  • Exception های اختصاصی بسازید: برای هر دامنه، یک Exception با نام مشخص.
  • لاگ‌ها را ساختاریافته بنویسید: JSON، با context، با سطح درست.
  • هندلرهای سراسری داشته باشید: سه هندلر خطا، exception و shutdown در فایل bootstrap.
  • در برابر شکست، مدارا کنید: الگوی Retry برای خطاهای موقتی، fallback برای خطاهای جدی.

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

یک نکته‌ی حرفه‌ای دیگر: در پروژه‌های خودم، همیشه یک dashboard کوچک دارم که تعداد خطاهای روزانه را نشان می‌دهد. اگر عدد خطاها ناگهان بالا برود، یعنی یک تغییر مشکل‌ساز اعمال شده. این نوع پایش، قبل از آنکه کاربر شکایت کند، به من خبر می‌دهد. اگر با ابزارهای تحلیل کد آشنایی دارید، بهینه‌سازی کدهای PHP راهنمای دقیقی از ابزارهای کیفیت کد را نشان می‌دهد. برای پروژه‌های بزرگ‌تر، ترکیب این پایش با یک سرویس لاگ متمرکز مثل Sentry، تفاوت بین یک تیم واکنشی و یک تیم پیشگیرانه را می‌سازد.

مدیریت خطا، مهارتی است که با تمرین ساخته می‌شود، نه با حفظ کردن. تفاوت بین یک توسعه‌دهنده‌ی آماتور و حرفه‌ای، در تعداد کتاب‌هایی نیست که خوانده، در تعداد خطاهایی است که فهمیده.

نقشه‌ی ادامه‌ی مسیر

بعد از تسلط بر مبانی مدیریت خطا در PHP، سه مسیر اصلی برای ادامه وجود دارد:

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

مسیر دوم — ابزارهای تحلیل ایستا: ابزارهایی مثل PHPStan، Psalm و Rector، کد شما را قبل از اجرا تحلیل می‌کنند و خطاهای پنهان را کشف می‌کنند. تجربه‌ی من: در پروژه‌های بزرگ، این ابزارها تفاوت بین کد سالم و کد آشفته را می‌سازند. اگر با Composer آشنایی ندارید، آموزش Composer در PHP پیش‌نیاز کار با این ابزارهاست.

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

در هر سه مسیر، یک توصیه‌ی مشترک دارم: پروژه‌ی واقعی بسازید. مدیریت خطا، بدون پروژه، شبیه یادگیری شنا روی کاغذ است. یک پروژه‌ی کوچک انتخاب کنید و از همان روز اول، مدیریت خطا را در آن پیاده کنید. مثلاً یک سیستم مدیریت مخاطبین ساده بسازید که هر خطایش را در یک فایل لاگ ساختاریافته ثبت کند و اگر خطای Fatal رخ داد، یک صفحه‌ی خطای دوستانه نشان دهد. اگر با مبانی PHP آشنایی ندارید، آموزش PHP از صفر برای مبتدیان نقطه‌ی شروع دقیقی است. و اگر می‌خواهید منابع به‌روزتری برای ادامه‌ی مسیر داشته باشید، بهترین منابع یادگیری PHP در سال ۲۰۲۶ فهرست کاملی از منابع معتبر را در اختیار شما می‌گذارد.

و حالا یک تمرین ساده که می‌تواند در همین امشب، مسیر حرفه‌ای‌شدن‌تان را شروع کند: یک فایل bootstrap کوچک بسازید که سه هندلر سراسری (خطا، exception، shutdown) را فعال کند، خطاها را به‌صورت JSON در یک فایل لاگ ثبت کند، و اگر محیط تولید بود، به کاربر یک صفحه‌ی خطای دوستانه نشان دهد. این فایل، هفتاد خط بیشتر نیست. اما اگر از فردا هر پروژه‌ای را با این فایل شروع کنید، شش ماه بعد متوجه می‌شوید که پروژه‌های‌تان، در برابر شرایط غیرمنتظره، چند برابر مقاوم‌تر از قبل شده‌اند. همین هفتاد خط، در طول چند سال، ساعت‌ها وقت شما را در دیباگ نجات می‌دهد.

اگر در مسیر، به یک رفتار غیرمنتظره رسیدید — مثلا اینکه چرا finally بعد از یک exit اجرا نمی‌شود، یا چرا E_DEPRECATED در PHP ۸.۴ به یک سطح دیگر منتقل شده — همان مشاهدات را در دیدگاه‌ها بنویسید. مدیریت خطا، از آن دسته موضوعاتی است که هر تجربه‌ی عملی، یک درس تازه در خودش دارد؛ و آن درس وقتی با جامعه به اشتراک گذاشته شود، چند برابر می‌شود. اگر هم در پروژه‌های وردپرسی با خطایی روبه‌رو شدید که مدیریتش سخت بود، راهنمای جامع رفع خطاهای رایج وردپرس ایستگاه بعدی شماست. 🌱