مقدمة إلى معالجة أخطاء الـ API
تعد معالجة الأخطاء المناسبة أحد أهم الجوانب التي يتم تجاهلها في تطوير واجهة برمجة التطبيقات. تساعد استجابات الأخطاء المصممة جيدًا المطورين على تصحيح المشكلات بسرعة، وتحسين تجربة المستخدم، والحفاظ على أمان واجهة برمجة التطبيقات. في هذا الدرس الشامل، سنستكشف نظام معالجة الاستثناءات في Laravel ونتعلم كيفية إنشاء استجابات أخطاء متسقة وغنية بالمعلومات لواجهات REST API الخاصة بك.
لماذا تهم معالجة الأخطاء المناسبة
توفر معالجة الأخطاء الفعالة فوائد متعددة لكل من مستهلكي واجهة برمجة التطبيقات ومشرفي الصيانة:
فوائد معالجة الأخطاء المناسبة:
- تجربة المطور: رسائل الخطأ الواضحة تساعد مستهلكي واجهة برمجة التطبيقات على فهم المشكلات وإصلاحها بسرعة
- الأمان: يمنع تسرب المعلومات الحساسة من خلال رسائل الخطأ
- التصحيح: الأخطاء المنظمة تجعل استكشاف الأخطاء وإصلاحها أسهل
- تجربة المستخدم: يمكّن تطبيقات الواجهة الأمامية من عرض رسائل خطأ ذات معنى
- اتساق واجهة برمجة التطبيقات: تنسيق خطأ موحد عبر جميع نقاط النهاية
- المراقبة: يسهل تتبع وتحليل فشل واجهة برمجة التطبيقات
رموز حالة HTTP القياسية
فهم رموز حالة HTTP أمر أساسي لمعالجة الأخطاء بشكل صحيح:
// 2xx النجاح
200 OK - نجح الطلب
201 Created - تم إنشاء المورد بنجاح
204 No Content - نجح ولكن لا يوجد محتوى للإرجاع
// 4xx أخطاء العميل
400 Bad Request - صيغة طلب غير صالحة أو فشل التحقق
401 Unauthorized - المصادقة مطلوبة أو فشلت
403 Forbidden - تمت المصادقة ولكن غير مصرح
404 Not Found - المورد غير موجود
405 Method Not Allowed - طريقة HTTP غير مدعومة
409 Conflict - يتعارض الطلب مع الحالة الحالية
422 Unprocessable Entity - أخطاء التحقق
429 Too Many Requests - تم تجاوز حد المعدل
// 5xx أخطاء الخادم
500 Internal Server Error - خطأ عام في الخادم
502 Bad Gateway - استجابة غير صالحة من الخادم العلوي
503 Service Unavailable - الخادم غير متاح مؤقتًا
504 Gateway Timeout - انتهت مهلة الخادم العلوي
معالج الاستثناءات في Laravel
يوجد معالج الاستثناءات في Laravel في app/Exceptions/Handler.php. هذا هو المكان الذي تخصص فيه استجابات الأخطاء:
<?php
// app/Exceptions/Handler.php
namespace App\Exceptions;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class Handler extends ExceptionHandler
{
protected $dontReport = [
// الاستثناءات التي لا ينبغي الإبلاغ عنها في السجلات
];
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
public function register(): void
{
$this->reportable(function (Throwable $e) {
// منطق الإبلاغ عن الأخطاء المخصص
});
}
public function render($request, Throwable $exception)
{
// إرجاع JSON لطلبات API
if ($request->expectsJson()) {
return $this->handleApiException($request, $exception);
}
return parent::render($request, $exception);
}
protected function handleApiException($request, Throwable $exception): JsonResponse
{
// معالجة أنواع استثناءات محددة
if ($exception instanceof ModelNotFoundException) {
return response()->json([
'error' => 'المورد غير موجود',
'message' => 'المورد المطلوب غير موجود.',
], 404);
}
if ($exception instanceof AuthenticationException) {
return response()->json([
'error' => 'غير مصادق عليه',
'message' => 'يجب أن تتم مصادقتك للوصول إلى هذا المورد.',
], 401);
}
if ($exception instanceof ValidationException) {
return response()->json([
'error' => 'فشل التحقق',
'message' => 'البيانات المقدمة غير صالحة.',
'errors' => $exception->errors(),
], 422);
}
if ($exception instanceof NotFoundHttpException) {
return response()->json([
'error' => 'نقطة النهاية غير موجودة',
'message' => 'نقطة النهاية المطلوبة غير موجودة.',
], 404);
}
// استجابة خطأ افتراضية
$statusCode = method_exists($exception, 'getStatusCode')
? $exception->getStatusCode()
: 500;
$message = config('app.debug')
? $exception->getMessage()
: 'حدث خطأ أثناء معالجة طلبك.';
return response()->json([
'error' => 'خطأ في الخادم',
'message' => $message,
], $statusCode);
}
}
</pre>
نصيحة احترافية: استخدم $request->expectsJson() لاكتشاف طلبات API وإرجاع استجابات JSON بدلاً من صفحات أخطاء HTML.
تنسيق استجابة الخطأ الموحد
إنشاء بنية استجابة خطأ متسقة عبر واجهة برمجة التطبيقات الخاصة بك:
<?php
// app/Traits/ApiResponses.php
namespace App\Traits;
use Illuminate\Http\JsonResponse;
trait ApiResponses
{
protected function successResponse($data, string $message = null, int $code = 200): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $code);
}
protected function errorResponse(string $message, int $code = 400, array $errors = []): JsonResponse
{
$response = [
'success' => false,
'message' => $message,
];
if (!empty($errors)) {
$response['errors'] => $errors;
}
if (config('app.debug')) {
$response['debug'] => [
'file' => debug_backtrace()[0]['file'] ?? null,
'line' => debug_backtrace()[0]['line'] ?? null,
];
}
return response()->json($response, $code);
}
protected function notFoundResponse(string $message = 'المورد غير موجود'): JsonResponse
{
return $this->errorResponse($message, 404);
}
protected function unauthorizedResponse(string $message = 'غير مصرح'): JsonResponse
{
return $this->errorResponse($message, 401);
}
protected function forbiddenResponse(string $message = 'ممنوع'): JsonResponse
{
return $this->errorResponse($message, 403);
}
protected function validationErrorResponse(array $errors): JsonResponse
{
return response()->json([
'success' => false,
'message' => 'البيانات المقدمة غير صالحة.',
'errors' => $errors,
], 422);
}
protected function serverErrorResponse(string $message = 'خطأ في الخادم'): JsonResponse
{
return $this->errorResponse($message, 500);
}
}
// الاستخدام في الكنترولرات
use App\Traits\ApiResponses;
class PostController extends Controller
{
use ApiResponses;
public function show($id)
{
$post = Post::find($id);
if (!$post) {
return $this->notFoundResponse('المنشور غير موجود');
}
return $this->successResponse($post, 'تم استرجاع المنشور بنجاح');
}
}
</pre>
استثناءات API المخصصة
إنشاء فئات استثناءات مخصصة لسيناريوهات أخطاء محددة:
<?php
// app/Exceptions/ResourceNotFoundException.php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\JsonResponse;
class ResourceNotFoundException extends Exception
{
protected $message;
protected $resourceType;
public function __construct(string $resourceType = 'المورد', string $message = null)
{
$this->resourceType = $resourceType;
$this->message = $message ?? "{$resourceType} غير موجود";
parent::__construct($this->message);
}
public function render($request): JsonResponse
{
return response()->json([
'error' => 'ResourceNotFound',
'message' => $this->message,
'resource_type' => $this->resourceType,
], 404);
}
public function report(): bool
{
// لا تبلغ عن 404s لتتبع الأخطاء
return false;
}
}
// app/Exceptions/UnauthorizedException.php
class UnauthorizedException extends Exception
{
public function render($request): JsonResponse
{
return response()->json([
'error' => 'غير مصرح',
'message' => $this->getMessage() ?: 'أنت غير مصرح لك بتنفيذ هذا الإجراء.',
], 403);
}
}
// app/Exceptions/InvalidRequestException.php
class InvalidRequestException extends Exception
{
protected $errors;
public function __construct(string $message, array $errors = [])
{
parent::__construct($message);
$this->errors = $errors;
}
public function render($request): JsonResponse
{
$response = [
'error' => 'InvalidRequest',
'message' => $this->getMessage(),
];
if (!empty($this->errors)) {
$response['errors'] => $this->errors;
}
return response()->json($response, 400);
}
}
// الاستخدام في الكنترولرات
public function update(Request $request, $id)
{
$post = Post::find($id);
if (!$post) {
throw new ResourceNotFoundException('المنشور');
}
if ($post->author_id !== auth()->id()) {
throw new UnauthorizedException('يمكنك فقط تحديث منشوراتك الخاصة.');
}
// منطق التحديث...
}
</pre>
معالجة أخطاء التحقق
يُرجع التحقق من Laravel تلقائيًا استجابات 422 مع تفاصيل الخطأ:
<?php
// استجابة خطأ التحقق التلقائية
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'status' => 'required|in:draft,published',
]);
// إذا فشل التحقق، يُرجع Laravel تلقائيًا:
// {
// "message": "البيانات المقدمة غير صالحة.",
// "errors": {
// "title": ["حقل العنوان مطلوب."],
// "status": ["حقل الحالة مطلوب."]
// }
// }
$post = Post::create($validated);
return response()->json($post, 201);
}
// رسائل خطأ التحقق المخصصة
$request->validate([
'email' => 'required|email',
'password' => 'required|min:8',
], [
'email.required' => 'يرجى تقديم عنوان بريدك الإلكتروني.',
'email.email' => 'يرجى تقديم عنوان بريد إلكتروني صالح.',
'password.required' => 'كلمة المرور مطلوبة.',
'password.min' => 'يجب أن تكون كلمة المرور 8 أحرف على الأقل.',
]);
</pre>
تنسيق خطأ التحقق المخصص
تجاوز تنسيق خطأ التحقق في معالج الاستثناءات:
<?php
// app/Exceptions/Handler.php
protected function invalidJson($request, ValidationException $exception): JsonResponse
{
return response()->json([
'success' => false,
'error' => 'ValidationError',
'message' => $exception->getMessage(),
'errors' => $exception->errors(),
'status_code' => 422,
], 422);
}
</pre>
استثناءات استعلام قاعدة البيانات
معالجة الأخطاء المتعلقة بقاعدة البيانات برفق:
<?php
use Illuminate\Database\QueryException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
public function handleApiException($request, Throwable $exception): JsonResponse
{
// النموذج غير موجود
if ($exception instanceof ModelNotFoundException) {
$model = class_basename($exception->getModel());
return response()->json([
'error' => 'ResourceNotFound',
'message' => "{$model} غير موجود",
], 404);
}
// أخطاء استعلام قاعدة البيانات
if ($exception instanceof QueryException) {
// لا تكشف عن تفاصيل SQL في الإنتاج
if (config('app.debug')) {
return response()->json([
'error' => 'DatabaseError',
'message' => $exception->getMessage(),
'sql' => $exception->getSql(),
], 500);
}
return response()->json([
'error' => 'DatabaseError',
'message' => 'حدث خطأ في قاعدة البيانات.',
], 500);
}
// ... معالجات استثناءات أخرى
}
</pre>
تحذير أمني: لا تكشف أبدًا عن بنية قاعدة البيانات أو استعلامات SQL أو تتبع المكدس في الإنتاج. استخدم config('app.debug') لتضمين معلومات التصحيح بشكل مشروط فقط في بيئات التطوير.
استثناءات التفويض
معالجة فشل التفويض من السياسات والبوابات:
<?php
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
public function handleApiException($request, Throwable $exception): JsonResponse
{
// المستخدم غير مصادق عليه
if ($exception instanceof AuthenticationException) {
return response()->json([
'error' => 'غير مصادق عليه',
'message' => 'المصادقة مطلوبة للوصول إلى هذا المورد.',
], 401);
}
// المستخدم مصادق عليه ولكن غير مصرح له
if ($exception instanceof AuthorizationException) {
return response()->json([
'error' => 'ممنوع',
'message' => $exception->getMessage() ?: 'أنت غير مصرح لك بتنفيذ هذا الإجراء.',
], 403);
}
// ... معالجات أخرى
}
// الاستخدام مع السياسات
public function update(Request $request, Post $post)
{
// يطرح تلقائيًا AuthorizationException إذا لم يكن مصرحًا
$this->authorize('update', $post);
$post->update($request->validated());
return response()->json($post);
}
</pre>
استثناءات تحديد المعدل
معالجة استثناءات حد المعدل مع معلومات إعادة المحاولة:
<?php
use Illuminate\Http\Exceptions\ThrottleRequestsException;
public function handleApiException($request, Throwable $exception): JsonResponse
{
if ($exception instanceof ThrottleRequestsException) {
$retryAfter = $exception->getHeaders()['Retry-After'] ?? 60;
return response()->json([
'error' => 'TooManyRequests',
'message' => 'تم تجاوز حد المعدل. يرجى المحاولة مرة أخرى لاحقًا.',
'retry_after' => (int) $retryAfter,
], 429)->withHeaders([
'Retry-After' => $retryAfter,
'X-RateLimit-Reset' => time() + $retryAfter,
]);
}
// ... معالجات أخرى
}
</pre>
تسجيل الأخطاء والمراقبة
تنفيذ تسجيل أخطاء شامل للتصحيح والمراقبة:
<?php
// app/Exceptions/Handler.php
use Illuminate\Support\Facades\Log;
public function report(Throwable $exception): void
{
// لا تبلغ عن استثناءات معينة
if ($this->shouldntReport($exception)) {
return;
}
// السجل مع السياق
Log::error($exception->getMessage(), [
'exception' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
'url' => request()->fullUrl(),
'method' => request()->method(),
'ip' => request()->ip(),
'user_id' => auth()->id(),
]);
parent::report($exception);
}
// استثناءات قابلة للإبلاغ مخصصة
$this->reportable(function (ResourceNotFoundException $e) {
// تسجيل مخصص للمورد غير موجود
Log::info('المورد غير موجود', [
'resource' => $e->resourceType,
'url' => request()->fullUrl(),
]);
});
$this->reportable(function (QueryException $e) {
// تنبيه الفريق حول أخطاء قاعدة البيانات
Log::critical('حدث خطأ في قاعدة البيانات', [
'sql' => $e->getSql(),
'bindings' => $e->getBindings(),
]);
});
</pre>
تتبع الأخطاء من جهات خارجية
التكامل مع خدمات مثل Sentry أو Bugsnag أو Flare لتتبع الأخطاء المتقدم:
<?php
// تثبيت Sentry
// composer require sentry/sentry-laravel
// config/sentry.php
return [
'dsn' => env('SENTRY_LARAVEL_DSN'),
'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE', 0.2),
'environment' => env('APP_ENV', 'production'),
];
// يلتقط الاستثناءات تلقائيًا
// إضافة سياق المستخدم
if (auth()->check()) {
\Sentry\configureScope(function (\Sentry\State\Scope $scope): void {
$scope->setUser([
'id' => auth()->id(),
'email' => auth()->user()->email,
]);
});
}
// التقاط الأخطاء يدويًا
try {
// عملية محفوفة بالمخاطر
} catch (Exception $e) {
\Sentry\captureException($e);
throw $e;
}
</pre>
اختبار استجابات الأخطاء
اكتب اختبارات للتأكد من أن معالجة الأخطاء تعمل بشكل صحيح:
<?php
// tests/Feature/ApiErrorHandlingTest.php
namespace Tests\Feature;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ApiErrorHandlingTest extends TestCase
{
use RefreshDatabase;
public function test_returns_404_for_non_existent_resource()
{
$response = $this->getJson('/api/posts/99999');
$response->assertStatus(404)
->assertJson([
'error' => 'ResourceNotFound',
]);
}
public function test_returns_401_for_unauthenticated_requests()
{
$response = $this->postJson('/api/posts', [
'title' => 'منشور تجريبي',
]);
$response->assertStatus(401)
->assertJson([
'error' => 'غير مصادق عليه',
]);
}
public function test_returns_403_for_unauthorized_actions()
{
$user = User::factory()->create();
$post = Post::factory()->create();
$response = $this->actingAs($user)
->deleteJson("/api/posts/{$post->id}");
$response->assertStatus(403)
->assertJson([
'error' => 'ممنوع',
]);
}
public function test_returns_422_for_validation_errors()
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson('/api/posts', [
'title' => '', // غير صالح: مطلوب
]);
$response->assertStatus(422)
->assertJsonStructure([
'message',
'errors' => ['title'],
]);
}
public function test_returns_429_for_rate_limit_exceeded()
{
// قم بإجراء طلبات حتى يتم تحديد المعدل
for ($i = 0; $i < 61; $i++) {
$response = $this->getJson('/api/posts');
}
$response->assertStatus(429)
->assertJsonStructure([
'error',
'retry_after',
]);
}
}
</pre>
تمرين عملي:
- أنشئ معالج استثناءات شامل يُرجع استجابات أخطاء JSON متسقة لجميع أنواع الاستثناءات (التحقق، المصادقة، التفويض، غير موجود، أخطاء الخادم)
- قم ببناء فئات استثناءات مخصصة لأخطاء خاصة بالمجال (PaymentFailedException، SubscriptionExpiredException، إلخ.)
- نفذ تسجيل الأخطاء مع المعلومات السياقية (معرف المستخدم، IP، بيانات الطلب) والتكامل مع Sentry أو Bugsnag
- أنشئ سمة أو فئة مساعدة توفر طرق استجابة خطأ موحدة للكنترولرات
- اكتب اختبارات شاملة تغطي جميع سيناريوهات الأخطاء (401، 403، 404، 422، 429، 500) وتتحقق من بنية استجابة الخطأ الصحيحة
أفضل الممارسات
أفضل ممارسات معالجة الأخطاء:
- كن متسقًا: استخدم نفس بنية استجابة الخطأ عبر جميع نقاط النهاية
- كن غنيًا بالمعلومات: قدم رسائل خطأ واضحة وقابلة للتنفيذ
- كن آمنًا: لا تكشف أبدًا عن معلومات حساسة في أخطاء الإنتاج
- استخدم رموز الحالة المناسبة: اتبع اتفاقيات رمز حالة HTTP
- سجل كل شيء: التسجيل الشامل يساعد في التصحيح
- قم بتضمين رموز الخطأ: استخدم رموز خطأ فريدة لمعالجة الأخطاء البرمجية
- وثق الأخطاء: وثق استجابات الأخطاء المحتملة في وثائق API
- اختبر سيناريوهات الأخطاء: اكتب اختبارات لجميع حالات الأخطاء
الملخص
في هذا الدرس، أتقنت معالجة أخطاء API في Laravel. أنت الآن تفهم كيفية تخصيص معالج الاستثناءات في Laravel، وإنشاء تنسيقات استجابة أخطاء متسقة، وبناء فئات استثناءات مخصصة لسيناريوهات محددة، ومعالجة أخطاء التحقق والتفويض، وتنفيذ تسجيل أخطاء شامل، ودمج خدمات تتبع الأخطاء من جهات خارجية، واختبار استجابات الأخطاء بدقة. تعد معالجة الأخطاء المناسبة ضرورية لإنشاء واجهات برمجة تطبيقات احترافية قابلة للصيانة توفر تجربة ممتازة للمطورين وتساعدك على تشخيص المشكلات بسرعة في الإنتاج.