Collections chunkBy() and Storage Path Hardening in Laravel 13.30
Laravel #Laravel 13.30 #Collections #Storage #Queue #Security

Collections chunkBy() and Storage Path Hardening in Laravel 13.30

3 min read Mohamed Said Mohamed Said

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() — filter dev commands by origin instead of naming every command explicitly.
  • Native sqlsrv: DSN stringssqlsrv:Server=host,1433;Database=db;Encrypt=true is now parsed correctly instead of being mangled by parse_url().
  • Artisan::commandNamed() — resolves a single command by name without constructing every registered command.
  • Cloud queue totalstotalPendingSize(), totalDelayedSize(), and totalReservedSize() are now implemented on the Laravel Cloud queue driver.
  • route:cache facade fix — the global facade application is restored after route:cache bootstraps its throwaway container, preventing Route is not bound errors during php artisan optimize.
  • Request::clamp() non-numeric fallback — inputs like ?per-page=foo now fall through to the default instead of returning a 500.

Key Takeaways

  • chunkBy() replaces verbose chunkWhile() 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 to path().
  • 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.

Source: Laravel News — Laravel 13.30.0

Found this useful?

Frequently Asked Questions

3 questions
Q01 What is the difference between chunkBy() and chunkWhile() in Laravel collections?
`chunkWhile()` splits a collection wherever a callback returns false, requiring you to manually compare the current item with the last item in the current chunk. `chunkBy()` wraps that pattern: you pass a key name or callback, and a new chunk starts automatically whenever the resolved value changes. It also supports dot notation via `data_get()`.
Q02 Why is the Storage::path() change in Laravel 13.30 a security improvement?
Before 13.30, `Storage::path()` skipped path normalization and simply concatenated the disk prefix with the given string. This meant passing `../../../.env` returned a valid native path outside the disk root, even though `Storage::get()` and other methods would have rejected the same input. The fix runs the argument through `WhitespacePathNormalizer` and throws `PathTraversalDetected` for any path that escapes the disk root, matching the behavior of every other filesystem call.
Q03 How do I see why a Laravel queue worker stopped without registering a listener?
In Laravel 13.30 and later, `queue:work` prints the stop reason as its final line automatically, for example `Worker STOPPED Memory limit exceeded`. With the `--json` flag the reason is included in a structured JSON record. Nothing is output under `--quiet` or `--silent`.

Continue reading

More Articles

View all