Реліз Laravel 13.30 приніс кілька важливих нововведень: метод chunkBy() для групування сусідніх елементів колекцій, посилення безпеки Storage::path() через нормалізацію шляхів, виведення причини зупинки queue worker у консолі, а також низку інших покращень та виправлень помилок.
Метод chunkBy() для колекцій
Раніше для розбиття колекції на частини за певною ознакою використовувався метод chunkWhile(), де найчастіше доводилося порівнювати поточний елемент з останнім елементом поточного chunk:
$products->chunkWhile(fn ($value, $key, $chunk) => $value->parent == $chunk->last()->parent);
Новий метод chunkBy() спрощує цей паттерн. Він приймає ключ або callback і починає новий chunk щоразу, коли значення змінюється:
$products->chunkBy('parent');
collect([1, 1, 2, 2, 1, 1])->chunkBy(fn ($value) => $value);
// [[1, 1], [2, 2], [1, 1]]
Ключ обробляється через data_get(), тому працює dot-нотація (наприклад, chunkBy('address.city')), а оригінальні ключі зберігаються всередині кожного chunk. Метод доступний як для звичайних, так і для lazy колекцій.
Посилення безпеки Storage::path()
Виявлено серйозну вразливість: усі методи файлової системи, крім одного, проходили через Flysystem драйвер, який нормалізує шлях і викидає PathTraversalDetected при спробі вийти за межі кореневої директорії диска. Метод Storage::path() пропускав цей крок:
Storage::get('../../../.env'); // відхилено
Storage::path('../../../.env'); // повертав шлях за межами диска
На стандартному local диску path() повертав нативний шлях до .env додатка, тоді як get(), delete() та readStream() відхиляли той самий аргумент. Це створювало проблему безпеки, особливо коли шлях надходив від користувача:
response()->download(Storage::path($request->query('path')));
Тепер path() пропускає аргумент через WhitespacePathNormalizer - той самий нормалізатор, що використовується для всіх інших викликів. Відносні сегменти розв'язуються так, як їх розв'язує драйвер, а все, що виходить за межі кореня, викидає PathTraversalDetected. Код, що покладався на прийняття .. сегментів методом path(), тепер викидатиме виключення.
Виведення причини зупинки queue worker
Раніше worker, що завершував роботу, не залишав жодних слідів у консолі про причину виходу. Подія WorkerStopping містила WorkerStopReason, але для його читання потрібно було реєструвати listener, що вимагало багато налаштувань для питання, яке зазвичай виникає при перегляді терміналу чи логів.
Тепер queue:work виводить причину як останній рядок:
2026-09-01 13:20:40 Worker STOPPED Memory limit exceeded
З прапорцем --json інформація виводиться як структурований запис поряд із виводом для кожного job:
{"level":"warning","status":"stopped","reason":"memory","exit_code":12,"jobs_processed":2,"memory":1.2,"timestamp":"2026-09-01T13:20:40.118273+00:00"}
Enum WorkerStopReason отримав метод description() для людиночитаних рядків, що охоплює дев'ять сценаріїв виходу: перервано, втрачено з'єднання, перевищено максимум jobs, перевищено ліміт пам'яті, перевищено максимальний час роботи, черга порожня, черга порожня протягом налаштованого часу, отримано сигнал перезапуску та час виконання job вичерпано. При використанні --quiet або --silent нічого не виводиться.
Відключення vendor та стандартних dev команд
DevCommands реєструє все, про що знає: стандартні команди фреймворку (serve, queue:listen, pail та Vite скрипт), будь-що з пакетів у vendor та команди додатка. Для звуження цього списку потрібно було вказувати всі потрібні команди через only() або небажані через except(). Перший підхід ламався при додаванні черги, другий - коли залежність починала реєструвати щось нове.
Два нові методи фільтрують за походженням команди:
use Illuminate\Foundation\DevCommands;
DevCommands::withoutVendorCommands(); // прибрати все з vendor/
DevCommands::withoutDefaultCommands(); // прибрати стандартні команди фреймворку
Обидва методи є opt-in і залишають команди додатка недоторканими. DevCommands уже відстежував походження кожної команди як пріоритет (default, vendor або userland) для вирішення конфліктів імен; ці методи фільтрують за тим самим значенням.
Нативні DSN рядки підключення SQL Server
Парсер database URL у Laravel припускав URL. Нативний рядок підключення PDO_SQLSRV не є URL:
sqlsrv:Server=127.0.0.1,1433;Database=example;Encrypt=true;TrustServerCertificate=true
parse_url() читав все це як шлях, Laravel видаляв перший символ, і назва бази повертались як erver=127.0.0.1,1433. Результатом було підключення з пошкодженою конфігурацією.
Тепер нативні sqlsrv: DSN виявляються перед парсингом URL і передаються спеціалізованому парсеру, який витягує хост, порт, базу даних та підтримувані опції PDO_SQLSRV (Encrypt, TrustServerCertificate, MultiSubnetFailover, ApplicationIntent, LoginTimeout тощо), відображаючи їх на ключі конфігурації Laravel. Значення, що містять крапки з комою, закриваючі дужки або пробіли, екрануються при відновленні рядка підключення. Власна конвенція Laravel sqlsrv://user:pass@host:port/database продовжує працювати через існуючий URL парсер.
Отримання однієї Artisan команди
Artisan команди розв'язуються ліниво, але не було способу отримати одну за іменем без втрати цієї переваги. Artisan::all() конструює всі зареєстровані команди для побудови масиву:
// Конструює всі команди
$command = Artisan::all()['app:my-command'] ?? null;
// Конструює лише MyCommand, якщо вона зареєстрована
$command = Artisan::commandNamed('app:my-command');
Метод commandNamed() повертає екземпляр команди або null.
Підсумки для Cloud Queue
Методи totalPendingSize(), totalDelayedSize() та totalReservedSize(), додані в 13.27, повертають 0 на SQS, оскільки SQS не може перелічити черги, які використовує підключення. Laravel Cloud queue може це робити: він уже знає, якими чергами управляє, тому три методи тепер реалізовані там шляхом підсумовування через managedQueues():
use Illuminate\Support\Facades\Cloud;
Cloud::queue()->totalReservedSize();
Інші виправлення та покращення
-
route:cache завантажував другий, тимчасовий додаток для збору чистої копії маршрутів, і bootstrapping переспрямовував глобальний facade application на той контейнер без відновлення. Під php artisan optimize це призводило до LogicException: Route is not bound. Тепер команда відновлює facade application одразу після bootstrap.
-
Request::clamp() повертав 500 для введення, яке не міг обмежити. Нечислові значення тепер проходять до значення за замовчуванням, яке потім обмежується min та max.
-
db:seed --database=mysql перемикає стандартне підключення і повертає його після завершення seeder. Виключення пропускало відновлення. Тепер перемикання обгорнуте в try/finally.
-
setVisibility() на read-through диску торкався лише primary. Тепер розв'язується через readerFor(), як getVisibility(), mimeType() та fileSize().
-
Queue::createPayloadUsing() проходив через QueueManager::__call(), що викликало помилку при реєстрації callback у service provider. Метод тепер визначений безпосередньо на manager.
-
Tooltips на сторінці виключень рендеряться з allowHTML: false за замовчуванням. Власний syntax-highlighted tooltip фреймворку вмикає HTML через окремий атрибут.
-
JSON:API ресурси відстежують відносини, запитані відносно кожного включеного ресурсу, тому вкладені include шляхи розв'язуються на правильному рівні.
-
spl_object_id() замінює spl_object_hash() у контейнері, Eloquent builder, global scopes та Onceable, портовано з 14.x для очищення deprecation warnings.
-
Handler::contextForException() публічно експонує контекст логування виключення, а BladeMapper::findCompiledView() та detectLineNumber() стали публічними.
-
Cloud events transport повертає boolean з emit() та emitMany(), отримав $socketFactory хук для тестування та реєструється під час bootstrap exception-handler.
-
Виправлення квадратичної експансії wildcard правил у ValidationRuleParser::explodeWildcardRules() портовано до 12.x.