News

First-Party Image Processing in Laravel 13.20

Published
First-Party Image Processing in Laravel 13.20 image

Release updates

Never miss a Laravel release

Get an email whenever a new Laravel release lands.

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

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Sponsored

masteringlaravel logo
Laravel Code Review

Get expert guidance in a few days with a Laravel code review

Visit Laravel Code Review

The latest

View all →
Official Laravel Zed Extension: LSP for PHP & Blade image

Official Laravel Zed Extension: LSP for PHP & Blade

Read article
Laravel Head: Manage Meta Tags, Open Graph, and JSON-LD image

Laravel Head: Manage Meta Tags, Open Graph, and JSON-LD

Read article
PhpStorm 2026.2 Released image

PhpStorm 2026.2 Released

Read article
Laravel Doctor: Diagnose Your App With One Artisan Command image

Laravel Doctor: Diagnose Your App With One Artisan Command

Read article
CPX: The Composer Package Executor for PHP image

CPX: The Composer Package Executor for PHP

Read article
Laravel AI SDK Adds Human-in-the-Loop Tool Approval image

Laravel AI SDK Adds Human-in-the-Loop Tool Approval

Read article