زیرساخت دستور: راهنمای پیادهسازی
نمای کلی
زیرساخت دستور یک روش استاندارد برای ماژولها فراهم میکند تا عملیات را فعال کنند یا وضعیت را تغییر دهند در ماژولهای دیگر بدون ایجاد وابستگی شدید. برخلاف پرسوجوها (که داده میخوانند)، دستورها از معماری رویداد-محور برای حفظ جداسازی سست استفاده میکنند.
اصل اصلی
معماری رویداد-محور: ماژولها با انتشار رویدادها به جای فراخوانی مستقیم سرویسهای یکدیگر ارتباط برقرار میکنند. این یک الگوی انتشار-اشتراک ایجاد میکند که در آن ناشران نمیدانند چه کسی (اگر کسی باشد) گوش میدهد.
تفکیک دستور و پرسوجو (CQS)
سیستم به شدت جدا میکند:
- پرسوجوها: خواندن داده، برگرداندن DTOهای پیچیده، همزمان
- دستورها/رویدادها: تغییر وضعیت، برگرداندن داده حداقلی (void/ID/boolean)، ناهمزمان
اجزای معماری
1. رویدادهای کسبوکار
رویدادها نمایانگر وقایع معنادار کسبوکار هستند که ماژولهای دیگر ممکن است به آنها اهمیت دهند.
- تغییرناپذیر (همه ویژگیها readonly)
- شامل داده حداقلی (IDها، نه موجودیتهای کامل)
- نامگذاری در زمان گذشته (OrderCompleted، UserRegistered)
- پیادهسازی رابط نشانگر برای ایمنی نوع
2. شنوندگان رویداد
شنوندگان به رویدادها واکنش نشان میدهند و کار را به سرویسها تفویض میکنند.
- پیادهسازی
ShouldQueueبرای پردازش ناهمزمان - هماهنگکنندههای نازک (تفویض به سرویسها)
- بدون منطق کسبوکار پیچیده
- مدیریت یک نوع رویداد
3. گذرگاه رویداد
سیستم رویداد Laravel به عنوان گذرگاه پیام عمل میکند.
مسئولیتها:
- مسیریابی رویدادها به شنوندگان ثبتشده
- مدیریت صفبندی برای شنوندگان ناهمزمان
- فراهم کردن قلابهای چرخه حیات رویداد
ارتباط رویداد-محور
چرا رویدادها به جای فراخوانی مستقیم؟
- ❌ فراخوانی مستقیم سرویس (وابستگی شدید)
- ✅ رویداد-محور (جداسازی سست)
class OrderService
{
public function __construct(
private InvoiceService $invoiceService, // وابستگی مستقیم
private NotificationService $notificationService, // وابستگی مستقیم
private AnalyticsService $analyticsService, // وابستگی مستقیم
) {}
public function completeOrder(int $orderId): void
{
// منطق تکمیل سفارش
$order = $this->orderRepository->find($orderId);
$order->status = 'completed';
$order->save();
// وابستگی شدید به ماژولهای دیگر
$this->invoiceService->createInvoice($orderId);
$this->notificationService->sendOrderEmail($orderId);
$this->analyticsService->trackOrderCompletion($orderId);
}
}
- OrderService از همه ماژولهای وابسته اطلاع دارد
- افزودن قابلیت جدید نیاز به تغییر OrderService دارد
- نمیتوان OrderService را بدون همه وابستگیها تست کرد
- اصل باز/بسته را نقض میکند
class OrderService
{
// هیچ وابستگی به ماژولهای دیگر!
public function completeOrder(int $orderId): void
{
// منطق تکمیل سفارش
$order = $this->orderRepository->find($orderId);
$order->status = 'completed';
$order->save();
// ارسال رویداد - اهمیتی ندارد چه کسی گوش میدهد
event(new OrderCompletedEvent(
orderId: $orderId,
userId: $order->user_id,
totalAmount: $order->total_amount,
));
}
}
- OrderService هیچ وابستگی به ماژولهای دیگر ندارد
- شنوندگان جدید میتوانند بدون تغییر OrderService اضافه شوند
- تست OrderService به صورت جداگانه آسان است
- از اصل باز/بسته پیروی میکند
جریان رویداد
پیادهسازی گام به گام
گام 1: ایجاد رویداد کسبوکار
<?php
declare(strict_types=1);
namespace App\Events;
use App\Contracts\Event;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
final class OrderCompletedEvent implements Event
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
public function __construct(
public readonly int $orderId,
public readonly int $userId,
public readonly int $totalAmount,
public readonly string $orderNumber,
) {
}
}
- پیادهسازی رابط نشانگر
Event - همه ویژگیها
public readonly(تغییرناپذیر) - استفاده از traitهای رویداد Laravel
- فقط شامل دادههای ضروری (IDها و انواع ابتدایی)
- نامگذاری در زمان گذشته (توصیف آنچه اتفاق افتاده)
- استفاده از
finalبرای جلوگیری از وراثت
گام 2: ارسال رویداد از سرویس
<?php
declare(strict_types=1);
namespace App\Modules\Orders\Services;
use App\Events\OrderCompletedEvent;
use App\Modules\Orders\Repositories\OrderRepository;
final class OrderService
{
public function __construct(
private readonly OrderRepository $repository
) {
}
public function completeOrder(int $orderId): bool
{
try {
$order = $this->repository->findOrFail($orderId);
// انجام عملیات کسبوکار
$order->status = 'completed';
$order->completed_at = now();
$order->save();
// highlight-start
// ارسال رویداد بعد از عملیات موفق
event(new OrderCompletedEvent(
orderId: $order->id,
userId: $order->user_id,
totalAmount: $order->total_amount,
orderNumber: $order->order_number,
));
// highlight-end
return true;
} catch (\Exception $e) {
// ثبت خطا
logger()->error('Failed to complete order', [
'order_id' => $orderId,
'exception' => $e->getMessage(),
]);
return false;
}
}
}
- رویداد را بعد از عملیات موفق ارسال کنید
- فقط دادههای ضروری را در رویداد قرار دهید
- رویدادها را در تراکنشها ارسال نکنید (بعد از commit ارسال کنید)
- از تابع کمکی
event()استفاده کنید
گام 3: ایجاد شنونده رویداد
<?php
declare(strict_types=1);
namespace App\Modules\Invoicing\Listeners;
use App\Events\OrderCompletedEvent;
use App\Modules\Invoicing\Services\InvoiceService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
final class CreateInvoiceOnOrderCompletion implements ShouldQueue
{
use InteractsWithQueue;
/**
* تعداد دفعات تلاش برای اجرای job
*/
public int $tries = 3;
/**
* تعداد ثانیههای انتظار قبل از تلاش مجدد
*/
public int $backoff = 60;
public function __construct(
private readonly InvoiceService $invoiceService
) {
}
public function handle(OrderCompletedEvent $event): void
{
try {
// تفویض به لایه سرویس
$this->invoiceService->createInvoiceForOrder(
orderId: $event->orderId,
userId: $event->userId,
totalAmount: $event->totalAmount,
);
logger()->info('Invoice created for order', [
'order_id' => $event->orderId,
'order_number' => $event->orderNumber,
]);
} catch (\Exception $e) {
logger()->error('Failed to create invoice', [
'order_id' => $event->orderId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// پرتاب مجدد برای فعالسازی مکانیزم تلاش مجدد
throw $e;
}
}
/**
* مدیریت شکست job
*/
public function failed(OrderCompletedEvent $event, \Throwable $exception): void
{
logger()->critical('Invoice creation failed after all retries', [
'order_id' => $event->orderId,
'exception' => $exception->getMessage(),
]);
// میتوان ادمین را مطلع کرد، هشدار ایجاد کرد و غیره
}
}
- پیادهسازی
ShouldQueueبرای پردازش ناهمزمان - تفویض به لایه سرویس (هماهنگکننده نازک)
- پیکربندی منطق تلاش مجدد (
$tries،$backoff) - لاگگذاری جامع خطاها
- پیادهسازی متد
failed()برای مدیریت شکست نهایی - استفاده از تزریق وابستگی برای سرویسها
گام 4: ثبت شنونده
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Events\OrderCompletedEvent;
use App\Modules\Invoicing\Listeners\CreateInvoiceOnOrderCompletion;
use App\Modules\Notifications\Listeners\SendOrderCompletionEmail;
use App\Modules\Analytics\Listeners\TrackOrderCompletion;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* نگاشت شنونده رویداد برای برنامه
*/
protected $listen = [
OrderCompletedEvent::class => [
CreateInvoiceOnOrderCompletion::class,
SendOrderCompletionEmail::class,
TrackOrderCompletion::class,
],
// سایر نگاشتهای رویداد...
];
/**
* ثبت هر رویدادی برای برنامه
*/
public function boot(): void
{
//
}
}
- نگاشت رویدادها به شنوندگان در آرایه
$listen - چندین شنونده میتوانند به یک رویداد مشترک شوند
- ترتیب شنوندگان در آرایه ترتیب اجرا را تضمین نمیکند (ناهمزمان)
- میتوان از کشف خودکار رویداد به جای ثبت دستی استفاده کرد
گام 5: ایجاد لایه سرویس
<?php
declare(strict_types=1);
namespace App\Modules\Invoicing\Services;
use App\Domains\Order\OrderQuery;
use App\Domains\Order\OrderId;
use App\Domains\User\UserQuery;
use App\Domains\User\UserId;
use App\Modules\Invoicing\Repositories\InvoiceRepository;
use Illuminate\Support\Facades\DB;
final class InvoiceService
{
public function __construct(
private readonly InvoiceRepository $repository,
private readonly OrderQuery $orderQuery,
private readonly UserQuery $userQuery,
) {
}
public function createInvoiceForOrder(
int $orderId,
int $userId,
int $totalAmount,
): int {
// دریافت داده اضافی با استفاده از پرسوجوها
$order = $this->orderQuery->getById(new OrderId($orderId));
$user = $this->userQuery->getById(new UserId($userId));
// ایجاد فاکتور در تراکنش
return DB::transaction(function () use ($order, $user, $totalAmount) {
$invoice = $this->repository->create([
'order_id' => $order->id->value,
'user_id' => $user->id->value,
'order_number' => $order->orderNumber,
'customer_name' => $user->fullName,
'total_amount' => $totalAmount,
'status' => 'pending',
'created_at' => now(),
]);
return $invoice->id;
});
}
}
- استفاده از رابطهای پرسوجو برای دریافت داده اضافی
- شامل منطق کسبوکار واقعی
- استفاده از تراکنشها برای یکپارچگی داده
- برگرداندن داده ساده (ID، boolean و غیره)
- بدون آگاهی از رویدادها
الگوهای استفاده
- رویداد ساده (بدون برگشت)
- رویداد با برگشت ساده
- شنوندگان متعدد
- ارسال شرطی رویداد
بهترین برای: عملیات fire-and-forget که نیازی به تأیید ندارید.
class UserService
{
public function registerUser(array $data): int
{
$user = $this->repository->create($data);
// ارسال رویداد - منتظر نتیجه نمانید
event(new UserRegisteredEvent(
userId: $user->id,
email: $user->email,
));
return $user->id;
}
}
بهترین برای: زمانی که نیاز به تأیید دارید اما نه داده پیچیده.
class PaymentService
{
public function processPayment(int $orderId, int $amount): bool
{
try {
// پردازش پرداخت
$payment = $this->gateway->charge($amount);
// ارسال رویداد
event(new PaymentProcessedEvent(
orderId: $orderId,
paymentId: $payment->id,
amount: $amount,
));
return true; // برگشت boolean ساده
} catch (\Exception $e) {
return false;
}
}
}
بهترین برای: زمانی که چندین ماژول نیاز به واکنش به یک رویداد کسبوکار دارند.
// رویداد
class OrderCompletedEvent implements Event
{
public function __construct(
public readonly int $orderId,
public readonly int $userId,
public readonly int $totalAmount,
) {}
}
// شنونده 1: ایجاد فاکتور
class CreateInvoiceOnOrderCompletion implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
$this->invoiceService->createInvoice($event->orderId);
}
}
// شنونده 2: ارسال ایمیل
class SendOrderCompletionEmail implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
$this->emailService->sendOrderEmail($event->orderId);
}
}
// شنونده 3: بهروزرسانی تحلیل
class TrackOrderCompletion implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
$this->analyticsService->track($event->orderId);
}
}
// ثبت
protected $listen = [
OrderCompletedEvent::class => [
CreateInvoiceOnOrderCompletion::class,
SendOrderCompletionEmail::class,
TrackOrderCompletion::class,
],
];
بهترین برای: زمانی که رویدادها باید فقط تحت شرایط خاصی ارسال شوند.
class OrderService
{
public function updateOrderStatus(int $orderId, string $status): bool
{
$order = $this->repository->findOrFail($orderId);
$oldStatus = $order->status;
$order->status = $status;
$order->save();
// ارسال رویداد فقط هنگام انتقال به تکمیلشده
if ($oldStatus !== 'completed' && $status === 'completed') {
event(new OrderCompletedEvent(
orderId: $order->id,
userId: $order->user_id,
totalAmount: $order->total_amount,
));
}
return true;
}
}
بهترین شیوهها
1. نامگذاری رویداد
- ✅ انجام دهید
- ❌ انجام ندهید
// زمان گذشته - توصیف آنچه اتفاق افتاده
class OrderCompletedEvent implements Event {}
class UserRegisteredEvent implements Event {}
class PaymentProcessedEvent implements Event {}
class InvoiceGeneratedEvent implements Event {}
// زمان حال یا امری
class CompleteOrderEvent implements Event {}
class RegisterUserEvent implements Event {}
class ProcessPayment implements Event {}
2. داده رویداد
- ✅ انجام دهید
- ❌ انجام ندهید
class OrderCompletedEvent implements Event
{
public function __construct(
public readonly int $orderId, // IDها
public readonly int $userId, // IDها
public readonly int $totalAmount, // انواع ابتدایی
public readonly string $orderNumber, // انواع ابتدایی
) {}
}
class OrderCompletedEvent implements Event
{
public function __construct(
public Order $order, // موجودیتها
public User $user, // موجودیتها
public Collection $items, // اشیاء پیچیده
) {}
}
3. مسئولیتهای شنونده
- ✅ انجام دهید
- ❌ انجام ندهید
class CreateInvoiceListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
// هماهنگکننده نازک - تفویض به سرویس
$this->invoiceService->createInvoice($event->orderId);
}
}
class CreateInvoiceListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
// منطق کسبوکار پیچیده در شنونده
$order = Order::find($event->orderId);
$user = User::find($event->userId);
$invoice = new Invoice();
$invoice->order_id = $order->id;
$invoice->user_id = $user->id;
// ... 50 خط دیگر منطق کسبوکار
$invoice->save();
}
}
4. مدیریت خطا
- ✅ انجام دهید
- ❌ انجام ندهید
class ProcessPaymentListener implements ShouldQueue
{
public int $tries = 3;
public int $backoff = 60;
public function handle(OrderCompletedEvent $event): void
{
try {
$this->paymentService->process($event->orderId);
} catch (\Exception $e) {
logger()->error('Payment processing failed', [
'order_id' => $event->orderId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e; // پرتاب مجدد برای تلاش مجدد
}
}
public function failed(OrderCompletedEvent $event, \Throwable $exception): void
{
// مدیریت شکست نهایی
logger()->critical('Payment failed after all retries', [
'order_id' => $event->orderId,
]);
}
}
class ProcessPaymentListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
try {
$this->paymentService->process($event->orderId);
} catch (\Exception $e) {
// خاموش کردن استثنا به صورت ساکت
// بدون لاگ، بدون تلاش مجدد، بدون اعلان
}
}
}
5. همزمان در مقابل ناهمزمان
ناهمزمان (توصیه میشود):
class SendEmailListener implements ShouldQueue // ناهمزمان
{
public function handle(UserRegisteredEvent $event): void
{
$this->emailService->sendWelcomeEmail($event->userId);
}
}
از ناهمزمان استفاده کنید وقتی:
- ارسال ایمیل
- فراخوانی APIهای خارجی
- پردازش سنگین
- عملیات غیرحیاتی
- اکثر موارد استفاده
همزمان (با احتیاط استفاده کنید):
class UpdateCacheListener // همزمان - بدون ShouldQueue
{
public function handle(ProductUpdatedEvent $event): void
{
// باید فوراً اتفاق بیفتد
Cache::forget("product:{$event->productId}");
}
}
از همزمان استفاده کنید وقتی:
- ابطال کش
- سازگاری داده حیاتی
- باید قبل از پاسخ کامل شود
- عملیات بسیار سریع (<100ms)
6. دانهبندی رویداد
- ✅ انجام دهید - رویدادهای خاص
- ❌ انجام ندهید - رویدادهای عمومی
class OrderCompletedEvent implements Event {}
class OrderCancelledEvent implements Event {}
class OrderRefundedEvent implements Event {}
چرا: رویدادهای خاص درک و نگهداری آسانتری دارند.
class OrderStatusChangedEvent implements Event
{
public function __construct(
public readonly int $orderId,
public readonly string $oldStatus,
public readonly string $newStatus,
) {}
}
چرا: رویدادهای عمومی نیاز به منطق شرطی در شنوندگان دارند.
استراتژیهای تست
تست واحد شنوندگان
<?php
namespace Tests\Unit\Modules\Invoicing\Listeners;
use App\Events\OrderCompletedEvent;
use App\Modules\Invoicing\Listeners\CreateInvoiceOnOrderCompletion;
use App\Modules\Invoicing\Services\InvoiceService;
use Mockery;
use Tests\TestCase;
final class CreateInvoiceOnOrderCompletionTest extends TestCase
{
public function test_creates_invoice_when_order_completed(): void
{
// آمادهسازی
$event = new OrderCompletedEvent(
orderId: 1,
userId: 10,
totalAmount: 10000,
orderNumber: 'ORD-001',
);
$invoiceService = Mockery::mock(InvoiceService::class);
$invoiceService->shouldReceive('createInvoiceForOrder')
->once()
->with(1, 10, 10000)
->andReturn(100); // شناسه فاکتور
$listener = new CreateInvoiceOnOrderCompletion($invoiceService);
// اجرا
$listener->handle($event);
// بررسی - تأیید شده توسط انتظارات Mockery
}
public function test_logs_error_on_failure(): void
{
// آمادهسازی
$event = new OrderCompletedEvent(
orderId: 1,
userId: 10,
totalAmount: 10000,
orderNumber: 'ORD-001',
);
$invoiceService = Mockery::mock(InvoiceService::class);
$invoiceService->shouldReceive('createInvoiceForOrder')
->once()
->andThrow(new \Exception('Database error'));
$listener = new CreateInvoiceOnOrderCompletion($invoiceService);
// اجرا و بررسی
$this->expectException(\Exception::class);
$listener->handle($event);
}
}
تست یکپارچگی رویدادها
<?php
namespace Tests\Integration\Events;
use App\Events\OrderCompletedEvent;
use App\Modules\Invoicing\Listeners\CreateInvoiceOnOrderCompletion;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
final class OrderCompletedEventTest extends TestCase
{
use RefreshDatabase;
public function test_event_dispatches_to_listeners(): void
{
// جعلی کردن رویدادها برای گرفتن آنها
Event::fake([OrderCompletedEvent::class]);
// فعالسازی رویداد
event(new OrderCompletedEvent(
orderId: 1,
userId: 10,
totalAmount: 10000,
orderNumber: 'ORD-001',
));
// بررسی ارسال رویداد
Event::assertDispatched(OrderCompletedEvent::class, function ($event) {
return $event->orderId === 1
&& $event->userId === 10
&& $event->totalAmount === 10000;
});
}
public function test_listener_is_queued(): void
{
// جعلی کردن صف
Queue::fake();
// ارسال رویداد
event(new OrderCompletedEvent(
orderId: 1,
userId: 10,
totalAmount: 10000,
orderNumber: 'ORD-001',
));
// بررسی صفبندی شنونده
Queue::assertPushed(CreateInvoiceOnOrderCompletion::class);
}
}
الگوهای رایج و مثالها
الگو: هماهنگی Saga/گردش کار
مورد استفاده: فرآیند چندمرحلهای در سراسر ماژولها.
مشاهده کد پیادهسازی
// مرحله 1: سفارش تکمیل شد
class OrderService
{
public function completeOrder(int $orderId): void
{
$order = $this->repository->findOrFail($orderId);
$order->status = 'completed';
$order->save();
event(new OrderCompletedEvent($orderId));
}
}
// مرحله 2: فاکتور ایجاد شد
class CreateInvoiceListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
$invoiceId = $this->invoiceService->create($event->orderId);
event(new InvoiceCreatedEvent($invoiceId, $event->orderId));
}
}
// مرحله 3: پرداخت پردازش شد
class ProcessPaymentListener implements ShouldQueue
{
public function handle(InvoiceCreatedEvent $event): void
{
$paymentId = $this->paymentService->process($event->invoiceId);
event(new PaymentProcessedEvent($paymentId, $event->invoiceId));
}
}
// مرحله 4: ارسال تأیید
class SendConfirmationListener implements ShouldQueue
{
public function handle(PaymentProcessedEvent $event): void
{
$this->emailService->sendConfirmation($event->paymentId);
}
}
الگو: تراکنشهای جبرانی
مورد استفاده: بازگشت در صورت شکست.
class PaymentProcessedEvent implements Event
{
public function __construct(
public readonly int $paymentId,
public readonly int $orderId,
public readonly bool $success,
) {}
}
class RollbackOrderOnPaymentFailure implements ShouldQueue
{
public function handle(PaymentProcessedEvent $event): void
{
if (!$event->success) {
// تراکنش جبرانی
$this->orderService->revertToPending($event->orderId);
logger()->warning('Order reverted due to payment failure', [
'order_id' => $event->orderId,
'payment_id' => $event->paymentId,
]);
}
}
}
الگو: نسخهبندی رویداد
مورد استفاده: حفظ سازگاری با نسخههای قبلی.
// رویداد V1
class OrderCompletedEventV1 implements Event
{
public function __construct(
public readonly int $orderId,
public readonly int $totalAmount,
) {}
}
// رویداد V2 (با داده اضافی)
class OrderCompletedEventV2 implements Event
{
public function __construct(
public readonly int $orderId,
public readonly int $userId,
public readonly int $totalAmount,
public readonly string $orderNumber,
) {}
}
// شنونده آداپتور
class OrderEventAdapter implements ShouldQueue
{
public function handle(OrderCompletedEventV1 $event): void
{
// تبدیل V1 به V2
$order = $this->orderQuery->getById(new OrderId($event->orderId));
event(new OrderCompletedEventV2(
orderId: $event->orderId,
userId: $order->userId->value,
totalAmount: $event->totalAmount,
orderNumber: $order->orderNumber,
));
}
}
عیبیابی
مشکل: شنونده اجرا نمیشود
- رویداد ارسال میشود اما شنونده اجرا نمیشود
- هیچ خطایی در لاگها نیست
راهحلها
1. بررسی ثبت:
protected $listen = [
OrderCompletedEvent::class => [
CreateInvoiceListener::class, // اطمینان از ثبت
],
];
2. پاک کردن کش رویداد:
php artisan event:clear
php artisan cache:clear
php artisan config:clear
3. بررسی Queue Worker:
# اگر شنونده ShouldQueue را پیادهسازی میکند
php artisan queue:work
# بررسی jobهای شکستخورده
php artisan queue:failed
مشکل: رویداد چندین بار ارسال میشود
- شنونده چندین بار برای یک رویداد اجرا میشود
- فاکتورها/ایمیلهای تکراری ایجاد میشوند
راهحلها
1. بررسی ارسالهای متعدد:
- ❌ اشتباه
- ✅ صحیح
// چندین بار ارسال میشود
public function completeOrder(int $orderId): void
{
$order = $this->repository->find($orderId);
event(new OrderCompletedEvent($orderId)); // اول
$order->status = 'completed';
$order->save();
event(new OrderCompletedEvent($orderId)); // دوم - تکراری!
}
// یک بار ارسال
public function completeOrder(int $orderId): void
{
$order = $this->repository->find($orderId);
$order->status = 'completed';
$order->save();
event(new OrderCompletedEvent($orderId)); // یک بار
}
2. پیادهسازی Idempotency:
class CreateInvoiceListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
// بررسی اینکه آیا فاکتور از قبل وجود دارد
if ($this->invoiceRepository->existsForOrder($event->orderId)) {
logger()->info('Invoice already exists, skipping', [
'order_id' => $event->orderId,
]);
return;
}
$this->invoiceService->create($event->orderId);
}
}
مشکل: مشکلات عملکرد
- پردازش کند رویداد
- رشد صف انتظار
- استفاده بالای حافظه
راهحلها
1. بهینهسازی شنونده:
- ❌ کند
- ✅ سریع
// بارگذاری همه دادهها
class ProcessOrderListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
$order = Order::with('items', 'user', 'payments')->find($event->orderId);
// پردازش...
}
}
// بارگذاری فقط دادههای مورد نیاز
class ProcessOrderListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
// رویداد از قبل دادههای مورد نیاز را دارد
$this->service->process(
$event->orderId,
$event->totalAmount
);
}
}
2. استفاده از Chunking برای عملیات دستهای:
class ProcessBulkOrdersListener implements ShouldQueue
{
public function handle(BulkOrdersCompletedEvent $event): void
{
// پردازش در قطعات
collect($event->orderIds)
->chunk(100)
->each(function ($chunk) {
$this->service->processBatch($chunk->toArray());
});
}
}
3. افزایش Queue Workerها:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
numprocs=8 # افزایش تعداد workerها
نتیجهگیری
زیرساخت دستور/رویداد یک روش قوی برای فعالسازی عملیات در مرزهای ماژول در عین حفظ جداسازی سست فراهم میکند. با پیروی از این راهنمای پیادهسازی و بهترین شیوهها، میتوانید:
- ✅ به استقلال واقعی ماژول دست یابید
- ✅ پردازش ناهمزمان را فعال کنید
- ✅ از گردش کارهای پیچیده پشتیبانی کنید
- ✅ قابلیت مشاهده سیستم را حفظ کنید
- ✅ با صفها به صورت افقی مقیاسبندی کنید
نکات کلیدی
- از رویدادها برای عملیات نوشتن استفاده کنید: دستورها/رویدادها وضعیت را تغییر میدهند، پرسوجوها داده میخوانند
- رویدادها را ساده نگه دارید: فقط IDها و انواع ابتدایی، نه موجودیتها
- شنوندگان هماهنگکننده هستند: به سرویسها تفویض کنید، منطق کسبوکار نداشته باشید
- به صورت پیشفرض ناهمزمان: از
ShouldQueueاستفاده کنید مگر دلیل خاصی داشته باشید - شکستها را مدیریت کنید: منطق تلاش مجدد و متدهای
failed()را پیادهسازی کنید - همه چیز را لاگ کنید: لاگگذاری جامع برای اشکالزدایی و مانیتورینگ
- به طور کامل تست کنید: تست واحد شنوندگان، تست یکپارچگی جریان رویداد
رویدادها نمایانگر آنچه اتفاق افتاده است، نه آنچه باید اتفاق بیفتد.