Try-Catch و مدیریت خطا
این سند استانداردهای تیم ما و بهترین شیوهها برای پیادهسازی مدیریت خطا و استفاده مؤثر از بلوکهای try-catch را تشریح میکند.
مقدمه
مدیریت صحیح خطا برای ساخت اپلیکیشنهای مقاوم و قابل نگهداری بسیار مهم است. این راهنما استانداردهای تیم ما را برای پیادهسازی مدیریت خطا و استفاده مؤثر از بلوکهای try-catch تعیین میکند.
اصول اساسی
1. عدم استفاده از بلوکهای Try-Catch تو در تو
کاملاً ممنوع: بلوکهای try-catch تو در تو تحت هر شرایطی ممنوع هستند. این یک خط قرمز اصلی است که هرگز نباید از آن عبور کرد.
اگر نیاز به بلوکهای try-catch تو در تو پیدا کردید، معماری باید بازطراحی شود.
// ❌ ممنوع: بلوکهای try-catch تو در تو
try {
// عملیات اول
try {
// عملیات دوم
} catch (SomeException $e) {
// مدیریت exception داخلی
}
} catch (AnotherException $e) {
// مدیریت exception خارجی
}
// ✅ بهتر: متدهای جداگانه با try-catch مجزا
public function performOperation(): Result
{
try {
$data = $this->fetchData();
return $this->processData($data);
} catch (CustomException $e) {
$this->logger->error('Operation failed', ['exception' => $e]);
return new Result(false, $e->getMessage());
}
}
private function fetchData(): Data
{
try {
// منطق دریافت داده
return new Data($result);
} catch (CustomException $e) {
$this->logger->error('Data fetch failed', ['exception' => $e]);
throw $e;
}
}
2. اجتناب از گرفتن Exception های عمومی
❌ ممنوع
Exceptionپایه PHPThrowable- Exception های پیشفرض PDO
DatabaseExceptionFileNotFoundException
✅ توصیه شده
- Exception های سفارشی دامنه
- انواع مشخص exception
- Exception های دارای زمینه واضح
- Exception هایی که قابل مدیریت معنادار هستند
گرفتن exception های عمومی در 99% موارد ممنوع است. استثناهای نادر این قانون بسیار مشخص و محدود هستند.
// ❌ اجتناب: گرفتن exception های عمومی
try {
// عملیات
} catch (Exception $e) {
// مدیریت همه exception ها به یک شکل
}
// ✅ بهتر: گرفتن exception های سفارشی مشخص
try {
// عملیات
} catch (UserNotFoundException $e) {
// مدیریت کاربر یافت نشد
} catch (InvalidInputException $e) {
// مدیریت ورودی نامعتبر
}
3. استفاده از Exception های سفارشی
Exception هایی که باید گرفته شوند، exception های سفارشی تعریف شده توسط تیم هستند. Exception های سفارشی باید از exception های پایه تعریف شده در پروژه ارثبری کنند.
برای اطلاعات بیشتر در مورد exception های سفارشی، به README پروژه مراجعه کنید یا با محمد و علی مشورت کنید.
// ✅ توصیه شده: سلسله مراتب exception سفارشی
class AppException extends Exception {}
class DomainException extends AppException {}
class InfrastructureException extends AppException {}
class UserNotFoundException extends DomainException {}
class InvalidInputException extends DomainException {}
class DatabaseConnectionException extends InfrastructureException {}
4. چه زمانی از Try-Catch استفاده کنیم
از بلوکهای try-catch فقط هنگام تعامل با سیستمهای خارجی یا هر چیزی خارج از مرزهای اپلیکیشن شما استفاده کنید:
- API های شخص ثالث
- عملیات فایل
- اتصالات پایگاه داده
- درخواستهای شبکه
- سرویسهای خارجی
// ✅ استفاده مناسب از try-catch
public function fetchUserDataFromExternalApi(string $userId): UserData
{
try {
$response = $this->apiClient->get("/users/{$userId}");
return new UserData($response['data']);
} catch (ApiConnectionException $e) {
$this->logger->error('API connection failed', ['exception' => $e, 'userId' => $userId]);
throw new UserDataFetchException("Failed to fetch user data: {$e->getMessage()}", 0, $e);
} catch (ApiResponseException $e) {
$this->logger->error('API returned error', ['exception' => $e, 'userId' => $userId]);
throw new UserDataFetchException("Invalid API response: {$e->getMessage()}", 0, $e);
}
}
5. چه زمانی از Try-Catch استفاده نکنیم
در جاهایی که قوانین کسبوکار اعمال میشوند (مانند سرویسهای Domain در DDD) از try-catch استفاده نکنید. به جای آن، از رویکرد "Safe Field" استفاده کنید — شرایط را بررسی کنید و مشکلات را در پاسخ سرویس به لایه بالاتر گزارش دهید.
// ❌ اجتناب: استفاده از try-catch برای قوانین کسبوکار
public function transferMoney(Account $from, Account $to, float $amount): void
{
try {
if ($from->getBalance() < $amount) {
throw new InsufficientFundsException();
}
$from->withdraw($amount);
$to->deposit($amount);
} catch (InsufficientFundsException $e) {
// مدیریت exception
}
}
// ✅ بهتر: استفاده از رویکرد "Safe Field"
public function transferMoney(Account $from, Account $to, float $amount): TransferResult
{
if ($from->getBalance() < $amount) {
return new TransferResult(false, 'موجودی ناکافی');
}
$from->withdraw($amount);
$to->deposit($amount);
return new TransferResult(true, 'انتقال با موفقیت انجام شد');
}
6. ملاحظات عملکرد
استفاده از بلوکهای try-catch از نظر عملکرد بهینه نیست و در گذشته باگهایی در خود PHP داشته است.
الگوی مدیریت خطای زبان Go را در نظر بگیرید که تقریباً استفاده از exception ها را به نفع مقادیر بازگشتی صریح خطا حذف کرده است.
7. یک عمل در هر بلوک Try
هر بلوک try باید فقط یک عمل انجام دهد. انجام چندین عمل در یک بلوک try یک آنتیپترن است. وضعیت آن عمل واحد باید بررسی شود.
// ❌ اجتناب: چندین عمل در یک بلوک try
try {
$user = $this->userRepository->find($userId);
$order = $this->orderRepository->create($user, $orderData);
$this->emailService->sendOrderConfirmation($order);
} catch (Exception $e) {
// کدام عملیات شکست خورد؟
}
// ✅ بهتر: یک عمل در هر بلوک try
public function processOrder(string $userId, array $orderData): OrderResult
{
try {
$user = $this->userRepository->find($userId);
} catch (UserNotFoundException $e) {
$this->logger->error('User not found', ['exception' => $e, 'userId' => $userId]);
return new OrderResult(false, 'کاربر یافت نشد');
}
try {
$order = $this->orderRepository->create($user, $orderData);
} catch (OrderCreationException $e) {
$this->logger->error('Order creation failed', ['exception' => $e, 'userId' => $userId]);
return new OrderResult(false, 'ایجاد سفارش شکست خورد');
}
try {
$this->emailService->sendOrderConfirmation($order);
} catch (EmailSendingException $e) {
$this->logger->warning('Order confirmation email failed', ['exception' => $e, 'orderId' => $order->getId()]);
// علیرغم شکست ایمیل ادامه دهید
}
return new OrderResult(true, 'سفارش با موفقیت پردازش شد', $order->getId());
}
8. همیشه Exception های گرفته شده را لاگ کنید
هر exception گرفته شده باید با زمینه مناسب لاگ شود.
// ✅ الزامی: لاگ کردن exception های گرفته شده
try {
// عملیات
} catch (CustomException $e) {
$this->logger->error('Operation failed', [
'exception' => $e,
'stack' => $e->getTraceAsString(),
'context' => $contextData
]);
// مدیریت exception
}
استثنا: اگر exception را گرفتید، عملی انجام دادید (مانند rollback) و سپس همان exception را دوباره پرتاب کردید تا در لایه بالاتر (مانند error handler) لاگ شود، لاگ کردن در بلوک catch اجباری نیست.
// ✅ استثنای معتبر برای قانون لاگ کردن
try {
$this->db->beginTransaction();
// عملیات پایگاه داده
$this->db->commit();
} catch (DatabaseException $e) {
$this->db->rollback();
throw $e; // در لایه بالاتر لاگ خواهد شد
}
9. شامل کردن زمینه در لاگها
لاگها باید همیشه زمینه (مانند stack trace) را شامل شوند تا برای دیباگ مفید باشند.
// ✅ الزامی: شامل کردن زمینه در لاگها
try {
// عملیات
} catch (CustomException $e) {
$this->logger->error('Operation failed', [
'exception' => $e,
'stack' => $e->getTraceAsString(),
'userId' => $userId,
'requestId' => $requestId,
'additionalData' => $data
]);
// مدیریت exception
}
مثال دنیای واقعی
- یکپارچگی API خارجی
- منطق کسبوکار
class UserApiService
{
private ApiClient $apiClient;
private LoggerInterface $logger;
public function __construct(ApiClient $apiClient, LoggerInterface $logger)
{
$this->apiClient = $apiClient;
$this->logger = $logger;
}
public function getUserProfile(string $userId): UserProfileResult
{
try {
$response = $this->apiClient->get("/users/{$userId}/profile");
return new UserProfileResult(
true,
new UserProfile(
$response['name'],
$response['email'],
$response['avatar']
)
);
} catch (ApiConnectionException $e) {
$this->logger->error('API connection failed', [
'exception' => $e,
'userId' => $userId,
'stack' => $e->getTraceAsString()
]);
return new UserProfileResult(
false,
null,
'امکان اتصال به سرویس کاربر وجود ندارد'
);
} catch (ApiResponseException $e) {
$this->logger->error('API returned error response', [
'exception' => $e,
'userId' => $userId,
'stack' => $e->getTraceAsString(),
'response' => $e->getResponse()
]);
return new UserProfileResult(
false,
null,
'سرویس کاربر خطا برگرداند'
);
} catch (UserNotFoundException $e) {
// این یک exception سفارشی است که انتظار داریم و به طور مشخص مدیریت میکنیم
$this->logger->info('User not found in API', [
'userId' => $userId
]);
return new UserProfileResult(
false,
null,
'کاربر یافت نشد'
);
}
}
}
class UserProfileResult
{
public readonly bool $success;
public readonly ?UserProfile $profile;
public readonly ?string $errorMessage;
public function __construct(
bool $success,
?UserProfile $profile = null,
?string $errorMessage = null
) {
$this->success = $success;
$this->profile = $profile;
$this->errorMessage = $errorMessage;
}
}
class TransferMoneyService
{
private AccountRepository $accountRepository;
private TransactionRepository $transactionRepository;
private LoggerInterface $logger;
public function __construct(
AccountRepository $accountRepository,
TransactionRepository $transactionRepository,
LoggerInterface $logger
) {
$this->accountRepository = $accountRepository;
$this->transactionRepository = $transactionRepository;
$this->logger = $logger;
}
public function transfer(string $fromAccountId, string $toAccountId, float $amount): TransferResult
{
// رویکرد safe field برای منطق کسبوکار - بدون try/catch
if ($amount <= 0) {
return new TransferResult(false, 'مبلغ باید مثبت باشد');
}
$fromAccount = $this->accountRepository->findById($fromAccountId);
if (!$fromAccount) {
return new TransferResult(false, 'حساب مبدأ یافت نشد');
}
$toAccount = $this->accountRepository->findById($toAccountId);
if (!$toAccount) {
return new TransferResult(false, 'حساب مقصد یافت نشد');
}
if ($fromAccount->getBalance() < $amount) {
return new TransferResult(false, 'موجودی ناکافی');
}
// فقط برای تعامل با سیستم خارجی (پایگاه داده در این مورد) از try/catch استفاده کنید
try {
$this->accountRepository->beginTransaction();
$fromAccount->withdraw($amount);
$toAccount->deposit($amount);
$this->accountRepository->save($fromAccount);
$this->accountRepository->save($toAccount);
$transaction = new Transaction(
$fromAccountId,
$toAccountId,
$amount,
new DateTime()
);
$this->transactionRepository->save($transaction);
$this->accountRepository->commitTransaction();
return new TransferResult(
true,
'انتقال با موفقیت انجام شد',
$transaction->getId()
);
} catch (DatabaseException $e) {
$this->accountRepository->rollbackTransaction();
$this->logger->error('Database error during transfer', [
'exception' => $e,
'fromAccountId' => $fromAccountId,
'toAccountId' => $toAccountId,
'amount' => $amount,
'stack' => $e->getTraceAsString()
]);
return new TransferResult(
false,
'خطایی در حین پردازش انتقال رخ داد'
);
}
}
}
class TransferResult
{
public readonly bool $success;
public readonly string $message;
public readonly ?string $transactionId;
public function __construct(
bool $success,
string $message,
?string $transactionId = null
) {
$this->success = $success;
$this->message = $message;
$this->transactionId = $transactionId;
}
}
خلاصه
راهنماییهای کلیدی
- هرگز از بلوکهای try-catch تو در تو استفاده نکنید
- از گرفتن exception های عمومی اجتناب کنید
- از exception های سفارشی که از exception های پایه پروژه ارثبری میکنند استفاده کنید
- فقط برای تعامل با سیستمهای خارجی از try-catch استفاده کنید
- برای قوانین کسبوکار به جای try-catch از رویکرد "Safe Field" استفاده کنید
- به یاد داشته باشید که try-catch پیامدهای عملکردی دارد
- فقط یک عمل در هر بلوک try قرار دهید
- همیشه exception های گرفته شده را با زمینه لاگ کنید
- stack trace ها و دادههای مرتبط را در لاگ exception ها شامل کنید
راهنمایی CTO تیم:
"مدیریت صحیح خطا فقط در مورد گرفتن exception ها نیست، بلکه در مورد طراحی سیستمهایی است که مقاوم و قابل نگهداری باشند. رویکرد ما باید بر جلوگیری از خطاها از طریق طراحی خوب متمرکز باشد تا اینکه آنها را پس از وقوع بگیریم."
قانون عملی:
- مدیریت خطا را در طول بررسی کد بازبینی کنید تا اطمینان حاصل شود که از این استانداردها پیروی میکند
- استفاده از ابزارهای تحلیل استاتیک برای تشخیص الگوهای ممنوع را در نظر بگیرید