اتصالات الخدمات الصغيرة
اتصالات الخدمات الصغيرة
مع نمو التطبيقات، يؤدي تقسيمها إلى خدمات صغيرة مستقلة إلى تحسين قابلية التوسع والصيانة ومرونة النشر. يوفر Laravel أدوات ممتازة لبناء بنى الخدمات الصغيرة، بما في ذلك عملاء HTTP للاتصال المتزامن والرسائل القائمة على قوائم الانتظار للأنماط غير المتزامنة والبنى المدفوعة بالأحداث للأنظمة المقترنة بشكل فضفاض. يستكشف هذا الدرس الأنماط والتطبيقات للاتصال بين الخدمات.
عميل HTTP للاتصال بين الخدمات
عميل HTTP في Laravel، المبني على Guzzle، يوفر واجهة سلسة لإجراء طلبات HTTP بين الخدمات الصغيرة مع ميزات مثل إعادة المحاولة والمهلات و middleware.
<?php
// config/services.php - تحديد نقاط نهاية الخدمة
return [
'user_service' => [
'url' => env('USER_SERVICE_URL', 'http://user-service:8001'),
'key' => env('USER_SERVICE_KEY'),
'timeout' => 10,
],
'payment_service' => [
'url' => env('PAYMENT_SERVICE_URL', 'http://payment-service:8002'),
'key' => env('PAYMENT_SERVICE_KEY'),
'timeout' => 30,
],
'notification_service' => [
'url' => env('NOTIFICATION_SERVICE_URL', 'http://notification-service:8003'),
'key' => env('NOTIFICATION_SERVICE_KEY'),
'timeout' => 5,
],
];
// app/Services/UserServiceClient.php
namespace App\Services;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class UserServiceClient
{
protected string $baseUrl;
protected string $apiKey;
protected int $timeout;
public function __construct()
{
$this->baseUrl = config('services.user_service.url');
$this->apiKey = config('services.user_service.key');
$this->timeout = config('services.user_service.timeout');
}
protected function client(): PendingRequest
{
return Http::baseUrl($this->baseUrl)
->withToken($this->apiKey)
->timeout($this->timeout)
->retry(3, 100) // إعادة المحاولة 3 مرات بتأخير 100ms
->withHeaders([
'X-Service-Name' => config('app.name'),
'X-Request-ID' => request()->header('X-Request-ID', uniqid()),
]);
}
public function getUser(int $userId): ?array
{
try {
$response = $this->client()->get("/users/{$userId}");
if ($response->successful()) {
return $response->json('data');
}
Log::warning("فشل جلب المستخدم {$userId}", [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
} catch (\Exception $e) {
Log::error("فشل الاتصال بخدمة المستخدم: {$e->getMessage()}");
return null;
}
}
public function createUser(array $data): ?array
{
$response = $this->client()
->post('/users', $data);
return $response->successful() ? $response->json('data') : null;
}
public function updateUser(int $userId, array $data): bool
{
$response = $this->client()
->put("/users/{$userId}", $data);
return $response->successful();
}
public function getUsersByIds(array $userIds): array
{
$response = $this->client()
->post('/users/batch', ['ids' => $userIds]);
return $response->successful() ? $response->json('data', []) : [];
}
}
// الاستخدام في Controller
use App\Services\UserServiceClient;
class OrderController extends Controller
{
public function __construct(
protected UserServiceClient $userService
) {}
public function store(Request $request)
{
// التحقق من صحة الطلب
$validated = $request->validate([
'user_id' => 'required|integer',
'items' => 'required|array',
]);
// جلب المستخدم من خدمة المستخدم
$user = $this->userService->getUser($validated['user_id']);
if (!$user) {
return response()->json([
'message' => 'المستخدم غير موجود'
], 404);
}
// إنشاء الطلب
$order = Order::create([
'user_id' => $validated['user_id'],
'user_email' => $user['email'], // تخزين بيانات المستخدم مؤقتاً
'items' => $validated['items'],
'total' => $this->calculateTotal($validated['items']),
]);
return response()->json($order, 201);
}
}
طلبات HTTP غير المتزامنة مع مجموعة متزامنة
عندما تحتاج إلى استدعاء خدمات متعددة في وقت واحد، تسمح مجموعة HTTP في Laravel بطلبات متوازية، مما يقلل بشكل كبير من إجمالي الكمون.
<?php
use Illuminate\Support\Facades\Http;
// طلبات تسلسلية (بطيئة - 300ms إجمالي)
$user = Http::get('http://user-service/users/1'); // 100ms
$orders = Http::get('http://order-service/orders?user_id=1'); // 100ms
$payments = Http::get('http://payment-service/payments?user_id=1'); // 100ms
// طلبات متزامنة (سريعة - 100ms إجمالي)
$responses = Http::pool(fn ($pool) => [
$pool->as('user')->get('http://user-service/users/1'),
$pool->as('orders')->get('http://order-service/orders?user_id=1'),
$pool->as('payments')->get('http://payment-service/payments?user_id=1'),
]);
$user = $responses['user']->json();
$orders = $responses['orders']->json();
$payments = $responses['payments']->json();
// مجموعة متقدمة مع معالجة الأخطاء
class DashboardService
{
public function getUserDashboard(int $userId): array
{
$responses = Http::pool(function ($pool) use ($userId) {
return [
$pool->as('profile')
->timeout(5)
->get("http://user-service/users/{$userId}"),
$pool->as('orders')
->timeout(10)
->get("http://order-service/users/{$userId}/orders"),
$pool->as('notifications')
->timeout(3)
->get("http://notification-service/users/{$userId}/notifications"),
$pool->as('stats')
->timeout(8)
->get("http://analytics-service/users/{$userId}/stats"),
];
});
return [
'profile' => $responses['profile']->successful()
? $responses['profile']->json()
: ['error' => 'الملف الشخصي غير متاح'],
'orders' => $responses['orders']->successful()
? $responses['orders']->json()
: [],
'notifications' => $responses['notifications']->successful()
? $responses['notifications']->json()
: [],
'stats' => $responses['stats']->successful()
? $responses['stats']->json()
: $this->getDefaultStats(),
];
}
protected function getDefaultStats(): array
{
return [
'orders_count' => 0,
'total_spent' => 0,
];
}
}
// نمط قاطع الدائرة
class ServiceClient
{
protected bool $isOpen = false;
protected int $failureCount = 0;
protected int $threshold = 5;
protected int $timeout = 60; // ثواني
public function call(string $url): mixed
{
if ($this->isOpen) {
if ($this->shouldAttemptReset()) {
$this->halfOpen();
} else {
throw new \Exception('قاطع الدائرة مفتوح');
}
}
try {
$response = Http::timeout(5)->get($url);
if ($response->successful()) {
$this->onSuccess();
return $response->json();
}
$this->onFailure();
return null;
} catch (\Exception $e) {
$this->onFailure();
throw $e;
}
}
protected function onSuccess(): void
{
$this->failureCount = 0;
$this->isOpen = false;
}
protected function onFailure(): void
{
$this->failureCount++;
if ($this->failureCount >= $this->threshold) {
$this->open();
}
}
protected function open(): void
{
$this->isOpen = true;
cache()->put('circuit_breaker_opened_at', now(), $this->timeout);
}
protected function shouldAttemptReset(): bool
{
$openedAt = cache()->get('circuit_breaker_opened_at');
return $openedAt && now()->diffInSeconds($openedAt) >= $this->timeout;
}
protected function halfOpen(): void
{
$this->isOpen = false;
$this->failureCount = $this->threshold - 1;
}
}
قوائم انتظار الرسائل للاتصال غير المتزامن
للعمليات التي لا تتطلب استجابات فورية، توفر قوائم انتظار الرسائل اتصالاً موثوقاً به غير متزامن بين الخدمات، مما يحسن المرونة وقابلية التوسع.
<?php
// خدمة الطلبات - نشر الأحداث
// app/Jobs/PublishOrderCreatedEvent.php
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class PublishOrderCreatedEvent implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Order $order
) {}
public function handle(): void
{
$payload = [
'event' => 'order.created',
'data' => [
'order_id' => $this->order->id,
'user_id' => $this->order->user_id,
'total' => $this->order->total,
'items' => $this->order->items,
'created_at' => $this->order->created_at->toIso8601String(),
],
'timestamp' => now()->toIso8601String(),
];
// النشر إلى وسيط الرسائل (RabbitMQ، Redis، إلخ)
$this->publishToQueue('order-events', $payload);
// أو الإرسال إلى خدمات متعددة
$this->notifyInventoryService($payload);
$this->notifyPaymentService($payload);
$this->notifyNotificationService($payload);
}
protected function publishToQueue(string $queue, array $payload): void
{
// استخدام Redis كوسيط للرسائل
\Illuminate\Support\Facades\Redis::rpush($queue, json_encode($payload));
}
protected function notifyInventoryService(array $payload): void
{
Http::baseUrl(config('services.inventory_service.url'))
->timeout(5)
->post('/events/order-created', $payload);
}
protected function notifyPaymentService(array $payload): void
{
Http::baseUrl(config('services.payment_service.url'))
->timeout(5)
->post('/events/order-created', $payload);
}
protected function notifyNotificationService(array $payload): void
{
Http::baseUrl(config('services.notification_service.url'))
->timeout(5)
->post('/events/order-created', $payload);
}
}
// خدمة الإشعارات - استهلاك الأحداث
// app/Console/Commands/ConsumeOrderEvents.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class ConsumeOrderEvents extends Command
{
protected $signature = 'queue:consume-order-events';
protected $description = 'استهلاك أحداث الطلبات من قائمة انتظار الرسائل';
public function handle(): void
{
$this->info('الاستماع إلى أحداث الطلبات...');
while (true) {
$message = Redis::blpop('order-events', 0); // إخراج محظور
if ($message) {
$payload = json_decode($message[1], true);
$this->processEvent($payload);
}
}
}
protected function processEvent(array $payload): void
{
$event = $payload['event'];
$data = $payload['data'];
match ($event) {
'order.created' => $this->handleOrderCreated($data),
'order.updated' => $this->handleOrderUpdated($data),
'order.cancelled' => $this->handleOrderCancelled($data),
default => $this->warn("حدث غير معروف: {$event}"),
};
}
protected function handleOrderCreated(array $data): void
{
$this->info("معالجة order.created: {$data['order_id']}");
// إرسال إشعار إلى المستخدم
$user = $this->fetchUser($data['user_id']);
if ($user) {
$this->sendNotification($user['email'], [
'subject' => 'تأكيد الطلب',
'message' => "تم وضع طلبك #{$data['order_id']} بنجاح.",
]);
}
}
protected function fetchUser(int $userId): ?array
{
$response = Http::get(config('services.user_service.url') . "/users/{$userId}");
return $response->successful() ? $response->json('data') : null;
}
protected function sendNotification(string $email, array $data): void
{
// إرسال إشعار بالبريد الإلكتروني
\Illuminate\Support\Facades\Mail::raw($data['message'], function ($message) use ($email, $data) {
$message->to($email)->subject($data['subject']);
});
}
}
// تسجيل المستهلك كخدمة
// تكوين supervisor: /etc/supervisor/conf.d/order-event-consumer.conf
[program:order-event-consumer]
command=php /path/to/artisan queue:consume-order-events
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/order-event-consumer.log
البنية المدفوعة بالأحداث مع أحداث Laravel
يمكن توسيع نظام الأحداث في Laravel لإنشاء بنية خدمات صغيرة مدفوعة بالأحداث حيث تتفاعل الخدمات مع الأحداث بدلاً من الاستدعاءات المباشرة، مما يعزز الاقتران الفضفاض.
<?php
// عقود الأحداث المشتركة عبر الخدمات
// packages/shared/src/Events/OrderEvent.php
namespace Shared\Events;
interface OrderEvent
{
public function getOrderId(): int;
public function getUserId(): int;
public function getPayload(): array;
}
// خدمة الطلبات - نشر أحداث النطاق
// app/Events/OrderCreated.php
namespace App\Events;
use Shared\Events\OrderEvent;
class OrderCreated implements OrderEvent
{
public function __construct(
public int $orderId,
public int $userId,
public float $total,
public array $items
) {}
public function getOrderId(): int
{
return $this->orderId;
}
public function getUserId(): int
{
return $this->userId;
}
public function getPayload(): array
{
return [
'order_id' => $this->orderId,
'user_id' => $this->userId,
'total' => $this->total,
'items' => $this->items,
'created_at' => now()->toIso8601String(),
];
}
}
// app/Listeners/PublishOrderCreatedToEventBus.php
namespace App\Listeners;
use App\Events\OrderCreated;
use App\Services\EventBusService;
class PublishOrderCreatedToEventBus
{
public function __construct(
protected EventBusService $eventBus
) {}
public function handle(OrderCreated $event): void
{
$this->eventBus->publish('order.created', $event->getPayload());
}
}
// app/Services/EventBusService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Redis;
class EventBusService
{
public function publish(string $eventName, array $payload): void
{
$message = [
'event' => $eventName,
'data' => $payload,
'service' => config('app.name'),
'timestamp' => now()->toIso8601String(),
'correlation_id' => request()->header('X-Correlation-ID', uniqid()),
];
// النشر إلى Redis Streams (أفضل من القوائم لمصدر الأحداث)
Redis::xadd('event-bus', '*', [
'message' => json_encode($message)
]);
// إخطار المشتركين في webhook أيضاً
$this->notifyWebhookSubscribers($eventName, $message);
}
protected function notifyWebhookSubscribers(string $eventName, array $message): void
{
$subscribers = $this->getWebhookSubscribers($eventName);
foreach ($subscribers as $subscriber) {
Http::timeout(5)
->withHeaders([
'X-Event-Name' => $eventName,
'X-Event-ID' => $message['correlation_id'],
])
->post($subscriber['url'], $message);
}
}
protected function getWebhookSubscribers(string $eventName): array
{
// الجلب من قاعدة البيانات أو التكوين
return \App\Models\WebhookSubscription::where('event', $eventName)
->where('is_active', true)
->get()
->toArray();
}
}
// خدمة المخزون - الاشتراك في الأحداث
// app/Console/Commands/ConsumeEventBus.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class ConsumeEventBus extends Command
{
protected $signature = 'events:consume {--group=inventory-service}';
public function handle(): void
{
$group = $this->option('group');
$consumer = gethostname();
// إنشاء مجموعة المستهلك إذا لم تكن موجودة
try {
Redis::xgroup('CREATE', 'event-bus', $group, '0');
} catch (\Exception $e) {
// المجموعة موجودة بالفعل
}
$this->info("استهلاك الأحداث كـ {$group}:{$consumer}");
while (true) {
$events = Redis::xreadgroup(
$group,
$consumer,
['event-bus' => '>'],
1,
2000 // حظر لمدة ثانيتين
);
if (empty($events)) {
continue;
}
foreach ($events['event-bus'] as $id => $event) {
$message = json_decode($event['message'], true);
$this->processEvent($message);
// إقرار بالمعالجة
Redis::xack('event-bus', $group, [$id]);
}
}
}
protected function processEvent(array $message): void
{
$eventName = $message['event'];
$data = $message['data'];
$this->info("معالجة {$eventName}");
match ($eventName) {
'order.created' => $this->reserveInventory($data),
'order.cancelled' => $this->releaseInventory($data),
default => null,
};
}
protected function reserveInventory(array $data): void
{
foreach ($data['items'] as $item) {
\App\Models\Inventory::where('product_id', $item['product_id'])
->decrement('quantity', $item['quantity']);
}
$this->info("تم حجز المخزون للطلب {$data['order_id']}");
}
protected function releaseInventory(array $data): void
{
foreach ($data['items'] as $item) {
\App\Models\Inventory::where('product_id', $item['product_id'])
->increment('quantity', $item['quantity']);
}
$this->info("تم الإفراج عن المخزون للطلب {$data['order_id']}");
}
}
نمط بوابة API
توفر بوابة API نقطة دخول واحدة للعملاء، وتتعامل مع التوجيه والمصادقة وتقييد المعدل وتجميع الاستجابة من خدمات صغيرة متعددة.
<?php
// خدمة بوابة API
// app/Http/Controllers/GatewayController.php
namespace App\Http\Controllers;
use App\Services\UserServiceClient;
use App\Services\OrderServiceClient;
use App\Services\PaymentServiceClient;
use Illuminate\Http\Request;
class GatewayController extends Controller
{
public function __construct(
protected UserServiceClient $userService,
protected OrderServiceClient $orderService,
protected PaymentServiceClient $paymentService
) {}
// تجميع البيانات من خدمات متعددة
public function getUserProfile(Request $request, int $userId)
{
// طلبات متوازية
$responses = \Illuminate\Support\Facades\Http::pool(fn ($pool) => [
$pool->as('user')->get(config('services.user_service.url') . "/users/{$userId}"),
$pool->as('orders')->get(config('services.order_service.url') . "/users/{$userId}/orders"),
$pool->as('payments')->get(config('services.payment_service.url') . "/users/{$userId}/payments"),
]);
return response()->json([
'user' => $responses['user']->json('data'),
'orders' => $responses['orders']->json('data', []),
'payments' => $responses['payments']->json('data', []),
'cached_at' => now()->toIso8601String(),
]);
}
// التوجيه إلى الخدمة المناسبة
public function proxy(Request $request, string $service, string $path)
{
$serviceUrl = config("services.{$service}.url");
if (!$serviceUrl) {
return response()->json(['error' => 'الخدمة غير موجودة'], 404);
}
$response = \Illuminate\Support\Facades\Http::withHeaders([
'X-Forwarded-For' => $request->ip(),
'X-Original-URI' => $request->getRequestUri(),
])
->send($request->method(), "{$serviceUrl}/{$path}", [
'query' => $request->query(),
'json' => $request->json()->all(),
]);
return response($response->body(), $response->status())
->withHeaders($response->headers());
}
}
// routes/api.php
Route::middleware(['auth:sanctum'])->group(function () {
// نقاط نهاية مجمعة
Route::get('/users/{userId}/profile', [GatewayController::class, 'getUserProfile']);
// الوكيل إلى الخدمات
Route::any('/proxy/{service}/{path}', [GatewayController::class, 'proxy'])
->where('path', '.*');
});
تمرين 1: بناء عملاء الخدمة
أنشئ عملاء HTTP للاتصال بخدمات متعددة:
- نفذ UserServiceClient بطرق: getUser()، createUser()، updateUser()
- نفذ ProductServiceClient بطرق: getProduct()، getProductsByIds()
- أضف منطق إعادة المحاولة وتكوين المهلة ومعالجة الأخطاء لكلا العميلين
- نفذ نمط قاطع الدائرة لمنع فشل متتالي
- اختبر العملاء بخدمات وهمية وتحقق من سلوك إعادة المحاولة
تمرين 2: معالجة الطلبات المدفوعة بالأحداث
ابنِ نظام معالجة طلبات مدفوع بالأحداث:
- أنشئ حدث OrderCreated في خدمة الطلبات
- انشر الحدث إلى Redis Streams عند إنشاء الطلب
- أنشئ مستهلكاً في خدمة المخزون يحجز المخزون
- أنشئ مستهلكاً في خدمة الإشعارات يرسل بريد تأكيد إلكتروني
- نفذ التماثلية لمنع المعالجة المكررة
- اختبر مع إنشاءات طلبات متعددة وتحقق من تفاعل جميع الخدمات بشكل صحيح
تمرين 3: تطبيق بوابة API
ابنِ بوابة API بسيطة:
- أنشئ بوابة توجه /api/users/* إلى خدمة المستخدم
- وجه /api/orders/* إلى خدمة الطلبات
- نفذ middleware للمصادقة يتحقق من رموز JWT
- أضف تقييد معدل لكل مستخدم (100 طلب في الدقيقة)
- أنشئ نقطة نهاية مجمعة /api/dashboard تجلب البيانات من 3+ خدمات بالتوازي
- اختبر البوابة مع خدمات متعددة وتحقق من عمل التوجيه والتجميع بشكل صحيح
الخلاصة
في هذا الدرس، تعلمت كيفية بناء أنماط اتصال الخدمات الصغيرة في Laravel. استكشفت الاتصال HTTP المتزامن مع عملاء الخدمة والطلبات المتوازية وقوائم انتظار الرسائل غير المتزامنة للاتصال الموثوق بين الخدمات والبنى المدفوعة بالأحداث باستخدام نظام الأحداث في Laravel و Redis Streams ونمط بوابة API لتوحيد الوصول إلى الخدمة. تمكّنك هذه الأنماط من بناء أنظمة موزعة قابلة للتوسع ومرنة يمكن أن تنمو بشكل مستقل وتتعامل مع الفشل بسلاسة.