Перейти к содержанию

Очередь и задача подготовки квитанции

Оглавление серии «Laravel 13 и PHP: портал «Заявка»»

Закрытие заявки уже атомарно записывает статус и историю. Подготовка квитанции может занять отдельную работу с файлом, поэтому вынесем её в очередь. Результат урока — job с ограниченными повторами, чтением заявки по id и отложенной отправкой после фиксации транзакции. HTTP-ответ сообщает о закрытии, а не утверждает, что фоновый файл уже создан.

Выберем database queue стандартного каркаса. Она требует таблиц jobs и failed_jobs, которые описаны базовыми миграциями Laravel. Само значение QUEUE_CONNECTION database не запускает worker. В собственной среде читатель должен отдельно организовать процесс обработки; в этой работе ни очередь, ни миграции, ни PHP-процесс не выполнялись. Объясним границы без выдуманных секунд ожидания.

Данные фонового задания

Создадим полный app/Jobs/BuildTicketReceipt.php. В очередь передаётся стабильный id, а обработчик читает актуальную строку. Квитанция записывается в предсказуемый частный путь. Повтор того же задания заменяет тот же файл вместо создания множества случайных имён.

<?php
namespace App\Jobs;
use App\Models\Ticket;
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\Storage;
class BuildTicketReceipt implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    public int $tries = 3;
    public int $timeout = 30;
    public int $backoff = 5;
    public function __construct(public int $ticketId) {}
    public function handle(): void
    {
        $ticket = Ticket::query()->findOrFail($this->ticketId);
        if ($ticket->status !== 'closed') return;
        $content = "Заявка #{$ticket->id}\n{$ticket->title}\nСтатус: closed\n{$ticket->body}\n";
        if (!Storage::disk('local')->put("receipts/{$ticket->id}.txt", $content))
            throw new \RuntimeException('Receipt storage failed');
    }
}

Документация очередей Laravel описывает ShouldQueue, число попыток, timeout и работу с транзакциями. Наши значения three, thirty и five являются выбранными параметрами учебного задания, не измеренным временем выполнения. При самостоятельной настройке timeout worker должен быть согласован с retry_after соединения: задание не должно получить новый обработчик до завершения допустимого времени прежнего.

Отправка после commit

В TicketCloseController добавляется импорт BuildTicketReceipt. Внутри transaction после успешного сохранения closure и перед return true добавьте следующий точный фрагмент. Полный итоговый контроллер присутствует в архиве.

BuildTicketReceipt::dispatch($ticket->id)->afterCommit();

afterCommit предотвращает запуск job до фиксации данных. Если transaction откатится, задание, отложенное этой границей, не должно начать подготовку квитанции по несуществующему результату. Context7 и раздел Jobs and Database Transactions подтверждают выбранный API. Это ровно одна гарантия порядка, которую не следует расширять до любых сбоев двух систем.

После успешного close браузер может ещё не увидеть файл: worker не обязан уже обработать запись очереди. Пока нет маршрута скачивания, файл существует как результат будущего задания на частном диске. Позже контроллер выдачи различит готовую квитанцию и отсутствие файла. Сообщение «Заявка закрыта» остаётся точным даже при временном отказе подготовки квитанции, потому что закрытие уже зафиксировано в базе.

Повторная обработка

Очереди следует проектировать с учётом повторов. Процесс может записать файл и завершиться до подтверждения обработки, после чего job будет доставлено снова. Предсказуемый путь receipts/id.txt и одинаковое содержимое делают такой повтор приемлемым для учебного результата. Мы не заявляем доставку ровно один раз. Запись файла является заменой одного результата, а не неограниченным созданием новых квитанций.

Содержимое строится из закрытой заявки. В нашей модели её title и body больше не редактируются, а переоткрытие отсутствует, поэтому повтор читает ту же предметную основу. Если продукт разрешит изменения закрытых заявок, квитанция перестанет быть фиксированным документом прошлого решения. Тогда следует сохранять снимок или отдельную версию квитанции. Текущий id сам по себе не гарантирует неизменность всех будущих моделей.

Не включаем текущее время обработки в текст. Иначе повтор мог бы менять значение, которое пользователь считал временем закрытия. Для даты решения лучше использовать сохранённую closure или отдельный timestamp перехода, а не момент, когда worker наконец получил задачу. Этот пример содержит только идентификатор, title, закрытый статус и body, поэтому не выдаёт время фоновой подготовки за историю обращения.

Сбой файла и сбой очереди

Storage put может вернуть false в зависимости от настройки диска. Мы превращаем такой результат в исключение, чтобы worker считал попытку неуспешной. Если просто игнорировать false, job могла бы завершиться успешно без квитанции. Полезный неправильный случай связан именно с объявленным договором результата, а не с предполагаемым названием файла. Отсутствующий результат должен быть наблюдаемым отказом фоновой операции.

После исчерпания попыток задание требует внимания к failed_jobs и причине отказа. Нельзя автоматически повторять его бесконечно: неверные права каталога или недостаток места не исправятся только увеличением tries. Перед повтором нужно восстановить ресурс и подтвердить, что та же операция допустима. Предсказуемый частный путь облегчает такую проверку, но не заменяет её.

Есть промежуток между фиксацией заявки и надёжным попаданием работы в очередной транспорт. afterCommit задаёт порядок, но не создаёт транзакционный outbox для безусловной доставки при любом отказе процесса. Для критического документа требуется сохранить намерение подготовки в той же базе и иметь повторяемый dispatcher. Учебная квитанция не объявлена таким финансовым или юридическим документом, поэтому выбранный ограниченный механизм описан честно.

Процесс обработки и выпуск

Worker живёт дольше отдельного HTTP-запроса и способен продолжать работать со старым кодом после изменения файлов. При настоящем выпуске нужна согласованная перезагрузка workers и управление процессом. Последний урок включит это в договор передачи, не выполняя запуска. Обычный запуск php artisan serve сам по себе не означает, что job начнёт обрабатываться.

Для локальной учебной базы допустимо самостоятельно использовать queue work после установки зависимостей и миграций. Но архив не содержит работающий демон или внешние настройки process supervisor. Не выдавайте отсутствие ошибки HTTP за доказательство работоспособности очереди: заявка может закрываться правильно, а квитанция оставаться неготовой из-за незапущенного процесса. Эти результаты надо проверять отдельно.

Перед следующей страницей объясните три момента: фиксацию closed, появление задания в очереди и появление файла. Они связаны, но не совпадают по времени и гарантиям. Затем рассмотрите повтор одного job и отказ Storage put. Следующий урок добавит частное уведомление владельцу после commit, сохранив то же различие между предметным решением и выполнением фонового действия.

Назначение квитанции

Текстовая квитанция служит кратким подтверждением учебного закрытия. Она не подписана и не имеет юридического статуса документа. Её назначение позволяет выбрать повторяемую запись по id без сложной нумерации выпусков. Если продукт требует строгую неизменность, подпись или учёт выданных версий, нужно определить отдельную модель документа и её жизненный цикл до изменения фонового задания.

Для самостоятельного воспроизведения результата «Отделить повторяемую фоновую работу от HTTP» выберите lesson-12 в архиве исходников. Статус выполнения этого состояния: not_run.