What's New in Laravel 13.30
Laravel 13.30 landed on September 2, 2026 with a focused set of developer-experience improvements and one notable security hardening. Here is a breakdown of the headline changes.
chunkBy() for Collections and Lazy Collections
The existing chunkWhile() method splits a collection whenever a callback returns false. The most common pattern was comparing the current item against the last item in the chunk being built:
$products->chunkWhile(
fn ($value, $key, $chunk) => $value->parent == $chunk->last()->parent
);
The new chunkBy() method encodes that comparison directly. Pass a key name or a callback, and a new chunk starts whenever the resolved value changes:
$products->chunkBy('parent');
collect([1, 1, 2, 2, 1, 1])->chunkBy(fn ($value) => $value);
// [[1, 1], [2, 2], [1, 1]]
The key is resolved through data_get(), so dot notation works (chunkBy('address.city')). Original keys are preserved inside each chunk. Contributed by @JosephSilber in #61357.
Storage::path() Now Rejects Path Traversal
Every Flysystem-backed filesystem call normalizes paths and throws PathTraversalDetected when a path resolves outside the disk root — except Storage::path(), which previously just concatenated the prefix:
Storage::get('../../../.env'); // rejected
Storage::path('../../../.env'); // resolved outside the disk root
This mattered anywhere user input reached path(), for example:
response()->download(Storage::path($request->query('path')));
path() now runs the argument through WhitespacePathNormalizer — the same normalizer every other Flysystem call uses — before prefixing. Anything that escapes the disk root throws PathTraversalDetected. Code relying on .. segments in path() will now receive an exception. Contributed by @KIKOmanasijev in #61343.
Worker Stop Reasons in queue:work
queue:work now prints the reason a worker exits as its final line:
2026-09-01 13:20:40 Worker STOPPED Memory limit exceeded
With --json, the reason is emitted as a structured record:
{"level":"warning","status":"stopped","reason":"memory","exit_code":12,"jobs_processed":2,"memory":1.2,"timestamp":"2026-09-01T13:20:40.118273+00:00"}
Nine exit scenarios are covered, including memory limit exceeded, maximum jobs exceeded, restart signal received, and job timed out. Nothing is written under --quiet or --silent. Contributed by @jackbayliss in #61339.
Other Notable Changes
DevCommands::withoutVendorCommands()/withoutDefaultCommands()— filterdevcommands by origin instead of naming every command explicitly.- Native
sqlsrv:DSN strings —sqlsrv:Server=host,1433;Database=db;Encrypt=trueis now parsed correctly instead of being mangled byparse_url(). Artisan::commandNamed()— resolves a single command by name without constructing every registered command.- Cloud queue totals —
totalPendingSize(),totalDelayedSize(), andtotalReservedSize()are now implemented on the Laravel Cloud queue driver. route:cachefacade fix — the global facade application is restored afterroute:cachebootstraps its throwaway container, preventingRoute is not bounderrors duringphp artisan optimize.Request::clamp()non-numeric fallback — inputs like?per-page=foonow fall through to the default instead of returning a 500.
Key Takeaways
chunkBy()replaces verbosechunkWhile()callbacks when grouping by a stable key or computed value.Storage::path()now enforces the same path-traversal protection as every other storage method — audit any code that passes user input topath().- Queue worker exit reasons appear in console output without any listener setup.
- Native SQL Server DSN strings are parsed correctly for the first time.
Artisan::commandNamed()avoids the performance cost of constructing all commands just to find one.