Laravel 12.51.0 Adds afterSending Callbacks, Validator whenFails, and MySQL Timeout
Published on by Paul Redmond
Release Date: February 10, 2026
Laravel Version: 12.51.0
Summary
Laravel v12.51.0 adds notification afterSending() callbacks, fluent whenFails() and whenPasses() methods on the Validator, a MySQL query builder timeout() method, and closure support in firstOrCreate and createOrFirst for lazy value evaluation. This release also introduces a BatchCancelled event, support for Eloquent builders as subqueries in update queries, and a withoutHeader() method on responses.
Key highlights include:
- Notification
afterSending()callbacks - Validator
whenFails()andwhenPasses()methods - MySQL query builder
timeout()method - Closure support in
firstOrCreate/createOrFirst BatchCancelledevent- Eloquent builders as subqueries in update queries
withoutHeader()on Response- Batch testing improvements and cache isolation for parallel tests
- Numerous bug fixes and type improvements
What's New
Notification afterSending() Callbacks
Notification classes can now define an afterSending method that runs after the notification is sent on each channel. This provides a convenient way to handle post-send logic — like updating a model or firing an event — without registering a dedicated NotificationSent listener:
class BookingNotification extends Notification{ public function __construct(public Booking $booking) {} public function via(): array { return ['mail']; } public function toMail(): MailMessage { // ... } public function afterSending($notifiable, $channel, $response) { $this->booking->update(['notified_at' => now()]); }}
The method receives the notifiable instance, the channel name, and the channel's response, giving you full context for any follow-up actions.
Pull Request: #58654
Validator whenFails() and whenPasses() Methods
The Validator now includes fluent whenFails() and whenPasses() methods for handling validation results, which is particularly useful outside the HTTP request cycle — such as in Artisan commands or queue jobs:
public function someMethod($file){ Validator::make( ['file' => $file], ['file' => 'required|image|dimensions:min_width=100,min_height=200'] )->whenFails(function () { throw new InvalidArgumentException('Provided file is invalid'); });}
These methods provide an alternative to manually checking $validator->fails() or wrapping validation in try/catch blocks.
Pull Request: #58655
MySQL Query Builder timeout() Method
A new timeout() method on the query builder sets a per-query execution timeout for MySQL using the MAX_EXECUTION_TIME optimizer hint:
Student::query() ->where('email', 'like', '%text%') ->timeout(60) ->get(); // Generates: select /*+ MAX_EXECUTION_TIME(60000) */ * from `students` where `email` like ?
You can also apply it as a default via a global scope:
use Illuminate\Database\Eloquent\Attributes\ScopedBy; class TimeoutScope implements Scope{ public function apply(Builder $builder, Model $model) { $builder->timeout(60); }} #[ScopedBy([TimeoutScope::class])]class User extends Model{ // ...}
This is MySQL-specific and accepts a timeout value in seconds.
Pull Request: #58644
Closures in firstOrCreate and createOrFirst
The firstOrCreate and createOrFirst methods now accept closures for the $values parameter, enabling lazy evaluation of expensive operations:
// Before — expensive operation always runs$location = Location::query()->firstWhere('address', $address); if ($location) { return $location;} return Location::create([ 'address' => $address, 'coordinates' => Geocoder::resolve($address),]); // After — geocoding only runs when a new record is needed$location = Location::firstOrCreate( ['address' => $address], fn () => ['coordinates' => Geocoder::resolve($address)],);
When the record already exists, the closure is never evaluated, avoiding unnecessary API calls, computations, or other expensive work.
Pull Request: #58639
BatchCancelled Event
A new BatchCancelled event fires globally when a batch is cancelled, whether automatically due to a job failure or through a manual cancellation call. This lets you listen for batch cancellations across your application without polling:
use Illuminate\Bus\Events\BatchCancelled;use Illuminate\Support\Facades\Event; Event::listen(BatchCancelled::class, function (BatchCancelled $event) { Log::warning("Batch {$event->batch->id} was cancelled.");});
Pull Request: #58627
Eloquent Builders as Subqueries in Updates
Eloquent builders and relations can now be used directly as subqueries in update statements without converting to a base query:
// BeforeFooModel::where('...')->update([ 'bar_id' => BarModel::where('...')->toBase()->select('id'),]); // AfterFooModel::where('...')->update([ 'bar_id' => BarModel::where('...')->select('id'),]);
The ->toBase() call is no longer required when passing an Eloquent builder as a subquery value.
Pull Request: #58692
withoutHeader() on Response
A new withoutHeader() method allows you to remove headers from HTTP responses, providing symmetry with the existing withoutCookie() method:
// Remove a single headerreturn response($content)->withoutHeader('X-Debug'); // Remove multiple headersreturn response($content)->withoutHeader(['X-Debug', 'X-Powered-By', 'Server']);
Pull Request: #58671
Bug Fixes and Improvements
Testing:
assertJobsmethod onPendingBatchFakefor batch job assertions (#58606)Bus::assertBatched()with array support (#58659)viewData()without key returns all view data onTestResponse(#58700)- Cache prefix isolation for parallel testing via
TestCachestrait (#58691)
HTTP Client:
throwIfStatus/throwUnlessStatusnow works for all status codes including 2xx and 3xx (#58724)
Database & Eloquent:
orderByPivotDesc()method for descending pivot column ordering (#58720)whereBetweenacceptsDatePeriodand handles missing end dates (#58687)- SSL cert/key support for MySQL schema dump and load (#58690)
- Fix Postgres sequence starting value for custom schemas/connections (#58199)
- Adjust
freshTimestampfor SQL Server (#58614) - Fix batch counts when
deleteWhenMissingModelsskips missing model jobs (#58541)
Strings & Helpers:
Stringable::deduplicate()accepts array of characters (#58649)- Fix
Str::substrReplacefor edge cases with negative offset or length (#58634) - Fix
Str::isUrl()returning false for single-character domain names (#58686) - Replace
substrwithmb_substrfor user agent encoding (#58703)
Queue & Middleware:
- Allow specifying Redis connection on Redis-based queue middleware (#58656)
- Fix
Queue::fake()not releasing unique job locks between tests (#58718)
Translation & Localization:
- Prevent duplicate locale checks in
Lang::get()when locale matches fallback (#58626) - Fix
trans_choiceregex to allow negative ranges (#58648)
Framework & Internals:
- Restore original dispatcher bindings after precognitive request — fixes Octane and Pest (#58716)
- Handle binary data in
Js::encode()debug renderer (#58618) - Add ArrayObject props to
AsEncryptedArrayObjectto matchAsArrayObject(#58619) - Fix exception page pop-in for non-main frames (#58698)
- Fix Laravel ASCII SVG character alignment (#58702, #58719)
- Add deprecation to
Request::get()(#58635) - Update reload tasks to include
schedule:interruption(#58637)