В Laravel 13.25 добавлен глобальный переключатель паузы, который останавливает каждую очередь на каждом соединении одной командой, заменён процесс-раннер для artisan dev на терминальный UI с вкладками, а также добавлена возможность возвращать экземпляр Image напрямую из маршрута. Команда Laravel выпустила версию v13.25.0 11 августа 2026 года.
-
queue:pause --allиqueue:resume --all, а такжеQueue::pauseAll()иQueue::resumeAll() -
artisan devработает через@laravel/multiplexс режимами вкладок (tabs), потока (stream) и встроенным (inline) -
ImageреализуетResponsable, получаетImage::fromStream(), а методtoFormat()теперь публичный - Событие
UniqueJobSkipped, значение таймаута вJobTimedOutи обработка сбоя при таймауте (fail-on-timeout) для уведомлений -
withoutCookies()для ответов и вспомогательный метод схемыforeignUlidFor() -
Request::all()теперь отдаёт приоритет инпуту перед файлами при их совпадении
#Что нового
#Приостановка всех очередей на всех соединениях
В Laravel уже давно можно было приостановить отдельную очередь, но такая гранулярность мешала при деплое. Приложению с дюжиной воркеров, распределённых по нескольким именованным очередям, приходилось приостанавливать каждую пару из соединения и очереди по имени, а эти имена меняются по мере добавления и удаления фич. Использование режима обслуживания (maintenance mode) вместо этого отключает весь сайт, хотя всё, что вам нужно, — это чтобы воркеры перестали резервировать задачи.
Обе консольные команды теперь принимают флаг --all :
php artisan queue:pause --all
php artisan queue:resume --all
Аргумент очереди стал необязательным, чтобы освободить для этого место, и тот же переключатель доступен в фасаде:
use Illuminate\Support\Facades\Queue;
Queue::pauseAll();
Queue::resumeAll();
Глобальный переключатель — это единый ключ кэша, который воркеры проверяют наряду с ключами отдельных очередей, поэтому isPaused() и getPausedQueues() сообщают, что очередь приостановлена, если включён любой из переключателей. Они независимы: resumeAll() сбрасывает глобальный флаг и оставляет всё, что вы приостановили индивидуально, в приостановленном состоянии — это сделано намеренно, чтобы скрипт деплоя случайно не перезапустил очередь, которую кто-то специально остановил.
Два новых события, QueuesPaused и QueuesResumed , срабатывают наряду с существующими для отдельных очередей QueuePaused и QueueResumed . Внесено @jackbayliss в #61126.
Читать далее: Приостановка всех очередей Laravel во время деплоя
# artisan dev работает через @laravel/multiplex
Команда artisan dev выставляла процессы наружу через concurrently , который переплетал вывод каждого процесса в одну прокручиваемую ленту. Когда пересборка Vite, воркер очереди и Pail пишут одновременно, поиск нужной строки превращается в бесконечную прокрутку.
Теперь она работает через @laravel/multiplex — интерфейс терминала, в котором каждый процесс находится на своей вкладке. Доступен поиск, перезапуск отдельных процессов, очистка логов и автоматический перезапуск упавшего процесса. Предусмотрено три режима: tabs (по умолчанию), stream (единый переплетённый поток с возможностью прокрутки и поиска) и inline (простой вывод, используемый автоматически при отсутствии TTY). Режим можно выбрать для каждого запуска:
php artisan dev --stream
php artisan dev --timestamps --no-restart
Или задать значение по умолчанию для проекта в сервис-провайдере:
use Illuminate\Foundation\DevCommands;
DevCommands::stream();
DevCommands::withTimestamps();
DevCommands::disableAutoRestart();
DevCommands::bufferSize(5000);
При завершении работы команды буферизованные логи выводятся в основной терминал, поэтому ничего не теряется. В Windows происходит откат к concurrently , так как multiplex на данный момент поддерживает только macOS и Linux, а минимальная версия Node для нового механизма — v22.13. Полный список режимов, флагов и методов регистрации доступен в статье о терминальном UI для artisan dev. Внесено @joetannenbaum в #61100.
#Изображения в качестве HTTP-ответов
Встроенный Image API мог трансформировать изображение и сохранять его, но для отдачи его по HTTP приходилось вызывать toBytes() и собирать ответ вручную. Теперь Image реализует Responsable , поэтому маршрут может напрямую возвращать экземпляр:
use Illuminate\Support\Facades\Image;
Route::get('/avatars/{user}', function (User $user) {
return Image::fromStorage($user->avatar_path)
->cover(200, 200)
->toWebp()
->quality(80);
});
toResponse() возвращает код 200 с обработанными байтами и заголовком Content-Type , прочитанным из выходных данных, поэтому заголовок соответствует итоговому формату пайплайна, а не исходному файлу.
Ещё два небольших дополнения в этой же области. Image::fromStream() создаёт экземпляр из ресурса потока, читая его лениво и выбрасывая ImageException , если поток ничего не возвращает:
$image = Image::fromStream(Storage::disk('s3')->readStream($path));
А метод toFormat() теперь сделан публичным. Он заменяет конструкцию match , которую вам иначе пришлось бы писать для преобразования полученной от пользователя строки формата в нужный вызов toWebp() или toAvif() :
return Image::fromUpload($request->file('photo'))
->toFormat($request->string('format'))
->quality(80);
Неподдерживаемый формат выбрасывает ImageException вместо тихой ошибки. Все три улучшения внесены @calebdw в #61111, #61109 и #61110.
#Дополнения для наблюдаемости очередей
Задача, которая не отправляется из-за удержания блокировки ShouldBeUnique , бесследно исчезает, из-за чего трудно отличить работающее ограничение уникальности от блокировки, которая никогда не снимается. Новое событие UniqueJobSkipped содержит пропущенную задачу:
use Illuminate\Queue\Events\UniqueJobSkipped;
Event::listen(function (UniqueJobSkipped $event) {
Log::info('Skipped unique job', ['job' => $event->job::class]);
});
Оно вызывается из PendingDispatch , когда не удаётся получить уникальную блокировку, наряду с существующим событием JobDebounced (#61039).
Событие JobTimedOut получило третье свойство, $timeout , содержащее превышенное количество секунд. Воркер, запущенный с queue:work --timeout=120 , применяет собственный таймаут к каждой выполняемой задаче, поэтому без этого значения в событии было невозможно отличить собственный таймаут задачи от таймаута воркера (#61060).
Уведомления теперь учитывают fail-on-timeout. SendQueuedNotifications считывает свойство $failOnTimeout или атрибут #[FailOnTimeout] из уведомления, что важно, когда из-за таймаута вы не уверены, доставила ли сторонняя система сообщение (#61072):
use Illuminate\Queue\Attributes\FailOnTimeout;
#[FailOnTimeout]
class OrderShipped extends Notification implements ShouldQueue
{
//
}
Наконец, QueueFake теперь присваивает UUID фейковым задачам, благодаря чему методы инспекции очередей возвращают ту же структуру, что и при работе с реальным драйвером, а код приложения, читающий $job->uuid , становится тестируемым (#60966).
# withoutCookies() в ответах
Удаление нескольких cookie ранее требовало цепочки вызовов withoutCookie() для каждого имени. Форма во множественном числе принимает массив:
return response('OK')->withoutCookies(['session', 'tracking', 'preferences']);
Метод проходится циклом по withoutCookie() , поэтому необязательные аргументы $path и $domain применяются к каждой cookie в массиве, причём поддерживаются как имена, так и экземпляры cookie. Внесено @xurshudyan в #61115.
#Вспомогательный метод схемы foreignUlidFor()
foreignIdFor() уже умеет определять трейт HasUlids и создавать колонку ULID, но не было явного метода, парного для foreignUuidFor() . Теперь тройка полна:
$table->foreignUlidFor(User::class)->constrained();
Этот метод определяет имя колонки, связанную таблицу и ссылочный ключ на основе модели, создавая колонку char(26) и соответствующее ограничение внешнего ключа (foreign key constraint). Внесено @talaridisTh в #61036.
# Request::all() отдаёт приоритет инпуту перед файлами
Request::all() объединял массив инпута и массив файлов с помощью array_replace_recursive() , причём файлы применялись последними, из-за чего файловое поле и поле инпута с одинаковым именем резолвились в UploadedFile . Теперь порядок изменён, и инпут имеет приоритет:
// POST with input email=taylor@laravel.com and a file also named email
$request->all(); // ['email' => 'taylor@laravel.com']
$request->email; // 'taylor@laravel.com'
$request->file('email') // still the UploadedFile
Вложенные ключи объединяются аналогичным образом: и файл profile.avatar , и инпут profile.name по-прежнему будут присутствовать, а приоритет инпута проявится только там, где ключи действительно пересекаются. Метод file() это не затрагивает. Внесено @taylorotwell в #61099.
#Другие исправления и улучшения
-
Http::globalOptions()и глобальные middleware применялись к long poll через unix-сокет собственного облачного агента фреймворка, где опции вродеforce_ip_resolve => v4ломали соединение сокета. Новый замыкающий методFactory::withoutGlobalConfiguration()изолирует трафик агента от конфигурации клиента на уровне приложения (#61064, #61068) - Помещённые в очередь события вещания (broadcast events) теряли значение
trueпо умолчанию для$deleteWhenMissingModels, из-за чего модель, удалённая до того, как воркер подхватит задачу, приводила к ошибкеModelNotFoundExceptionвместо отбрасывания задачи (#61074) -
Gate::forUser()копировал abilities, политики и коллбэки, но не настроенный по умолчанию ответ с отказом доступа, из-за чего гейт, настроенный сResponse::denyAsNotFound(), откатывался к дефолту фреймворка (#61087) -
Str::substrReplace()выбрасывалTypeErrorпри передаче аргументов-массивов после того, как в результате переработки поддержки многобайтовых строк они стали передаваться напрямую вmb_substr(). Вызовы с массивами теперь делегируются нативномуsubstr_replace()в PHP (#61105) - Учитываются имена очередей на базе backed enum при отправке mailable-классов в очередь (#61066), драйверы очередей соответствуют фейку для enum-имен очередей (#61116),
MailFakeсохраняет разрешение очередей для отправляемых в очередь mailable (#61114), а массовые отправки вDatabaseQueueучитывают отправку после коммита (after-commit) (#60996) - Деактивация режима обслуживания на базе кэша между двумя обращениями middleware к кэшу приводила к
TypeError; теперь middleware повторно проверяет состояние так же, как делал это для файлового драйвера (#61121) -
Container::callоставлял записи в стеке сборки, если зависимость выбрасывала исключение, из-за чего последующие разрешения видоизменяли устаревший стек (#61041) - Типизированные геттеры кэша подставляли enum-ключ в сообщение о несоответствии типов, вызывая
Errorвместо предполагаемогоInvalidArgumentException(#61056) - Файлы HEIC сообщали некорректные размеры (#61010),
LazyCollection::flip()пропускает значения, которые не могут быть ключами массива (#61081), а атрибут#[WithoutTimestamps]проверяется, когда модель решает, нужно ли обновлять таймстампы (#61073) - Коллбэки повторных попыток (retry callbacks) в асинхронных HTTP-запросах получают HTTP-метод в качестве третьего аргумента, аналогично синхронным запросам, что ранее вызывало
ArgumentCountError(#61106) - Непотоковые ресурсы отклоняются в телах фейковых HTTP-ответов (#61047), драйвер логов Cloud устанавливает таймаут сокета (#61065, #61082), а поддержка подписанных URL была скорректирована для Vapor (#61129)
-
schedule:listправильно конвертирует часовые пояса для диапазонов, шагов и wildcard-выражений cron (#60913),Factory::insert()обрабатывает нулевое количество (#60911), а сбой при логировании устаревшего кода (deprecation) больше не приводит к фатальной ошибке (#60907) - Исправлены аннотации типов для
Arr::prependKeysWith()(#61034),Str::numbers()(#61053),getMigrationBatches()(#60973),getRememberToken()(#61067), регистратора связывания маршрутов (route binding registrar) (#61124) иColumnDefinition::unsigned()(#61123) - Поддержка
brick/math^0.19 (#61133)
Ссылки
Комментарии (0)
Пока нет комментариев — будьте первым.