Laravel 13.40.0 вийшов 3 листопада. Головне в ньому - спостереження за фоновою роботою: фреймворк тепер сам повідомляє, коли завдання черги чи задача планувальника виконуються задовго, без сторонніх пакетів і власних слухачів. Поруч - зручніше налаштування вбудованого стеку middleware, строгий режим для читання даних запиту і продовження теми реплік, яку ми розбирали в огляді 13.36-13.38.
Завдання черги, що виконуються задовго
Для запитів до бази в Laravel давно є DB::whenQueryingForLongerThan(), для HTTP-запитів і команд - whenRequestLifecycleIsLongerThan() і whenCommandLifecycleIsLongerThan(). Для завдань черги аналога не було. Тепер він є (#62141):
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). Тому колбек спрацьовує і для спроб, що впали з винятком, на відміну від Queue::after(), який бачить лише успішні. Автор PR пояснює сценарій просто: знати, яке завдання і скільки працювало, щоб вчасно перенести його на потужніший воркер або помітити, що воно підбирається до свого timeout чи ліміту хостингу.
Задачі планувальника, що виконуються задовго
Той самий автор додав парний метод для планувальника (#62233):
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). Раніше був лише рядок «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), а поверх цього з'явилися короткі методи (#62261):
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): ?name[]=chris змушує $request->string('name') кинути «Array to string conversion», а ?count=abc123 перетворюється на 0, і значення за замовчуванням так і не використовується. Новий третій аргумент strict повертає значення за замовчуванням, якщо дані не того типу:
// ?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):
// на 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), так само, як це вже робив вивід --json. Решта - кілька десятків виправлень для SQL Server, PostgreSQL, SQLite, пагінації та Redis-кешу з тегами. Окремо - виправлення безпеки в драйвері process фасаду Concurrency (#62297), про яке ми напишемо окремо.
Чи варто оновлюватися
Так, і бажано не відкладати: хоча б через виправлення в Concurrency, якщо ви користуєтеся Concurrency::run(). Нові методи опційні й поведінку наявного коду не змінюють. Після оновлення має сенс додати в AppServiceProvider обидва пороги з CarbonInterval, підібравши їх під ваш timeout і найдовші задачі розкладу, а у місцях, де ви читаєте з запиту числа без валідації, увімкнути strict: true.