Laravel 13.20.0 adds first-party image processing to the framework, with an immutable, driver-based API for transforming images that come from uploads, storage, URLs, or raw bytes. The release also brings a WithoutMiddleware controller attribute, a dedicated session prefix for Redis, and a batch of Eloquent and queue additions.

  • First-party image processing via the new Image facade
  • A new #[WithoutMiddleware] controller attribute
  • Configure a separate Redis prefix for sessions
  • Quietly increment and decrement on Eloquent models
  • Enums accepted as WithoutOverlapping queue keys
  • And more

What's New

First-Party Image Processing

Laravel 13.20 introduces an Illuminate\Image component that handles resizing, cropping, format conversion, and storage without reaching for a third-party wrapper.

Images are immutable: every transformation returns a new instance, and the queued transformations are applied when you ask for the result.

The most common path starts with an upload. Request::image() returns an Image instance for a given file key, or null if the key does not hold an uploaded file:

$request->image('avatar')

->cover(200, 200)

->toWebp()

->store('avatars');

You can also build an image from a path, a URL, raw bytes, base64, or a storage disk:

Image::fromPath('/path/to/photo.jpg');

Image::fromUrl('https://example.com/photo.jpg');

Image::fromBytes($bytes);

Image::fromStorage('photos/avatar.jpg', 's3');

Storage::disk('s3')->image('photos/avatar.jpg');

Transformations, output options, and inspection methods are all available on the instance:

// Transformations...

$image->cover(200, 200);

$image->contain(800, 600);

$image->crop(200, 200);

$image->resize(1024, 768);

$image->scale(1200, 800);

$image->rotate(90);

$image->blur(10);

$image->sharpen(10);

$image->grayscale();

$image->flip();

$image->flop();

$image->orient(); // Applies EXIF rotation...

// Output format and quality...

$image->toWebp();

$image->toJpg()->quality(80);

$image->optimize(); // WebP at quality 70...

$image->optimize('jpg', 85);

// Reading and inspecting...

$image->toBytes();

$image->toBase64();

$image->toDataUri();

$image->width();

$image->height();

$image->dimensions();

$image->mimeType();

$image->extension();

Because instances are immutable, you can branch from one source image to produce several variants:

$image = $request->image('photo');

$image->cover(200, 200)->toWebp()->store('thumbnails');

$image->grayscale()->toWebp()->store('grayscale');

Two drivers ship with the component, both backed by Intervention Image v4: GD and Imagick. You can pick a driver per image, and the Image facade lets you override how a driver handles a given transformation:

use Illuminate\Image\Transformations\Sharpen;

// Pick a driver per image...

$image->using('imagick');

$image->usingGd();

$image->usingImagick();

// Override how a driver handles a transformation...

Image::transformUsing('gd', Sharpen::class, function ($image, Sharpen $sharpen) {

// Custom sharpen handling for the GD driver...

return $image;

});

Intervention Image is a suggested dependency rather than a required one, so you install it yourself:

composer require intervention/image

See #59276.

A #[WithoutMiddleware] Controller Attribute

The routing attributes gain a counterpart to #[Middleware]. Where the latter attaches middleware to a controller class or method, #[WithoutMiddleware] excludes it, giving you the "excluding middleware" behavior of route groups at the attribute level:

use App\Http\Middleware\EnsureTokenIsValid;

use Illuminate\Routing\Controllers\Attributes\Middleware;

use Illuminate\Routing\Controllers\Attributes\WithoutMiddleware;

#[Middleware(EnsureTokenIsValid::class)]

class UserController

{

public function index()

{

// Middleware applies here...

}

#[WithoutMiddleware(EnsureTokenIsValid::class)]

public function profile()

{

// ...but not here.

}

}

Matching uses ReflectionAttribute::IS_INSTANCEOF, so subclasses of a middleware are excluded too. See #60709.

Redis Session Prefix

Applications that point both SESSION_DRIVER and CACHE_STORE at Redis have until now had sessions inherit the cache prefix, which makes session keys hard to pick out when you are cleaning up or debugging a shared keystore. A new prefix option in config/session.php lets you set a separate one:

'prefix' => env('SESSION_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-session-'),

The cache store is cloned before the session prefix is applied, so setting it does not affect cache keys. The option is opt-in and mirrors the existing session.connection setting. See #60700.

Quiet Bulk Increments on Eloquent

Eloquent already had incrementQuietly() and decrementQuietly() for single columns. This release fills in the multi-column gap with incrementEachQuietly() and decrementEachQuietly(), which update several columns at once while suppressing model events:

$user->incrementEachQuietly(['posts_count' => 1, 'points' => 10]);

$user->decrementEachQuietly(['credits' => 3, 'tokens' => 2]);

See #60720 and the follow-up fix for dynamic calls in #60737.

Enums as Queue Overlap Keys

The WithoutOverlapping job middleware now accepts a PHP enum as its key, so jobs keyed by an enumerated value no longer need a manual conversion to a string:

class UpdateCategory implements ShouldQueue

{

use Queueable;

public function __construct(protected Category $category) {}

public function middleware(): array

{

return [new WithoutOverlapping($this->category)];

}

}

The change is backward compatible with existing string keys. See #60722.

Other Fixes and Improvements

  • beforePushing() and afterPushing() callbacks on QueueFake (#60689), and MailFake::assertQueuedTimes() is now public (#60710)
  • assertEmpty() added to the Storage facade (#60658), and memory usage reported on the WorkerStopping event (#60613)
  • make:migration now generates collision-free, ordered timestamp prefixes (#60771)
  • #[SensitiveParameter] applied to parameters carrying secrets, keeping them out of stack traces (#60753)
  • A capitalize parameter for Stringable::initials() (#60741)
  • Str::containsAll() no longer returns true for an empty needles array (#60746), and Number::forHumans() and abbreviate() no longer return -0 for tiny negative values (#60736, #60768)
  • Fixes for BelongsToMany::touch() when the related key is not id (#60708), TrustProxies with at:* and multiple proxies (#60726), and providesTemporaryUploadUrls() on the S3 driver (#60755)

Upgrade Notes

No breaking changes are expected for typical applications. The image component's drivers depend on intervention/image (^4.0), which is a suggested package, so install it before using the Image facade. Review the changelog for PR-by-PR details when upgrading.

References