О насБлогКонтакты
Backend разработка18 апреля 2016 г. 4 мин 138Обновлено: 18 мая 2026 г.

Как настроить фоновые задачи Laravel Queue для интернет-магазина (2016, Казахстан)

AunimedaAunimeda
📋 Содержание

Как настроить фоновые задачи Laravel Queue для интернет-магазина (2016, Казахстан)

Коротко: QUEUE_DRIVER=redis в .env, создайте Job через php artisan make:job SendOrderNotification, dispatch через dispatch(new SendOrderNotification($order)), запустите worker php artisan queue:work. Для production - Supervisor управляет workers. Проблемы: Mobizon SMS API может зависнуть - добавьте tries=3 и timeout=30.


Что выносить в очередь (казахстанский контекст)

✓ SMS через Mobizon / SMSC.kz - 500-2000ms
✓ Email (SMTP через KazPost, mail.kz) - 1000-5000ms
✓ Генерация PDF чека - 800-3000ms
✓ Отправка данных в 1C (если интеграция) - нестабильно
✓ Push-уведомления FCM - 200-500ms (можно и синхронно)
✓ Обновление поисковых индексов - 500ms+
✗ НЕ выносить: проверка наличия товара (нужен мгновенный ответ)
✗ НЕ выносить: расчёт стоимости корзины

.env конфигурация

QUEUE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=null

# Несколько очередей с приоритетами
# high: SMS уведомления курьеру (срочно)
# default: email клиентам
# low: обновление индексов поиска

Создание Job

<?php
// app/Jobs/SendOrderSms.php
// php artisan make:job SendOrderSms

namespace App\Jobs;

use App\Models\Order;
use App\Services\SmsService;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendOrderSms implements ShouldQueue {
    use InteractsWithQueue, Queueable, SerializesModels;

    protected Order $order;

    // Настройки повторных попыток
    public int $tries   = 3;     // 3 попытки при ошибке
    public int $timeout = 30;    // Таймаут 30 секунд (Mobizon может тормозить)

    public function __construct(Order $order) {
        $this->order = $order;
        $this->queue = 'high';  // Высокоприоритетная очередь для SMS
    }

    public function handle(SmsService $sms): void {
        $phone = $this->normalizeKazakhPhone($this->order->customer_phone);

        $text = "Заказ #{$this->order->id} подтверждён. "
              . "Сумма: " . number_format($this->order->total, 0, '.', ' ') . " тг. "
              . "Доставка " . $this->order->delivery_date . ". "
              . "myshop.kz";

        $sms->send($phone, $text);
    }

    public function failed(\Exception $exception): void {
        // Вызывается после исчерпания попыток
        \Log::error("SMS failed for order #{$this->order->id}: " . $exception->getMessage());

        // Уведомить менеджера что SMS не дошёл
        \Mail::to('manager@myshop.kz')->send(new SmsFailedNotification($this->order));
    }

    private function normalizeKazakhPhone(string $phone): string {
        $phone = preg_replace('/[^0-9]/', '', $phone);
        if (strlen($phone) === 10 && $phone[0] === '7') {
            return '7' . $phone;  // 7xxxxxxxxx → 77xxxxxxxxx
        }
        if (strlen($phone) === 10) {
            return '7' . $phone;  // 0xxxxxxxxx → 70xxxxxxxxx
        }
        return $phone;
    }
}

Несколько типов Jobs

<?php
// app/Jobs/GenerateOrderPdf.php

class GenerateOrderPdf implements ShouldQueue {
    use InteractsWithQueue, Queueable, SerializesModels;

    public int $tries   = 2;
    public int $timeout = 60;  // PDF генерация тяжелее SMS

    public function __construct(
        protected Order $order,
        protected string $email
    ) {
        $this->queue = 'default';
    }

    public function handle(): void {
        // Генерировать PDF чек
        $pdf = PDF::loadView('orders.receipt', ['order' => $this->order])
                  ->setPaper('a4');

        $pdfPath = storage_path("app/receipts/order_{$this->order->id}.pdf");
        $pdf->save($pdfPath);

        // Отправить email с PDF
        \Mail::to($this->email)
            ->send(new OrderReceiptMail($this->order, $pdfPath));
    }
}

// app/Jobs/UpdateSearchIndex.php

class UpdateSearchIndex implements ShouldQueue {
    use InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(protected int $productId) {
        $this->queue = 'low';  // Низкий приоритет
    }

    public function handle(SearchService $search): void {
        $product = Product::find($this->productId);
        if ($product) {
            $search->indexProduct($product);
        }
    }
}

Dispatch Jobs из контроллера

<?php
// OrderController.php

public function store(CreateOrderRequest $request): JsonResponse {
    $order = Order::create([
        'user_id'     => auth()->id(),
        'total'       => $request->total,
        'status'      => 'confirmed',
        'customer_name'  => $request->name,
        'customer_phone' => $request->phone,
        // ...
    ]);

    // Диспатч в разные очереди
    dispatch(new SendOrderSms($order));           // high queue, срочно
    dispatch(new GenerateOrderPdf($order, auth()->user()->email));  // default queue
    dispatch(new UpdateSearchIndex($order->product_id));  // low queue, не срочно

    // Ответ пользователю: мгновенный, не ждём SMS/email
    return response()->json([
        'success'  => true,
        'order_id' => $order->id,
        'message'  => 'Заказ принят. SMS придёт в течение минуты.',
    ], 201);
}

Supervisor: запуск workers

; /etc/supervisor/conf.d/laravel-workers.conf

; Высокоприоритетные задачи: 2 worker
[program:laravel-high]
command=php /var/www/myshop/artisan queue:work redis --queue=high --sleep=3 --tries=3 --timeout=30
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/supervisor/laravel-high.log

; Обычные задачи: 2 worker
[program:laravel-default]
command=php /var/www/myshop/artisan queue:work redis --queue=default,high --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=2
stdout_logfile=/var/log/supervisor/laravel-default.log

; Низкоприоритетные: 1 worker
[program:laravel-low]
command=php /var/www/myshop/artisan queue:work redis --queue=low,default,high --sleep=5 --tries=2
autostart=true
autorestart=true
user=www-data
numprocs=1
stdout_logfile=/var/log/supervisor/laravel-low.log
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status

# После деплоя: перезапустить workers
php artisan queue:restart
# Workers прочитают сигнал из Redis и перезапустятся после текущей задачи

Failed Jobs: мониторинг ошибок

<?php
// Миграция для таблицы failed_jobs:
// php artisan queue:failed-table && php artisan migrate

// Просмотр упавших задач:
// php artisan queue:failed

// Повторить конкретную задачу:
// php artisan queue:retry 5

// Повторить все упавшие:
// php artisan queue:retry all
// Cron: ежечасно проверять failed jobs и алертить
// app/Console/Commands/CheckFailedJobs.php

public function handle(): void {
    $count = DB::table('failed_jobs')
        ->where('failed_at', '>=', now()->subHour())
        ->count();

    if ($count > 5) {
        \Mail::to('ops@myshop.kz')
            ->send(new FailedJobsAlert($count));
    }
}

Результаты после внедрения Queue

Операция До После
Ответ на создание заказа 4.2 с 0.18 с
SMS через Mobizon синхронно фоново
Email с PDF чеком синхронно фоново, ~30 сек
Конверсия чекаута 68% 79% (+11%)

Пользователи, которые раньше ждали 4+ секунд перед страницей «Заказ принят», теперь попадали туда за 200ms. Это и дало рост конверсии.

Читайте также

MySQL и казахский текст в 2012: коллации, полнотекстовый поиск и подводные камни UTF-8aunimeda
Backend разработка

MySQL и казахский текст в 2012: коллации, полнотекстовый поиск и подводные камни UTF-8

Казахский алфавит содержит символы, которые MySQL обрабатывал непредсказуемо в 2012 году: ә, ғ, қ, ң, ө, ұ, ү, h с точкой. Сортировка была неверной, полнотекстовый поиск не находил нужное, а collation utf8_general_ci давал одинаковый результат для разных казахских букв. Вот как мы это решили.

Event-Driven Architecture в Node.js: Outbox Pattern, Kafka и гарантии доставкиaunimeda
Backend разработка

Event-Driven Architecture в Node.js: Outbox Pattern, Kafka и гарантии доставки

Практическое руководство по надёжной event-driven архитектуре в Node.js: Outbox Pattern с PostgreSQL, Kafka с идемпотентностью, Saga для распределённых транзакций - с полным рабочим кодом.

tRPC + Zod: типобезопасный fullstack без кодогенерации - практическое руководствоaunimeda
Backend разработка

tRPC + Zod: типобезопасный fullstack без кодогенерации - практическое руководство

Полное практическое руководство по tRPC v11 + Zod + Next.js: роутеры, процедуры, middleware авторизации, обработка ошибок и React Query интеграция на реальном CRUD примере.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Казахстане. Бесплатная консультация.

Разработка Telegram-ботов

Получить консультацию Все статьи