
در خیلی از اپلیکیشنهای Laravel، بخشی از کارها نباید همزمان با پاسخ HTTP انجام شوند؛ مثل ارسال ایمیل، پردازش تصویر، همگامسازی با API خارجی یا ساخت گزارش. اگر این کارها را داخل Controller نگه دارید، کاربر منتظر میماند و احتمال Timeout بالا میرود.
Laravel برای همین سناریو، سیستم Queue و کلاسهای Job را فراهم کرده است. در این مقاله قدمبهقدم میبینیم Job چیست، چطور ساخته میشود، چطور Dispatch میشود و چه نکاتی را در Production باید رعایت کنید.
یک Job واحد کاری است که میتواند بلافاصله یا با تأخیر، در پسزمینه اجرا شود. معمولاً منطق سنگین یا وابسته به سرویس خارجی را داخل Job میگذارید و از Request فقط آن را Dispatch میکنید.
مزیت اصلی این الگو:
پاسخ سریعتر به کاربر
امکان Retry هنگام خطا
جدا کردن منطق سنگین از لایه HTTP
کنترل بهتر Rate Limit و بار سرور
قبل از شروع مطمئن شوید:
یک پروژه Laravel (نسخه ۸ به بالا پیشنهاد میشود) دارید
در .env درایور Queue را مشخص کردهاید
برای محیط واقعی، یک Worker در حال اجرا دارید
در فایل .env معمولاً یکی از این گزینهها را میبینید:
QUEUE_CONNECTION=database # یا QUEUE_CONNECTION=redis # برای تست سریع محلی: QUEUE_CONNECTION=sync
sync یعنی Job همان لحظه در همان Process اجرا میشود و برای Production مناسب نیست. برای شروع توسعه، database یا redis گزینههای رایجاند.
اگر از database استفاده میکنید، جدول jobs را بسازید:
php artisan queue:table php artisan migrate
با Artisan یک Job بسازید:
php artisan make:job SendWelcomeEmail
فایل معمولاً در مسیر app/Jobs/SendWelcomeEmail.php ساخته میشود. ساختار پایه چیزی شبیه این است:
<?php namespace App\Jobs; use App\Models\User; 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\Mail; use App\Mail\WelcomeMail; class SendWelcomeEmail implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public User $user ) {} public function handle(): void { Mail::to($this->user->email)->send(new WelcomeMail($this->user)); } }
اگر کلاس Job اینترفیس ShouldQueue را پیادهسازی کند، Laravel آن را به Queue میفرستد. بدون این اینترفیس، Job بهصورت همزمان (Synchronous) اجرا میشود.
از Controller، Listener یا Service میتوانید Job را Dispatch کنید:
use App\Jobs\SendWelcomeEmail; public function store(Request $request) { $user = User::create($request->validated()); SendWelcomeEmail::dispatch($user); return response()->json([ 'message' => 'ثبتنام انجام شد', ], 201); }
SendWelcomeEmail::dispatch($user) ->delay(now()->addMinutes(5)); SendWelcomeEmail::dispatch($user) ->onQueue('emails'); SendWelcomeEmail::dispatch($user) ->onConnection('redis');
جدا کردن صفها (مثلاً emails، reports، default) کمک میکند کارهای سنگین، کارهای سبک را بلوکه نکنند.
Job فقط وقتی اجرا میشود که Worker در حال گوشدادن به Queue باشد:
php artisan queue:work # صف مشخص: php artisan queue:work --queue=emails,default # با محدودیت تلاش مجدد: php artisan queue:work --tries=3
در Production معمولاً از Supervisor یا systemd استفاده میشود تا Worker بعد از Crash دوباره بالا بیاید و بعد از Deploy ریاستارت شود.
داخل خود Job میتوانید رفتار خطا را دقیقتر تعریف کنید:
class SendWelcomeEmail implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 3; public int $timeout = 60; public function backoff(): array { return [10, 30, 60]; } public function handle(): void { // ... } public function failed(\Throwable $exception): void { // لاگ، اعلان به تیم، یا علامتگذاری کاربر logger()->error('SendWelcomeEmail failed', [ 'user_id' => $this->user->id, 'error' => $exception->getMessage(), ]); } }
برای دیدن Jobهای شکستخورده، جدول failed jobs را بسازید:
php artisan queue:failed-table php artisan migrate php artisan queue:failed php artisan queue:retry all
<?php namespace App\Jobs; use App\Models\Report; use App\Services\ReportGenerator; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class GenerateUserReport implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 2; public int $timeout = 120; public function __construct( public Report $report ) {} public function handle(ReportGenerator $generator): void { $path = $generator->generate($this->report); $this->report->update([ 'status' => 'ready', 'file_path' => $path, ]); } }
اینجا منطق ساخت گزارش داخل Service است و Job فقط orchestration میکند؛ این جداسازی تستپذیری و خوانایی را بهتر میکند.
Job را کوچک نگه دارید: یک مسئولیت مشخص داشته باشد.
Model را با SerializesModels پاس بدهید: Laravel فقط ID را در Queue ذخیره میکند و هنگام اجرا دوباره Model را میخواند.
دادههای حساس را در Payload نگذارید: رمز عبور، توکن خام یا اطلاعات محرمانه را Serialize نکنید.
Idempotent بنویسید: اگر Job دوباره اجرا شد، نتیجه تکراری یا خراب نسازد.
صفها را جدا کنید: ایمیل، گزارش و کارهای سنگین را در Queueهای مجزا بگذارید.
لاگ و Monitoring داشته باشید: Horizon (برای Redis)، failed jobs و لاگ اپلیکیشن را جدی بگیرید.
استفاده از QUEUE_CONNECTION=sync در Production و فکر کردن که Queue دارید
فراموش کردن اجرای queue:work یا Supervisor
گذاشتن منطق خیلی سنگین داخل Controller بهجای Job
Pass کردن Objectهای غیرقابل Serialize
نداشتن استراتژی Retry و متد failed()
یک صف واحد برای همه کارها و ایجاد گلوگاه
برای نوشتن Job در Laravel این مسیر کافی است: Job بسازید، ShouldQueue را پیادهسازی کنید، منطق را در handle() بنویسید، با dispatch() ارسال کنید و Worker را اجرا نگه دارید. اگر کارهای پسزمینه را از Request جدا کنید، اپلیکیشن سریعتر، پایدارتر و مقیاسپذیرتر میشود.
Listener به Event واکنش نشان میدهد. اگر Listener اینترفیس ShouldQueue داشته باشد، خودش مثل یک Job صف میشود. Job را معمولاً مستقیم Dispatch میکنید؛ Listener را با Fire شدن Event.
وقتی چند Job مرتبط دارید و میخواهید پیشرفت، تکمیل یا شکست کل مجموعه را یکجا مدیریت کنید، Batch مناسب است.
بله. درایور database برای شروع کاملاً قابل استفاده است. برای حجم بالاتر، Redis معمولاً عملکرد بهتری میدهد.