---
title: "Laravel 13.40: довгі завдання черги, insertBefore() для middleware і strict для запиту"
url: https://laravelukraine.com/blog/laravel-1340-dovgi-zavdannia-cergi-insertbefore-dlia-middleware-i-strict-dlia-zapitu
date: 2026-11-04
---

# Laravel 13.40: довгі завдання черги, insertBefore() для middleware і strict для запиту

[Laravel 13.40.0](https://github.com/laravel/framework/releases/tag/v13.40.0) вийшов 3 листопада. Головне в ньому - спостереження за фоновою роботою: фреймворк тепер сам повідомляє, коли завдання черги чи задача планувальника виконуються задовго, без сторонніх пакетів і власних слухачів. Поруч - зручніше налаштування вбудованого стеку middleware, строгий режим для читання даних запиту і продовження теми реплік, яку ми розбирали в [огляді 13.36-13.38](https://laravelukraine.com/blog/laravel-1336-1338-repliki-bazi-danix-bez-zastarilix-danix).

### Завдання черги, що виконуються задовго

Для запитів до бази в Laravel давно є `DB::whenQueryingForLongerThan()`, для HTTP-запитів і команд - `whenRequestLifecycleIsLongerThan()` і `whenCommandLifecycleIsLongerThan()`. Для завдань черги аналога не було. Тепер він є ([#62141](https://github.com/laravel/framework/pull/62141)):

```php
use Carbon\CarbonInterval;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;

// AppServiceProvider::boot()
Queue::whenJobRunsLongerThan(CarbonInterval::seconds(90), function ($job, $duration) {
    Log::warning("{$job->resolveName()} виконувалося {$duration} мс");
});
```

Метод слухає подію `JobAttempted`, яка після 13.39 несе тривалість кожної спроби в мілісекундах (про це ми писали в [огляді 13.39](https://laravelukraine.com/blog/laravel-1339-modeli-bez-pervinnogo-kliuca-sessiongarbage-collect-i-trivalist-sprob-zavdan)). Тому колбек спрацьовує і для спроб, що впали з винятком, на відміну від `Queue::after()`, який бачить лише успішні. Автор PR пояснює сценарій просто: знати, яке завдання і скільки працювало, щоб вчасно перенести його на потужніший воркер або помітити, що воно підбирається до свого `timeout` чи ліміту хостингу.

### Задачі планувальника, що виконуються задовго

Той самий автор додав парний метод для планувальника ([#62233](https://github.com/laravel/framework/pull/62233)):

```php
use Carbon\CarbonInterval;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Support\Facades\Schedule;

Schedule::whenTaskRunsLongerThan(CarbonInterval::minutes(45), function (Event $task, float $runtime) {
    Log::warning("Задача [{$task->getSummaryForDisplay()}] виконувалася {$runtime} с");
});
```

Колбек отримує задачу і час виконання в секундах та спрацьовує як після успішного завершення (`ScheduledTaskFinished`), так і після невдачі (`ScheduledTaskFailed`).

Ще одна зміна для тих, хто запускає планувальник на кількох серверах: коли задачу з `onOneServer()` уже виконав інший сервер, тепер диспатчиться подія `ScheduledTaskSkipped` ([#62256](https://github.com/laravel/framework/pull/62256)). Раніше був лише рядок «Skipping [...] because the command already ran on another server» у виводі, який на частині інфраструктури взагалі ніхто не бачить. Тепер пропуск можна порахувати в метриках і довести, що задача не загубилася.

### Пастка з одиницями

Обидва методи приймають поріг як `CarbonInterval`, `DateTimeInterface` або число. І саме з числом легко помилитися: у черзі ціле число порівнюється з тривалістю в **мілісекундах**, а в планувальнику - з часом у **секундах**. `Queue::whenJobRunsLongerThan(90, ...)` спрацює вже на завданні довжиною в 90 мілісекунд, тобто майже на всіх. Передавайте `CarbonInterval`, і метод сам переведе його в потрібні одиниці.

Друге обмеження видно з коду `schedule:run`. Для задач з `runInBackground()` подія `ScheduledTaskFinished` надходить одразу після запуску фонового процесу, і в `runtime` потрапляє лише час на його старт. Тож довгу фонову задачу `whenTaskRunsLongerThan()` не помітить.

### Власний middleware поруч із вбудованим

Щоб поставити свій middleware в глобальний стек саме після `TrustProxies` чи перед `TrimStrings`, раніше доводилося переоголошувати частину стеку за замовчуванням. У 13.40 `$middleware->replace()` приймає масив ([#62230](https://github.com/laravel/framework/pull/62230)), а поверх цього з'явилися короткі методи ([#62261](https://github.com/laravel/framework/pull/62261)):

```php
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
use Illuminate\Http\Middleware\TrustProxies;

->withMiddleware(function (Middleware $middleware) {
    // Після вбудованого TrustProxies
    $middleware->insertAfter(TrustProxies::class, StripCrossOriginReferer::class);

    // Перед вбудованим TrimStrings
    $middleware->insertBefore(TrimStrings::class, SanitiseStrings::class);

    // Замінити один middleware двома
    $middleware->replace(Foo::class, [Bar::class, Baz::class]);
})
```

Для груп є `insertBeforeInGroup()` і `insertAfterInGroup()` з назвою групи першим аргументом. Методи враховують уже зроблені заміни: якщо `TrimStrings` до того замінили на щось інше, нове вставиться поруч із заміною.

### Строгий режим для даних запиту

`$request->integer()`, `boolean()`, `string()` і `float()` досі робили звичайне приведення типу. Звідси дві проблеми, які описав автор PR ([#62254](https://github.com/laravel/framework/pull/62254)): `?name[]=chris` змушує `$request->string('name')` кинути «Array to string conversion», а `?count=abc123` перетворюється на `0`, і значення за замовчуванням так і не використовується. Новий третій аргумент `strict` повертає значення за замовчуванням, якщо дані не того типу:

```php
// ?count=abc123
$request->integer('count', 5);               // 0
$request->integer('count', 5, strict: true); // 5

// ?name[]=chris
$request->string('name', '', strict: true);  // порожній Stringable замість винятку
```

Для `boolean()` строгий режим приймає лише `1`, `true`, `on`, `yes`, `0`, `false`, `off` і `no`. Методи живуть у трейті `InteractsWithData`, тож прапорець працює всюди, де він підключений. Валідацію це не замінює, але для логіки «якщо прийшло число, беремо його» тепер не потрібне окреме правило.

### Репліки: чи дійшов запис

У 13.38 з'явився `PreventStaleDatabaseReads`, який після запису веде читання на write-сервер протягом заданого часу. Слабке місце - сам цей час: невідомо, чи вистачить секунди. Тобіас Петрі (Tobias Petry) додав основу, щоб не вгадувати, а знати ([#62198](https://github.com/laravel/framework/pull/62198)):

```php
// на primary
$user->orders()->create([...]);
$commitId = DB::getLastCommitId();

// на репліці
DB::hasCommitId($commitId);          // миттєва перевірка
DB::hasCommitId($commitId, wait: 2); // чекати до 2 секунд
```

Є ще `DB::mergeCommitIds()`, щоб об'єднати кілька ідентифікаторів в один і чекати на всі. Підтримуються MySQL (потрібні `log_bin` і `gtid_mode = ON`), MariaDB (потрібен `log_bin`) і PostgreSQL, де очікування з `wait` вимагає PostgreSQL 19. Інші драйвери кидають `RuntimeException`. Поки це саме будівельні блоки: `PreventStaleDatabaseReads` їх ще не використовує, хоча автор пише, що це наступний крок.

### Що ще в релізі

`queue:work` тепер показує завдання, повернуті в чергу через `WithoutOverlapping`, `RateLimited` чи вручну, як `RELEASED`, а не `DONE` ([#62180](https://github.com/laravel/framework/pull/62180)), так само, як це вже робив вивід `--json`. Решта - кілька десятків виправлень для SQL Server, PostgreSQL, SQLite, пагінації та Redis-кешу з тегами. Окремо - виправлення безпеки в драйвері `process` фасаду `Concurrency` ([#62297](https://github.com/laravel/framework/pull/62297)), про яке ми напишемо окремо.

### Чи варто оновлюватися

Так, і бажано не відкладати: хоча б через виправлення в `Concurrency`, якщо ви користуєтеся `Concurrency::run()`. Нові методи опційні й поведінку наявного коду не змінюють. Після оновлення має сенс додати в `AppServiceProvider` обидва пороги з `CarbonInterval`, підібравши їх під ваш `timeout` і найдовші задачі розкладу, а у місцях, де ви читаєте з запиту числа без валідації, увімкнути `strict: true`.
