مقاله

ساخت Job در Laravel — dispatch، queue و ShouldQueue

ساخت Job در Laravel — dispatch، queue و ShouldQueue

ایجاد Job class، dispatch، delay، chain، batch و بهترین شیوه handle — با مثال ارسال ایمیل و پردازش سفارش.

جواب کوتاه: در Laravel، dispatch یعنی «این Job را به صف بسپار» — نه «همین الان اجرا کن». اجرای واقعی کار worker است (php artisan queue:work). اگر می‌خواهید Job همین الان و در همان درخواست اجرا شود از dispatchSync استفاده کنید، برای اجرای دیرتر از delay()، و وقتی Job داخل transaction دیتابیس ساخته می‌شود از afterCommit(). متد dispatchNow که در آموزش‌های قدیمی می‌بینید از Laravel 9 حذف شده است.

برای ساخت Job در Laravel مسیر عملی این است: php artisan make:job Name → منطق در handle() → برای صف implements ShouldQueue → Name::dispatch(...) → worker با php artisan queue:work. بعد از فهم مفهوم Queue، این مقاله همان Job class را عمیق می‌کند — تمیزتر از closure برای logic قابل تست و reuse. مثال‌ها با Laravel 12 نوشته شده‌اند.

ساخت Job

php artisan make:job SendOrderConfirmation

در Laravel 11 و 12، کلاس ساخته‌شده فقط یک trait دارد: Illuminate\Foundation\Queue\Queueable. این trait همان چهار trait قدیمی — Dispatchable، InteractsWithQueue، Queueable و SerializesModels — را یک‌جا اضافه می‌کند؛ پس اگر در پروژه قدیمی‌تر چهار trait جدا می‌بینید، رفتار یکی است.

namespace App\Jobs;

use App\Models\Order;
use App\Services\MailService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class SendOrderConfirmation implements ShouldQueue
{
    use Queueable;

    public function __construct(public Order $order) {}

    public function handle(MailService $mailService): void
    {
        $mailService->sendOrderConfirmation($this->order);
    }
}

ShouldQueue دقیقاً چه کاری می‌کند؟

ShouldQueue به Laravel می‌گوید این Job باید به صف برود. اگر کلاسی این interface را نداشته باشد، dispatch() آن را همان لحظه و در همان پروسه اجرا می‌کند — یعنی ظاهر کد «صف» است ولی کاربر منتظر می‌ماند. اولین چیزی که وقتی «Job کند است و صفحه دیر لود می‌شود» باید چک کنید، همین interface است.

dispatch با worker چه فرقی دارد؟

این دو را زیاد با هم قاطی می‌کنند:

  • dispatch در کد شما (Controller، Action، Listener) اتفاق می‌افتد و فقط Job را serialize می‌کند و در صف — جدول jobs، Redis یا سرویس دیگر — می‌گذارد.
  • worker یک پروسه جداست که مدام صف را می‌خواند و handle() را اجرا می‌کند.

اگر worker روشن نباشد، Job خطا نمی‌دهد؛ فقط در صف می‌ماند. در Laravel 12 درایور پیش‌فرض QUEUE_CONNECTION=database است، پس می‌توانید Jobهای منتظر را در جدول jobs ببینید. برای اجرای محلی:

php artisan queue:work

یک تفاوت مهم دیگر: QUEUE_CONNECTION=sync کل صف پروژه را sync می‌کند (همه Jobها فوری اجرا می‌شوند) ولی dispatchSync فقط همان یک Job را. راه‌اندازی worker در سرور و Supervisor موضوع مقدمه Queue است؛ اینجا روی خود dispatch تمرکز می‌کنیم.

Dispatch

SendOrderConfirmation::dispatch($order);

// queue مشخص
SendOrderConfirmation::dispatch($order)->onQueue('emails');

// تأخیر
SendOrderConfirmation::dispatch($order)->delay(now()->addMinutes(5));

آرگومان‌های dispatch() مستقیم به constructor همان Job داده می‌شوند. معادل helper آن dispatch(new SendOrderConfirmation($order)) است. dispatch مقدار برگشتی Job را به شما نمی‌دهد — نتیجه کار را باید در دیتابیس، event یا notification ذخیره کنید.

dispatchSync — اجرای فوری بدون صف

SendOrderConfirmation::dispatchSync($order);

// معادل با helper
dispatch_sync(new SendOrderConfirmation($order));

Job همان لحظه، در همان درخواست و بدون worker اجرا می‌شود — حتی اگر ShouldQueue داشته باشد. کاربردش: اسکریپت‌های یک‌باره، Artisan command‌هایی که باید منتظر نتیجه بمانند، و جاهایی که همان logic را گاهی در صف و گاهی فوری لازم دارید. برای کار سنگین در درخواست کاربر مناسب نیست، چون پاسخ تا تمام شدن Job معطل می‌ماند.

dispatchNow کجا رفت؟

dispatchNow در Laravel 8 منسوخ و در Laravel 9 حذف شد. در Laravel 12 جایگزین آن dispatchSync است. اگر کد قدیمی را ارتقا می‌دهید، کافی است نام متد را عوض کنید.

delay — اجرای Job با تأخیر

SendReviewRequest::dispatch($order)->delay(now()->addDay());

// یا با ثانیه
SendReviewRequest::dispatch($order)->delay(600);

delay یعنی «زودتر از این زمان اجرا نشود» — نه اجرای دقیق سر ساعت. اگر worker مشغول یا خاموش باشد، Job دیرتر اجرا می‌شود. بعضی درایورها برای delay سقف دارند؛ مثلاً Amazon SQS حداکثر ۱۵ دقیقه را می‌پذیرد. برای کارهای زمان‌بندی‌شده تکراری (هر شب، هر ساعت) به‌جای delay از Scheduler و cron استفاده کنید.

afterCommit — dispatch بعد از commit شدن transaction

DB::transaction(function () use ($data) {
    $order = Order::create($data);

    SendOrderConfirmation::dispatch($order)->afterCommit();
});

مشکل رایج: Job داخل transaction dispatch می‌شود، worker زودتر از commit آن را برمی‌دارد و رکورد را در دیتابیس پیدا نمی‌کند. afterCommit() Job را تا commit شدن transaction نگه می‌دارد و اگر transaction rollback شود، اصلاً dispatch نمی‌کند. اگر transaction باز نباشد، Job فوراً dispatch می‌شود. راه‌های دیگر: گزینه 'after_commit' => true برای یک connection در config/queue.php (در Laravel 12 پیش‌فرض false است) — که در این حالت beforeCommit() برای استثنا کردن یک Job است — یا callback با DB::afterCommit(fn () => ...).

dispatchAfterResponse — بعد از ارسال پاسخ HTTP

LogPageVisit::dispatchAfterResponse($visit);

Job به صف نمی‌رود؛ بعد از اینکه پاسخ برای کاربر فرستاده شد، در همان پروسه PHP اجرا می‌شود. برای کارهای خیلی کوتاه (لاگ، ping) مناسب است و worker نمی‌خواهد. برای کار طولانی یا کاری که باید retry شود، صف واقعی انتخاب درست‌تری است.

dispatchIf و dispatchUnless

SendWelcomeSms::dispatchIf($user->phone_verified, $user);
SendWelcomeSms::dispatchUnless($user->opted_out, $user);

همان dispatch معمولی با شرط — کد را از if اضافه تمیز نگه می‌دارد.

dispatch از Artisan command یا tinker

از داخل یک Artisan command همان SendOrderConfirmation::dispatch($order) را صدا بزنید. برای تست دستی در محیط توسعه:

php artisan tinker
App\Jobs\SendOrderConfirmation::dispatch(App\Models\Order::first());

یادتان باشد بدون worker روشن، Job فقط در صف قرار می‌گیرد.

کدام روش dispatch را انتخاب کنم؟

موقعیتروش
کار کند (ایمیل، API خارجی، پردازش فایل) در درخواست کاربرdispatch() + Job با ShouldQueue
باید همین الان تمام شود و نتیجه‌اش لازم استdispatchSync()
باید چند دقیقه یا چند ساعت بعد اجرا شودdispatch()->delay(...)
Job داخل transaction ساخته می‌شودdispatch()->afterCommit()
کار خیلی کوتاه که نباید پاسخ را کند کندdispatchAfterResponse()
کار تکراری زمان‌بندی‌شدهScheduler، نه delay
چند Job که باید به ترتیب اجرا شوندBus::chain()

تنظیمات Job

public int $tries = 3;
public int $timeout = 120;
public int $backoff = 30;

یا متد backoff() برای exponential. $timeout باید از --timeout خود worker کمتر باشد، وگرنه worker زودتر Job را قطع می‌کند.

SerializesModels

Model در constructor serialize می‌شود — worker دوباره از DB می‌خواند. اگر model حذف شده باشد ModelNotFoundException — handle کنید. اگر حذف شدن model یعنی «دیگر لازم نیست»، property public $deleteWhenMissingModels = true; Job را بی‌صدا حذف می‌کند.

نتیجه عملی برای dispatch: به Job شناسه یا model بدهید، نه آرایه‌های بزرگ یا آبجکت‌های سنگین. هرچه payload کوچک‌تر، صف سبک‌تر و داده تازه‌تر.

Chain — ترتیب

Bus::chain([
    new ProcessPayment($order),
    new SendOrderConfirmation($order),
    new UpdateInventory($order),
])->dispatch();

اگر یکی از Jobهای زنجیره fail شود، بقیه اجرا نمی‌شوند — دقیقاً همان چیزی که برای «اول پرداخت، بعد ایمیل» می‌خواهید.

Batch — موازی با گزارش

Bus::batch([
    new ResizeImage($path1),
    new ResizeImage($path2),
])->dispatch();

Jobهای داخل batch باید trait Illuminate\Bus\Batchable را هم داشته باشند. جدول job_batches در migration پیش‌فرض Laravel 12 ساخته می‌شود؛ در پروژه‌های قدیمی‌تر با php artisan make:queue-batches-table اضافه‌اش کنید.

Failed و retry

راهنمای failed jobs. متد failed(Throwable $e) برای cleanup.

تست

Bus::fake();
// action
Bus::assertDispatched(SendOrderConfirmation::class);

برای روش‌های دیگر dispatch هم assertion جدا هست: Bus::assertDispatchedSync() برای dispatchSync و Bus::assertDispatchedAfterResponse() برای dispatchAfterResponse. اگر می‌خواهید queue و delay را هم بررسی کنید، Queue::fake() و Queue::assertPushedOn('emails', SendOrderConfirmation::class) دقیق‌ترند.

اشتباهات

  • logic سنگین sync در controller
  • dispatch قبل از commit — راه‌حل: afterCommit()
  • queue نام اشتباه بدون worker — Job با onQueue('emails') فقط توسط workerی اجرا می‌شود که آن queue را گوش می‌دهد: php artisan queue:work --queue=emails,default
  • model بزرگ در job property
  • فراموش کردن ShouldQueue — Job بی‌صدا sync اجرا می‌شود
  • استفاده از dispatchNow در Laravel 9 به بعد — متد وجود ندارد؛ dispatchSync
  • انتظار اجرای دقیق سر زمان از delay()
  • ری‌استارت نکردن worker بعد از deploy — worker کد قدیمی را در حافظه نگه می‌دارد؛ php artisan queue:restart

جمع‌بندی

برای بیشتر کارها dispatch() روی یک Job با ShouldQueue کافی است؛ dispatchSync وقتی نتیجه را همین الان لازم دارید، delay برای بعداً، و afterCommit هر جا transaction در کار است. Job + Service = الگوی تمیز — اگر logic را در Action یا Service نگه دارید، همان کد را هم از Controller و هم از Job صدا می‌زنید (Service یا Action). اگر کاری باید در واکنش به یک رویداد اجرا شود، Event و Listener را هم ببینید. Redis: Horizon. معماری: اصول Laravel.

اتوماسیون workflow

سوالات متداول

make:job → handle() → ShouldQueue → dispatch() → queue:work. بدون ShouldQueue، dispatch همان درخواست را sync اجرا می‌کند.

Job برای task مشخص؛ Listener برای واکنش به event — Listener هم می‌تواند queued باشد.

dispatch(function () { ... }) ممکن اما class بهتر برای تست.

چند connection redis/database در config/queue.

ShouldBeUnique — جلوگیری duplicate همزمان.

worker --timeout باید بیشتر از job timeout باشد.

ممکن اما fragile — class ترجیح.

dispatchSync فوری بدون queue.

چند queue name — workers جدا per queue.
اشتراک‌گذاری