مدیریت خطا در PHP
مدیریت خطا در PHP از انواع خطا تا سه هندلر سراسری، لاگگیری حرفهای، الگوی Retry و تفکیک محیط توسعه از تولید — با نمونههای عملی و اشتباهات رایج.
در سالهای اول کارم با PHP، خطاها را مثل مزاحم میدیدم. کافی بود یک Warning ظاهر شود تا سریع با error_reporting(0) پنهانش کنم و بروم سراغ کار بعدی. آن روزها فکر میکردم خطا یعنی «چیزی خراب است» و پاک کردنش از صفحه، یعنی «حل شد». اما در پروژهی سومم، وقتی یک باگ مرموز یک هفته تمام وقت تیم را گرفت و در نهایت معلوم شد که ریشهاش یک Warning بوده که ماهها پیش ساکت شده بود، فهمیدم که خطاها نه مزاحم، بلکه پیامرسان هستند. آن روز اولین روزی بود که مدیریت خطا در PHP را جدی گرفتم. این مقاله، همان مسیری است که در سالهای بعد طی کردم تا از یک توسعهدهندهی «خفهکنندهی خطا» به یک توسعهدهندهی «شنوندهی خطا» تبدیل شوم.
مدیریت خطا در PHP، فقط استفاده از try/catch نیست. یک چارچوب کامل است که شامل چهار لایه میشود: تشخیص درست انواع خطا، مدیریت سطوح مختلف، ثبت و لاگگیری، و طراحی سیستمهایی که در برابر شکست مقاوم باشند. در این مقاله، همان چهار لایه را گامبهگام باز میکنم. اگر با مبانی PHP آشنایی ندارید، پیشنهاد میکنم پیش از این مقاله، آموزش PHP از صفر برای مبتدیان را بخوانید؛ چون بدون درک مبانی، لایههای بالاتر مدیریت خطا معنای دقیقی پیدا نمیکنند.
چرا مدیریت خطا، مهارت پایه است؟
در تجربهی من، تفاوت بین یک توسعهدهندهی حرفهای و یک توسعهدهندهی آماتور، بیشتر از هر چیز دیگری، در نحوهی برخورد با خطاها ظاهر میشود. توسعهدهندهی آماتور، خطا را میبیند و میخواهد خفهاش کند. توسعهدهندهی حرفهای، خطا را میبیند و میخواهد حرفش را بشنود. این تفاوت، در سه سطح پروژهها را از هم جدا میکند:
- سطح دیباگ: کدی که خطاهایش را میشنود، در چند دقیقه دیباگ میشود؛ کدی که خطاهایش را خفه میکند، ساعتها وقت میگیرد.
- سطح پایداری: سیستمی که خطاها را مدیریت میکند، در برابر شرایط غیرمنتظره مقاوم است؛ سیستمی که خطاها را نادیده میگیرد، در اولین شکست واقعی از هم میپاشد.
- سطح امنیت: کدی که خطاها را درست مدیریت میکند، اطلاعات حساس را افشا نمیکند؛ کدی که خطاها را خام نمایش میدهد، اولین در ورودی برای مهاجم است. برای درک عمیقتر این لایه، امنیت وب چیست و چه اصولی دارد نقطهی شروع دقیقی است.
یک نکتهی ظریف: مدیریت خطا، فقط به کد شما مربوط نیست. به روابط بین کد شما و کل اکوسیستم — کتابخانهها، دیتابیس، سرور، و کاربر — هم مربوط است. یک خطای مدیریتشدهی حرفهای، تفاوت بین یک تجربهی نرم برای کاربر و یک صفحهی خطای وحشتناک است. اگر با سیستمهای تولیدی کار میکنید، این مهارت به تنهایی میتواند اعتبار حرفهای شما را بسازد.
خطا، پیامرسان است، نه مزاحم. کسی که پیامرسان را از خانه بیرون میکند، در روز مبادا خبری از بیرون ندارد. کسی که به پیامرسان گوش میدهد، در همان روز مبادا، از قبل آماده است.
انواع خطا در PHP: از Notice تا Fatal
PHP چند سطح مختلف خطا دارد و هر کدام معنی و کاربرد خاصی دارند. تفکیک این سطوح، اولین گام در مدیریت درست است:
| سطح | مقدار عددی | توضیح | مثال |
|---|---|---|---|
E_ERROR | 1 | خطای Fatal، اجرای اسکریپت متوقف میشود | فراخوانی تابعی که وجود ندارد |
E_WARNING | 2 | هشدار، اجرای اسکریپت ادامه مییابد | باز کردن فایلی که وجود ندارد |
E_NOTICE | 8 | اطلاعیه، معمولاً نکتهی کیفی کد | دسترسی به آرایه با کلید ناموجود |
E_DEPRECATED | 8192 | استفاده از قابلیت منسوخشده | استفاده از تابعی که در نسخهی بعدی حذف میشود |
E_STRICT | 2048 | پیشنهاد برای سازگاری بهتر (منسوخ شده) | استفاده از متد استاتیک بهصورت غیراستاتیک |
E_PARSE | 4 | خطای سینتکسی، اجرای اسکریپت متوقف میشود | فراموش کردن یک ; |
در تجربهی من، سه سطح اول بیشترین سهم را در پروژههای واقعی دارند:
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' );
سه نکتهی مهم در این تنظیمات:
- در محیط توسعه، همهچیز را نمایش دهید: حتی E_NOTICE و E_DEPRECATED. این عادت، باگهای پنهان را قبل از تولید کشف میکند.
- در محیط تولید، هیچچیز را نمایش ندهید: نمایش خطاها به کاربر، اولین در ورودی برای مهاجم است؛ چون خطاها میتوانند شامل مسیر فایل، نام دیتابیس، و حتی رمز عبور باشند. خطاها را در لاگ ثبت کنید، اما به کاربر نشان ندهید.
- خطاها را حتما لاگ کنید:
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. تفاوت این دو، یکی از پرتکرارترین سوالات در پروژههای واقعی است:
| معیار | Exception | Error |
|---|---|---|
| کلاس پایه | Exception | Error (هر دو از Throwable) |
| چه چیزی را نشان میدهد؟ | خطاهای منطقی برنامه | خطاهای سطح پایین PHP |
| مثال | دادهی نامعتبر، ورودی نامناسب | فراخوانی تابع ناموجود، تقسیم بر صفر |
| قابل مدیریت در try/catch | بله | بله (از PHP ۷ به بعد) |
| معمولاً کجا رخ میدهد؟ | کد شما | هستهی PHP یا کد خارجی |
در تجربهی من، سه نکتهی مهم در این تفکیک:
- هر دو از
Throwableارث میبرند: در PHP ۷ به بعد، هر دوExceptionوErrorاز یک interface مشترک به نامThrowableارث میبرند. یعنی میتوانید هر دو را در یکcatch ( Throwable $e )بگیرید. این الگو، در پروژههای واقعی برای گرفتن همهی خطاها بسیار مفید است. - خطاهای Error نشانهی باگ هستند: وقتی یک
TypeErrorیاDivisionByZeroErrorمیگیرید، این معمولاً نشانهی یک باگ در کد شماست. مدیریت این خطاها، به معنای پنهان کردن باگ نیست؛ به معنای ثبت و پاسخدهی درست است. - 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 ۸.۴ به یک سطح دیگر منتقل شده — همان مشاهدات را در دیدگاهها بنویسید. مدیریت خطا، از آن دسته موضوعاتی است که هر تجربهی عملی، یک درس تازه در خودش دارد؛ و آن درس وقتی با جامعه به اشتراک گذاشته شود، چند برابر میشود. اگر هم در پروژههای وردپرسی با خطایی روبهرو شدید که مدیریتش سخت بود، راهنمای جامع رفع خطاهای رایج وردپرس ایستگاه بعدی شماست. 🌱