Тема
Services, Jobs, Events, Notifications
Laravel 13 (PHP 8.3+) · сверено с laravel.com/docs/13.x Контекст «почему»: best/architecture-patterns (Service/Action, DTO), best/performance (кеш, очереди), best/ai-sdk-vector-search (AI-паттерны)
Services
Когда создавать
- Логика используется в 2+ контроллерах
- Метод контроллера > 10 строк логики
- Нужна DB-транзакция (несколько записей атомарно)
- Есть побочные эффекты (email, telegram, внешний API)
- Нужно тестировать логику изолированно от HTTP
Правила
- Один сервис = одна доменная область (
OrderService,ContactService,SeoService) - DI через конструктор — не
app()/resolve()внутри методов DB::transaction()— когда несколько записей должны быть атомарны- Сервис НЕ знает про Request / Response / View — работает только с данными
- Возвращает модели, коллекции, массивы, скаляры — не Response
- Не более 300 строк — если больше, разбивать по ответственности
Регистрация в контейнере
- Простые сервисы: Laravel резолвит через auto-wiring, регистрация не нужна
- Интерфейс → реализация: bind в
AppServiceProvider::register() - Singleton — только когда состояние должно жить весь request lifecycle
Anti-patterns сервисов
| Паттерн | Проблема | Решение |
|---|---|---|
| God Service (1000+ строк) | Нарушает SRP | Разбить по доменам |
app(Service::class) внутри метода | Скрытая зависимость | Constructor DI |
| Сервис возвращает Response | Знает про HTTP | Возвращать данные |
Сервис читает request() | Связь с HTTP-слоем | Принимать данные аргументами |
Jobs (Queue)
Когда использовать
- Отправка email / telegram / SMS — не блокировать HTTP response
- Обработка файлов (resize, import, export CSV)
- Вызов внешних API (могут быть медленными / падать)
- Тяжёлые вычисления (отчёты, генерация PDF)
Правила
- Попытки и задержки — ВСЕГДА указывать, атрибутами (Laravel 13):
#[Tries],#[Backoff],#[Timeout],#[FailOnTimeout],#[MaxExceptions]изIlluminate\Queue\Attributes\*; свойства$tries/$backoff— legacy, в старом коде допустимы #[Timeout]всегда меньшеretry_afterочереди (иначе job выполнится дважды);#[FailOnTimeout]— если повтор после таймаута бессмыслен;#[Tries(25)]+#[MaxExceptions(3)]— для release-паттернов с rate limitfailed()— ВСЕГДА определять (обработка финального провала)- Передавать в Job только ID или модель (SerializesModels) — не массивы данных
- Job НЕ возвращает результат в HTTP — если нужен ответ, делай синхронно
- Бизнес-логика — в Service, Job только вызывает Service
php
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;
#[Tries(3)]
#[Backoff([10, 60, 300])]
#[Timeout(120)]
class SendLeadNotification implements ShouldQueue
{
// ...
}Laravel 13 в слушателях queue-событий: JobAttempted::$exception (вместо bool $exceptionOccurred), QueueBusy::$connectionName (вместо $connection).
Очереди
default— общая очередьnotifications— email, telegram, SMSheavy— импорт, экспорт, генерация отчётов- Разделение предотвращает блокировку быстрых задач тяжёлыми
Маршрутизация job → очередь — централизованно через Queue::route() (Laravel 13) в AppServiceProvider::boot(), а не ->onQueue() в каждом месте dispatch:
php
Queue::route([
SendLeadNotification::class => 'notifications',
GenerateReportJob::class => ['heavy', 'redis'], // очередь + connection
]);Queue::route()матчит и по интерфейсу/трейту/родителю — общий маркер-интерфейс (RequiresHeavyQueue) маршрутизирует все реализации разом- Явный
->onQueue()/->onConnection()при dispatch приоритетнее правила маршрутизации — использовать только для осознанных исключений
Events & Listeners
Когда использовать
- 3+ независимых реакций на одно действие (создание заказа → обновить статистику + отправить email + записать лог)
- Decoupling модулей, которые не должны знать друг о друге
Когда НЕ использовать
- Одна реакция на действие — просто вызов метода в сервисе
- Нужна гарантия порядка выполнения — listeners порядок не гарантируют
- Отладка важнее гибкости — прямой вызов проще дебажить
- Меньше 3 listeners — overengineering
Правила
- Event — только DTO (данные), без логики
- Listener — вызывает Service, не содержит логику сам
- Для async: listener implements
ShouldQueue
Notifications
Каналы
| Канал | Когда | Queue? |
|---|---|---|
| Важные: заявки, ошибки, подтверждения | Да, всегда | |
| database | История в админке, непрочитанные | Нет (быстрая запись) |
| Telegram (custom) | Мгновенные уведомления команде | Да |
| SMS (custom) | Критичные: OTP, подтверждение | Да |
Правила
- Всегда через queue (
ShouldQueue) кроме критичных (OTP, подтверждение) - Notification — только форматирование, логика — в Service
via()— осознанный выбор каналов, не "все сразу"- Текст уведомлений на русском для пользовательских, на английском для технических/dev
- Laravel 13: queued notifications уважают
#[DeleteWhenMissingModels]— вешать на уведомления, чья модель может быть удалена до обработки очереди
AI-функциональность (Laravel 13)
- Первопартийный Laravel AI SDK — единый API для текста, агентов, embeddings, изображений и аудио; не писать самодельные HTTP-обёртки над провайдерами
- Embeddings:
Str::of($text)->toEmbeddings(); семантический поиск — vector-колонки +whereVectorSimilarTo()(см. specs/migrations.md) - Вызовы AI — только из Service/Job (медленные и платные), никогда синхронно из контроллера; queue-воркерам под AI —
--timeout=120+ - Ключи провайдеров —
.env→config(); выбор модели и лимиты — в config, не хардкодом - Failover — средствами SDK (
provider: ['openai', 'anthropic']), не самописной fallback-логикой - Агент всегда с
#[MaxSteps(N)]— потолок ReAct-цикла (защита от зацикливания = счёт за токены) - Tools агента НЕ бросают исключения — возвращают строку ошибки (модель должна её прочитать и среагировать)
- Embeddings при массовом ingest — кеш SDK (
->toEmbeddings(cache: true)/config/ai.php→caching.embeddings) и батчи из Job - В тестах — только
Agent::fake()+preventStrayPrompts(), реальные вызовы провайдера в CI запрещены (см. specs/testing.md) - Мультиагентные паттерны и streaming — best/ai-sdk-vector-search; начинать всегда с одного агента
Кеш (Laravel 13)
- Новый дефолт
cache.serializable_classes = false— защита от deserialization gadget chains при утечкеAPP_KEY - Кеширование PHP-объектов (Eloquent-модели/коллекции, DTO) теперь требует явного allow-list классов в
config/cache.php— иначе объект не десериализуется - Предпочитать кеширование массивов/скаляров (
->toArray()) вместо объектов — allow-list не нужен, payload компактнее Cache::touch($key, $seconds)— продление TTL без чтения и перезаписи значения- Инвалидация — по-прежнему Redis cache tags:
Cache::tags([...])->flush()(см. specs/filament.md)
Общие anti-patterns
| Паттерн | Проблема | Решение |
|---|---|---|
| Синхронная отправка email в контроллере | Блокирует HTTP response на 2-5 сек | Job / Queue |
| Event ради одного listener | Overengineering | Прямой вызов |
Job без #[Tries] / failed() | Silent failures, бесконечные retry | Всегда определять |
| Бизнес-логика в Job/Listener | Невозможно тестировать / переиспользовать | Job → Service |
| Notification без queue | Медленный response | ShouldQueue |
| Передача Collection/массива в Job | Serialization bloat, stale data | Передавать ID/модель |